Refactor Javascript to Optimize for Speed - javascript

I was able to figure out a working solution to the problem but I would like to know how I could go about refactoring this code to make it faster. Thank you.
/*
Given a sequence of integers as an array, determine whether it is
possible to obtain a strictly increasing sequence by removing no more
than one element from the array.
*/
//var array2 = [0, -2, 5, 6]; // expect true
//var array2 = [1, 1, 1, 2, 3]; // expect false
var array2 = [1, 2, 5, 5, 5] // expect false
function almostIncreasingSequence(sequence) {
let passing = false;
sequence.forEach((number, idx, array) => {
let sequence2 = sequence.filter((num, id) => id !== idx);
let answer = sequence2.every((num, idx, array) => {
return idx === sequence2.length - 1 || num < sequence2[idx + 1];
});
if (answer) {
passing = true;
}
});
return passing;
}
console.log(almostIncreasingSequence(array2));

If I'm understanding the problem right, you only need to make 1 pass through the sequence. You're looking for arrays where the difference between index (i-1) and (i) decreases only one time in the whole array.
(this code has gone through several edits to handle edge cases)
function almostIncreasingSequence(sequence) {
if(sequence.length == 1)
return true;
var countDecreases = 0;
var i = -1;
for(var index=1; index<sequence.length; index++)
{
if(sequence[index-1] > sequence[index])
{
countDecreases++;
i = index;
if(countDecreases > 1)
return false;
}
}
var canRemovePreviousElement = (i == 1 || sequence[i-2] <= sequence[i]);
var cannotRemoveSelectedElement = (i+1 < sequence.length && sequence[i-1] > sequence[i+1]);
if(canRemovePreviousElement)
return cannotRemoveSelectedElement;
return (countDecreases == 1 && !cannotRemoveSelectedElement);
}
// Testing /////////////////////////////////
var passTestCases = {
"remove index 0": [2, 0, 1, 2],
"remove index 1": [0, 1, 0, 0, 1],
"remove index (last-1)": [0, 1, -1, 2],
"remove index (last)": [0, 1, 2, -1],
"remove only element": [1]
};
var failTestCases = {
"remove index 0 or 1": [0, -1, 1, 2],
"remove any index, all increasing": [1, 2, 5, 5, 5],
"remove any index, with decrease": [1, 0],
"one decrease": [0, 1, 2, 0, 1],
"two decreases": [1, 0, 2, 1],
"no elements to remove": []
};
runTests(passTestCases, true, almostIncreasingSequence);
runTests(failTestCases, false, almostIncreasingSequence);
function runTests(testCases, expectedResult, method) {
console.log("\nShould Return " + expectedResult);
for(var key in testCases)
{
var testCase = testCases[key];
var result = method(testCase);
console.log(key + ": " + testCase + ": result " + result);
}
}
On optimizing for speed:
This method will only make one pass through the array, while the original post uses nested loops. In general, a linear algorithm will be faster than a non-linear algorithm for large data sets.
Comparing the speeds of the two algorithms with these tiny test cases gives inconclusive results.

Note that we have a conflict when a[i] <= a[i - 1] as we want a strictly increasing sequence. There are two ways to solve the conflict. One is to remove a[i] from the array, hoping that a[i + 1] won't conflict with a[i - 1]. Another is to remove a[i - 1], hoping that a[i - 2] won't conflict with a[i].
As we have the case that a[i] <= a[i - 1], it should be better to remove a[i - 1] so that our new max element in the sequence is less or equal than before, thus giving more chances to have no conflict on next element. So if we can choose between removing a[i] and a[i - 1], removing a[i - 1] would be better.
We can remove a[i - 1] when a[i - 2] does not conflict with a[i]. In the code, sequence[prev - 1] is this a[i - 2], so in the case where there is a conflict, our only choice left is removing a[i].
To avoid actually removing elements on the array, I used the little trick of just doing a continue, which makes i increase but prev stays the same, thus making the new a[i] compare against a[i - 2] as they should be now next to each other in the strictly increasing sequence.
When we don't fall into the continue, we do prev = i instead of prev += 1 because we would actually need to add 2 to prev after we got into the continue and there was no conflict between a[i] and a[i - 2]. If we added just 1 to prev, we would have prev pointing to a[i - 1], but we can't have that as its the element we "removed" from the sequence.
function isAlmostIncreasingSequence(sequence) {
var prev = 0;
var removed = false;
for (var i = 1; i < sequence.length; i++) {
if (sequence[i] <= sequence[prev]) {
if (removed == false) {
removed = true;
if (prev > 0 && sequence[i] <= sequence[prev - 1]) { // need to remove sequence[i] instead of sequence[prev]
continue;
}
} else {
return false;
}
}
prev = i;
}
return true;
}
var tests = [[0, -2, 5, 6], [1, 1, 1, 2, 3], [1, 2, 5, 5, 5], [1, 3, 2], [0, -2, 5, 6], [1, 2, 3, 4, 5, 3, 5, 6], [1, 1], [1, 2, 5, 3, 5], [1, 2, 3, 4, 3, 6]];
tests.forEach(function(arr){
console.log(arr.join(","), isAlmostIncreasingSequence(arr));
});

