Knapsack - determining set from total value - javascript

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.)

Related

Returning Null - why is this going wrong?

I am returning null with this code, however I cannot see why/where this is going wrong. (It may be due to the sort function since I got this from a well upvoted snippet on how to sort an array). I care more about the understanding than the correct answer.
Task I am trying to achieve :
Ratiorg got statues of different sizes as a present from CodeMaster for his birthday, each statue having an non-negative integer size. Since he likes to make things perfect, he wants to arrange them from smallest to largest so that each statue will be bigger than the previous one exactly by 1. He may need some additional statues to be able to accomplish that. Help him figure out the minimum number of additional statues needed.
Example
For statues = [6, 2, 3, 8], the output should be
solution(statues) = 3.
Ratiorg needs statues of sizes 4, 5 and 7.
My code & thinking:
function solution(statues) {
let total = 0;
statues.sort(function(a, b) { //From what I understand this should sort the array numerically
return a - b;
});
for (let i = 1; i < statues.length; i++) { //iterate through the array comparing the index to the one before it
if (statues[i + 1] != (statues[i] + 1)) { //if there is a diff between index of more than 1 it will add the diff
total += statues[i + 1] - statues[i]; //to the total variable
}
}
return total;
}
const result = solution([6, 2, 3, 8]);
console.log(result);
Thanks to people who commented who helped me see that there were some errors in my logic - I have now changed the if statement to compare an index starting at 1 to the behind it i.e index 1 compared to index 0. On top of that, there was an issue with the following line total += statues[i] - statues[i-1];
I replaced that with a for loop so that it counts up to the number rather than a simple subtraction of it.
function solution(statues) {
//[6, 2, 3, 8] --- 2,3,6,8 --- 3,6 7-2,
let total = 0;
statues.sort(function(a, b) {
return a - b;
});
for(let i =1; i<statues.length; i++){
if(statues[i] != (statues[i-1]+1) ){
//total += statues[i] - statues[i-1];
for(let j = statues[i-1]; j<statues[i]-1; j++){
total += 1;
}
}
}
return total;
}

Find possible numbers in array that can sum to a target value

