The problem as a single call is trivial at best with the optimal solution being
const values = [1,2,3,4,5,6,7,8,9];
function sum(i,j) {
var sum = 0;
while(i < j) { sum += values[i++] }
return sum;
}
Caching is impractical
The answer by Marc Rohloff suggests that caching the results is one way to do this. It has some flaws. \$O(n^2)\$ storage \$O(n^3)\$ complexity to get all possible solutions and make have sum perform at \$O(1)\$
Much in little, MIP maps
This problem is very similar to a CG problem regarding the rendering of images. To avoid small versions of a larger image looking pixelated each rendered pixel is the mean of the many pixels it represented. In other words a rendered pixel is sumPixels(a,b) / (b-a)
(pixels from a
to b
) which is identical to this problem.
The solution was called MIP mapping (MIP is for the Latin "multum in parvo", meaning "much in little") (now more often called mipmaps)
Mipmaps work by pre processing the image, creating multiple copies, each copy is half the size of the previous image. So for a square 256pixel image you would have to create a 8 more images of 128, 64, 32, 16, 8, 4, 2, and 1 pixels (normally the maps would be limited to only a few steps)
The storage thus becomes \$O(2n)\$ and the same for the pre-processing complexity. In other words a pre-processing \$O(n)\$ storage and processing solution to the problem of summing continuous sets from a larger set.
The rendering per pixel was typically \$O(2)\$ or \$O(1)\$
Applying the MIP
In javascript we can create a function that will return a function. That means we can use closure to encapsulate the pre-processed data.
As we need the precise value for each set we can not use the CG mipmap method as it calculates a near enough approximation. To get the precise value we need a little more complexity. Its something near \$O(log(log(n))\$ per sum
First the pre-processing
We create multiple maps each half the size of the previous map. We use the results of the previous map to calculate the new map. As some map sizes can not be divided by two we need to tack on some zeros
function createSumFunction(array) {
const mipMaps = [array];
(function () {
var from = array;
while(from.length > 2) {
let i = 0;
const to = [];
while(i < from.length) {
to.push(from[i++] + (from[i] !== undefined ? from[i] : 0));
i++;
}
mipMaps.push(to)
from = to;
}
mipMaps.push([from[0] + from[1]]);
})();
// more code to follow
Getting the sum
The inputs are i
and j
. (left and right)
First thing is reduce j
by 1
To get the sum we start at the first map, from the left if we are at an odd position get the value to the left and add that value to a sum for the left side. For the right side we do the same but rather if we are at an even location get the value to the right and add it to the sum for the right side.
The half and round the values for i, and j and step to the next map up (half sized). If i === j
get the value at i and subtract the left sum and right sum. The result is the value we are after.
The max number of steps will be \$O(log(n))\$ and the min will be \$O(1)\$
The second half of the solution returning the sum function. Note, it is missing some argument vetting, see snippet at bottom for better argument vetting.
return function(i,j) {
j--;
var k = 0, subL = 0, subR = 0;
while(k < mipMaps.length) { // could also be while(true)
const m = mipMaps[k ++];
subL += i % 2 ? m[i - 1] : 0;
subR += j % 2 ? 0 : m[j + 1];
i >>= 1;
j >>= 1;
if (i === j) { return mipMaps[k][i] - subL - subR }
}
}
Solution pre \$O(n)\$ RAM/CPU and sum
\$O(1)\$ RAM \$O(log(n))\$ CPU
As one function that does the pre process and returns the sum function. Add some checks to the sum input to ensure that the values i,j are not in conflict. It is assumed that they are in range. It is also assumed that the input array is more than 1 item long (or what would the point be?)
I have only tested positive values (but assume it will work for all numbers) and I have only tested for power of two array sizes, I leave that for you to work out (hint the right side may not end at an odd index)
function createSumFunction(array) {
const mipMaps = [array];
(()=>{ // to keep closure clean
var from = array;
while (from.length > 2) {
let i = 0;
const to = [];
while(i < from.length) {
to.push(from[i++] + (from[i] !== undefined ? from[i] : 0));
i++;
}
mipMaps.push(to)
from = to;
}
mipMaps.push([from[0] + from[1]]);
})();
return function(i, j) {
var k = 0, subL = 0, subR = 0;
if (i === j) { return NaN }
if (j < i) { [j, i] = [i, j] }
j--;
if (i === j) { return mipMaps[0][i] }
while (k < mipMaps.length) { // could also be while(true)
const m = mipMaps[k ++];
subL += i % 2 ? m[i - 1] : 0;
subR += j % 2 ? 0 : m[j + 1]; // << hint fix
i >>= 1;
j >>= 1;
if (i === j) { return mipMaps[k][i] - subL - subR }
}
}
}
const sum = createSumFunction([1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8]);
console.log(sum(0,16));
[0,1,2,3,4,5,6]
dosum(0,3)
,sum(2,4)
,sum(3,6)
. The example shows a small datasets, but in real world apps datasets can be huge, millions, billions and more. This is when complexity makes the difference between seconds to compete or hours to complete. Pre processing can reduce the overall complexity \$\endgroup\$