0

I am working on a problem where I have to find all the possible subsets (powerSet) of an array. I am using Recursive backtracking for this. I was playing with the solution to get a feel for how the recursive calls are connected. I am having trouble understanding some parts of it. For example, this is the array from where I want to make subsets.

var arr = ['apple', 'banana', 'orange'];

I am using Javascript. The algorithm is: At each recursive step, I am either including the current element or not.

Here is the solution code:


// Code that works
var makeSubset = function (arr) {

  var output = []; // Array for storing all the subsets

  var subset = function(arr, soFar, idx) { // Helper function

    if (idx >= arr.length) {
      output.push([...soFar]);
      return;
    }
  
     soFar.push(arr[idx]);
     subset(arr, soFar, idx + 1); 
     soFar.pop(); // But this works
     subset(arr, soFar, idx + 1);


  };

  subset(arr, [], 0);
  return output;
}
console.log(makeSubset(['apple', 'banana', 'ornage']))
// code that does not work
var makeSubset1 = function (arr) {

  var output = []; // Array for storing all the subsets

  var subset = function(arr, soFar, idx) { // Helper function

    if (idx >= arr.length) {
      output.push([...soFar]);
      return;
    }
  
     soFar.push(arr[idx]);
     subset(arr, soFar, idx + 1); 
     soFar = soFar.slice(0, soFar.length - 1);
     subset(arr, soFar, idx + 1);


  };

  subset(arr, [], 0);
  return output;
}
console.log(makeSubset1(['apple', 'banana', 'ornage']))

Now, I have changed the solution a bit to see how it affects. Instead of passing the modified soFar array as arguments, I am changing it outside.

