Combining different number ranges in O(n) - javascript

I'm currently tracking user play times of videos, and I'm trying to determine the % of a video a user watches. I've generalised the problem to given a series of number ranges that potentially overlap, how to combine them into a series of non-overlapping number ranges (i.e. converting "0-10, 5-15, 30-45, 20-25" into "0-15, 20-25, 30-45".
I have a relatively long-winded solution based on the premise that if the number ranges are sorted, then it is relatively trivial to combine two adjacent number ranges (either combine them if they overlap or they remain separate). Thus, we sort the number ranges first then iterate through the ranges and combining them.
Since sorting is worst case O(nlgn), this means my solution should be O(nlgn), and I was wondering if anyone knows of a O(n) solution to the problem?
http://jsfiddle.net/457PH/2
var testcase = [
[0, 30], [40, 50], [5, 15], [70, 95], [45, 75], [0, 10],
[110, 115], [115, 120], [140, 175], [125, 160]
];
//sorts the array in ascending order (based on first element)
//if the first elements are the same, order based on second element (prioritising elements that are bigger)
testcase.sort(function(a, b) {
if (a[0] !== b[0]) return a[0] - b[0];
return b[1] - a[1]
})
function evaluate(a, b) {
var result = [];
//tests that the array is sorted properly
if ((a[0] > b[0]) || ((a[0] === b[0] ) && (a[1] < b[1]))) throw new Error('Array not sorted properly');
//if a and b do not overlap, then push both in the result
if(b[0] > a[1]) {
result.push(a, b);
}
//if a and b overlap
else {
var newElement = [a[0], Math.max(a[1], b[1])];
result.push(newElement);
}
return result;
}
console.log(testcase)
var combinedArr = [testcase[0]];
for (var i = 1; i < testcase.length; i++) {
var popped = combinedArr.pop();
combinedArr = combinedArr.concat(evaluate(popped, testcase[i]));
}
console.log(combinedArr);

An alternative solution that is O(W+n*|S|) where |S| is the average size of each interval and W is the maximal value in the list will be using a bitset, and iterate each element and set all relevant bits.
In another iteration - print all intervals in the bitset (which is sorted).
So, the algorithm for this approach is basically:
Create a bitset of size W where a bit is set only if it is in some interval.
Iterate the bitset and print the intervals - this is fairly easy now.
While this could be much worse in terms of asymptotic complexity if W or |S| are large - note that the constants here are fairly small, since bit operations are fairly easy to implement.
Choosing which is actually better should be done using empirical benchmark and achieving statistical significance.
Pseudo-code:
//create the bitset:
b <- new bitset
for each interval [x1,x2]:
for each element i from x1 to x2:
b[i] = 1
//print intervals:
first <- -1
for each element i from 0 to W+1: //regard b[W] as 0
if b[i] == 0 and first != -1:
print (first,i-1)
first = -1
else if b[i] == 1 and first == -1:
first = i

If you just restrict to the case where each of the first half of the intervals are overlapping a distinct member of the second half of the intervals, then the number of possibilities for overlapping combinations of intervals is at least Omega((n/2)!) (i.e. n/2 factorial). Thus, in any comparison based algorithm, you will need at least log((n/2)!) = Omega(n log n) comparisons to distinguish between all these cases. Thus, in any comparison based algorithm, you will need Omega(n log n) time in the worst case.

