4

Say I have a list of N members:

const list = [0, 1, 2, ...(N-1)];

I want to do (N choose X), mathematically, so I need to create a function:

const findAllCombinations = (x, list) => {
     // return all x combinations of the list
};

if X were 2, I could do this:

const findAllCombinations = (x, list) => {
    for(let i = 0; i < list.length; i++){
      for(let j = i+1; j < list.length; j++){
           // N choose 2
       }
     }
};

but not certain off-hand how to loop in a way to capture N choose X, it would be nice to do this iteratively instead of recursively if possible! But a recursive solution would be just fine.

Here is my attempt, but it's wrong:

 const combine = (x, list) => {
    
    // Note: N = list.length

    if(list.length < x){
        throw new Error('not enough elements to combine.');
    }

    if (x < 1) {
        return [];
    }

    const ret = [];

    for(let v of combine(x-1, list.slice(1))){
        ret.push([list[0], ...v]);
    }

    return ret;
}
    

console.log(
   combine(3, ['a','b,'c','d'])
)

the goal would be to get these 4 combinations:

[a,b,c]
[a,b,d]
[a,c,d]
[b,c,d]

..because (4 choose 3) = 4.

Here is my desired output:

combine(0,[1,2,3]) => [[]] // as  N choose 0 = 1
combine(1,[1,2,3]) => [[1],[2],[3]] // as  N choose 1 = N
combine(2,[1,2,3]) => [[1,2],[1,3],[2,3]]]] // as  N choose N-1 = N
combine(3,[1,2,3]) => [[1,2,3]] // as  N choose N = 1

to see a better list of desired output, see: https://gist.github.com/ORESoftware/941eabac77cd268c826d9e17ae4886fa

Alexander Mills
  • 90,741
  • 139
  • 482
  • 817
  • Note that (N choose X) = (N choose (N-X)) – Alexander Mills Oct 29 '22 at 01:17
  • 1
    Assuming you want a 2D array, "(N choose X) = (N choose (N-X))" is only correct in terms of array length of the first dimension. But then if you store everything in a 2D array, it's easy to run out of memory. It should be better to use a structure that doesn't store everything at the same time, but generates the possibilities one by one. – qrsngky Oct 29 '22 at 01:17
  • @qrsngky sure agreed – Alexander Mills Oct 29 '22 at 01:29
  • 1
    Does this answer your question? "[Compute all possible combinations of a certain number group with no regard to the order](/q/20768746/90527)", "[Given an array, how to generate all combinations of subset size k?](/q/46880094/90527)", "[Algorithm to return all combinations of k elements from n](/q/127704/90527)" – outis Oct 29 '22 at 02:41
  • here is a link to desired output: https://gist.github.com/ORESoftware/941eabac77cd268c826d9e17ae4886fa – Alexander Mills Oct 29 '22 at 15:16
  • I found that the iterative approaches can be hard to read. It maybe better to use recursion, assuming you're not trying hundreds of elements. Your current code always chooses list[0], but in reality it should be able to choose from list[0] to list[n-k]. For example, for 5 choose 2, list =["a", "b", "c", "d", "e"], it should be able to choose "d" (list[3]) in the first pass, as ["d", "e"] is a valid outcome. And also, I think it may be better to give an empty result than throwing errors when there are not enough elements? – qrsngky Oct 30 '22 at 06:50

4 Answers4

4

Here is an iterative approach that uses combinations of indexes from the given set.

Starting from an initial combination of k indexes, those are shifted/incremented in-place in order to obtain the next combination of length k, and so on :

function * combinations(set, k) {
  const n = set.length;
  if (k > n)
    return;

  // Shift combi indexes to make it the next k-combination. Return true if
  // successful, false otherwise (when there is no next of that length).
  const next = (combi, i) => {
    if (++combi[i] < n)
      return true;
    let limit = n;
    while (i > 0) {
      if (++combi[--i] < --limit) {
        let idx = combi[i];
        while (++i < combi.length)
          combi[i] = ++idx;
        return true;
      }
    }
    return false;
  };

  const combi = Array.from(Array(k), (_, i) => i);
  const i = k-1;

  do yield combi.map(j => set[j]);
  while (next(combi, i));
}

This is much more efficient that one might think, especially when compared to a recursive algorithm that always start from the the empty combination regardless of k (the function next() could probably be optimized though).

