寻找以字典顺序访问多集的所有k组合的非递归算法

时间:2015-08-20 13:05:55

标签: algorithm combinations combinatorics

更具体地说,我正在寻找一种算法 A 作为其输入

  • 已排序的多个集合 M = { a 1 a 2 , ..., n }非负整数;
  • 整数0≤ k n = | M |;
  • “访客”回调 V (将 k - M 的组合作为输入);
  • (可选) M 的排序 k - 组合 K (默认: k -combination { <子> 1 <子> 2 ,..., <子> ķ })。

然后,算法将在lexicographic order中访问 M 的所有 k - 组合,从 K 开始,将回调 V 应用于每个。

例如,如果 M = {0,0,1,2}, k = 2, K = {0, 1},然后执行 A M k V K )将导致访问者回调 V 应用于 k -combinations {0,1},{0,2},{1,2}中的每一个这个订单。

关键要求是算法是非递归的。

不太重要的是访问 k 组合的精确排序,只要排序是一致的。例如,colexicographic order也可以。此要求的原因是能够通过批量运行算法来访问所有 k 组合。

如果我的术语有任何含糊之处,在本文的其余部分我会给出一些我希望澄清问题的定义。

multiset 就像一个集合,但允许重复。例如, M = {0,0,1,2}是一个大小为4的多集。对于这个问题,我只对有限多重集合感兴趣。此外,对于这个问题,我假设multiset的元素都是非负整数。

将多个 M k -combination 定义为 M的任何子多重集大小 k 。例如。 M = {0,0,1,2}的2种组合是{0,0},{0,1},{0,2}和{1,2}。< / p>

与集合一样,多集合元素的排序无关紧要。 (例如 M 也可以表示为{2,0,1,0}或{1,2,0,0}等)但我们可以定义规范表示< multiset的/ em>是元素(这里假设为非负整数)按升序排列的元素。在这种情况下,多个集合的 k - 组合的任何集合本身可以通过其成员的规范表示按字典顺序排序。 (先前给出的 M 的所有2种组合的序列表现出这样的排序。)

更新:下面我尽可能忠实地将rici's elegant algorithm从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使用的比较器的选项在这里变得多余。)我的主要目的是在一段有效的代码中尽可能清楚地保留原始算法背后的想法。

2 个答案:

答案 0 :(得分:1)

我检查了TAoCP的相关部分,但这个问题至多是一个练习。基本思路与算法L相同:首先尝试“增加”最不重要的位置,在成功增加后填充位置以获得最小允许值。

这里有一些可能有用的Python,但却渴望获得更好的数据结构。

def increment(M, K):
    M = list(M)  # copy them
    K = list(K)
    for x in K:  # compute the difference
        M.remove(x)
    for i in range(len(K) - 1, -1, -1):
        candidates = [x for x in M if x > K[i]]
        if len(candidates) < len(K) - i:
            M.append(K[i])
            continue
        candidates.sort()
        K[i:] = candidates[:len(K) - i]
        return K
    return None


def demo():
    M = [0, 0, 1, 1, 2, 2, 3, 3]
    K = [0, 0, 1]
    while K is not None:
        print(K)
        K = increment(M, K)

答案 1 :(得分:1)

在迭代编程中,为了组合K大小,你需要K for循环。首先,我们从排序的输入中删除重复,然后我们创建一个表示for..loop索引的数组。虽然索引数组不会溢出,但我们仍然会生成组合。

加法器函数模拟堆叠for循环中计数器的过程。在下面的实现中有一点改进的余地。

N = size of the distinct input
K = pick size
i = 0 To K - 1
for(var v_{i0} = i_{0}; v_{i} < N - (K - (i + 1)); v_{i}++) {
...
for(var v_{iK-1} = i_{K-1}; v_{iK-1} < N - (K - (i + 1)); v_{iK-1}++) {
  combo = [ array[v_{i0}] ... array[v_{iK-1}] ];
}
...
}

这是JavaScript中的工作源代码

function adder(arr, max) {
  var k = arr.length;
  var n = max;
  var carry = false;
  var i;
  do {
  for(i = k - 1; i >= 0; i--) {
    arr[i]++;
    if(arr[i] < n - (k - (i + 1))) {
      break;
    }
    carry = true;
  }
  if(carry === true && i < 0) {
    return false; // overflow;
  }
  if(carry === false) { 
    return true;
  }
  carry = false;
  for(i = i + 1; i < k; i++) {
    arr[i] = arr[i - 1] + 1;
    if(arr[i] >= n - (k - (i + 1))) {
      carry = true;
    }
  }
  } while(carry === true);
  return true;
}
function nchoosekUniq(arr, k, cb) {
  // make the array a distinct set
  var set = new Set();
  for(var i=0; i < arr.length; i++) { set.add(arr[i]); }
  arr = [];
  set.forEach(function(v) { arr.push(v); });
  //
  var n = arr.length;
  // create index array
  var iArr = Array(k);
  for(var i=0; i < k; i++) { iArr[i] = i; }
  // find unique combinations;
  do {
    var combo = [];
    for(var i=0; i < iArr.length; i++) {
      combo.push(arr[iArr[i]]);
    }
    cb(combo);
  } while(adder(iArr, n) === true);
}
var arr = [0, 0, 1, 2]; 
var k = 2;
nchoosekUniq(arr, k, function(set) { 
  var s=""; 
  set.forEach(function(v) { s+=v; }); 
  console.log(s); 
}); // 01, 02, 12