47

I need to get all possible subsets of an array with a minimum of 2 items and an unknown maximum. Anyone that can help me out a bit?

Say I have the following array:

[1, 2, 3]

How do I get this?

[
    [1, 2],
    [1, 3],
    [2, 3],
    [1, 2, 3]
]
Penny Liu
  • 15,447
  • 5
  • 79
  • 98
Stephen Belanger
  • 6,251
  • 11
  • 45
  • 49

10 Answers10

70

After stealing this JavaScript combination generator, I added a parameter to supply the minimum length resulting in,

var combine = function(a, min) {
    var fn = function(n, src, got, all) {
        if (n == 0) {
            if (got.length > 0) {
                all[all.length] = got;
            }
            return;
        }
        for (var j = 0; j < src.length; j++) {
            fn(n - 1, src.slice(j + 1), got.concat([src[j]]), all);
        }
        return;
    }
    var all = [];
    for (var i = min; i < a.length; i++) {
        fn(i, a, [], all);
    }
    all.push(a);
    return all;
}

To use, supply an array, and the minimum subset length desired,

var subsets = combine([1, 2, 3], 2);

Output is,

[[1, 2], [1, 3], [2, 3], [1, 2, 3]]
BrunoLM
  • 97,872
  • 84
  • 296
  • 452
Anurag
  • 140,337
  • 36
  • 221
  • 257
21

Combinations, short one:

function combinations(array) {
  return new Array(1 << array.length).fill().map(
    (e1, i) => array.filter((e2, j) => i & 1 << j));
}

console.log(combinations([1, 2, 3]).filter(a => a.length >= 2))
Nick
  • 138,499
  • 22
  • 57
  • 95
zovio
  • 552
  • 5
  • 4
  • Nice, But it is an algorithm so 'terse' that it does deserve some more words than what you put there :) No need for much, just a little summary to give it some context. – user3658510 Aug 17 '21 at 17:36
  • 1
    `fill()` requires at least one value... this does not work – Jonathan Aug 25 '21 at 02:18
  • This is nice to read, but generates an empty value (the first array in the result)! – Sampgun Aug 12 '22 at 18:17
13

With a small tweak from this question, I hope my solution is more efficient because it uses bit operator to generate all the subsets.

var sets = (function(input, size) {
    var results = [], result, mask, i, total = Math.pow(2, input.length);
    for (mask = size; mask < total; mask++) {
        result = [];
        i = input.length - 1;

        do {
            if ((mask & (1 << i)) !== 0) {
                result.push(input[i]);
            }
        } while (i--);

        if (result.length >= size) {
            results.push(result);
        }
    }

    return results; 
})(['a','b','c','d','e','f'], 2);
console.log(sets);
Community
  • 1
  • 1
Dewey
  • 1,193
  • 14
  • 18
  • What if one only wanted subsets of one particular size? One option is to change `if(result.length >= size)` to `if(result.length === size)`. But this seems inefficient since it is still producing all 2^n subsets and then throwing out ones of the wrong size. Can your code be tweaked further to focus just on one size? – Gabe Conant Nov 05 '15 at 16:26
  • The results are backwards because of the reverse while loop. If a forward loop was used it would have the correct order – megawac Mar 25 '16 at 22:32
  • 1
    This is a good solution, but breaks down once your array reaches any unique assortment of greater than 16 characters. – mjwrazor Apr 21 '17 at 14:23
12

This algorithm cries for recursion... this is how i would do it

var arr = [1,2,3,4,5];
function getSubArrays(arr){
  if (arr.length === 1) return [arr];
  else {
   subarr = getSubArrays(arr.slice(1));
   return subarr.concat(subarr.map(e => e.concat(arr[0])), [[arr[0]]]);
  }
}
console.log(JSON.stringify(getSubArrays(arr)));

Another fancy version of the above algorithm;

var arr = [1,2,3,4,5],
    sas = ([n,...ns],sa) => !ns.length ? [[n]]
                                       : (sa = sas(ns),
                                          sa.concat(sa.map(e => e.concat(n)),[[n]]));