A more sophisticated version, which allows to specify a list of values for k and whether or not to allow repetitions, can be found here (and just above it the recursive implementation).

EricLavault
  • 12,130
  • 3
  • 23
  • 45
  • thanks, I updated the OP, does your output much the desired output in the OP? – Alexander Mills Oct 29 '22 at 15:10
  • here is a link to desired output: https://gist.github.com/ORESoftware/941eabac77cd268c826d9e17ae4886fa – Alexander Mills Oct 29 '22 at 15:16
  • Yes it matches the desired output, the main difference is that, being a generator function, either you iterate each combination individually `for (const c of combinations(set, k)) {...}`, either you collect all of them at once `console.log([...combinations(set, k)])`; – EricLavault Oct 29 '22 at 15:30
  • Also you can easily make it a regular function by replacing `yield ` by `A.push()` where A is the output array, replacing `return;` by `return [];` and of course adding `return A;` at the end. – EricLavault Oct 29 '22 at 15:36
  • the generator makes it pretty convenient in fact, for sure. you could skip the generator and pass a fn with an operation do for each combination, but this is cool – Alexander Mills Oct 29 '22 at 16:13
  • do you see any potential in the code I have in OP? I feel like it's close. I like your code but it would be hard for me to debug/update later if I needed to. – Alexander Mills Oct 29 '22 at 16:14
  • The issue is that the code never enter the for loop : the recursion goes until x=0, which is the bottom case where an empty array is returned, but since it is empty, there is no `v` at all in the parent call for x=1, and thus for every parent calls. Also there is no condition for checking if the current combination length == x before push. – EricLavault Oct 29 '22 at 18:52
3

Here is a more readable rewrite of @EricLavault's code, using the same iterative algorithm.

//Creates an iterator over (n C k)
function* findAllCombinations(k, arr) {
  const n = arr.length

  if (k > n)
    return
  
  //Array of indices of elements of `arr`; this is our current combination
  const iArr = Array.from({length: k}, (_, i) => i)
  
  function next() {
    //Find the last index that has room to advance without resetting
    const toAdvance = iArr.findLastIndex((i, j) => n-i > k-j)
    //                                             ^^^   ^^^
    //                                             |||   +++--- Number of elements that have to be chosen 
    //                                             +++--------- Number of elements that we can choose from
    
    //If there's no such index, we've reached the last combination
    if(toAdvance === -1)
      return false

    //Advance the index
    iArr[toAdvance]++

    //Reset all indices after the advanced one to (previous + 1)
    for(let i = toAdvance + 1; i < k; i++)
      iArr[i] = iArr[i-1] + 1

    return true
  }
  
  do 
    //Map indices to real values from the array
    yield iArr.map(e => arr[e])
  while(next())
}

for(const v of findAllCombinations(2, ['a', 'b', 'c', 'd', 'e'])) 
  console.log(v)

The .findLastIndex() method is a very recent addition to the language, so it may not be supported by all platforms.

It's possible to do without that, but I think it's cleaner to just implement findLastIndex separately and use that.

function findLastIndex(arr, predicate) {
  for(let i = arr.length - 1; i >= 0; i--) {
    if(predicate(arr[i], i, arr)) {
      return i
    }
  }
  return -1
}

//Creates an iterator over (n C k)
function* findAllCombinations(k, arr) {
  const n = arr.length

  if (k > n) {
    return
  }
  
  //Array of indices of elements of `arr`; this is our current combination
  const iArr = Array.from({length: k}, (_, i) => i)
  
  function next() {
    //Find the last index that has room to advance without resetting
    const toAdvance = findLastIndex(iArr, (i, j) => n-i > k-j)
    //                                              ^^^   ^^^
    //                                              |||   +++--- Number of elements that have to be chosen 
    //                                              +++--------- Number of elements that we can choose from
    
    //If there's no such index, we've reached the last combination
    if(toAdvance === -1) {
      return false
    }

    //Advance the index
    iArr[toAdvance]++

    //Reset all indices after the advanced one to (previous + 1)
    for(let i = toAdvance + 1; i < k; i++) {
      iArr[i] = iArr[i-1] + 1
    }

    return true
  }
  
  do {
    //Map indices to real values from the array
    yield iArr.map(e => arr[e])
  } while(next())
}

for(const v of findAllCombinations(2, ['a', 'b', 'c', 'd', 'e'])) {
  console.log(v)
}
FZs
  • 16,581
  • 13
  • 41
  • 50
