3

I've had a look around this site but I have been unable to find an answer that includes duplicate elements. For example, given the array:

[1,2,3,4]

With a length of 3, A function should generate a list of every single possible combination with those numbers, using each one more than once:

[
  [1,1,1],
  [1,1,2],
  [1,1,3],
  ...
  [4,4,2],
  [4,4,3],
  [4,4,4]
]

I just haven't been able to get my head around the algorithm that I should use. I don't expect a code answer, but a push in the right direction would be appreciated.

I've tried using reduce like so:

const arr = [1, 2, 3, 4]
const len = 3

arr.reduce((acc, n) => {
    for (let i = 0; i < len; i++) {
        acc.push(/* ???? */)
    }
    return acc
}, [])

but I really don't know how to continue.

As a side note, ideally, I would like to do this as efficiently as possible.

KEKW
  • 31
  • 2
  • 1
    Does this answer your question? [Permutations in JavaScript?](https://stackoverflow.com/questions/9960908/permutations-in-javascript) – Aksen P Dec 02 '19 at 09:56
  • 2
    @AksenP No. Like I mentioned, these answers don't use duplicates, but only each of the elements once – KEKW Dec 02 '19 at 09:58
  • You can inspect their algorithms and turn on duplicates. Or you cannot read the others code? – Aksen P Dec 02 '19 at 09:59

5 Answers5

3

One approach would be to use the length of the array as a base. You could then just access the array's elements from 0, and count up to the amount of combinations (array length ** length). If you're working with a small dataset, performance really shouldn't be an issue, but this answer is very performance oriented:

const getCombos = (arr, len) => {
  const base = arr.length
  const counter = Array(len).fill(base === 1 ? arr[0] : 0)
  if (base === 1) return [counter]
  const combos = []
  const increment = i => {
    if (counter[i] === base - 1) {
      counter[i] = 0
      increment(i - 1)
    } else {
      counter[i]++
    }
  }

  for (let i = base ** len; i--;) {
    const combo = []
    for (let j = 0; j < counter.length; j++) {
      combo.push(arr[counter[j]])
    }
    combos.push(combo)
    increment(counter.length - 1)
  }

  return combos
}

const combos = getCombos([1, 2, 3, 4], 3)
console.log(combos)
Kobe
  • 6,226
  • 1
  • 14
  • 35
  • Thanks for the answer, what is the line where you assign counter doing? – KEKW Dec 02 '19 at 09:56
  • It either assigns the counter to an array filled with zeros, or if the array given only has one element, it will fill it with that element and return it on the following line. – Kobe Dec 02 '19 at 09:59
2

You could take an algorithm for getting a cartesian prduct with an array of three arrays with the wanted values.

var values = [1, 2, 3, 4],
    length = 3,
    result = Array
        .from({ length }, _ => values)
        .reduce((a, b) => a.reduce((r, v) => r.concat(b.map(w => [].concat(v, w))), []));

console.log(result.map(a => a.join(' ')));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
2

Picture an ancient mechanical rotary counter:

enter image description here

To count from 00000 to 99999 you rotate the rightmost wheel until it reaches 9, then it resets to 0 and the second right wheel is advanced by 1 etc.

In code: locate the rightmost wheel which position is less than the max. digit. Advance that wheel by 1 and reset all wheels on the right of it to 0. Repeat until there's no such wheel.

function counter(digits, size) {
    let wheels = Array(size).fill(0),
        len = digits.length,
        res = [];

    while (1) {
        res.push(wheels.map(n => digits[n]));
        for (let i = size - 1; i >= 0; i--) {
            if (wheels[i] < len - 1) {
                wheels[i]++;
                wheels.fill(0, i + 1);
                break;
            }
            if (i === 0)
                return res;
        }
    }
}

all = counter('1234', 3)
console.log(...all.map(c => c.join('')))

Performance measures:

const kobe = (arr, len) => {
    const base = arr.length
    const counter = Array(len).fill(base === 1 ? arr[0] : 0)
    if (base === 1) return [counter]
    const combos = []
    const increment = i => {
        if (counter[i] === base - 1) {
            counter[i] = 0
            increment(i - 1)
        } else {
            counter[i]++
        }
    }

    for (let i = base ** len; i--;) {
        const combo = []
        for (let j = 0; j < counter.length; j++) {
            combo.push(arr[counter[j]])
        }
        combos.push(combo)
        increment(counter.length - 1)
    }

    return combos
}

function nina(values, length) {
    return Array
        .from({length}, _ => values)
        .reduce((a, b) => a.reduce((r, v) => r.concat(b.map(w => [].concat(v, w))), []));
}

function trincot(digits, size) {
    if (!size) return [[]]; // base case
    let shorter = trincot(digits, size - 1); // get all solutions for smaller size
    // ...and prefix each of those with each possible digit
    return Array.from(digits, dig => shorter.map(arr => [dig, ...arr])).flat();
}

function georg(digits, size) {
    let wheels = Array(size).fill(0),
        len = digits.length,
        res = [];

    while (1) {
        res.push(wheels.map(n => digits[n]));
        for (let i = size - 1; i >= 0; i--) {
            if (wheels[i] < len - 1) {
                wheels[i]++;
                wheels.fill(0, i + 1);
                break;
            }
            if (i === 0)
                return res;
        }
    }
}

const gilad = (A, k) =>
  k ? gilad(A, k - 1).reduce((a, s) => a.concat(A.map(e => s.concat(e))), []) : [[]]


//////////////////////////////////////////////////////////////

fns = [kobe, nina, trincot, georg, gilad];
ary = [0, 1, 2, 3, 4, 5, 6, 7, 8]
size = 5
res = []

for (f of fns) {
    console.time(f.name);
    res.push(f(ary, size));
    console.timeEnd(f.name)
}

Using the same technique to generate cartesian products:

function product(...arrays) {
    let size = arrays.length,
        wheels = Array(size).fill(0),
        lens = arrays.map(a => a.length),
        res = [];

    while (1) {
        res.push(wheels.map((n, w) => arrays[w][n]));
        for (let w = size - 1; w >= 0; w--) {
            if (wheels[w] < lens[w] - 1) {
                wheels[w]++;
                wheels.fill(0, w + 1);
                break;
            }
            if (w === 0)
                return res;
        }
    }
}

// demo

p = product('12', 'abcde', 'XYZ')
console.log(...p.map(q => q.join('')))

// some timings

// https://stackoverflow.com/a/43053803/989121
const f = (a, b) => [].concat(...a.map(d => b.map(e => [].concat(d, e))));
const cartesian = (a, b, ...c) => (b ? cartesian(f(a, b), ...c) : a);

arrays = Array(7).fill(0).map(_ => Array(5).fill(0)) // 5**7=78125 combinations

console.time('func')
cartesian(...arrays)
console.timeEnd('func')

console.time('iter')
product(...arrays)
console.timeEnd('iter')
georg
  • 211,518
  • 52
  • 313
  • 390
  • I gave this one the upvote because code should be simple to understand and the concept of just putting the values on a selector (wheel), adding more wheels for each value, and then just running through them is the easiest for any beginner programmer to understand. If I saw the other two answers as a programmer in real code I would have to stop and look up some meanings to make sure it makes sense and works. KISS applies here. – Guy Coder Dec 02 '19 at 11:47
1

Maybe also add the recursive solution:

function counter(digits, size) {
    if (!size) return [[]]; // base case
    let shorter = counter(digits, size-1); // get all solutions for smaller size
    // ...and prefix each of those with each possible digit
    return Array.from(digits, dig => shorter.map(arr => [dig, ...arr])).flat();
}
// demo
let all = counter("1234", 3);
console.log(...all.map(c => c.join("")));
trincot
  • 317,000
  • 35
  • 244
  • 286
1

This is known as "n multichoose k" and has the following recurrence relation:

function f(ns, n, k){
  if (n == 0)
    return []
  if (k == 0)
    return [[]]
    
  return f(ns, n - 1, k).concat(
    f(ns, n, k - 1).map(s => s.concat(ns[n-1])))
}

var multiset = [1, 2, 3, 4]
var k = 3
console.log(JSON.stringify(f(multiset, multiset.length, k)))

An alternative, as others have answered, is to also include every permutation of every combination. One way is to append each element to each combination as we build towards the final length. (This idea is similar to trincot's.)

const f = (A, k) =>
  k ? f(A, k - 1).reduce((a, s) => a.concat(A.map(e => s.concat(e))), []) : [[]]

var A = [1, 2, 3, 4]
var k = 3
console.log(JSON.stringify(f(A, k)))
גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61
  • unfortunately, this doesn't seem to be correct, e.g. missing `1,2,1`, `1,3,1` etc. – georg Dec 03 '19 at 08:02
  • @georg you might ask the OP to clarify. As it stands, this answer corresponds with the OP's description: "function should generate a list of every single possible combination with those numbers, using each one more than once." Perhaps yours is incorrect? – גלעד ברקן Dec 03 '19 at 08:51
  • @georg by the way, mine is one of two up votes on your answer :) – גלעד ברקן Dec 03 '19 at 08:53
  • thanks ;) I think the description is quite clear: given a set of N digits, produce all N-ary numbers of length L. – georg Dec 03 '19 at 08:55
  • @georg those are your words, not the OP's. But the words I quoted *were*. And their example doesn't make it any clearer either since it misses the 121. – גלעד ברקן Dec 03 '19 at 08:59
  • @georg (I added the alternative others have interpreted the problem as.) – גלעד ברקן Dec 03 '19 at 15:00