In order to understand whats going on lets go step by step

  • Up until we end up with an array of length 1 as argument we keep calling the same getSubArrays function with the tail of the argument array. So tail of [1,2,3,4,5] is [2,3,4,5].
  • Once we have a single item array as argument such as [5] we return [[5]] to the previous getSubArrays function call.
  • Then in the previous getSubArrays function arr is [4,5] and subarr gets assigned to [[5]].
  • Now we return [[5]].concat([[5]].map(e => e.concat(4), [[4]]) which is in fact [[5], [5,4], [4]] to the to the previous getSubArrays function call.
  • Then in the previous getSubArrays function arr is [3,4,5] and subarr gets assigned to [[5], [5,4], [4]].
  • and so on...
Redu
  • 25,060
  • 6
  • 56
  • 76
  • After running the snippet, I can tell this gets me the answer I'm looking for and I love the fact that it's using recursion. But I have trouble understanding it. @Redu could you perhaps explain how did you arrive to this method? – user3295436 Mar 23 '18 at 15:45
  • @user3295436 Thanks.. A more modern version of the very same algorithm and an explanation is added to the answer. – Redu Mar 24 '18 at 17:05
10

Here is a way to find all combinations using ECMAScript 2015 generator function:

function* generateCombinations(arr) {
  function* doGenerateCombinations(offset, combo) {
    yield combo;
    for (let i = offset; i < arr.length; i++) {
      yield* doGenerateCombinations(i + 1, combo.concat(arr[i]));
    }
  }
  yield* doGenerateCombinations(0, []);
}

for (let combo of generateCombinations([1, 2, 3, 4, 5])) {
  console.log(JSON.stringify(combo));
}

To restrict to a minimum size as requested in the question, simply ensure the length of the combination before yielding it:

function* generateCombinations(arr, minSize) {
  function* doGenerateCombinations(offset, combo) {
    if (combo.length >= minSize) {
      yield combo;
    }
    for (let i = offset; i < arr.length; i++) {
      yield* doGenerateCombinations(i + 1, combo.concat(arr[i]));
    }
  }
  yield* doGenerateCombinations(0, []);
}

for (let combo of generateCombinations([1, 2, 3, 4, 5], 2)) {
  console.log(JSON.stringify(combo));
}

Restricting at the point of yield allows a readable way to adapt this function to other common use cases, e.g., to choose all combinations of an exact size:

function* generateCombinations(arr, size) {
  function* doGenerateCombinations(offset, combo) {
    if (combo.length == size) {
      yield combo;
    } else {
      for (let i = offset; i < arr.length; i++) {
        yield* doGenerateCombinations(i + 1, combo.concat(arr[i]));
      }
    }
  }
  yield* doGenerateCombinations(0, []);
}

for (let combo of generateCombinations([1, 2, 3, 4, 5], 2)) {
  console.log(JSON.stringify(combo));
}
heenenee
  • 19,914
  • 1
  • 60
  • 86
  • I'm using your approach but the function times out when trying it on very large inputs. It's because we are generating all possible combinations first and then yield each one. How could we modify this function to make it more efficient? Here is a link to the function and description of the challenge: https://repl.it/E2QW/1 – Piotr Berebecki Nov 18 '16 at 14:53
  • @PiotrBerebecki it looks likes you need an `else` around your for loop. – heenenee Nov 18 '16 at 17:00
  • Thanks, I will try to test it out and let you know the outcomes. – Piotr Berebecki Nov 18 '16 at 17:02
  • I still try to wrap my head around this answer, but I really like the code. – Hinrich Nov 11 '18 at 15:41
2

Using binary numbers

// eg. [2,4,5] ==> {[],[2],[4],[5],[2,4],[4,5],[2,5], [2,4,5]}

var a = [2, 4, 5], res = [];
for (var i = 0; i < Math.pow(2, a.length); i++) {
    var bin = (i).toString(2), set = [];
    bin = new Array((a.length-bin.length)+1).join("0")+bin;
    console.log(bin);
    for (var j = 0; j < bin.length; j++) {
        if (bin[j] === "1") {
            set.push(a[j]);
        }
    }
    res.push(set);
}
console.table(res);
Darshan
  • 750
  • 7
  • 19
1

This may or may not benchmark out well, but it's another way of doing it and it's pretty concise:

const combinations = arr => arr.reduce((acc, item) => {
  return acc.concat(acc.map(x => [...x, item]));
}, [[]]);


console.log(combinations([1, 2, 3]).filter(a => a.length > 1));
BobRodes
  • 5,990
  • 2
  • 24
  • 26
0

I've modified the accepted solution a little bit to consider the empty set when min is equal to 0 (empty set is a subset of any given set).

Here is a full sample page to copy paste, ready to run with some output.

<html>

<head>

<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<title>All Subsets</title>

<script type="text/javascript">

// get all possible subsets of an array with a minimum of X (min) items and an unknown maximum
var FindAllSubsets = function(a, min) {
    var fn = function(n, src, got, all) {
        if (n == 0) {
            if (got.length > 0) {
                all[all.length] = got;
            }
            return;
        }
        for (var j = 0; j < src.length; j++) {
            fn(n - 1, src.slice(j + 1), got.concat([src[j]]), all);
        }
        return;
    }
    var all = [];

    // empty set is a subset of the set (only when min number of elements can be 0)
    if(min == 0)
      all.push([-1]); // array with single element '-1' denotes empty set

    for (var i = min; i < a.length; i++) {
        fn(i, a, [], all);
    }

    all.push(a);
    return all;
}

function CreateInputList(){
  var inputArr = [];
  var inputArrSize = 4;
  var maxInputValue = 10;
  for(i=0; i < inputArrSize; i++){
    var elem = Math.floor(Math.random()*maxInputValue);
    // make sure to have unique elements in the array
    while(inputArr.contains(elem)){ // OR - while(inputArr.indexOf(elem) > -1){
      elem = Math.floor(Math.random()*maxInputValue);
    }
    inputArr.push(elem);
  }
  return inputArr;
}

Array.prototype.contains = function(obj) {
    var i = this.length;
    while (i--) {
        if (this[i] === obj) {
            return true;
        }
    }
    return false;
}

function ArrayPrinter(arr){
  var csv = 'input = [';
  var i = 0;
  for(i; i<arr.length - 1; i++){
    csv += arr[i] + ', ';
  }
  csv += arr[i];

  var divResult = document.getElementById('divResult');
  divResult.innerHTML += csv + ']<br />';
}

// assumes inner array with single element being '-1' an empty set
function ArrayOfArraysPrinter(arr){
  var csv = 'subsets = ';
  var i = 0;
  for(i; i<arr.length; i++){
    csv += '[';
    var j = 0;
    var inArr = arr[i];
    for(j; j<inArr.length - 1; j++){
      csv += inArr[j] + ', ';
    }
    // array with single element '-1' denotes empty set
    csv += inArr[j] == -1 ? '&lt;E&gt;' : inArr[j];
    csv += ']';
    if(i < arr.length - 1)
      csv += '&nbsp;&nbsp;';
  }

  csv += ' &nbsp; (&#35; of subsets =' + arr.length + ')';

  var divResult = document.getElementById('divResult');
  divResult.innerHTML += csv + '<br />';
}

function Main(){
  // clear output
  document.getElementById('divResult').innerHTML = '';

  // sample run (min = 0)
  document.getElementById('divResult').innerHTML += '<hr/>MIN = 0 (must include empty set)<br />';
  var list = CreateInputList();
  ArrayPrinter(list);
  var subsets = FindAllSubsets(list, 0);
  ArrayOfArraysPrinter(subsets);
  document.getElementById('divResult').innerHTML += '<hr />';

  // sample run (min = 1)
  document.getElementById('divResult').innerHTML += 'MIN = 1<br />'; 
  var list = CreateInputList();
  ArrayPrinter(list);
  var subsets = FindAllSubsets(list, 1);
  ArrayOfArraysPrinter(subsets);
  document.getElementById('divResult').innerHTML += '<hr />';

  // sample run (min = 2)
  document.getElementById('divResult').innerHTML += 'MIN = 2<br />'; 
  var list = CreateInputList();
  ArrayPrinter(list);
  var subsets = FindAllSubsets(list, 2);
  ArrayOfArraysPrinter(subsets);
  document.getElementById('divResult').innerHTML += '<hr />';

  // sample run (min = 3)
  document.getElementById('divResult').innerHTML += 'MIN = 3<br />'; 
  var list = CreateInputList();
  ArrayPrinter(list);
  var subsets = FindAllSubsets(list, 3);
  ArrayOfArraysPrinter(subsets);
  document.getElementById('divResult').innerHTML += '<hr />';

  // sample run (min = 4)
  document.getElementById('divResult').innerHTML += 'MIN = 4<br />'; 
  var list = CreateInputList();
  ArrayPrinter(list);
  var subsets = FindAllSubsets(list, 4);
  ArrayOfArraysPrinter(subsets);
  document.getElementById('divResult').innerHTML += '<hr />';
}

</script>

</head>

<body>
  <input type="button" value="All Subsets" onclick="Main()" />
  <br />
  <br />
  <div id="divResult"></div>
</body>

</html>
Asciiom
  • 9,867
  • 7
  • 38
  • 57
Meeting Attender
  • 826
  • 11
  • 17
0

If element order is important:

// same values, different order:

[1,2]
[2,1]

[1,3]
[3,1]

Then you may also want to consider a permutation.

// ---------------------
// Permutation
// ---------------------
function permutate (src, minLen, maxLen){

    minLen = minLen-1 || 0;
    maxLen = maxLen || src.length+1;
    var Asource = src.slice(); // copy the original so we don't apply results to the original.

    var Aout = [];

    var minMax = function(arr){
        var len = arr.length;
        if(len > minLen && len <= maxLen){
            Aout.push(arr);
        }
    }

    var picker = function (arr, holder, collect) {
        if (holder.length) {
           collect.push(holder);
        }
        var len = arr.length;
        for (var i=0; i<len; i++) {
            var arrcopy = arr.slice();
            var elem = arrcopy.splice(i, 1);
            var result = holder.concat(elem);
            minMax(result);
            if (len) {
                picker(arrcopy, result, collect);
            } else {
                collect.push(result);
            }
        }   
    }

    picker(Asource, [], []);

    return Aout;

}

var combos = permutate(["a", "b", "c"], 2);


for(var i=0; i<combos.length; i++){
    var item = combos[i];
    console.log("combos[" + i + "]" + " = [" + item.toString() + "]");
}

BE WARNED !!! - Your machine can't handle arrays with >10 items.

  • If your array has 9 items, there are nearly 1 million combinations.
  • If your array has 12 items, there are over 1 billion combinations.
  • If your array has 15 items, there are over 3 trillion combinations.
  • If your array has 18 items, there are over 17 quadrillion combinations.
  • If your array has 20 items, there are over 6 quintillion combinations.
  • If your array has 21 items, there are over 138 sextillion combinations.
  • If your array has 22 items, there are over 3 zillion combinations.
bob
  • 7,539
  • 2
  • 46
  • 42
  • 1
    The question asks for combinations, not permutations – River Tam Jul 03 '14 at 15:17
  • From my understanding, a permutation is the (only?) way to derive all possible combinations. – bob Jul 09 '14 at 06:09
  • I think that's simply untrue. Create a distinct array of the same size, filled with booleans. Go through all possibilities of that array (each boolean goes through true or false), and, for each possibility, add each "true"'s corresponding value to a set. This will create all combinations in 2^n, but it will not create all permutations. You also didn't describe a process to extract the combinations from the set of permutations, just a way to create a large set of permutations. – River Tam Jul 09 '14 at 13:26
  • Yup you're right, I was thinking that element order also played a role. (Thinking as if one would want lock combinations rather than a unique value sets.) Permutations take into consideration element order, whereas combinations only seek unique sets. So if the returned sets from a permutation were sorted (individually), there would be many identical results. So now my understanding is better. – bob Jul 11 '14 at 18:16
0

First answer submission ever! Hope it helps someone. I used this for a similar recursive solution that involved returning an array of arrays, I found flatMap() method to be extremely useful.

var arr = [1,2,3,4,5];

function getAllCombos(arr){
   if(arr[0] === undefined) return [arr]
   return getAllCombos(arr.slice(1)).flatMap(el => [el.concat(arr[0]), el])
}
console.log(JSON.stringify(getAllCombos(arr)));

And if you want your console.log() output to start with [1, 2, 3, 4 , 5] instead of [5, 4, 3, 2, 1], you can add sort() method to the mix like this:

var arr = [1,2,3,4,5];

function getAllCombos(arr){
   if(arr[0] === undefined) return [arr]
   return getAllCombos(arr.slice(1)).flatMap(el => [el.concat(arr[0]).sort(), el])
}
console.log(JSON.stringify(getAllCombos(arr)));
Sector88
  • 21
  • 4