Given I have an array of numbers for example [14,6,10] - How can I find possible combinations/pairs that can add upto a given target value.
for example I have [14,6,10], im looking for a target value of 40
my expected output will be
10 + 10 + 6 + 14
14 + 14 + 6 + 6
10 + 10 + 10 + 10
*Order is not important
With that being said, this is what I tried so far:
function Sum(numbers, target, partial) {
var s, n, remaining;
partial = partial || [];
s = partial.reduce(function (a, b) {
return a + b;
}, 0);
if (s === target) {
console.log("%s", partial.join("+"))
}
for (var i = 0; i < numbers.length; i++) {
n = numbers[i];
remaining = numbers.slice(i + 1);
Sum(remaining, target, partial.concat([n]));
}
}
>>> Sum([14,6,10],40);
// returns nothing
>>> Sum([14,6,10],24);
// return 14+10
It is actually useless since it will only return if the number can be used only once to sum.
So how to do it?
You could add the value of the actual index as long as the sum is smaller than the wanted sum or proceed with the next index.
function getSum(array, sum) {
function iter(index, temp) {
var s = temp.reduce((a, b) => a + b, 0);
if (s === sum) result.push(temp);
if (s >= sum || index >= array.length) return;
iter(index, temp.concat(array[index]));
iter(index + 1, temp);
}
var result = [];
iter(0, []);
return result;
}
console.log(getSum([14, 6, 10], 40));
.as-console-wrapper { max-height: 100% !important; top: 0; }
For getting a limited result set, you could specify the length and check it in the exit condition.
function getSum(array, sum, limit) {
function iter(index, temp) {
var s = temp.reduce((a, b) => a + b, 0);
if (s === sum) result.push(temp);
if (s >= sum || index >= array.length || temp.length >= limit) return;
iter(index, temp.concat(array[index]));
iter(index + 1, temp);
}
var result = [];
iter(0, []);
return result;
}
console.log(getSum([14, 6, 10], 40, 5));
.as-console-wrapper { max-height: 100% !important; top: 0; }
TL&DR : Skip to Part II for the real thing
Part I
#Nina Scholz answer to this fundamental problem just shows us a beautiful manifestation of an algorithm. Honestly it confused me a lot for two reasons
When i try [14,6,10,7,3] with a target 500 it makes 36,783,575 calls to the iter function without blowing the call stack. Yet memory shows no significant usage at all.
My dynamical programming solution goes a little faster (or may be not) but there is no way it can do above case without exhousting the 16GB memory.
So i shelved my solution and instead started investigating her code a little further on dev tools and discoverd both it's beauty and also a little bit of it's shortcomings.
First i believe this algorithmic approach, which includes a very clever use of recursion, might possibly deserve a name of it's own. It's very memory efficient and only uses up memory for the constructed result set. The stack dynamically grows and shrinks continuoously up to nowhere close to it's limit.
The problem is, while being very efficient it still makes huge amounts of redundant calls. So looking into that, with a slight modification the 36,783,575 calls to iter can be cut down to 20,254,744... like 45% which yields a much faster code. The thing is the input array must be sorted ascending.
So here comes a modified version of Nina's algorithm. (Be patient.. it will take like 25 secs to finalize)
function getSum(array, sum) {
function iter(index, temp) {cnt++ // counting iter calls -- remove in production code
var s = temp.reduce((a, b) => a + b, 0);
sum - s >= array[index] && iter(index, temp.concat(array[index]));
sum - s >= array[index+1] && iter(index + 1, temp);
s === sum && result.push(temp);
return;
}
var result = [];
array.sort((x,y) => x-y); // this is a very cheap operation considering the size of the inpout array should be small for reasonable output.
iter(0, []);
return result;
}
var cnt = 0,
arr = [14,6,10,7,3],
tgt = 500,
res;
console.time("combos");
res = getSum(arr,tgt);
console.timeEnd("combos");
console.log(`source numbers are ${arr}
found ${res.length} unique ways to sum up to ${tgt}
iter function has been called ${cnt} times`);
Part II
Eventhough i was impressed with the performance, I wasn't comfortable with above solution for no solid reason that i can name. The way it works on side effects and the very hard to undestand double recursion and such disturbed me.
So here comes my approach to this question. This is many times more efficient compared to the accepted solution despite i am going functional in JS. We have still have room to make it a little faster with ugly imperative ways.
The difference is;
Given numbers: [14,6,10,7,3]
Target Sum: 500
Accepted Answer:
Number of possible ansers: 172686
Resolves in: 26357ms
Recursive calls count: 36783575
Answer Below
Number of possible ansers: 172686
Resolves in: 1000ms
Recursive calls count: 542657
function items2T([n,...ns],t){cnt++ //remove cnt in production code
var c = ~~(t/n);
return ns.length ? Array(c+1).fill()
.reduce((r,_,i) => r.concat(items2T(ns, t-n*i).map(s => Array(i).fill(n).concat(s))),[])
: t % n ? []
: [Array(c).fill(n)];
};
var cnt = 0, result;
console.time("combos");
result = items2T([14, 6, 10, 7, 3], 500)
console.timeEnd("combos");
console.log(`${result.length} many unique ways to sum up to 500
and ${cnt} recursive calls are performed`);
Another important point is, if the given array is sorted descending then the amount of recursive iterations will be reduced (sometimes greatly), allowing us to squeeze out more juice out of this lemon. Compare above with the one below when the input array is sorted descending.
function items2T([n,...ns],t){cnt++ //remove cnt in production code
var c = ~~(t/n);
return ns.length ? Array(c+1).fill()
.reduce((r,_,i) => r.concat(items2T(ns, t-n*i).map(s => Array(i).fill(n).concat(s))),[])
: t % n ? []
: [Array(c).fill(n)];
};
var cnt = 0, result;
console.time("combos");
result = items2T([14, 10, 7, 6, 3], 500)
console.timeEnd("combos");
console.log(`${result.length} many unique ways to sum up to 500
and ${cnt} recursive calls are performed`);

Biggest sum from array without adding 2 consecutive value