Here's an attempt at the bitset implementation in JavaScript:
function percentWatched(ranges,totalMinutes){
var numPartitions = Math.ceil(totalMinutes / 31),
bitset = new Array(numPartitions)
for (var i in ranges){
var top = ranges[i][1]
, bottom = ranges[i][0]
, m, shift, k
while (bottom < top){
m = Math.floor(bottom / 31)
shift = bottom % 31
k = shift + top - bottom <= 31 ? top - bottom : 31 - shift
bitset[m] |= (1 << k) - 1 << shift
bottom += k
}
}
var minutesWatched = 0
for (var i in bitset)
minutesWatched += numSetBits(bitset[i])
return {percent: 100 * minutesWatched / totalMinutes
, ranges: bitset}
}
function numSetBits(i) //copied from http://stackoverflow.com/questions/109023
{
i = i - ((i >> 1) & 0x55555555);
i = (i & 0x33333333) + ((i >> 2) & 0x33333333);
return (((i + (i >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24;
}
Console output:
> var a = percentWatched([[0,10], [5,15], [30,45], [20,25]],100)
> for (var i in a.ranges) console.log(a.ranges[i].toString(2))
"1000001111100000111111111111111"
"11111111111111"
> a.percent
35

Related

Find the maximum product that can be formed by taking any one element from each sub-array

I am trying to write an efficient algorithm in JavaScript to solve this task. Please see the next examples of input data and correct results:
Array: [ [-3,-4], [1,2,-3] ] Result: (-4)*(-3) = 12
Array: [ [1,-1], [2,3], [10,-100,20] ] Result: (-1)*3*(-100) = 300
Array: [ [-3,-15], [-3,-7], [-5,1,-2,-7] ] Result: (-15)*(-7)*1 = 105
It can be any number of sub-arrays and any number of elements in each sub-array. What I already found is that I probably should leave only min and max values in the each sub-array, I did it using .map(a => [Math.min(...a), Math.max(...a)]) and sort them using .sort((a, b) => a[0] - b[0]).
And now I am stuck. Probably there is a way to calculate all possible products but I am sure that it's not an effective way to solve this task.
Please help!
The problem you post can be solved with a simple algorithm. We just need to keep tracking the maximum/minimum when iterating over each sub-array. We can keep finding the next maximum/minimum by multiplying the current maximum/minimum with the max/min value in each sub-array. We pick the maximum when the iterating is over. Its time complexity is O(n) where n is total number of elements in an array (i.e. sum of number of elements in each sub-array).
Here's the complete code. find_maximum_product function keeps tracking the minimum/maximum and returns the maximum eventually, and it also keeps tracking the multipliers and return it:
/**
* arr: array of any number of sub-arrays and
* any number of elements in each sub-array.
* e.g. [[1, -1], [2, 3], [10, -100, 20]]
*/
function find_maximum_product(arr) {
let max = 1;
let min = 1;
let max_multipliers = [];
let min_multipliers = [];
for (let i = 0; i < arr.length; i++) {
const a = Math.max(...arr[i]);
const b = Math.min(...arr[i]);
const candidates = [max * a, max * b, min * a, min * b];
max = Math.max(...candidates);
min = Math.min(...candidates);
let new_max_multipliers;
let new_min_multipliers;
switch (max) {
case candidates[0]:
new_max_multipliers = max_multipliers.concat(a);
break;
case candidates[1]:
new_max_multipliers = max_multipliers.concat(b);
break;
case candidates[2]:
new_max_multipliers = min_multipliers.concat(a);
break;
case candidates[3]:
new_max_multipliers = min_multipliers.concat(b);
break;
}
switch (min) {
case candidates[0]:
new_min_multipliers = max_multipliers.concat(a);
break;
case candidates[1]:
new_min_multipliers = max_multipliers.concat(b);
break;
case candidates[2]:
new_min_multipliers = min_multipliers.concat(a);
break;
case candidates[3]:
new_min_multipliers = min_multipliers.concat(b);
break;
}
max_multipliers = new_max_multipliers;
min_multipliers = new_min_multipliers;
}
if (max >= min) {
return [max, max_multipliers];
}
return [min, min_multipliers];
}
const arrays = [
[
[-3, -4],
[1, 2, -3],
],
[
[1, -1],
[2, 3],
[10, -100, 20],
],
[
[-3, -15],
[-3, -7],
[-5, 1, -2, -7],
],
[
[14, 2],
[0, -16],
[-12, -16],
],
[
[-20, -4, -19, -18],
[0, -15, -10],
[-13, 4],
],
[
[-2, -15, -12, -8, -16],
[-4, -15, -7],
[-10, -5],
],
];
for (let i = 0; i < arrays.length; i++) {
const [max, max_multipliers] = find_maximum_product(arrays[i]);
console.log('Array:', JSON.stringify(arrays[i]));
console.log('Result:', `${max_multipliers.join(' * ')} = ${max}`);
console.log('');
}
UPDATE
Simpler version for just getting the maximum, not getting the multipliers:
/**
* arr: array of any number of sub-arrays and
* any number of elements in each sub-array.
* e.g. [[1, -1], [2, 3], [10, -100, 20]]
*/
function get_maximum_product(arr) {
return arr
.map((a) => [Math.min(...a), Math.max(...a)])
.reduce(
(acc, current) => {
const candidates = [
acc[0] * current[0],
acc[0] * current[1],
acc[1] * current[0],
acc[1] * current[1],
];
return [Math.min(...candidates), Math.max(...candidates)];
},
[1, 1]
)[1];
}
Here is a top-down recurrence that could be adapted to bottom-up (a loop) and utilises O(n) search space.
Until I can complete it, the reader is encouraged to add a third return value in the tuple, largest_non_positive for that special case.
// Returns [highest positive, lowest negative]
// Does not address highest non-positive
function f(A, i){
const high = Math.max(...A[i]);
const low = Math.min(...A[i]);
if (i == 0){
if (low < 0 && high >= 0)
return [high, low];
if (low <= 0 && high <= 0)
return [-Infinity, low];
if (low >= 0 && high >= 0)
return [high, Infinity];
}
const [pos, neg] = f(A, i - 1);
function maybeZero(prod){
return isNaN(prod) ? 0 : prod;
}
let hp = maybeZero(high * pos);
let hn = maybeZero(high * neg);
let ln = maybeZero(low * neg);
let lp = maybeZero(low * pos);
if (low < 0 && high >= 0)
return [Math.max(hp, ln), Math.min(hn, lp)];
if (low <= 0 && high <= 0)
return [ln, lp];
if (low >= 0 && high >= 0)
return [hp, hn];
}
var As = [
[[-3,-4], [1,2,-3]],
[[1,-1], [2,3], [10,-100,20]],
[[-3,-15], [-3,-7], [-5,1,-2,-7]],
[[-11,-6], [-20,-20], [18,-4], [-20,1]],
[[-1000,1], [-1,1], [-1,1], [-1,1]],
[[14,2], [0,-16], [-12,-16]],
[[-20, -4, -19, -18], [0, -15, -10],[-13, 4]]
];
for (let A of As){
console.log(JSON.stringify(A));
console.log(f(A, A.length - 1)[0]);
console.log('');
}
Sort values in arrays by their absolute value in descending order
Check the product of first array elements if its positive its the answer
Otherwise lets call product p and we know p < 0, so if we change some positive element to some negative element or vice verse we will improve answer
we can simply check all possible elements to change, for each array a element x we can check if p / a[0] * x is better than current result if it is we update our answer
*Special case: all elements in arrays are negative and we have odd number of arrays, then we simply sort in increasing order
Complexity: O(n log n) where n is total amount of elements across all arrays
Take the product of the highest number of all the arrays that have at least one positive number.
If there's an odd number of remaining arrays (with only negatives), find the one with the highest (closest to zero) negative number, and set its absolute aside.
Take the arrays that remain after step 2, take the product of their lowest number (furthest from zero), and multiply it by the products from step 1 and (if any) step 2.
(also, avoid 0 if it would be the chosen number)
The first thing to notice is that there are only two specific cases where it's not possible to get a positive product. So I think an algorithm should first check if those specific cases are happening, then call a different subalgorithm for each of the three possible situations:
it is possible to get a positive product, so we want to find the highest positive product;
one of the arrays is full of zeroes, so all products are zero;
it is impossible to get a positive product, because no array has both positive and negative numbers, and there is an odd number of arrays with only negative numbers, so we want to find the closest to zero negative product.
The second and third cases lead to trivial algorithms.
Let's consider the first case.
For every array, the only numbers that can be useful in the highest product are the highest positive number, and the lowest negative number. If an array only has positive numbers or only has negative numbers, then there is only one useful number in that array, which can be chosen immediately.
For all the remaining arrays, you have to choose whether to use the positive or the negative number. Ideally, you want to use the one with the highest absolute value; but if you do that for every array, then the result might be negative.
This leads to a linear algorithm:
For all of the remaining arrays, initially select the number with the highest absolute value
If the resulting product is positive, you're done.
If the resulting product is negative, then a compromise has to be done in one of the arrays. For every array, compute the "cost" of this compromise (equal to the difference between the absolute values of the two interesting numbers in that array, multiplied by the product of all the other selected numbers).
Finally, choose the array whose cost is the lowest, and change the chosen number in that array.
Here is an example execution of the algorithm on the list of arrays [[18,19,20,-23], [12,-10,9,8],[-10,-3],[5,3],[-10,-5]].
Here we notice that it is possible to find a positive solution, because at least one of the arrays contains both negative and positive numbers.
For the last three arrays we have no choice between positive and negative: so we can already choose -10, 5 and -10 as the three numbers for these three arrays. For the first array, we'll have to choose between 20 and -23; and for the second array we'll have to choose between 12 and -10.
So the final product will be: (20 or -23) * (12 and -10) * (-10) * 5 * (-10).
Ideally, we would prefer 23 to 20, and 12 to 10. That would result in:
(-23) * 12 * (-10) * 5 * (-10)
Unfortunately, this is negative. So the question is: do we replace -23 with 20, or 12 with -10?
The cost of replacing -23 with 20 would be (23-20) * 11 * (10*5*10) = 33 * (10*5*10).
The cost of replacing 12 with -10 would be (12-10) * 21 * (10*5*10) = 42 * (10*5*10).
Finally, we choose to replace -23 with 20, because that is the less costly compromise.
The final product is 20 * 12 * (-10) * 5 * (-10).

LeetCode 120: Triangle - Minimum path sum

I'm doing this problem on leetcode:
Given a triangle, find the minimum path sum from top to bottom. Each step you may move to adjacent numbers on the row below.
My logic is to find the minimum number in each array and add that to the sum.
This is my code in javascript:
var minimumTotal = function(triangle) {
let sum = 0;
for (let i = 0; i < triangle.length; i++) {
sum += Math.min.apply(null, triangle[i])
}
return sum;
};
But it doesn't work for this test case: [[-1],[2,3],[1,-1,-3]].
The expected output is -1. I'm confused how it should equal -1, because -1 + 2 = 1 and none of the numbers in third array equal -1 when summed with 1.
I looked at the discussion answers and they all used some sort of dynamic programming solution.
What am I doing wrong?
There is a path where the output should be -1.
[
[ -1 ],
[ 2, 3 ],
[ 1, -1, -3 ],
]
-1 + 3 - 3 = -1
The problem is that you only have 2 possible branches at each fork, whereas it appears you're taking the lowest number from the entire row.
Under the rules of the challenge, you shouldn't be able to go from 2 in the second row to -3 in the third row, which would be the most efficient path under your approach.
What you are doing does not seem to follow the desired data structure of the problem. You are generating an int, while the desired output should be an array.
The correct answers are already posted in the discussion board:
This solution is an accepted one (just tested it):
JavaScript
var minimumTotal = function(triangle) {
for (let row = triangle.length - 2; row > -1; row--)
for (let col = 0; col < triangle[row].length; col++)
triangle[row][col] += Math.min(triangle[-~row][col], triangle[-~row][-~col])
return triangle[0][0]
}
-~row is the same as row + 1 (bitwise version).
Reference
Explains it here
If you might be interested in Python and Java, these are "accepted" solutions:
Python
class Solution:
def minimumTotal(self, triangle):
if not triangle:
return
dp = triangle[-1]
for row in range(len(triangle) - 2, -1, -1):
for col in range(len(triangle[row])):
dp[col] = min(dp[col], dp[-~col]) + triangle[row][col]
return dp[0]
Java
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
int[] dp = new int[-~triangle.size()];
for (int row = triangle.size() - 1; row > -1; row--)
for (int col = 0; col < triangle.get(row).size(); col++)
dp[col] = Math.min(dp[col], dp[-~col]) + triangle.get(row).get(col);
return dp[0];
}
}
For each step, you may move to an adjacent number of the row below. More formally, if you are on index i on the current row, you may move to either index i or index i + 1 on the next row.
The above statement is part of the question and it helps to create a graph like this.
Dynamic programming is used where we have problems, which can be divided into similar sub-problems, so that their results can be re-used. We start from the bottom and determine which minimum value we take and then we are going to be using that minimum value above. That is why we use dynamic programming here. Now each row gets 1 size bigger, so you can imagine under the last row, we have 4 0's.
that is why in dynamic programming we always create an array whose size is always 1 greater than the original array. We fill the array with default values. I will be explaining in python but later on I will challenge myself to write in javascript. first, initialize the dp array
dp=[0]*(len(triangle)+1) #[[0],[0],[0],[0]]
Now, we are starting from the level=[1,-1,3]. For this level, since the bottom is full of 0's, our dp array will be
dp=[1,-1,-3,0]
now we are moving above level, level=[2,3], but we are using the data from the bottom. (That is why we are using dynamic programming). From 2, its index is 0, so I ask myself what is the minimum between 0'th and 1'st value of the dp array. Whichever is minimum we add it to 2? Obviously, -1 is minimum, 2+(-1)=1 and dp gets updated.
dp=[1, -1, -3, 0]
We do the same for 3. its index is 1, and we ask what is the min(-1,3), because 3 is pointing to -1 and 3 and then we add -1 to 3 which is 2. new dp
dp=[1,0,-3,0]
Now we are at the root level. -1 and its index is 0. We ask what is min value of index 0'th and index 1'st of the dp array. min(1,0)=0 and we add it to -1. dp gets updated
dp=[-1,0,3,0]
and finally, we return dp[0]=0
Here is the code in python:
from typing import List
class Solution:
def min_total(self,triangle:List[List[int]])->int:
# dp[] is the bottom row
dp=[0]*(len(triangle)+1)
// triangle[::-1] says start from the last element of triangle
for row in triangle[::-1]:
for i,n in enumerate(row):
dp[i]=n+min(dp[i],dp[i+1])
return dp[0]
Here is javascript code:
function minPath(triangle) {
let dp = new Array(triangle.length + 1).fill(0);
for (let row = triangle.length - 1; row >= 0; row--) {
for (let i = 0; i <= triangle[row].length - 1; i++) {
dp[i] = triangle[row][i] + Math.min(dp[i], dp[i + 1]);
}
}
console.log(dp);
return dp[0];
}
const triangle = [[-1], [2, 3], [1, -1, -3]];
console.log(minPath(triangle));
As this was brought back up, it's worth pointing out that the dynamic programming technique discussed in answers and comments can be done very simply with a reduceRight:
const minSum = (triangle) =>
triangle .reduceRight ((ms, ns) => ns .map (
(n, i) => n + (i > ms.length ? ms[i] : Math .min (ms [i], ms [i + 1]))
)) [0]
console .log (minSum ([[-1], [2, 3], [1, -1, -3]])) //=> -1
console .log (minSum ([[2], [3, 4], [6, 5, 7], [4, 1, 8, 3]])) //=> 11
console .log (minSum ([[-10]])) //=> -10
At each step we calculate the minimum paths through all the elements in a row. ms is the minimum paths through the row below (and on the initial pass will just be the bottom row) and for each n in our row ns, if we're at the rightmost element, we just copy the value from the corresponding row below, otherwise we take the minimum of the element right below and the one to its right.
Since this is called a triangle, I think we can assume that the first row has only one element. But if that's not the case, we could simply take the minimum of the results rather than taking the first element in the final value. That is, we could replace triangle .reduceRight ( ... ) [0] with Math .min (... triangle .reduceRight ( ... )).
/*
[120] Triangle
https://leetcode.com/problems/triangle/description/
*/
import java.util.List;
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
if (triangle.size() == 0)
return 0;
int rows = triangle.size();
int cols = triangle.get(rows - 1).size();
int[] tmp = new int[cols];
int index = 0;
for (int var : triangle.get(rows - 1)) {
tmp[index++] = var;
}
for (int i = rows - 2; i >= 0; i--) {
for (int j = 0; j <= triangle.get(i).size() - 1; j++) {
tmp[j] = triangle.get(i).get(j) + Math.min(tmp[j], tmp[j + 1]);
}
}
return tmp[0];
}
}

