更具体地说,我正在寻找一种算法A,它的输入为:
- 一个非负整数排序的多重集合M = {a1, a2, ..., an};
- 一个整数0 ≤ k ≤ n = |M|;
- 一个“访问者”回调函数V(以M的k组合作为输入);
- (可选)一个排序的k组合K(默认值:{a1, a2, ..., ak})。
例如,如果M= {0, 0, 1, 2},k= 2,K={0, 1},则执行A(M, k, V, K)将导致以此顺序将访问者回调V应用于每个k-组合{0, 1},{0, 2},{1, 2}。 一个重要的要求是该算法是非递归的。
更不重要的是,k组合被访问的确切顺序,只要顺序一致即可。例如,字典序顺序也可以。这个要求的原因是为了能够通过批量运行算法来访问所有k组合。
如果我的术语有任何歧义,在本文的其余部分中,我会提供一些定义,希望能澄清问题。
一个多重集合类似于一个集合,但允许重复。例如,M= {0, 0, 1, 2}是一个大小为4的多重集合。对于这个问题,我只关心有限的多重集合。此外,对于这个问题,我假设多重集合的元素都是非负整数。
将多重集合M的大小为k的任意子多重集合定义为k-combination。例如,M= {0, 0, 1, 2}的2-combinations是{0, 0},{0, 1},{0, 2}和{1, 2}。
与集合类似,多重集合元素的顺序不重要。(例如,M也可以表示为{2,0,1,0}或{1,2,0,0}等)但是我们可以定义多重集合的规范表示为其中元素(假定为非负整数)按升序排序的表示方式。在这种情况下,多重集合的任何k -组合集合本身都可以通过其成员的规范表示按字典顺序排序。(前面给出的所有M的2-组合序列展示了这样一种排序方式。)
更新:下面我尽可能忠实地将 rici的优雅算法从C++翻译成JavaScript,并在其周围包装了一个简单的包装器以符合问题的规范和符号。
function A(M, k, V, K) {
if (K === undefined) K = M.slice(0, k);
var less_than = function (a, b) { return a < b; };
function next_comb(first, last,
/* first_value */ _, last_value,
comp) {
if (comp === undefined) comp = less_than;
// 1. Find the rightmost value which could be advanced, if any
var p = last;
while (p != first && ! comp(K[p - 1], M[--last_value])) --p;
if (p == first) return false;
// 2. Find the smallest value which is greater than the selected value
for (--p; comp(K[p], M[last_value - 1]); --last_value) ;
// 3. Overwrite the suffix of the subset with the lexicographically
// smallest sequence starting with the new value
while (p !== last) K[p++] = M[last_value++];
return true;
}
while (true) {
V(K);
if (!next_comb(0, k, 0, M.length)) break;
}
}
演示:
function print_it (K) { console.log(K); }
A([0, 0, 0, 0, 1, 1, 1, 2, 2, 3], 8, print_it);
// [0, 0, 0, 0, 1, 1, 1, 2]
// [0, 0, 0, 0, 1, 1, 1, 3]
// [0, 0, 0, 0, 1, 1, 2, 2]
// [0, 0, 0, 0, 1, 1, 2, 3]
// [0, 0, 0, 0, 1, 2, 2, 3]
// [0, 0, 0, 1, 1, 1, 2, 2]
// [0, 0, 0, 1, 1, 1, 2, 3]
// [0, 0, 0, 1, 1, 2, 2, 3]
// [0, 0, 1, 1, 1, 2, 2, 3]
A([0, 0, 0, 0, 1, 1, 1, 2, 2, 3], 8, print_it, [0, 0, 0, 0, 1, 2, 2, 3]);
// [0, 0, 0, 0, 1, 2, 2, 3]
// [0, 0, 0, 1, 1, 1, 2, 2]
// [0, 0, 0, 1, 1, 1, 2, 3]
// [0, 0, 0, 1, 1, 2, 2, 3]
// [0, 0, 1, 1, 1, 2, 2, 3]
当然,这不是生产就绪的代码。特别是为了易读性,我省略了所有的错误检查。此外,生产环境中的实现可能会以不同的方式组织事物。(例如,在这里指定由
next_combination
使用的比较器选项将变得多余。) 我的主要目标是在一段可运行的代码中尽可能清晰地保留原始算法背后的思想。