I'm working on a challenge from codefights.com.
Given an array of integer (possibly negative) I need to return the biggest sum I can achieve without adding two consecutive integer (I can't change the order of the array).
Not easy to explain so here's a few examples:
input: [1, 2, 3, 4]: you're gonna pass the '1', take 2, can't take 3, take 4 and you get 6.
input: [1, 3, 1]: pass the '1', take 3 and you can't take 1 so you have 3.
I though I had it with this code :
function solve(vals) {
var even=0; var odd=0;
for(var i=0; i<vals.length; i++){
if(i%2==0){
even+=vals[i];
} else {
odd+=vals[i];
}
}
return Math.max(even, odd);
}
But then I got this testcase: [1,0,0,3] where it should return 4, skipping the two '0' which made me realize I've been looking at it all wrong.
And now I'm stuck, don't really know how to do it.
Any ideas ?
edit:
Using MrGreen's answer I got this:
function target_game(a) {
var dp=[], l=a.length-1;
dp[0]=a[0];
dp[1]=Math.max(a[0],a[1]);
for(var i=2; i<=a.length-1; i++){
dp[i]=Math.max(dp[i - 1], dp[i - 2] + a[i]);
}
return dp[l];
}
Which works fine unless the array contains negative value.
This input: [-1,0,1,-1] returns 0.
I'm still working on a fix but I'm editing the question to have a bullet proof solution :p
This is a classical dynamic programming problem.
Define dp[i] to be the maximum sum we can get if we consider the elements from 0 to i.
Then dp[i] = max(dp[i - 1], dp[i - 2] + a[i])
The intuition behind this, if you takea[i] in the sum then you cannot take a[i - 1]
Base cases: dp[0] = max(0, a[0]) and dp[1] = max(0, a[0], a[1])
You can check this lesson:
part-1 part-2 part-3 part-4
Here is the "best" answer from the challenge (shortest actually):
function solve(a) {
b = t = 0
for (i in a) {
c = b + a[i]
b = t
t = c > t ? c : t
}
return t
}
Here is a version where I renamed the variables to make it more understandable:
function solve(vals) {
prevTotal = total = 0
for (i in vals) {
alt = prevTotal + vals[i]
prevTotal = total
total = alt > total ? alt : total
}
return total
}

Adding numbers together

I want to loop over an array whilst addding the numbers together.
Whilst looping over the array, I would like to add the current number to the next.
My array looks like
[0,1,0,4,1]
I would like to do the following;
[0,1,0,4,1] - 0+1= 1, 1+0= 1, 0+4=4, 4+1=5
which would then give me [1,1,4,5] to do the following; 1+1 = 2, 1+4=5, 4+5=9
and so on until I get 85.
Could anyone advise on the best way to go about this
This transform follows the specified method of summation, but I also get an end result of 21, so please specify how you get to 85.
var ary = [0,1,0,4,1],
transform = function (ary) {
var length = ary.length;
return ary.reduce(function (acc, val, index, ary) {
if (index + 1 !== length) acc.push(ary[index] + ary[index + 1]);
return acc;
}, []);
};
while (ary.length !== 1) ary = transform(ary);
If you do in fact want the answer to be 21 (as it seems like it should be), what you are really trying to do is closely related to the Binomial Theorem.
I am not familiar with javascript, so I will write an example in c-style pseudocode:
var array = [0,1,0,4,1]
int result = 0;
for (int i = 0; i < array.length; i++)
{
int result += array[i] * nChooseK(array.length - 1, i);
}
This will put the following numbers into result for each respective iteration:
0 += 0 * 1 --> 0
0 += 1 * 4 --> 4
4 += 0 * 6 --> 4
4 += 4 * 4 --> 20
20 += 1 * 1 --> 21
This avoids all the confusing array operations that arise when trying to iterate through creating shorter-and-shorter arrays; it will also be faster if you have a good nChooseK() implementation.
Now, finding an efficient algorithm for a nChooseK() function is a different matter, but it is a relatively common task so it shouldn't be too difficult (Googling "n choose k algorithm" should work just fine). Some languages even have combinatoric functions in standard math libraries.
The result I get is 21 not 85. This code can be optimised to only use single array. Anyway it gets the job done.
var input = [0, 1, 0, 4, 1];
function calc(input) {
if (input.length === 1) {
return input;
}
var result = [];
for (var i = 0; i < input.length - 1; i++) {
result[i] = input[i] + input[i + 1];
}
return calc(result);
}
alert(calc(input));
This is an O(n^2) algorithm.

StakeOut: Find Optimal Expectation Using Recursion

I was recently presented with a coding assignment by a startup I was interviewing with. They gave me the following problem and asked for a solution:
Congratulations! You are the new elite hacker in a group of villainous
ne'er-do-wells.
Luckily this group is more saavy than your last band of ruffians, and
they are looking to software (and you) to improve their take. The con
man for the team, has gone door-to-door down each street posing as a
termite inspector so he could covertly total the valuable goods in
each house. Normally the gang would just rob all the valuable homes,
but there's a catch! Whenever a house is robbed in this wealthy
neighborhood, the police watch it and the neighboring houses for
months.
So the gang can't simply rob all the homes, and if they choose to rob
one, they can no longer rob the house on either side of it.
The ringleader wants to know what houses he should rob to maximize the
team's profit, and he wants to know now. Write a function that takes
in an array of positive integers (home values) and returns the maximum
expected value of robbing that street.
For example:
[ 20, 10, 50, 5, 1 ] should return $71, as robbing the first, third,
and fifth houses is optimal [ 20, x, 50, x, 1 ]
[ 20, 50, 10, 1, 5 ] should return $55, as robbing the second and
fifth houses is optimal [ x, 50, x, x, 5 ]
Basically, you can only sum the values of alternative houses because if a house is robbed, the houses immediately before and after it cannot be robbed, due to heavy security in the street.
I wrote a solution using recursion and presented it to them.
I presented a solution which accounted for all cases except for the following:
[2, 3, 2], for which the returned answer was 3, instead of 4.
They told me to fix the bug. Since, it was the only case for which the algorithm didn't work, I used the following code to fix the problem (written in JavaScript):
// The recursive alrogithm doesn't account for arrays of length 3.
if(array.length === 3) {
if(array[0] + array[2] > array[1]) {
return 'Optimal expectation of robbing street: ' + String(array[0] + array[2]);
} else{
return 'Optimal expectation of robbing street: ' + String(array[1]);
}
}
So the complete final solution is as follows (with the above snippet of code included):
// Global variable for expectation:
var expectation = 0;
function optimalExpectation(array) {
// 'array' contains the home values
// The recursive alrogithm doesn't account for arrays of length 3.
if(array.length === 3) {
if(array[0] + array[2] > array[1]) {
return 'Optimal expectation of robbing street: ' + String(array[0] + array[2]);
} else{
return 'Optimal expectation of robbing street: ' + String(array[1]);
}
}
// Base case for recursion:
if(array.sum() === 0) {
return 'Optimal expectation of robbing street: ' + String(expectation);
} else{
expectation += array.max();
var maxIndex = array.indexOf(array.max());
// Recursive call:
return optimalExpectation(injectZeros(array, maxIndex));
}
}
//===============================================================================
// Protypal methods for maximum & sum:
// All array objects inherit these methods from the Array prototype:
Array.prototype.max = function(){
if(this.length === 1){
return this[0];
} else if(this.length === 0){
return null;
}
var maximum = 0;
for(var i = 0; i < this.length; i++){
if(maximum < Math.max(this[i], this[i + 1])){
maximum = Math.max(this[i], this[i + 1]);
}
}
return maximum;
};
Array.prototype.sum = function(){
var sum = 0;
this.forEach(function(el){
sum += el;
});
return sum;
};
// Function to replace maximum values, already accounted for, with zeroes:
function injectZeros(array, index){
if(array.length > 0){
if(index < array.length - 1)
array[index + 1] = 0;
if(index > 0)
array[index - 1] = 0;
}
array[index] = 0;
return array;
}
//==================================================================================
console.log(optimalExpectation([2, 3, 2])); // correctly returns 4, instead of 3
I was rejected after I submitted the above solution. They didn't reject me after my initial solution (they definitely could have). They wanted me to explain my thought process and how I fixed the bug.
I would appreciate any input on where I might have gone wrong. Is there a way to improve my algorithm? Is my code well organized? Is this the right approach? If you are a professional developer who has knowledge of how hiring decisions are made in startups, could you elaborate on what might have lead the team to reject me?
You should go a watch Professor Roughgarden's lecture about Weighted Independent Set in Path Graphs, https://class.coursera.org/algo2-2012-001/lecture/83. It uses Dynamic Programming to solve the interview question. The solution is fairly trivial. You can do it using either recursion or iterations.
I am not a hiring manager. But a quick inspection says that you only picked biggest houses as much as you could. The problem is conceptual, and you swept it under the rug instead of fixing your algorithm (the issue would also present itself with [2, 3, 2, 0.5], as you pick 3 then 0.5, instead of the optimal 2 and 2 - there is nothing special about length 3). The correct solution would at least use backtracking to explore if picking different elements would have made more profit, rather than greedily picking the local maximum without regard to context. It is sufficient to inspect the maximum and the neighbours at each step.
var values = [2, 3, 2, 0.5];
function rob(values) {
if (values.every(function(e) { return e === null })) return [];
var len = values.length;
var max = Math.max.apply(null, values);
var subMax = -Infinity;
var subArray = [];
for (var i = 0; i < len; i++) {
if (values[i] !== null && (values[i - 1] === max || values[i] === max || values[i + 1] === max)) {
nextValues = values.slice();
nextValues[i] = null;
if (i > 0) nextValues[i - 1] = null;
if (i < len - 1) nextValues[i + 1] = null;
var subResult = rob(nextValues);
var subSum = subResult.reduce(function(a, b) { return a + b; }, 0) + values[i];
if (subSum > subMax) {
subArray = subResult;
subArray.push(values[i]);
subMax = subSum;
}
}
}
return subArray;
};
alert(rob(values));
function computeRob (arr, index, canRob) {
if(arr.length == index){
return 0;
}
if (canRob == true) {
var withRob = arr[index] + computeRob(arr, index + 1, false);
var withoutRob = computeRob(arr, index + 1, true);
return Math.max(withRob, withoutRob)
} else {
return withoutRob = computeRob(arr, index + 1, true);
}
}
console.log(computeRob([4,3,8,9,2],0,true));

Categories

Resources