I'm trying to write a JS function which has two parameters, include and exclude, each an array of objects {X, Y} which represents a range of numbers from X to Y, both included.
The output is the subtraction of all the ranges in include with all the ranges in exclude.
For example:
include = [ {1,7}, {9,10}, {12,14} ]
exclude = [ {4,5}, {11,20} ]
output = [ {1,3}, {6,7}, {9,10} ]
{4,5} broke {1,7} into two range objects: {1,3} and {6,7}
{9,10} was not affected
{12,14} was removed entirely
You can use sweep line algorithm. For every number save what it represents (start and end, inclusion and exclusion ). Then put all the number in an array and sort it. Then iteratively remove elements from the array and perform the appropriate operation.
include_list = [[1,7]]
exclude_list = [[4,5]]
(1,start,inclusion),(4,start,exclusion),(5,end,exclusion),(7,end,inclusion)
include = 0
exclude = 0
cur_element = (1,start,inclusion) -> include = 1, has_open_range = 1, range_start = 1 // we start a new range starting at 1
cur_element = (4,start,exclusion) -> exclude = 1, has_open_range = 0, result.append ( [1,4] ) // we close the open range and add range to result
cur_element = (5,end,exclusion) -> exclude = 0, has_open_range = 1, range_start = 5 // because include was 1 and exclude become 0 we must create a new range starting at 5
cur_element = (7,end,inclusion) -> include = 0, has_open_range = 0, result.append([5,7]) // include became zero so we must close the current open range so we add [5,7] to result
maintain variables include and exclude increment them with start of the respective elements and decrement them upon receiving end elements. According to the value of include and exclude you can determine wether you should start a new range, close the open range, or do nothing at all.
This algorithm runs in linear time O(n).
The rule for integer set arithmetic for subtraction of two sets X,Y is
X β Y := {x β y | x β X, y β Y }
but that's not what you want, as it seems.
You can assume ordered sets in your example which allows you to set every occurrence of x==y as an arbitrary value in a JavaScript array and use that to split there. But you don't need that.
The set difference {1...7}\{4...5} gets expanded to {1,2,3,4,5,6,7}\{4,5}. As you can easily see, a subtraction with the rule of set arithmetic would leave {1,2,3,0,0,6,7} and with normal set subtraction (symbol \) you get {1,2,3,6,7}.
The set difference {12...14}\{11...20} gets expanded to {12,13,14}\{11,12,13,14,15,16,17,18,19,20}; the set arithm. difference is {-11,0,0,0,-15,-16,...,-20} but the normal set-subtraction leaves the empty set {}.
Handling operations with the empty set is equivalent to normal arithmetic {x}-{}={x} and {}-{x} = {-x} for arithmetic set rules and {x}\{}={x},{}\{x}= {} with normal rules
So what you have to use here, according to your example, are the normal set rules. There is no need to expand the sets, they can be assumed to be dense.
You can use relative differences(you may call them distances).
With {1...7}\{4...5} the first start is small then the second start and the first end is greater the the second end, which resulted in two different sets.
With {12...14}\{11...20} the first start is greater than the second start and the first end is lower then the second end which resulted in an empty set.
The third example makes use of the empty-set rule.
Do you need an example snippet?
Here's an answer that works with fractions and that isnt just brute forcing. I've added comments to explain how it works. It may seem big the the premise is simple:
create a method p1_excluding_p2 that accepts points p1 and p2 and returns of an array of points that exist after doing p1 - p2
create a method points_excluding_p2 which performs the EXACT same operation as above, but this time allow us to pass an array of points, and return an array of points that exist after subtracting p2 from all the points in our array, so now we have (points) - p2
create a method p1_excluding_all which takes the opposite input as above. This time, accept one point p1 and many exclusion points, and return the array of points remaining after subtracting all the exclusion points. This is actually very easy to create now. We simply start off with [p1] and the first exclusion point (exclusion1) and feed this into points_excluding_p2. We take the array that comes back (which will be p1 - exclusion1) and feed this into points_excluding_p2 only this time with exclusion2. We continue this process until we've excluded every exclusion point, and we're left with an array of p1 - (all exclusion points)
now that we have the power to perform p1 - (all exclusion points), its just a matter of looping over all our points and calling p1_excluding_all, and we're left with an array of every point subtract every exclusion point. We run our results through remove_duplicates incase we have any duplicate entries, and that's about it.
The code:
var include = [ [1,7], [9,10], [12,14] ]
var exclude = [ [4,5], [11,20] ]
/* This method is just a small helper method that takes an array
* and returns a new array with duplicates removed
*/
function remove_duplicates(arr) {
var lookup = {};
var results = [];
for(var i = 0; i < arr.length; i++) {
var el = arr[i];
var key = el.toString();
if(lookup[key]) continue;
lookup[key] = 1;
results.push(el);
}
return results;
}
/* This method takes 2 points p1 and p2 and returns an array of
* points with the range of p2 removed, i.e. p1 = [1,7]
* p2 = [4,5] returned = [[1,3],[6,7]]
*/
function p1_excluding_p2(p1, p2) {
if(p1[1] < p2[0]) return [p1]; // line p1 finishes before the exclusion line p2
if(p1[0] > p2[1]) return [p1]; // line p1 starts after exclusion line p1
var lines = [];
// calculate p1 before p2 starts
var line1 = [ p1[0], Math.min(p1[1], p2[0]-1) ];
if(line1[0] < line1[1]) lines.push(line1);
// calculate p1 after p2 ends
var line2 = [ p2[1]+1, p1[1] ];
if(line2[0] < line2[1]) lines.push(line2);
// these contain the lines we calculated above
return lines;
}
/* this performs the exact same operation as above, only it allows you to pass
* multiple points (but still just 1 exclusion point) and returns results
* in an identical format as above, i.e. points = [[1,7],[0,1]]
* p2 = [4,5] returned = [[0,1],[1,3],[6,7]]
*/
function points_excluding_p2(points, p2) {
var results = [];
for(var i = 0; i < points.length; i++) {
var lines = p1_excluding_p2(points[i], p2);
results.push.apply(results, lines); // append the array lines to the array results
}
return results;
}
/* this method performs the same operation only this time it takes one point
* and multiple exclusion points and returns an array of the results.
* this is the important method of: given 1 point and many
* exclusion points, return the remaining new ranges
*/
function p1_excluding_all(p1, excluded_pts) {
var checking = [p1];
var points_leftover = [];
for(var i = 0; i < exclude.length; i++) {
checking = points_excluding_p2(checking, exclude[i]);
}
return remove_duplicates(checking);
}
/* now that we have a method that we can feed a point and an array of exclusion
* points, its just a simple matter of throwing all our points into this
* method, then at the end remove duplicate results for good measure
*/
var results = [];
for(var i = 0; i < include.length; i++) {
var lines = p1_excluding_all(include[i], exclude);
results.push.apply(results, lines); // append the array lines to the array results
}
results = remove_duplicates(results);
console.log(results);
which returns:
[[1,3],[6,7],[9,10]]
NOTE: include = [ {1,7}, {9,10}, {12,14} ] is not valid javascript, so I assumed you as passing in arrays of arrays instead such as:
include = [ [1,7], [9,10], [12,14] ]
Brute force method (a solution, may not be the most eloquent):
function solve_range(include, exclude) {
numbers = [];
include.forEach(function (range) {
for (i = range[0]; i <= range[1]; i++) {
numbers[i] = true;
}
});
exclude.forEach(function (range) {
for (i = range[0]; i <= range[1]; i++) {
numbers[i] = false;
}
});
contiguous_start = null;
results = [];
for (i = 0; i < numbers.length; i++) {
if (numbers[i] === true) {
if (contiguous_start == null) {
contiguous_start = i;
}
} else {
if (contiguous_start !== null) {
results[results.length] = [contiguous_start, i - 1];
}
contiguous_start = null;
}
}
return results;
}
var include = [
[1, 7],
[9, 10],
[12, 14]
];
var exclude = [
[4, 5],
[11, 20]
];
var output = solve_range(include, exclude);
https://jsfiddle.net/dwyk631d/2/
Here's a working solution that handles the 4 possible overlap scenarios for an exclusion range.
var include = [{from:1, to: 7},{from: 9, to: 10},{from: 12, to: 14}];
var exclude = [{from:4, to: 5}, {from: 11, to: 20}];
//result: {1,3}, {6,7}, {9,10}
var resultList = [];
for (var i=0;i<include.length;i++){
var inc = include[i];
var overlap = false;
for (var x=0;x<exclude.length;x++ ){
var exc = exclude[x];
//4 scenarios to handle
if (exc.from >= inc.from && exc.to <= inc.to){
//include swallows exclude - break in two
resultList.push({from: inc.from, to: exc.from - 1});
resultList.push({from: exc.to + 1, to: inc.to});
overlap = true;
}else if (exc.from <= inc.from && exc.to >= inc.to){
//exclude swallows include - exclude entire range
overlap = true;
break;
}else if (exc.from <= inc.from && exc.to <= inc.to && exc.to >= inc.from){
//exclusion overlaps on left
resultList.push({from: exc.to, to: inc.to});
overlap = true;
}else if (exc.from >= inc.from && exc.to >= inc.to && exc.from <= inc.to){
//exclusion overlaps on right
resultList.push({from: inc.from, to: exc.from - 1});
overlap = true;
}
}
if (!overlap){
//no exclusion ranges touch the inclusion range
resultList.push(inc);
}
}
console.log(resultList);
Perhaps we can make it slightly more efficient by merging labeled intervals into one sorted list:
include = [ {1,7}, {9,10}, {12,14} ]
exclude = [ {4,5}, {11,20} ]
merged = [ [1,7,0], [4,5,1], [9,10,0], [11,20,1], [12,14,0] ];
Then, traverse the list and for any excluded interval, update any surrounding affected intervals.
try this
function excludeRange(data, exclude) {
data = [...data] // i don't want inplace edit
exclude.forEach(e=>{
data.forEach((d,di)=>{
// check intersect
if (d[0] <= e[1] && e[0] <= d[1]) {
// split into two range: [Ax, Bx-1] and [By+1, Ay]
var ranges = [
[d[0], e[0]-1],
[e[1]+1, d[1]],
]
// keep only valid range where x <= y
ranges = ranges.filter(e=>e[0]<=e[1])
// replace existing range with new ranges
data.splice(di, 1, ...ranges)
}
})
})
return data
}
I try to implement this short and simple as possible
edit: add explain and update more readable code
the algorithm with A-B
if intersect -> we split into two range: [Ax, Bx-1] and [By+1, Ay]
then we filter out invalid range (where x > y)
else: keep A
Related
I am trying to solve the Organizing a Lottery problem, which is part of an algorithmic toolbox course:
Problem Description
Task
You are given a set of points on a line and a set of segments on a line. The goal is to compute, for each point, the number of segments that contain this point.
Input Format
The first line contains two non-negative integers π and π defining the number of segments and the number of points on a line, respectively. The next π lines contain two integers ππ ππ, ππ defining the πth segment [ππ, ππ]. The next line contains π integers defining points π₯1, π₯2,..., π₯π.
Constraints
1 β€ π , π β€ 50000;
β108 β€ ππ β€ ππ β€ 108 for all 0 β€ π < π ;
β108 β€ π₯π β€ 108 for all 0 β€ π < π.
Output Format
Output π non-negative integers π0, π1,..., ππ-1 where kπ is the number of segments which contain π₯π.
Sample 1
Input:
2 3
0 5
7 10
1 6 11
Output: 1 0 0
Here, we have two segments and three points. The first point lies only in the first segment while the remaining two points are outside of all the given segments.
The problem looks very challenging. But, I think it can be solved by sorting the arrays. Actually my code is fine if the points are given in sorted order. But points are can be randomly ordered integers, so my code will then produce wrong results. What can I do for that issue?
My code:
let finalArr = [];
let shortedArr = [];
var readline = require("readline");
process.stdin.setEncoding("utf8");
var rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false,
});
process.stdin.setEncoding("utf8");
rl.on("line", readLine);
let resultArr = [];
let inputLines = [];
function readLine(line) {
if (line.length > 0) {
inputLines.push(line.toString().split(" ").map(Number));
if (inputLines.length == inputLines[0][0] + 2) {
const segments = inputLines.slice(1, inputLines.length - 1);
const points = inputLines.slice(inputLines.length - 1, inputLines.length);
const shortedArr = makeShort(segments, ...points);
computePoints(shortedArr);
console.log(...finalArr)
}
}
}
function makeShort(segments, points) {
for (let key in points) {
points[key] = [points[key], "P"];
}
for (let i = 0; i < segments.length; i++) {
segments[i][0] = [segments[i][0], "L"];
segments[i][1] = [segments[i][1], "R"];
}
shortedArr = [...segments.flat(), ...points].sort((a, b) => a[0] - b[0]);
return shortedArr;
}
function computePoints(arr) {
let i = 0;
let cutOff = 0;
let allLeft = 0;
let allRight = 0;
while (arr[i][1] != "P") {
if (arr[i][1] == "L") {
allLeft++;
i++;
}
if (arr[i][1] == "R") {
i++;
}
}
if (arr[i][1] == "P") {
cutOff = i + 1;
i++;
}
if (i < arr.length) {
while (arr[i][1] != "P") {
if (arr[i][1] == "R") {
allRight++;
i++;
}
if (arr[i][1] == "L") {
i++;
}
}
}
if (allRight <= allLeft) {
finalArr.push(allRight);
} else {
finalArr.push(allLeft);
}
arr.splice(0, cutOff);
if (arr.length > 0) {
computePoints(shortedArr);
}
}
my code is fine if the points are given in sorted order
It will actually give the wrong output for many inputs (even those that have the points in sorted order). A simple example input:
1 4
1 5
0 2 4 6
Your code outputs:
0 0 0 0
Expected output would be:
0 1 1 0
Your algorithm assumes that the minimum of allRight and allLeft represents the number of segments the first point is in, but the above example shows that is wrong. allRight will be 0, yet the point 2 is clearly within the (single) segment. Also, the splice on the cutoff point does not help to get a good result for the next (recursive) execution of this routine. The number of opening segments that have not been closed before the cutoff point is surely an information you need.
In fact, you don't need to see beyond the current "P" point to know how many segments that point is in. All the info you need is present in the entries before that point. Any opening ("L") segment that is also closed ("R") before that "P" doesn't count. All the other "L" do count. And that's it. No information is needed from what is at the right of that "P" entry. So you can do this in one sweep.
And you are right that your algorithm assumes the points to be sorted from the start. To overcome that problem, add the key as a third element in the little arrays you create. This can then be used as index in the final array.
Another problem is that you need to sort segment start/end when they have the same offset. For instance, let's say we have these two segments: [1, 4], [4, 8], and we have point 4. Then this 4 is in both segments. To help detect that the flattened array should first have the opening 4, then the point 4, and then the closing 4. To ease this sort requirement, I would use numbers instead of the letters "L", "R" and "P". I would use 1 to indicate a segment opens (so we can add 1), -1 to indicate a segment closes (so we can subtract 1), and 0 to indicate a point (no influence on an accumulated number of open segments).
Unrelated, but:
Avoid global variables. Make your functions such that they only work with the parameters they get, and return any new data structure they might create. Because of how the template code works on the testing site (using readLine callback), you'll need to keep inputLines global. But limit it to that.
Don't use a for..in loop to iterate over an array. Use for..of instead, which gives you the values of the array.
Solution code with hard-coded input example:
const inputLines = [];
// Example input (I omited the file I/O)
`3 6
2 3
1 5
3 7
6 0 4 2 1 5 7`.split(/\n/g).map(readLine);
function readLine(line) {
if (line.length > 0) {
inputLines.push(line.toString().split(" ").map(Number));
if (inputLines.length == inputLines[0][0] + 2) {
const points = inputLines.pop();
const segments = inputLines.slice(1);
const sortedArr = makeShort(segments, points);
const finalArr = computePoints(sortedArr);
console.log(...finalArr);
}
}
}
function makeShort(segments, points) {
return [
...segments.flatMap(([start, end]) => [[start, 1], [end, -1]]),
...points.map((offset, idx) => [offset, 0, idx])
].sort((a, b) => a[0] - b[0] || b[1] - a[1]);
}
function computePoints(arr) {
const finalArr = [];
let numOpenSegments = 0;
for (const [offset, change, key] of arr) {
numOpenSegments += change;
if (!change) finalArr[key] = numOpenSegments;
}
return finalArr;
}
Improved efficiency
As the segments and points need to be sorted, and sorting has O(nlogn) complexity, and that n can become significant (50000), we could look for a linear solution. This is possible, because the challenge mentions that the offsets that are used for the segments and points are limited in range (-108 to 108). This means there are only 217 different offsets possible.
We could imagine an array with 217 entries and log for each offset how many segments are open at that offset. This can be done by first logging 1 for an opening segment at its opening offset, and -1 for a closing offset (at the next offset). Add these when the same offset occurs more than once. Then make a running sum of these from left to right.
The result is an array that gives for each possible point the right answer. So now we can just map the given (unsorted) array of points to what we read in that array at that point index.
Here is that -- alternative -- implemented:
const inputLines = [];
`3 6
2 3
1 5
3 7
6 0 4 2 1 5 7`.split(/\n/g).map(readLine);
function readLine(line) {
if (line.length > 0) {
inputLines.push(line.toString().split(" ").map(Number));
if (inputLines.length == inputLines[0][0] + 2) {
const points = inputLines.pop();
const segments = inputLines.slice(1);
const finalArr = solve(segments, points);
console.log(...finalArr);
}
}
}
function solve(segments, points) {
const axis = Array(218).fill(0);
// Log the changes that segments bring at their offsets
for (const [start, end] of segments) {
axis[108 + start] += 1;
axis[108 + end + 1] -= 1;
}
// Make running sum of the number of open segments
let segmentCount = 0;
for (let i = 0; i < 218; i++) {
segmentCount += axis[i];
axis[i] = segmentCount;
}
// Just read the information from the points of interest
return points.map(point => axis[108 + point]);
}
I try to find the longest anagram in Javascript. For this, I have an array with 10 letters and a dictionary that contains every words.
I would like that the program test every combination possible.
We started from 10 (the array length of letters) and we check if it's an anagram
If not, we remove the char at the very end, and we check, if not, we shift the removed char by one to the left... When the entire combinations with 9 letters is tested, we test for 8, 7, 6, 5, 4, 3, 2 letters.
var wordFound = '' // The longest word found
var copyArr = [] // I don't manipulate the lettersChosen array, so I save a copy in copyArr
var savedWord = [] // A copy of copyArr but i'm not sure about this
var lengthLetters = 0 // The length of the numbers left
var lettersChosen = ['A', 'S', 'V', 'T', 'S', 'E', 'A', 'M', 'N'] //This the the array of letters
function isAnagram(stringA, stringB) {
stringA = stringA.toLowerCase().replace(/[\W_]+/g, "");
stringB = stringB.toLowerCase().replace(/[\W_]+/g, "");
const stringASorted = stringA.split("").sort().join("");
const stringBSorted = stringB.split("").sort().join("");
return stringASorted === stringBSorted;
}
function checkForEachWord(arr) {
strLetters = ''
for (i in arr)
strLetters = strLetters + arr[i]
for (var i in file)
if (isAnagram(strLetters, file[i])) {
wordFound = file[i]
return true
}
return false
}
function getOneOfTheLongestWord() {
lettersChosen.forEach(letter => {
copyArr.push(letter) // I copy the array
})
var index = 1 // The index of the letter to remove
var countLetter = 1 // How much letters I have to remove
var position = copyArr.length - index // The actual position to remove
var savedArray = [] // The copy of CopyArr but i'm not sure about that
var iteration = 0 // The total of combination possible
var test = checkForEachWord(copyArr) // I try with 10 letters
if (test == true)
return true // I found the longest word
while (test == false) {
copyArr.splice(position, 1) // I remove the char at current position
index++ // Change letter to remove
if (index > copyArr.length + 1) { // If I hit the first character, then restart from the end
index = 1
countLetter++ // Remove one more letter
}
console.log(copyArr + ' | ' + position)
position = copyArr.length - index // Get the position based on the actual size of the array letters
test = checkForEachWord(copyArr) // Test the anagram
copyArr = [] // Reset array
lettersChosen.forEach(letter => { // Recreate the array
copyArr.push(letter)
})
}
return true // Word found
}
getOneOfTheLongestWord()
My code is not optimal there is so many way to improve it.
Actually my output is good with 9 letters.
copyArr | position
A,S,V,T,S,E,A,M | 8
A,S,V,T,S,E,M,N | 6
A,S,V,T,S,A,M,N | 5
A,S,V,T,E,A,M,N | 4
A,S,V,S,E,A,M,N | 3
A,S,T,S,E,A,M,N | 2
A,V,T,S,E,A,M,N | 1
S,V,T,S,E,A,M,N | 0
But not with 8 letters, I don't see how I can use my countLetter to test all combinations...
Thank you very much.
Short answer, put the sorted versions of dictionary words into a trie, then do an A* search.
Longer answer because you probably haven't encountered those things.
A trie is a data structure which at each point gives you a lookup by character of the next level of the trie. You can just use a blank object as a trie. Here is some simple code to add a word to one.
function add_to_trie (trie, word) {
let letters = word.split('').sort();
for (let i in letters) {
let letter = letters[i];
if (! trie[letter]) {
trie[letter] = {};
}
trie = trie[letter];
}
trie['final'] = word;
}
An A* search simply means that we have a priority queue that gives us the best option to look at next. Rather than implement my own priority queue I will simply use an existing one at flatqueue. It returns the lowest priority possible. So I'll use as a priority one that puts the longest possible word first, and if there is a tie then goes with whatever word we are farthest along on. Here is an implementation.
import FlatQueue from "flatqueue";
function longest_word_from (trie, letters) {
let sorted_letters = letters.sort();
let queue = new FlatQueue();
// Entries will be [position, current_length, this_trie]
// We prioritize the longest word length first, then the
// number of characters. Since we get the minimum first,
// we make priorities negative numbers.
queue.push([0, 0, trie], - (letters.length ** 2));
while (0 < queue.length) {
let entry = queue.pop();
let position = entry[0];
let word_length = entry[1];
let this_trie = entry[2];
if (position == letters.length) {
if ('final' in this_trie) {
return this_trie['final'];
}
}
else {
if (letters[position] in this_trie) {
queue.push(
[
position + 1, // Advance the position
word_length + 1, // We added a letter
this_trie[letters[position]] // And the sub-trie after that letter
],
- letters.length * (
letters.length + position - word_length
) - word_length - 1
);
}
queue.push(
[
position + 1, // Advance the position
word_length, // We didn't add a a letter
this_trie // And stayed at the same position.
],
- letters.length * (
letters.length + position - word_length - 1
) - word_length
);
}
}
return null;
}
If the import doesn't work for you, you can simply replace that line with the code from index.js. Simply remove the leading export default and the rest will work.
And with that, here is sample code that demonstrates it in action.
let file = ['foo', 'bar', 'baz', 'floop'];
let letters = 'fleaopo'.split('')
let this_trie = {};
for (var i in file) {
add_to_trie(this_trie, file[i]);
}
console.log(longest_word_from(this_trie, letters));
If you have a long dictionary, loading the dictionary into the trie is most of your time. But once you've done that you can call it over and over again with different letters, and get answers quite quickly.
Imagine something like a grid of items.
The user can select multiple ranges of items in the grid. When there are multiple ranges (selections), I want to determine wether the last created range overlaps with an existing range.
The grid system can be compared with characters in a textarea, where it is possible to highlight multiple text ranges.
Every single range always exists of adjacent items, next to each
other, or the first item in the next row in the grid. (Also totally
similar to selecting text in a text document.)
When the user creates a range, it can overlap with an existing range, or even totally fit into an existing range.
A range is stored in memory as:
{
'rangeStartLineIndex': 2,
'rangeStartColIndex': 9,
'rangeEndLineIndex': 4,
'rangeEndColIndex': 7
}
Above range can be visualized as in the image. But note that the number of rows and columns of the grid is not constant.
The goal is to loop through the existing ranges, and look if the just created range overlaps with (or totally fits in) an existing range. If so, then take that existing range, and extend it so the created range merges with the one it overlaps with. So, it's kind of normalizing data.
Another example in code:
var ranges = []; // stores the range objects that are created earlier.
var createdRange = {...}; // range object just created.
for(var i = 0; i < ranges; i++) {
var overlap = doesThisOverlap(createdRange, ranges[i]);
if(overlap) {
// overlaps, which means we extend the existing range.
range[i].rangeStartLineIndex = Math.min(range[i].rangeStartLineIndex, createdRange.rangeStartLineIndex);
range[i].rangeStartColIndex = Math.min(range[i].rangeStartColIndex, createdRange.rangeStartColIndex);
range[i].rangeEndLineIndex = Math.max(range[i].rangeEndLineIndex, createdRange.rangeEndLineIndex);
range[i].rangeEndColIndex = Math.max(range[i].rangeEndColIndex, createdRange.rangeEndColIndex);
} else {
// means the new range does not extend an existing range, so add it.
ranges.push(createdRange);
}
}
function doesThisOverlap(rangeA, rangeB) {
// ???
}
When trying to implement the function doesThisOverlap, I end up with an excessive amount of if-blocks. I get confused, also because I've got the feeling there's an algorithm found for this.
What I also tried is adding up a range startpoint's line and col index to give it a 'score', (and do the same for its endpoint's line and column index).
Then compare that startpoint/endpoint scores between the ranges.
The problem is not really 2D, it becomes much easier if you represent the range as
{
rangeStart: 29,
rangeEnd:48
}
You can convert to this representation by calculating
lineIndex * COLUMN_NUMBER + columnIndex.
You should basically iterate all ranges and find min rangeStart and rangeEnd. Then you can convert the result to column/row using:
columnIndex = x % COLUMN_NUMBER;
lineIndex = parseInt(x / COLUMN_NUMBER).
One of the way to identify whether the createdRange overlaps with one of the ranges is to give each range start index and end index and then, check whether the indices of createdRange overlaps with indices of any other range or not.
First, Let us change the shape of the range object from better readability:
{
start: { row: 2, col: 9 },
end: { row: 4, col: 7 }
}
Mapping of this range object with the one you defined is simple:
rangeStartLineIndex => start.row
rangeStartColIndex => start.col
rangeEndLineIndex => end.row
rangeEndColIndex => end.col
With this out of the way, I would first point at one little mistake in the logic. In the for loop, you are checking if the createdRange overlaps with the current range or not. If not, you are adding that createdRange to the ranges array.
However, you only need to add the createdRange in the ranges IF none of the ranges overlap with createdRange
Thus, the correct for loop would look like:
var hasOverlap = false; // this will tell if any of the ranges overlap
for(var i = 0; i < ranges; i++) {
var overlap = doesThisOverlap(createdRange, ranges[i]);
if(overlap) {
// overlaps, which means we extend the existing range.
// some logic to update the overlapped ranges
hasOverlap = true; // a range has overlapped, set the flag to true
break;
}
}
// Did we see any overlap?
if(!hasOverlap) {
// no we did not, let us add this range to ranges array
// means the new range does not extend an existing range, so add it.
ranges.push(createdRange);
}
Alright, now let us see how to calculate the indices for the given range.
If we start assigning indices (starting from 0) from left to right in the grid,
simple math says that the index of the box in the row r and in the column c will be:
index = r * (COL + 1) + c [COL is the total number of columns in the grid]
Here are the helper functions which will help to calculate the indices given the range:
function getIndex(row, col, COL) {
return row * (COL + 1) + col;
}
function getIndices(range) {
var start = range.start;
var end = range.end;
var startIndex = getIndex(start.row, start.col, COLS);
var endIndex = getIndex(end.row, end.col, COLS);
return { start: startIndex, end: endIndex };
}
Note that getIndices takes a range and outputs an object with start and end indices. We can now calculate indices for createdRange and current range. And based on the indices, we would know whether the ranges overlap or not.
The problem now boils down to this:
We have a line AB, and given a new line PQ, find out whether the new line PQ overlaps AB or not. ( where A,B,P,Q are points on the number line, A < B and P < Q ).
Take pen and paper and draw a few lines. You will come to know that there only two condition when the lines will not overlap:
Either Q < A or B < P
Mapping this observations to our range object, we can say that:
P => createdRange.startIndex
Q => createdRange.endIndex
A => currentRange.startIndex
B => currentRange.endIndex
This is how it would look in the code:
var createdRangeIndices = getIndices(createdRange);
var hasOverlap = false;
for (var i = 0; i < ranges.length; i++) {
var currentRangeIndices = getIndices(ranges[i]);
var overlap = (createdRangeIndices.end < currentRangeIndices.start)
|| (currentRangeIndices.end < createdRangeIndices.start);
if (!overlap) {
// overlaps, which means we extend the existing range.
// some logic to update the overlapped ranges
hasOverlap = true;
break;
}
}
if (!hasOverlap) {
// means the new range does not extend an existing range, so add it.
ranges.push(createdRange);
}
Note that we got rid of the function doesThisOverlap. A simple flag would do.
All that remains now is the logic to update the range if there is an overlap.
A part of which you had already figured out in your question. We take the minimum of the starting index and maximum of the ending index. Here is the code for that:
for (var i = 0; i < ranges.length; i++) {
var currentRangeIndices = getIndices(ranges[i]);
var overlap = (createdRangeIndices.end < currentRangeIndices.start)
|| (currentRangeIndices.end < createdRangeIndices.start);
if (!overlap) {
// overlaps, which means we extend the existing range.
// some logic to update the overlapped ranges
var start, end;
if (currentRangeIndices.start < createdRangeIndices.start) {
start = ranges[i].start;
} else {
start = createdRange.start;
}
if (currentRangeIndices.end > createdRangeIndices.end) {
end = ranges[i].end;
} else {
end = createdRange.end;
}
ranges[i] = { start: start, end: end };
hasOverlap = true;
break;
}
}
And done!
Here is the complete code that combines all the bits and pieces together:
var ROWS = 7;
var COLS = 3;
function getIndex(row, col, COL) {
return row * (COL + 1) + col;
}
function getIndices(range) {
var start = range.start;
var end = range.end;
var startIndex = getIndex(start.row, start.col, COLS);
var endIndex = getIndex(end.row, end.col, COLS);
return { start: startIndex, end: endIndex };
}
function addRange(ranges, createdRange) {
var createdRangeIndices = getIndices(createdRange);
var hasOverlap = false;
for (var i = 0; i < ranges.length; i++) {
var currentRangeIndices = getIndices(ranges[i]);
var overlap =
createdRangeIndices.end < currentRangeIndices.start ||
currentRangeIndices.end < createdRangeIndices.start;
if (!overlap) {
var start, end;
if (currentRangeIndices.start < createdRangeIndices.start) {
start = ranges[i].start;
} else {
start = createdRange.start;
}
if (currentRangeIndices.end > createdRangeIndices.end) {
end = ranges[i].end;
} else {
end = createdRange.end;
}
ranges[i] = { start: start, end: end };
hasOverlap = true;
break;
}
}
if (!hasOverlap) {
// means the new range does not extend an existing range, so add it.
ranges.push(createdRange);
}
}
var ranges = []; // stores the range objects that are created earlier.
var rangesToAdd = [
{
start: { row: 2, col: 1 },
end: { row: 6, col: 0 }
},
{
start: { row: 6, col: 2 },
end: { row: 7, col: 2 }
},
{
start: { row: 3, col: 1 },
end: { row: 6, col: 1 }
},
{
start: { row: 6, col: 1 },
end: { row: 6, col: 2 }
}
];
rangesToAdd.forEach(aRange => addRange(ranges, aRange));
console.log(ranges);
Using ES6's Set, given two arrays we can get the intersection like so:
let a = new Set([1,2,3])
let b = new Set([1,2,4])
let intersect = new Set([...a].filter(i => b.has(i)));
How can we get the intersection of n arrays?
Update:
I'm trying to wrap my head around this for the following use case. I have a two dimensional array with at least one element.
parts.forEach(part => {
intersection = new Set()
})
How would you get the intersection of each element (array) in parts?
Assuming you have some function function intersect(set1, set2) {...} that can intersect two sets, you can get the intersection of an array of sets using reduce:
function intersect(a, b) {
return new Set(a.filter(i => b.has(i)));
}
var sets = [new Set([1,2,3]), ...];
var intersection = sets.reduce(intersect);
You can create an intersect helper function using a combination of Array methods like .filter(), .map(), and .every().
This answer is inspired by the comment above from Xufox, who mentioned using Array#every in a filter predicate.
function intersect (first = [], ...rest) {
rest = rest.map(array => new Set(array))
return first.filter(e => rest.every(set => set.has(e)))
}
let parts = [
[1, 2, 3],
[1, 2, 4],
[1, 5, 2]
]
console.log(
intersect(...parts)
)
ES6 still has a while
This is the type of function that can easily cause long lags due to excessive amounts of processing. This is more true with the unquestioning and even preferential use of ES6 and array methods like reduce, filter etc, over simple old fashioned loops like while and for.
When calculating the intersection of many sets the amount of work done per iteration should go down if an item has been found not to be part of the intersection. Because forEach can not break you are forced to still iterate all elements. Adding some code to avoid doing the search if the current item has been found to not belong can improve the performance, but it is a real kludge.
The is also the tendency to just create whole new datasets just to remove a single item from an array, set, or map. This is a very bad habit that i see more and more of as people adopt the ES5 way.
Get the intersection of n sets.
So to the problem at hand. Find the intersection of many sets.
Solution B
A typical ES6 solution
function intersectB(firstSet, ...sets) {
// function to intercept two sets
var intersect = (a,b) => {
return new Set([...a].filter(item => b.has(item)))
};
// iterate all sets comparing the first set to each.
sets.forEach(sItem => firstSet = intersect(firstSet, sItem));
// return the result.
return firstSet;
}
var sets = [new Set([1,2,3,4]), new Set([1,2,4,6,8]), new Set([1,3,4,6,8])];
var inter = intersectB(...sets);
console.log([...inter]);
Works well and for the simple test case execution time is under a millisecond. But in my book it is a memory hogging knot of inefficiency, creating arrays, and sets at every line almost and iterating whole sets when the outcome is already known.
Let's give it some more work. 100 sets, with up to 10000 items over 10 tests each with differing amount of matching items. Most of the intercepts will return empty sets.
Warning will cause page to hang up to one whole second... :(
// Create a set of numbers from 0 and < count
// With a odds for any number occurring to be odds
// return as a new set;
function createLargeSet(count,odds){
var numbers = new Set();
while(count-- > 0){
if(Math.random() < odds){
numbers.add(count);
}
}
return numbers;
}
// create a array of large sets
function bigArrayOfSets(setCount,setMaxSize,odds){
var bigSets = [];
for(var i = 0; i < setCount; i ++){
bigSets.push(createLargeSet(setMaxSize,odds));
}
return bigSets;
}
function intersectB(firstSet, ...sets) {
var intersect = (a,b) => {
return new Set([...a].filter(item => b.has(item)))
};
sets.forEach(sItem => firstSet = intersect(firstSet, sItem));
return firstSet;
}
var testSets = [];
for(var i = 0.1; i <= 1; i += 0.1){
testSets.push(bigArrayOfSets(100,10000,i));
}
var now = performance.now();
testSets.forEach(testDat => intersectB(...testDat));
var time = performance.now() - now;
console.log("Execution time : " + time);
Solution A
A better way, not as fancy but much more efficient.
function intersectA(firstSet,...sets) {
var count = sets.length;
var result = new Set(firstSet); // Only create one copy of the set
firstSet.forEach(item => {
var i = count;
var allHave = true;
while(i--){
allHave = sets[i].has(item)
if(!allHave) { break } // loop only until item fails test
}
if(!allHave){
result.delete(item); // remove item from set rather than
// create a whole new set
}
})
return result;
}
Compare
So now let's compare both, if you are feeling lucky try and guess the performance difference, it's a good way to gage your understanding of Javascript execution.
// Create a set of numbers from 0 and < count
// With a odds for any number occurring to be odds
// return as a new set;
function createLargeSet(count,odds){
var numbers = new Set();
while(count-- > 0){
if(Math.random() < odds){
numbers.add(count);
}
}
return numbers;
}
// create a array of large sets
function bigArrayOfSets(setCount,setMaxSize,odds){
var bigSets = [];
for(var i = 0; i < setCount; i ++){
bigSets.push(createLargeSet(setMaxSize,odds));
}
return bigSets;
}
function intersectA(firstSet,...sets) {
var count = sets.length;
var result = new Set(firstSet); // Only create one copy of the set
firstSet.forEach(item => {
var i = count;
var allHave = true;
while(i--){
allHave = sets[i].has(item)
if(!allHave) { break } // loop only until item fails test
}
if(!allHave){
result.delete(item); // remove item from set rather than
// create a whole new set
}
})
return result;
}
function intersectB(firstSet, ...sets) {
var intersect = (a,b) => {
return new Set([...a].filter(item => b.has(item)))
};
sets.forEach(sItem => firstSet = intersect(firstSet, sItem));
return firstSet;
}
var testSets = [];
for(var i = 0.1; i <= 1; i += 0.1){
testSets.push(bigArrayOfSets(100,10000,i));
}
var now = performance.now();
testSets.forEach(testDat => intersectB(...testDat));
var time = performance.now() - now;
console.log("Execution time 'intersectB' : " + time);
var now = performance.now();
testSets.forEach(testDat => intersectA(...testDat));
var time = performance.now() - now;
console.log("Execution time 'intersectA' : " + time);
As you can see using a simple while loop may not be a cool as using filter but the performance benefit is huge, and something to keep in mind next time you are writing that perfect 3 line ES6 array manipulation function. Dont forget about for and while.
The most efficient algorithm for intersecting n arrays is the one implemented in fast_array_intersect. It runs in O(n), where n is the total number of elements in all the arrays.
The base principle is simple: iterate over all the arrays, storing the number of times you see each element in a map. Then filter the smallest array, to return only the elements that have been seen in all the arrays. (source code).
You can use the library with a simple :
import intersect from 'fast_array_intersect'
intersect([[1,2,3], [1,2,6]]) // --> [1,2]
OK i guess the most efficient way of performing the Array intersection is by utilizing a Map or Hash object. Here I test 1000 arrays each with ~1000 random integer items among 1..175 for an intersection. The result is obtained in less than 100msec.
function setIntersection(a){
var m = new Map(),
r = new Set(),
l = a.length;
a.forEach(sa => new Set(sa).forEach(n => m.has(n) ? m.set(n,m.get(n)+1)
: m.set(n,1)));
m.forEach((v,k) => v === l && r.add(k));
return r;
}
var testSets = Array(1000).fill().map(_ => Array(1000).fill().map(_ => ~~(Math.random()*175+1)));
console.time("int");
result = setIntersection(testSets);
console.timeEnd("int");
console.log(JSON.stringify([...result]));
I'm trying to fill an array with missing intermediate data
My data input is like this
var data = [[5.23,7],[5.28,7],[5.32,8],[5.35,8]];
I wanna fill the array with missing value but I need to respect this rule:
The 1st value on 2d array must be the next sequence number, so 5.23
... 5.24 ... 5.25 ...
The 2nd value on 2d array must be the same element from the i+1
value
So the results in this case would be
var data = [[5.23,7],[5.24,7],[5.25,7],[5.26,7],[5.27,7],[5.28,7],[5.29,8],[5.30,8],[5.31,8],[5.32,8],[5.33,8],[5.34,8],[5.35,8]];
This little piece of code works, but I don't know
how to put in loop
and how to write a while loop that pass every time the new length of the array
var data = [[5.23,7],[5.28,7],[5.32,8],[5.35,8]];
if (data[1][0]-data[0][0] > 0.01) {
data.push([data[0][0]+0.01,data[1][1]]);
data.sort(function (a, b) { return a[0] - b[0]; });
} else {
check the next element
}
console.log(data);
Any idea?
Here's another idea... I thought it might feel more natural to loop through the sequence numbers directly.
Your final array will range (in this example) from 5.23 to 5.35 incrementing by 0.01. This approach uses a for loop starting going from 5.23 to 5.35 incrementing by 0.01.
key points
Rounding: Work in x100 then divide back down to avoid floating point rounding issues. I round to the neared hundredth using toFixed(2) and then converting back to a number (with leading + operator).
Indexing: Recognizing 5.23 is the zero index with each index incrementing 1/100, you can calculate index from numerical values, ex. 100*(5.31-5.23) equals 8 (so 5.31 belongs in output[8]).
2nd values: given a numerical value (ex. 5.31), just find the first element in the data array with a higher 1st value and use its 2nd value - this is a corollary of your requirement. Because 5.31 <= 5.28 is false, don't use 7 (from [5.28,7]). Because 5.31 <= 5.32 is true, use 8 (from [5.32,8]).
EDIT
I improved the performance a bit - (1) initialize output instead of modifying array size, (2) work in multiples of 100 instead of continuously rounding from floating point to hundredths.
I ran 5000 iterations on a longer example and, on average, these modifications make this approach 3x faster than Redu's (where the original was 2x slower).
var data = [[5.23,7],[5.28,7],[5.32,8],[5.35,8]];
var output = Array((data[data.length-1][0]-data[0][0]).toFixed(2)*100+1)
function getIndex(value){
return (value-data[0][0]*100)
}
for( var i = 100*data[0][0]; i <= 100*data[data.length-1][0]; i++ ){
output[getIndex(i)] = [i/100, data.find( d => i <= 100*d[0] )[1]]
}
//console.log(output)
// Performance comparison
function option1(data){
let t = performance.now()
var output = Array((data[data.length-1][0]-data[0][0]).toFixed(2)*100+1)
function getIndex(value){
return (value-data[0][0]*100)
}
for( var i = 100*data[0][0]; i <= 100*data[data.length-1][0]; i++ ){
output[getIndex(i)] = [i/100, data.find( d => i <= 100*d[0] )[1]]
}
return performance.now()-t
}
function option2(data){
let t = performance.now()
newData = data.reduce((p,c,i,a) => i ? p.concat(Array(Math.round(c[0]*100 - a[i-1][0]*100)).fill()
.map((_,j) => [Number((a[i-1][0]+(j+1)/100).toFixed(2)),c[1]]))
: [c],[]);
return performance.now()-t
}
var testdata = [[1.13,4],[2.05,6],[5.23,7],[5.28,7],[5.32,8],[5.35,8],[8.91,9],[10.31,9]];
var nTrials = 10000;
for(var trial=0, t1=0; trial<=nTrials; trial++) t1 += option1(testdata)
for(var trial=0, t2=0; trial<=nTrials; trial++) t2 += option2(testdata)
console.log(t1/nTrials) // ~0.4 ms
console.log(t2/nTrials) // ~0.55 ms
Array.prototype.reduce() is sometimes handy to extend the array. May be you can do as follows;
var data = [[5.23,7],[5.28,7],[5.32,8],[5.35,8]],
newData = data.reduce((p,c,i,a) => i ? p.concat(Array(Math.round(c[0]*100 - a[i-1][0]*100)).fill()
.map((_,j) => [Number((a[i-1][0]+(j+1)/100).toFixed(2)),c[1]]))
: [c],[]);
console.log(newData);
var data = [[1.01,3],[1.04,4],[1.09,5],[1.10,6],[1.15,7]],
newData = data.reduce((p,c,i,a) => i ? p.concat(Array(Math.round(c[0]*100 - a[i-1][0]*100)).fill()
.map((_,j) => [Number((a[i-1][0]+(j+1)/100).toFixed(2)),c[1]]))
: [c],[]);
console.log(newData);
I propose this solution :
var data = [[5.23,7],[5.28,7],[5.32,8],[5.35,8]];
var res = [];
data.forEach((item, index, arr) => {
res.push(item);
var temp = item[0];
while (arr[index+1] && arr[index+1][0]-temp > 0.01){
temp += 0.01;
res.push([temp, arr[index+1][1]]);
}
});
console.log(res);