Related

Can't understand how I get a certain value from an array when a function is called using recursive method

Ok, first, sorry for the not so clear question.
Here's the thing:
function sum(arr, n) {
// Only change code below this line
if (n <= 0) {
return 0;
} else {
return sum(arr, n - 1) + arr[n - 1];
}
// Only change code above this line
}
var a = sum([2, 3, 4], 1);
console.log(a);
When a is called the result will be 2. I want to know which value will replace arr in the sum(arr, n - 1). Isn't supposed to be like arr[] so that I can pull the on of the three numbers from the array instead?
For ex.
sum([2, 3, 4], 1) = sum([2, 3, 4], 0) + 2
sum([2, 3, 4], 0) = 0
So result will be;
sum([2, 3, 4], 1) = 0 + 2
sum([2, 3, 4], 1) = 2

Maximum Length of Repeated Subarray (leetcode)

I'm looking at this leetcode question, and am having an issue completing the naive approach. I was able to come to an optimal solution here. But I'm not sure what's wrong with my naive attempt.
The question is as follows:
Given two integer arrays A and B, return the maximum length of an
subarray that appears in both arrays.
Example:
Input: A: [1,2,3,2,1] B: [3,2,1,4,7]
Output: 3
Explanation: The repeated subarray with maximum length is [3, 2, 1].
Here is my current code:
var findLength = function(a, b) {
if (a.length === 0 || b.length === 0) {
return 0;
}
let aWithoutFinalNumber = a.slice(0, a.length - 1);
let bWithoutFinalNumber = b.slice(0, b.length - 1);
let aFinalNumber = a[a.length - 1];
let bFinalNumber = b[b.length - 1];
// matching numbers
if(aFinalNumber === bFinalNumber) {
return 1 + findLength(aWithoutFinalNumber, bWithoutFinalNumber);
} else { // mismatch. Compete to find the maximum length.
return Math.max(findLength(a, bWithoutFinalNumber), findLength(aWithoutFinalNumber, b));
}
};
My solution passes several test cases, but fails on cases like a: [0,1,1,1,1] b: [1,0,1,0,1]. Any insight on my mistake would be appreciated!
The problem comes from the way you calculate the max length when the last elements match. here is a minimal example:
var findLength = function(a, b) {
if (a.length === 0 || b.length === 0) {
return 0;
}
let aWithoutFinalNumber = a.slice(0, a.length - 1);
let bWithoutFinalNumber = b.slice(0, b.length - 1);
let aFinalNumber = a[a.length - 1];
let bFinalNumber = b[b.length - 1];
// matching numbers
if(aFinalNumber === bFinalNumber) {
return 1 + findLength(aWithoutFinalNumber, bWithoutFinalNumber); //< -- problem here
} else { // mismatch. Compete to find the maximum length.
return Math.max(findLength(a, bWithoutFinalNumber), findLength(aWithoutFinalNumber, b));
}
};
console.log(findLength([1, 0, 2, 1], [1, 0, 3, 1]));
If there is any match you add 1 to the max length but that's not necessarily true if there is a mismatch later. Here is a shortened version what happens with illustration for easier understanding:
[1, 0, 2, 1]
^-------|
[1, 0, 3, 1] | -- match, max length +1
^-------|
______
[1, 0, 2, 1]
^----------|
[1, 0, 3, 1] | -- mismatch, max length +0
^----------|
______
[1, 0, 2, 1]
^-------------|
[1, 0, 3, 1] | -- match, max length +1
^-------------|
______
[1, 0, 2, 1]
^----------------|
[1, 0, 3, 1] | -- match, max length +1
^----------------|
When you total all the matches, you get 3 however, the count should have been reset when there was a mismatch.
One simple change that can be done to the algorithm to avoid this problem is to pass the current count as an argument to the function. This way, you can control when the count needs to be reset:
var findLength = function(a, b, maxSoFar = 0) { //<-- default count of zero
if (a.length === 0 || b.length === 0) {
return maxSoFar; //<-- return the count
}
let aWithoutFinalNumber = a.slice(0, a.length - 1);
let bWithoutFinalNumber = b.slice(0, b.length - 1);
let aFinalNumber = a[a.length - 1];
let bFinalNumber = b[b.length - 1];
// matching numbers
if(aFinalNumber === bFinalNumber) {
const newMax = maxSoFar + 1; //<-- increment the count
return Math.max(newMax, findLength(aWithoutFinalNumber, bWithoutFinalNumber, newMax)); //<-- return the newMax in case the next is a mismatch
} else { // mismatch. Compete to find the maximum length.
return Math.max(findLength(a, bWithoutFinalNumber), findLength(aWithoutFinalNumber, b)); //<-- reset the count
}
};
console.log(findLength([1, 0, 2, 1], [1, 0, 3, 1]));
console.log(findLength([1, 2, 3, 2, 1], [3, 2, 1, 4, 7]));
console.log(findLength([0, 1, 1, 1, 1], [1, 0, 1, 0, 1]));
You could use nested loop and when same element occur in both arrays you could start incrementing both of those indexes until elements in both arrays are the same. The result that gives you the largest set of elements will be the returned result.
function maxSub(a, b) {
let result = null;
function findAll(i, j) {
const res = [];
if (a[i] !== b[j] || a[i] == undefined || b[j] == undefined) {
return res;
}
return res.concat(a[i], ...findAll(i + 1, j + 1))
}
a.forEach((e, i) => {
b.forEach((c, j) => {
if (e == c) {
const sub = findAll(i, j);
if (!result || sub.length > result.length) {
result = sub
}
}
})
})
return result;
}
console.log(maxSub([0, 1, 1, 1, 1], [1, 0, 1, 0, 1]))
console.log(maxSub([1, 2, 3, 2, 1], [3, 2, 1, 4, 7]))
you can use dp with a table as well. i tried the other code, and it gave errors in cases like: [0,0,0,0,1,0,0] and [0,0,0,0,0,1,0].
Here is a python code for the same.
def findLength(a, b):
if len(a)==0 or len(b)==0:
return 0
n=len(a)
m=len(b)
dp=[[0 for _ in range(n+1)] for _ in range(m+1)]
maxSoFar=0
for i in range(1,m+1):
for j in range(1,n+1):
if a[j-1]==b[i-1]:
dp[i][j]=dp[i-1][j-1]+1
maxSoFar=max(maxSoFar,dp[i][j])
else:
dp[i][j]=0
return maxSoFar