1

Here is a recursive approach I got from modifying your code:

const combine = (x, list) => {
  if (x < 1) return [[]];

  let N = list.length;
  if (N < x) return [];
 
  const ret = [];
  for (let i = 0; i <= N - x; i++) {
    for (let v of combine(x - 1, list.slice(i + 1))) {
      ret.push([list[i], ...v]);
    }
  }
  return ret;
}

console.log(JSON.stringify(combine(0, [1, 2, 3])));
console.log(JSON.stringify(combine(1, [1, 2, 3])));
console.log(JSON.stringify(combine(2, [1, 2, 3])));
console.log(JSON.stringify(combine(3, [1, 2, 3])));

console.log(JSON.stringify(combine(0, [1, 2, 3, 4])));
console.log(JSON.stringify(combine(1, [1, 2, 3, 4])));
console.log(JSON.stringify(combine(2, [1, 2, 3, 4])));
console.log(JSON.stringify(combine(3, [1, 2, 3, 4])));
console.log(JSON.stringify(combine(4, [1, 2, 3, 4])));

I originally wrote a base case for choosing 1 element (if (x === 1) return list.map(e => [e]);), but it turns out that it isn't actually necessary, my base case for "x<1" works just fine.

The i <= N - x part was not that obvious to come up with, but you could replace it with i < N (easier to understand but less efficient). It still works since there is no error thrown; when there are not enough elements, the recursion will just give you [], so "let v of ..." won't really do anything.

But if you changed the "not enough elements" case to if (N < x) throw new Error("not enough elements");, then it works fine with i <= N-x but throws an error if you used i < N.

qrsngky
  • 2,263
  • 2
  • 13
  • 10
0

It turned out we can avoid using recursions.

I thought of itertools.combinations in Python.
CPython is open source so we can see the source code in C. https://github.com/python/cpython/blob/main/Modules/itertoolsmodule.c
You can see the function definitions for itertools_combinations_impl and combinations_next.

There are those refCount and garbage collector stuff, though, which may not be relevant to another language.

I attempted to write a version of "combinationsobject" in TypeScript and added some tests after that ( TS playground ), then it's easy to get JS from the compilation output:

"use strict";
//this class is based on https://github.com/python/cpython/blob/main/Modules/itertoolsmodule.c
class CombinationObject {
    constructor(list, r) {
        let n = list.length;
        this.pool = list;
        this.indices = [];
        for (let i = 0; i < r; i++)
            this.indices[i] = i;
        this.result = null;
        this.r = r;
        this.stopped = r > n;
    }
    next() {
        if (this.stopped)
            return null;
        let { pool, indices, r } = this;
        if (this.result === null) {
            //first pass 
            this.result = [];
            for (let i = 0; i < r; i++) {
                let index = indices[i];
                this.result[i] = pool[index];
            }
        }
        else {
            this.result = this.result.slice();
            let n = pool.length;
            let i; //needs to be used outside the for loop 
            /* Scan indices right-to-left until finding one that is not at its maximum (i + n - r). */
            for (i = r - 1; i >= 0 && indices[i] == i + n - r; i--) { }
            if (i < 0) {
                this.stopped = true;
                return null;
            }
            /* Increment the current index which we know is not at its maximum.
              Then move back to the right setting each index to its lowest possible value (one higher than the index to its left).
            */
            indices[i]++;
            for (let j = i + 1; j < r; j++) {
                indices[j] = indices[j - 1] + 1;
            }
            /* Update the result for the new indices starting with i, the leftmost index that changed */
            for (; i < r; i++) {
                let index = indices[i];
                this.result[i] = pool[index];
            }
        }
        return this.result;
    }
}
let list = ["One", 2, "Three", 4, 5, 'Six'];
let K = 3;
const LIMIT = 1000;
let co = new CombinationObject(list, K);
let count = 0;
while (!co.stopped && count < LIMIT) {
    let result = co.next();
    if (result === null)
        break;
    console.log(result);
    count++;
}
function factorial(n) {
    let t = 1;
    for (let i = 2; i <= n; i++)
        t *= i;
    return t;
}
function nCk(n, k) {
    if (k > n)
        return 0;
    return factorial(n) / factorial(n - k) / factorial(k);
}
console.log(nCk(list.length, K));
console.log(count);
qrsngky
  • 2,263
  • 2
  • 13
  • 10