soFar.push(arr[idx);

As I have changed the soFar array, and it is a reference, I undo the changes when considering not including the current element.

soFar = soFar.slice(0, soFar.length - 1);

But the above line does not produce the correct output which the following line does.

soFar.pop();

My question is: Don't they do the same thing here? I understand it has something to do with the reference but I am not understanding where is the problem. Also, any general suggestion while working with reference type data structure like an array in recursion backtracking. Any idea would be greatly appreciated. Here is the code snippet that works:

h_a
  • 119
  • 2
  • 8
  • They sould work the same, are you getting errors in console? – AlexSp3 Jul 30 '21 at 16:22
  • This is the reuslt with soFar.slice() code [ [ 'apple', 'banana', 'orange' ], [ 'apple', 'banana' ], [ 'apple', 'banana', 'orange' ], [ 'apple', 'banana' ], [ 'apple', 'banana', 'banana', 'orange' ], [ 'apple', 'banana', 'banana' ], [ 'apple', 'banana', 'banana', 'orange' ], [ 'apple', 'banana', 'banana' ] ] – h_a Jul 30 '21 at 16:30
  • Can you please provide the snippet of the code that does not work? Right now it is confusing to see code mixed together, where we have to understand which parts are in and which parts are out, in order to get the malfunctioning code. Just provide two separate, runnable snippets, which also *call* the function with sample input, demonstrating that one snippet works and the other not, just by us running it. – trincot Jul 30 '21 at 17:15
  • Hi @trincot, I have provided a snippet containing both functioning and non-functioning code. Kindly let me know if that helps. Thank you. – h_a Jul 30 '21 at 17:37
  • [How do I create a runnable stack snippet?](https://meta.stackoverflow.com/questions/358992) – Scott Sauyet Jul 30 '21 at 17:48

3 Answers3

3

The working code uses one array object, which extends and shrinks in its life time during the complete recursion exercise.

When the current state of that array needs to be registered as an output, a copy is made of it, but the important thing here is that each soFar variable (a local variable in each execution context) is always the same array reference.

Also, as each push is mirrored by a corresponding pop, the caller of the makeSubset function can be assured that the soFar array is in the same state after the call as it had before the call. This invariant is essential to the correct functioning of the algorithm.

In the malfunctioning version, the following code creates a new array:

 soFar = soFar.slice(0, soFar.length - 1);

This is not a problem during the next recursive call, but it is a problem for the caller, because now that caller will get back an array that still has element(s) in it that were added by the push actions in the recursive call(s). The reason is that the above assignment does not modify the array that the caller had passed as argument, but creates a new array and leaves the original array with the pushed element still included.

trincot
  • 317,000
  • 35
  • 244
  • 286
  • Thank you very much for your response. It makes much more sense now. Still, I am a little bit unclear about the last part of your response. I understood that the _caller_ is getting the old array with the pushed element included, but after slicing the array, I have assigned it to the _soFar_ reference again, so should not it refer to the new array where the element has been excluded? – h_a Jul 30 '21 at 18:09
  • No, *assigning* to a variable *never* changes what the caller has passed as value for that local variable. This follows from the call-by-value system that JavaScript (and many other languages) use. By assigning to a parameter variable, you detach yourself from what the caller passed. The only way to make changes that the caller will see, is to *mutate* that parameter variable... and this is what `push` and `pop` do. – trincot Jul 30 '21 at 18:16
1

trincot gave you the reason that your change doesn't work.

I would like to point out that a fundamental reason for the confusion is the continual mutation of our data. If you wrote this in an immutable style, you might get code easier to understand and much less likely to fail.

Here is one way I might write such a function:

const powerSet = (xs) =>
  xs.length == 0
    ? [[]]
    : powerSet (xs .slice (1)) .flatMap (s => [[xs [0], ...s], s])

    
console .log (powerSet (['a', 'b', 'c']))
.as-console-wrapper {max-height: 100% !important; top: 0}

(This generates its results in a different order than your version. We could certainly fix it up to match if that's important.)

Here we recur on the length of the array. If we hit zero, we return an array containing only the empty array. Otherwise we recursively call our function on the tail of the array, and for each result we return two values, one which includes the head value and one that doesn't. Combining these with flatMap turns this into a flat array containing all the subsets.

There are a number of variants of this same function. We might destructure the initial array like this:

const powerSet = ([x, ...xs]) =>
  x == undefined
    ? [[]]
    : powerSet (xs) .flatMap (s => [[x, ...s], s])

or we might use some reusable head and tail functions like this:

const head = (xs) => xs [0]
const tail = (xs) => xs .slice (1)

const powerSet = (xs) =>
  xs.length == 0
    ? [[]]
    : powerSet (tail (xs)) .flatMap (s => [[head (xs), ...s], s])

However we write it, we can notice that there's no variables being mutated along the way. To my mind, that's a huge win.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
0

I noticed that the error actually happens with the push() method. I have cut some parts of the code to make it simple. This is using push():

var arr = ['apple', 'banana', 'orange'];

var subset = function(soFar = [], idx = 0) {
  if (idx >= arr.length) {
    console.log(soFar);
    return;
  }
  
  //soFar = soFar.concat(arr[idx]);
  soFar.push(arr[idx]);
  
  subset(soFar, idx + 1); 

  soFar = soFar.slice(0, soFar.length - 1); 
  subset(soFar, idx + 1);
};

console.log(subset());

You can see that it´s not giving the result you expect. This is using concat(), similar to push:

var arr = ['apple', 'banana', 'orange'];

var subset = function(soFar = [], idx = 0) {
  if (idx >= arr.length) {
    console.log(soFar);
    return;
  }
  
  soFar = soFar.concat(arr[idx]);
  //soFar.push(arr[idx]);
  
  subset(soFar, idx + 1); 

  soFar = soFar.slice(0, soFar.length - 1); 
  subset(soFar, idx + 1);
};

console.log(subset());

Actually I cannot see the difference between both scripts, since push and concat should work the same. It seems that using push the soFar variable loses its context and get the context of the recently called function, like if it is a global variable. So this happens:


soFar.push(arr[idx]);
 
console.log(soFar);   // prints --> ['apple']    

subset(soFar, idx + 1); 

console.log(soFar);   // somehow prints --> ['apple', 'banana', 'orange'] after calling function

I hope it makes the problem clear.

E_net4
  • 27,810
  • 13
  • 101
  • 139
AlexSp3
  • 2,201
  • 2
  • 7
  • 24