Find all the same numbers in the array

I have an array with numbers in the range of 0 - 100. I need to find all the same numbers and add 1 to them.
my code worked well with arrays like [100, 2, 1, 1, 0]
const findAndChangeDuplicates = (arr: any) => {
for (let i = arr.length - 1; i >= 0; i--) {
if (arr[i + 1] === arr[i] && arr[i] <= 5) {
arr[i] += 1;
} else if (arr[i - 1] === arr[i] && arr[i] >= 5) {
arr[i] -= 1;
findAndChangeDuplicates(arr);
}
}
return arr;
};
but when I came across this
[100, 6, 6, 6, 5, 5, 5, 5, 5, 4, 4, 4, 3, 3, 2, 2, 2, 2, 1, 1, 0, 0]
my code let me down.
Expected Result:
[100, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
Have any ideas?
An approach by using at least one loop from the end to adjust the values and if necessary another loop from the beginning to set the largest value to 100.
Both loops feature a value variable v. In the first loop, it starts with the last value of the array and increments its value and check is the item is smaller than this value.
If smaller, then the value is assigned, otherwise the actual value is taken for the next item.
if necessary, the other loop works in opposite direction and with a start value of 100 and checks if the item is greater than wanted and takes the smaller value, or the value is taken from the item.
The result is an array which has a gereatest value of 100 at start and goes until zero or greater to the end of the array.
function update(array) {
var i = array.length,
v = array[--i];
while (i--) if (array[i] < ++v) array[i] = v; else v = array[i];
if (array[0] > 100) {
v = 100;
for (i = 0; i < array.length; i++) {
if (array[i] > v) array[i] = v; else v = array[i];
v--;
}
}
return array;
}
console.log(update([100, 2, 1, 1, 0]));
console.log(update( [100, 100, 99, 86, 6, 5, 5, 5, 5, 5, 4, 4, 4, 3, 3, 2, 2, 2, 2, 1, 1, 0, 0]))
.as-console-wrapper { max-height: 100% !important; top: 0; }
The following assumes you want them ordered from highest to lowest, if not this might ba as well as useless to you.
The idea is to first create an Object to keep track of how many of each number exist. We then map each value by first checking whether it's unique and if not increasing it until we can't find any value inside the Object anymore. This will not neatly order the numbers by itself so we will have to sort afterwards.
let arr1 = [100, 6, 6, 6, 5, 5, 5, 5, 5, 4, 4, 4, 3, 3, 2, 2, 2, 2, 1, 1, 0, 0],
arr2 = [100, 2, 1, 1, 0];
const f = (arr) => arr.reduce((a,c) => (a[c] = (a[c] || 0) + 1, a),{}),
g = (arr, obj) => arr.map(v => {
if (obj[v] > 1) {
let i = 1;
obj[v] = obj[v] - 1;
while (obj[v + i]) {
i++;
}
obj[v + i] = (obj[v + i] || 0) + 1;
return v + i;
} else {
return v;
}
}).sort((a,b) => +b - +a);
console.log(g(arr1, f(arr1)))
console.log(g(arr2, f(arr2)))
Here is a verbose solution that will work with unordered arrays as well.
It's not efficient, neither brilliant, but it takes care of unordered arrays as well.
Basically, it takes advantage of reduce to collect all the occurrences of each element. Each time it finds more than one, it increases all the occurrences by 1 except the last one.
Next, it checks whether there still are duplicates. If there are, it repeats the process until none is found. Of course, it's not the cleverest approach, but it works.
// Increases all duplicates until there are no more duplicates.
const increaseDuplicates = (arr, index) => {
// Repeat the code until no duplicate is found
while (!noDuplicates(arr)) {
// Acquire all the occurrences of each item, keeping track of the index.
Object.entries(arr.reduce((acc, next, i) => {
acc[next] = acc[next] || [];
return acc[next].push(i), acc;
}, {})).forEach(([n, indexes]) => {
// for each value found, check whether it appears at least twice.
if (indexes.length > 1) {
// if it does, increase the value of every item but the last one.
for (var i = 0; i < indexes.length - 1; i++) {
arr[indexes[i]]++;
}
}
});
}
return arr;
};
// Asserts an array has no duplicates.
const noDuplicates = (arr) => [...new Set(arr)].length === arr.length;
const input = [100, 6, 6, 6, 5, 5, 5, 5, 5, 4, 4, 4, 3, 3, 2, 2, 2, 2, 1, 1, 0, 0];
console.log(increaseDuplicates(input));
const unorderedInput = [6,4,5,6,6,6,6,5,6,3,1,2,3,99,403,100, 6, 6, 6, 5, 5, 5, 5, 5, 4, 4, 4, 3, 3, 2, 2, 2, 2, 1, 1, 0, 0];
console.log(increaseDuplicates(unorderedInput));
You can use a forEach on your array to do this, using the 3rd parameter of the callback, the array itself, and a bit of recursivity
const increment_to_unicity = (value, index, self) => {
if (self.indexOf(value) !== index) {
self[index]++
increment_to_unicity(self[index], index, self)
}
return self[index];
}
arr = arr.map(increment_to_unicity).sort((a, b) => b - a);

How to determine which element was moved in an array?

I am trying to find the difference between two arrays, by finding out which element was moved. I know that one element exactly will be moved and that the order will be maintained for the rest of the list, but am unable to figure out how to find this.
Example:
A: 1 2 3 4 5 6
B: 2 3 4 5 1 6
All of the elements exist in both lists, but how do I find out that the element 1 moved from index 0 to index 4?
My basic approach that I took but is not working is:
//Original array
var a = [1, 2, 3, 4, 5, 6];
//New array
var b = [2, 3, 4, 5, 1, 6];
for(var i=0; i < a.length; i++) {
if(a[i] != b[i] && a[i+1] != b[i]) {
console.log(b[i] + " moved");
}
}
I have fixed by code to print out b[i] instead of a[i], but it is not working in all cases such as:
A: 1, 2, 3, 4
B: 1, 4, 2, 3
The problem is with the second condition in your if statement. In your example, when element a[0] has moved, a[0+1] === b[0], so the if clause evaluates to false.
Try instead,
var idx = 0;
var len = a.length;
while ((a[idx] === b[idx] || a[idx] === b[idx+1]) && idx < len) {
idx++;
}
console.log('Element a[' + idx + ']=' + a[idx] + ' moved.');
basically, if I understand correctly, an element moving means it is deleted and inserted
somewhere else.
so you first find the first point where there was a deletion/insertion:
function whichMoved(a, b) {
for(var i=0; i < a.length; i++) {
if (a[i] != b[i]) {
now, if it was a deletion, then the element has been moved forward, meaning, inserted in a greater index in b, and all elements between the indices are shifted to the left,
meaning the the next element has moved one place to backward:
if(a[i+1] == b[i]) {
console.log(a[i] + " moved forward");
break;
}
otherwise, the element was moved backward:
else {
console.log(b[i] + " moved backward")
break;
}
the whole thing:
//Original array
var a = [1, 2, 3, 4, 5, 6];
//testing
whichMoved(a, [2,3,4,5,1,6]); //prints 1 moved forward
whichMoved(a, [5,1,2,3,4,6]); //prints 5 moved backward
function whichMoved(a, b) {
for(var i=0; i < a.length; i++) {
if (a[i] != b[i]) {
if(a[i+1] == b[i]) {
console.log(a[i] + " moved forward");
break;
} else {
console.log(b[i] + " moved backward")
break;
}
}
}
}
You can use jQuery .inArray() it will return the index, starting at 0 and returns -1 if not found:
var a = [1, 2, 3, 4, 5, 6];
//New array
var b = [2, 3, 4, 5, 1, 6];
for(i=0; i < a.length; i++) {
var j = $.inArray(a[i], b);
if(i != j){
console.log(a[i], "moved to index "+j);
}else{
console.log(a[i], "not moved");
}
}
See this jsfiddle: http://jsfiddle.net/Rdzj4/
Edited- probably not needed, but I hate to leave a wrong answer.
Here I look at the distance each item is from its original index,
and figure the one that is most out of order is the mover-
This assumes in [2,1,3,4,5,6] it is the two that moved, not the 1,
and in [1,2, 3, 4, 6, 5] it is the 6, not the 5.
function whoMoved(a, b){
var max= 0, min= a.length, dist,
order= b.map(function(itm, i){
dist= i-a.indexOf(itm);
if(dist<min) min= dist;
if(dist>max) max= dist;
return dist;
});
if(Math.abs(min)>= max) max= min;
return b[order.indexOf(max)];
}
//test
var a= [1, 2, 3, 4, 5, 6];
var b= [1, 6, 2, 3, 4, 5];//6 to left
var c= [1, 3, 4, 2, 5, 6];//2 to to right
var d= [3, 1, 2, 4, 5, 6];//3 to left
var e= [2, 3, 4, 5, 1, 6];//1 to right
[whoMoved(a, b), whoMoved(a, c), whoMoved(a, d),whoMoved(a, e)];
/* returned value: (Array) [6,2,3,1] */

calculating 3 similar sized groups in Javascript or jQuery

We have:
var range = [9,18,3,14,2,6,12,7,11,2,1,4]
var total = 89;
var group_size = total / 3;
As result I need 3 groups similar sized but group1 and group2 can never be bigger than group_size.
The result for this example would be
var group1 = 27; // 9,18
var group2 = 25; // 3,14,2,6
var group3 = 37; // 12,7,11,2,1,4
How can I achieve this in Javascript/jQuery?
I think you need to pop values with Javascript if you want this. Something like:
var range = [9,18,3,14,2,6,12,7,11,2,1,4]
var total = 89;
var group_size = total / 3;
var values = [0];
var groupnr = 0;
// Reverse the array, because pop will get the last element
range = range.reverse();
// While elements
while( range.length ) {
// get the last element and remove from array
var curvalue = range.pop();
// To large, create a new element
if( values[groupnr] + curvalue > group_size && groupnr < 2 ) {
groupnr++;
values[groupnr] = 0;
}
// Increase
values[groupnr] += curvalue;
}
console.log(values);
Update: This really answers a similar question (since removed by the author) which didn't include the restriction that the first two groups could not exceed the mean. With that restriction, it's a much simpler problem, and one that probably wouldn't have caught my attention. I'm leaving my answer here as the question it answers seems to be algorithmically interesting.
I have an answer using the Ramda functional programming library. You can see it in action on JSFiddle. (See below for an updated version of this that doesn't depend upon Ramda.) Ramda offers a number of convenient functions that make the code simpler. None of them should be surprising if you're at all used to functional programming, although if you're used to tools like Underscore or LoDash the parameter orders might seem backwards. (Believe me, there is a good reason.)
var equalSplit = (function() {
var square = function(x) {return x * x;};
var variance = function(groups) {
var sizes = map(sum, groups),
mean = sum(sizes) / sizes.length;
return sum(map(pipe(subtract(mean), square), sizes));
};
var firstGroupChoices = function(group, count) {
if (group.length < 2 || count < 2) {return group;}
var mean = sum(group) / count;
var current = 0, next = group[0], idx = 0;
do {
current = next;
next = next + group[++idx];
} while (next < mean);
if (next === mean) {
return [group.slice(0, idx + 1)]
} else {
return [
group.slice(0, idx),
group.slice(0, idx + 1)
];
}
};
var val = function(group, count, soFar) {
if (count <= 0 || group.length == 0) {
return {groups: soFar, variance: variance(soFar)};
}
if (count == 1) {
return val([], 0, soFar.concat([group]));
}
var choices = firstGroupChoices(group, count);
var values = map(function(choice){
return val(group.slice(choice.length), count - 1,
soFar.concat([choice]));
}, choices);
return minWith(function(a, b) {
return a.variance - b.variance;
}, values);
};
return function(group, count) {
return val(group, count, []).groups;
}
}());
Here is some sample output from the Fiddle:
==================================================
Input: [9,18,3,14,2,6,12,7,11,2,1,4]
Split into 3 groups
--------------------------------------------------
Groups: [[9,18,3],[14,2,6,12],[7,11,2,1,4]]
Totals: [30,34,25]
Variance: 40.66666666666667
==================================================
==================================================
Input: [9,18,3,2,6,12,11,2,4]
Split into 3 groups
--------------------------------------------------
Groups: [[9,18],[3,2,6,12],[11,2,4]]
Totals: [27,23,17]
Variance: 50.66666666666667
==================================================
==================================================
Input: [23,10,6,22,22,21,22,14,16,21,13,14,22,16,22,6,16,14,8,20,10,19,12,14,12]
Split into 5 groups
--------------------------------------------------
Groups: [[23,10,6,22,22],[21,22,14,16],[21,13,14,22],[16,22,6,16,14,8],
[20,10,19,12,14,12]]
Totals: [83,73,70,82,87]
Variance: 206
==================================================
I am not at all convinced that this algorithm will give you an actual optimal solution. I think there is some chance that a search might fall into a local minimum and not notice the global minimum in a nearby valley of the search space. But I haven't tried very hard to either prove it or come up with a counterexample. There is also a reasonable chance that it actually is correct. I have no idea if there is some more efficient algorithm than this. The problem feels vaguely like partitioning problems and knapsack problems, but I know I've never run across it before. Many of those problems are NP Hard/NP Complete, so I wouldn't expect this to have a really efficient algorithm available.
This one works in a fairly simple recursive manner. The internal val function accepts an array of numbers, the number of groups to create, and and accumulator (soFar) containing all the groups that have been created so far. If count is zero, it returns a simple result based upon the accumulator. If count is one, it recurs with an empty group, a count of zero, a an accumulator that now includes the original group as its last element.
For any other value of count it calculates the mean size of the remaining group, and then chooses the last initial partial sum of the group smaller than the mean and the first one larger than it (with a special case if there is one exactly equal to it), recurs to finds the value if those partial sequences are used as groups in the anser, and then returns the one with the smaller value.
Values are determined by calculated the variance in the total values of each subgroup formed. The variance is the sum of the squares of the distance from the mean.
For instance, if you started with these values:
[8, 6, 7, 5, 3, 1, 9]
And wanted to break them into three groups, you would have a mean of
(8 + 6 + 7 + 5 + 3 + 1 + 9 = 39) / 3 => 13
If you broke them up like this:
[[8, 6], [7, 5, 3], [1, 9]]
you would have totals
[14, 15, 10]
and a variance of
(14 - 13)^2 + (15 - 13)^2 + (10 - 13)^2 => 14
But if you broke them like this:
[[8, 6], [7, 5], [3, 1, 9]]
your totals would be
[14, 12, 13]
and your variance would be
[14 - 13)^2 + (12 - 13)^2 + (13 - 13)^2 => 2
And since the latter split has the lower variance it is considered better.
Example
equalSplit([9, 18, 3, 2, 6, 12, 11, 2, 4], 3 []) = minVal(
equalSplit([18, 3, 2, 6, 12, 11, 2, 4], 2, [[9]]),
equalSplit([3, 2, 6, 12, 11, 2, 4], 2, [[9, 18]])
);
equalSplit([18, 3, 2, 6, 12, 11, 2, 4], 2, [[9]]) =
equalSplit([12, 11, 2, 4], 1, [[9], [18, 3, 2, 6]]);
equalSplit([3, 2, 6, 12, 11, 2, 4], 2, [9, 18]) = minVal(
equalSplit([12, 11, 2, 4], 1, [[9, 18], [3, 2, 6]])
equalSplit([11, 2, 4], 1, [[9, 18], [3, 2, 6, 12]]
);
equalSplit([12, 11, 2, 4], 1, [[9], [18, 3, 2, 6]]) =
equalSplit([], 0, [[9], [18, 3, 2, 6], [12, 11, 2, 4]])
equalSplit([12, 11, 2, 4], 1, [[9, 18], [3, 2, 6]]) =
equalSplit([], 0, [[9, 18], [3, 2, 6], [12, 11, 2, 4]]) =
equalSplit([11, 2, 4], 1, [[9, 18], [3, 2, 6, 12]]
equalSplit([], 0, [[9, 18], [3, 2, 6, 12], [11, 2, 4]]
equalSplit([], 0, [[9], [18, 3, 2, 6], [12, 11, 2, 4]]) =
variance((9), (18 + 3 + 2 + 6), (12 + 11 + 2 + 4)) =
variance(9, 29, 29) = 266.67
equalSplit([], 0, [[9, 18], [3, 2, 6], [12, 11, 2, 4]]) =
variance((9 + 18), (3 + 2 + 6), (12 + 11 + 2 + 4)) =
variance(27, 11, 29) = 194.67
equalSplit([], 0, [[9, 18], [3, 2, 6, 12], [11, 2, 4]] =
variance((9 + 18), (3 + 2 + 6 + 12), (11 + 2 + 4)) =
variance(27, 23, 17) = 50.67
There is almost certainly plenty that could be done to clean up this code. But perhaps it's at least a start on your problem. It's been a very interesting challenge.
Update
I did create a version which does not depend upon Ramda. The code is very similar. (I guess I really didn't need Ramda, at least not for the final version.):
var equalSplit = (function() {
var sum = function(list) {return list.reduce(function(a, b) {
return a + b;
}, 0);};
var square = function(x) {return x * x;};
var variance = function(groups) {
var sizes = groups.map(sum),
mean = sum(sizes) / sizes.length;
return sum(sizes.map(function(size) {
return square(size - mean);
}, sizes));
};
var firstGroupChoices = function(group, count) {
if (group.length < 2 || count < 2) {return group;}
var mean = sum(group) / count;
var current = 0, next = group[0], idx = 0;
do {
current = next;
next = next + group[++idx];
} while (next < mean);
if (next === mean) {
return [group.slice(0, idx + 1)]
} else {
return [
group.slice(0, idx),
group.slice(0, idx + 1)
];
}
};
var val = function(group, count, soFar) {
if (count <= 0 || group.length == 0) {
return {groups: soFar, variance: variance(soFar)};
}
if (count == 1) {
return val([], 0, soFar.concat([group]));
}
var choices = firstGroupChoices(group, count);
var values = choices.map(function(choice){
return val(group.slice(choice.length), count - 1,
soFar.concat([choice]));
});
return values.sort(function(a, b) {
return a.variance - b.variance;
})[0];
};
return function(group, count) {
return val(group, count, []).groups;
}
}());
Of course, as now noted at the top, this answers a somewhat different question than is now being asked, but I think it's a more interesting question! :-)

Categories

Resources