Trying to optimize my code to either remove nested loop or make it more efficient

A friend of mine takes a sequence of numbers from 1 to n (where n > 0)
Within that sequence, he chooses two numbers, a and b
He says that the product of a and b should be equal to the sum of all numbers in the sequence, excluding a and b
Given a number n, could you tell me the numbers he excluded from the sequence?
Have found the solution to this Kata from Code Wars but it times out (After 12 seconds) in the editor when I run it; any ideas as too how I should further optimize the nested for loop and or remove it?
function removeNb(n) {
var nArray = [];
var sum = 0;
var answersArray = [];
for (let i = 1; i <= n; i++) {
nArray.push(n - (n - i));
sum += i;
}
var length = nArray.length;
for (let i = Math.round(n / 2); i < length; i++) {
for (let y = Math.round(n / 2); y < length; y++) {
if (i != y) {
if (i * y === sum - i - y) {
answersArray.push([i, y]);
break;
}
}
}
}
return answersArray;
}
console.log(removeNb(102));
.as-console-wrapper { max-height: 100% !important; top: 0; }
I think there is no reason for calculating the sum after you fill the array, you can do that while filling it.
function removeNb(n) {
let nArray = [];
let sum = 0;
for(let i = 1; i <= n; i++) {
nArray.push(i);
sum += i;
}
}
And since there could be only two numbers a and b as the inputs for the formula a * b = sum - a - b, there could be only one possible value for each of them. So, there's no need to continue the loop when you find them.
if(i*y === sum - i - y) {
answersArray.push([i,y]);
break;
}
I recommend looking at the problem in another way.
You are trying to find two numbers a and b using this formula a * b = sum - a - b.
Why not reduce the formula like this:
a * b + a = sum - b
a ( b + 1 ) = sum - b
a = (sum - b) / ( b + 1 )
Then you only need one for loop that produces the value of b, check if (sum - b) is divisible by ( b + 1 ) and if the division produces a number that is less than n.
for(let i = 1; i <= n; i++) {
let eq1 = sum - i;
let eq2 = i + 1;
if (eq1 % eq2 === 0) {
let a = eq1 / eq2;
if (a < n && a != i) {
return [[a, b], [b, a]];
}
}
}
You can solve this in linear time with two pointers method (page 77 in the book).
In order to gain intuition towards a solution, let's start thinking about this part of your code:
for(let i = Math.round(n/2); i < length; i++) {
for(let y = Math.round(n/2); y < length; y++) {
...
You already figured out this is the part of your code that is slow. You are trying every combination of i and y, but what if you didn't have to try every single combination?
Let's take a small example to illustrate why you don't have to try every combination.
Suppose n == 10 so we have 1 2 3 4 5 6 7 8 9 10 where sum = 55.
Suppose the first combination we tried was 1*10.
Does it make sense to try 1*9 next? Of course not, since we know that 1*10 < 55-10-1 we know we have to increase our product, not decrease it.
So let's try 2*10. Well, 20 < 55-10-2 so we still have to increase.
3*10==30 < 55-3-10==42
4*10==40 < 55-4-10==41
But then 5*10==50 > 55-5-10==40. Now we know we have to decrease our product. We could either decrease 5 or we could decrease 10, but we already know that there is no solution if we decrease 5 (since we tried that in the previous step). So the only choice is to decrease 10.
5*9==45 > 55-5-9==41. Same thing again: we have to decrease 9.
5*8==40 < 55-5-8==42. And now we have to increase again...
You can think about the above example as having 2 pointers which are initialized to the beginning and end of the sequence. At every step we either
move the left pointer towards right
or move the right pointer towards left
In the beginning the difference between pointers is n-1. At every step the difference between pointers decreases by one. We can stop when the pointers cross each other (and say that no solution can be obtained if one was not found so far). So clearly we can not do more than n computations before arriving at a solution. This is what it means to say that the solution is linear with respect to n; no matter how large n grows, we never do more than n computations. Contrast this to your original solution, where we actually end up doing n^2 computations as n grows large.
Hassan is correct, here is a full solution:
function removeNb (n) {
var a = 1;
var d = 1;
// Calculate the sum of the numbers 1-n without anything removed
var S = 0.5 * n * (2*a + (d *(n-1)));
// For each possible value of b, calculate a if it exists.
var results = [];
for (let numB = a; numB <= n; numB++) {
let eq1 = S - numB;
let eq2 = numB + 1;
if (eq1 % eq2 === 0) {
let numA = eq1 / eq2;
if (numA < n && numA != numB) {
results.push([numA, numB]);
results.push([numB, numA]);
}
}
}
return results;
}
In case it's of interest, CY Aries pointed this out:
ab + a + b = n(n + 1)/2
add 1 to both sides
ab + a + b + 1 = (n^2 + n + 2) / 2
(a + 1)(b + 1) = (n^2 + n + 2) / 2
so we're looking for factors of (n^2 + n + 2) / 2 and have some indication about the least size of the factor. This doesn't necessarily imply a great improvement in complexity for the actual search but still it's kind of cool.
This is part comment, part answer.
In engineering terms, the original function posted is using "brute force" to solve the problem, iterating every (or more than needed) possible combinations. The number of iterations is n is large - if you did all possible it would be
n * (n-1) = bazillio n
Less is More
So lets look at things that can be optimized, first some minor things, I'm a little confused about the first for loop and nArray:
// OP's code
for(let i = 1; i <= n; i++) {
nArray.push(n - (n - i));
sum += i;
}
??? You don't really use nArray for anything? Length is just n .. am I so sleep deprived I'm missing something? And while you can sum a consecutive sequence of integers 1-n by using a for loop, there is a direct and easy way that avoids a loop:
sum = ( n + 1 ) * n * 0.5 ;
THE LOOPS
// OP's loops, not optimized
for(let i = Math.round(n/2); i < length; i++) {
for(let y = Math.round(n/2); y < length; y++) {
if(i != y) {
if(i*y === sum - i - y) {
Optimization Considerations:
I see you're on the right track in a way, cutting the starting i, y values in half since the factors . But you're iterating both of them in the same direction : UP. And also, the lower numbers look like they can go a little below half of n (perhaps not because the sequence start at 1, I haven't confirmed that, but it seems the case).
Plus we want to avoid division every time we start an instantiation of the loop (i.e set the variable once, and also we're going to change it). And finally, with the IF statements, i and y will never be equal to each other the way we're going to create the loops, so that's a conditional that can vanish.
But the more important thing is the direction of transversing the loops. The smaller factor low is probably going to be close to the lowest loop value (about half of n) and the larger factor hi is probably going to be near the value of n. If we has some solid math theory that said something like "hi will never be less than 0.75n" then we could make a couple mods to take advantage of that knowledge.
The way the loops are show below, they break and iterate before the hi and low loops meet.
Moreover, it doesn't matter which loop picks the lower or higher number, so we can use this to shorten the inner loop as number pairs are tested, making the loop smaller each time. We don't want to waste time checking the same pair of numbers more than once! The lower factor's loop will start a little below half of n and go up, and the higher factor's loop will start at n and go down.
// Code Fragment, more optimized:
let nHi = n;
let low = Math.trunc( n * 0.49 );
let sum = ( n + 1 ) * n * 0.5 ;
// While Loop for the outside (incrementing) loop
while( low < nHi ) {
// FOR loop for the inside decrementing loop
for(let hi = nHi; hi > low; hi--) {
// If we're higher than the sum, we exit, decrement.
if( hi * low + hi + low > sum ) {
continue;
}
// If we're equal, then we're DONE and we write to array.
else if( hi * low + hi + low === sum) {
answersArray.push([hi, low]);
low = nHi; // Note this is if we want to end once finding one pair
break; // If you want to find ALL pairs for large numbers then replace these low = nHi; with low++;
}
// And if not, we increment the low counter and restart the hi loop from the top.
else {
low++;
break;
}
} // close for
} // close while
Tutorial:
So we set the few variables. Note that low is set slightly less than half of n, as larger numbers look like they could be a few points less. Also, we don't round, we truncate, which is essentially "always rounding down", and is slightly better for performance, (though it dosenit matter in this instance with just the single assignment).
The while loop starts at the lowest value and increments, potentially all the way up to n-1. The hi FOR loop starts at n (copied to nHi), and then decrements until the factor are found OR it intercepts at low + 1.
The conditionals:
First IF: If we're higher than the sum, we exit, decrement, and continue at a lower value for the hi factor.
ELSE IF: If we are EQUAL, then we're done, and break for lunch. We set low = nHi so that when we break out of the FOR loop, we will also exit the WHILE loop.
ELSE: If we get here it's because we're less than the sum, so we need to increment the while loop and reset the hi FOR loop to start again from n (nHi).

The solution does not work for values between 1 - 20

I'm trying to solve Euler's fifth problem.
2520 is the smallest number that can be divided by each of the numbers from 1 to 10 without any remainder.
What is the smallest positive number that is evenly divisible by all of the numbers from 1 to 20?
It works well with the sample number 2520.
But not with a number from 1 - 20, and it does not give me anything back, so what is my mistake?
function findFactor(n, max) {
var acc = 0;
for(var i = 2; i <= max; ++i) {
acc += !(n%i) ? 1 : 0;
}
return acc === max - 1 ? true : false;
}
for(var i = 2520; true; ++i) {
if(findFactor(i, 20)) {
console.log(i)
}
if(i > 1e7) console.log("Limit")
}
You have some flaws in your code:
You never exit your loop for (var i = 2520; true; ++i). The browser freezes and doesn't log anything even if there is a match.
You increment i only by one, it's redundant. Increment by 20, as your answer must be divisible by 20.
acc += !(n%i) ? 1 : 0; is redundant too. You don't need to iterate further if n % i !== 0, just return false.
Taking into account all these corrections you may have something like this:
function findFactor(n, max) {
for (let i = 2; i <= max; i++) {
if (n % i !== 0) return false;
}
return true;
}
let n = 20;
//measuring performance
let start = performance.now();
for (let i = n; true; i += n) {
if (findFactor(i, n)) {
console.log(i);
break;
} else if (i > 1e10) {
console.log("Limit");
break;
}
}
console.log(`time spent: ${performance.now() - start}`);
There is another way to calculate the least common multiple (LCM) of more than two numbers - by iteratively computing the LCM of two numbers:
lcm(a, b, c) = lcm(a, lcm(b, c))
The least common multiple of two numbers can be computed as follows:
lcm(a, b) = a * b / gcd(a, b)
where gcd(a, b) is the greatest common divisor. To find it you can use the Euclidean algorithm
//greatest common divisor
const gcd = (a,b) => {
while (a !== 0 && b !== 0) {
if (a > b) a %= b;
else b %= a;
}
return a + b;
};
//least common multiple
const lcm = (a, b) => a * b / gcd(a, b);
const leastMultipleOfRange = n => {
if (n < 3) return n;
let acc = 2;
for (let i = 3; i <= n ; i++) {
acc = lcm(acc, i);
}
return acc;
};
let start = performance.now();
console.log(leastMultipleOfRange(20));
console.log(`time spent: ${performance.now() - start}`);
Most likely, there are some more effective ways of calculating the least common multiple of several numbers, for example, mentioned by Paul, but my knowledge of mathematics is not so deep to explain them.
A more efficient solution to this problem would be to calculate the least common multiple (lcm). The basic idea we can use is similar to calculating the lcm via factorization (though we don't use factorization directly).
Some basics
We can denote that a evenly divides b by a|b and that it doesn't by a∤b. Two numbers are coprime if they don't have a common factor; this also entails that lcm(a, b) = a * b, if a and b are coprime. m = lcm(a, b) has the properties a|m and b|m and there doesn't exists m_<m such that a|m_ and b|m_. Since for each integer a unique factorization exists (as stated in the fundamental theorem of arithmetic) we can express a, b and m as products of primes:
The factorization of m shows that there are no superfluous factors in m. It is exactly as large as it has to be to be divisible by both a and b.
The lcm of multiple numbers can be calculated from the lcm of two numbers recursively:
lcm(a, b, c) = lcm(lcm(a, b), c)
These are the basic mathematical tools required to solve the problem efficiently. Now we're left with two problems: which primes have a power > 0 in the factorization of our lcm, and what values have the corresponding expontents?
Finding the set of primes
We can determine which primes are in the factorization of lcm([1..n]) using the following fact: let p ∊ P and p <= n, then p is obviously in the sequence, so it must also be a factor of the least common multiple. Now how about p > n? Let's start off with the lcm of two values: a and b, where a < p and b < p. From this we can conclude that p∤a and p∤b, so p|lcm(a, b) can't hold either. In general, if p∤a and p∤b, then p∤lcm(a, b) must hold. Proof:
Assume m = lcm(a, b) and p | m
m = a * n1 = b * n2
but since p∤a and p∤b we also get
m = a * p * n1_ = b * p * n2_
n1_ * p = n1
n2_ * p = n2
and thus we can construct m_ with the following properties:
m_ * p = m
a|m_
b|m_
So a prime that is larger than a and b can never be a factor of lcm(a, b). Thanks to the recursive definition of the lcm of more than two integers, we can easily show that this entails that any prime larger that n can't be a factor of lcm([1..n]).
So the primes our factorization will consist of are all in the range [1..n]. E.g. for n=20, like in the problem on project euler, this would be the primes [2, 3, 5, 7, 11, 13, 17, 19].
Exponents
Remains one last problem to solve: the exponent of each prime in the factorization. In a first step we can look at powers of a single numbers:
lcm(x^e1,x^e2) = x^e2, if e1 < e2
So for example in our problem the exponent of 2 must be 4:
The largest power of 2 in the range [1..20] is 16 = 2^4. Any smaller power of 2 divides 16. So for a given n we could calculate the exponent as
So now we have the lcm of a part of the sequence (all powers of primes):
lcm(2,4,8,16, 3,9, 5, 7, 11, 13, 17, 19) =
lcm(lcm(2, 4, 8, 16), lcm(3, 9), 5, 7, 11, 13, 17, 19) =
lcm(16, 9, 5, 7, 11, 13, 17, 19) =
2^4 * 3^2 * 5^1 * 7^1 * ... * 19^1
The last lines of the above equation results from the fact that primes and their powers are always coprime to each other (see above).
But what about the remaining numbers in the sequence? We actually don't need them. Each number that isn't a power of a prime itself is a product of powers of primes (unique factorization). So let's say we have c = p1^e1 * p2^e2 and also a = p1^f1 and b = p2^f2, where a, b, and c are in the range [1..n] and f1 and f2 are maximal. Then e1 <= f1 and e2 <= f2 must hold, as otherwise c <= n couldn't possibly hold (remember that f1 and f2 are already the maximum-exponents for the corresponding primes, so e.g. p1^(f1 + 1) > n). Thus c | lcm(a, b) for a, b and c as defined above, which can be derived from the factorization of lcm(a, b) based on a, b (see above).
The actual implementation
Well, that's been the number theoretical part, time for some actual code (just in case you still read this :D ). At least we have some really pretty code now:
run = function(){
document.getElementById('output_id').innerHTML = 'Calculating...'
var n = document.getElementById('input_n_id').value;
// sieve of eratosthenes, the quick and dirty way
var primes = Array((n - 1) >> 1).fill(0).map((v, i) => i * 2 + 3).reduce((p, v) => {!~p.findIndex(p_ => !(v % p_)) && p.push(v); return p;}, [2]);
// actually calculating n
var sol = primes.map(p => Math.pow(p, Math.floor(Math.log(n) / Math.log(p))))
.reduce((a, b) => a * b, 1);
// output
document.getElementById('output_id').innerHTML = 'Solution: ' + sol;
}
<input maxlength="512" id="input_n_id" placeholder="Enter a value here"/>
<button onclick="run()">Start</button>
<p id="output_id">Waiting for input...</p>
So now there remains only one question to answer: what's the point of all the math? The answer: speed (and beauty ;) ). With this code you can calculate the least common multiple of any range of numbers up to [1..708] (in fact you could go further, but from 709 upwards the solution is beyond the range of javascripts floatingpoint-numbers).
You're setting max to 20, and then your loop relies on all the the numbers between 2 and max (20) being factors of n. That method won't work if n<20 because clearly a number larger than n can't be a factor of n. You'd need to set max to n if n < 20.
This is about primes. Think of which prime numbers make all the numbers between 1 and 20, remember to count the minimum number of each prime you would need and multiply them all together to get the solution. For example, for 9, we'll need two 3's, for 16, we'll need 4 2's, etc.

Knapsack - determining set from total value

After seeing this lecture I created the following knapsack code. In the lecture, the professor says it will be easy to determine the set from the optimal value (minute 19:00), however I can not find how to do it. I provide an example in the code which sums the values to 21, how can I determine the set (in this case 12, 7, 2) from this value?
/*
v = value
w = weight
c = capacity
*/
function knapsack(v, w, c) {
var n = v.length,
table = [];
// create two-dimensional array to hold values in memory
while (table.length <= c) {
table.push([]);
}
return ks(c, 0);
function ks(c, i) {
if (i >= n) {
table[c][i] = 0;
return table[c][i];
}
if (c < w[i]) {
if (table[c][i+1] === undefined) {
table[c][i + 1] = ks(c, i + 1);
}
return table[c][i + 1];
}
else {
if (table[c][i + 1] === undefined) {
table[c][i + 1] = ks(c, i + 1);
}
if (table[c - w[i]][i + 1] === undefined) {
table[c - w[i]][i + 1] = ks(c - w[i], i + 1);
}
return Math.max(table[c][i + 1], v[i] + table[c - w[i]][i + 1]);
}
}
}
//This is a test case
var v = [7, 2, 1, 6, 12];
var w = [3, 1, 2, 4, 6];
var c = 10;
var result = knapsack(v, w, c);
document.getElementById("solution").innerHTML = result;
<pre>Optimal solution value is: <span id="solution"></span></pre>
That's not easy at all. Determining whether a subset of some set of numbers has a certain sum is known as the subset sum problem, and it is NP-complete, just like knapsack itself. It would be a lot easier to just keep pointers to the solution of the subproblem from which you constructed the optimal solution to a larger subproblem. That way you can just walk back along the pointers from the globally optimal solution to find the actual set that gave you the optimal value.
(EDIT: as noted in the comments by j_random_hacker, once we have the DP table, we can actually determine the set that gave the optimal value in O(n2) time, by starting from the optimal solution and working backwards through the table, consider each possible item that could have been the last item added and checking if that solution matches the expected value.)
On a different note, I'd recommend watching some different lectures. The guy makes some strange claims, like that O(nc) -- n number of items, c capacity -- is much less than O(2n), which is simply not true when c is large. (In fact, this is called a pseudo-polynomial time solution, and it is still exponential in the length of the input, measured in bits.)

Categories

Resources