查找幂集的第n个集

Find n-th set of a powerset

本文关键字:个集 查找      更新时间:2023-10-16

我正试图在幂集中找到n-th集。通过n-th,我的意思是幂集是按照以下顺序生成的——首先是大小,然后是词典——因此,[a, b, c]的幂集中集合的索引是:

0 - []
1 - [a]
2 - [b]
3 - [c]
4 - [a, b]
5 - [a, c]
6 - [b, c]
7 - [a, b, c]

在寻找解决方案时,我所能找到的只是一个返回元素列表的第n个排列的算法——例如,在这里。

上下文

我试图检索元素的向量V的整个幂集,但我需要一次只检索一个幂集。

要求

  • 我只能同时维护两个向量,第一个向量包含列表中的原始项,第二个向量包含V的幂集中的n-th集——这就是为什么我愿意在这里使用n-th set函数
  • 我需要在解空间上的线性时间中而不是这样做——这意味着它不能列出所有集合,而它们选择n-th集合
  • 我最初的想法是使用位来表示位置,并获得我所需要的有效映射——作为我发布的"不完整"解决方案

我没有这个函数的闭合形式,但我确实有一个非循环next_combination函数,如果有帮助的话,欢迎使用它。它假设您可以将位掩码拟合为某种整数类型,这可能不是一个不合理的假设,因为64元素集有264的可能性。

正如评论所说,我觉得"词典排序"的定义有点奇怪,因为我认为词典排序应该是:[], [a], [ab], [abc], [ac], [b], [bc], [c]。但我以前不得不做"先按大小,然后按字典"的枚举。

// Generate bitmaps representing all subsets of a set of k elements,
// in order first by (ascending) subset size, and then lexicographically.
// The elements correspond to the bits in increasing magnitude (so the
// first element in lexicographic order corresponds to the 2^0 bit.)
//
// This function generates and returns the next bit-pattern, in circular order
// (so that if the iteration is finished, it returns 0).
//
template<typename UnsignedInteger>
UnsignedInteger next_combination(UnsignedInteger comb, UnsignedInteger mask) {
  UnsignedInteger last_one = comb & -comb;
  UnsignedInteger last_zero = (comb + last_one) &~ comb & mask;
  if (last_zero) return comb + last_one + (last_zero / (last_one * 2)) - 1;
  else if (last_one > 1) return mask / (last_one / 2);
  else return ~comb & 1;
}

第5行执行相当于(扩展)正则表达式替换的位破解操作,它找到字符串中的最后一个01,将其翻转到10,并将所有后续的1一直向右移动。

s/01(1*)(0*)$/1021/

第6行执行此操作(仅在前一行失败的情况下),再添加一个1,并将1一直向右移动:

s/(1*)0(0*)/211/

我不知道这个解释是有帮助还是阻碍:)


这里有一个快速而肮脏的驱动程序(命令行参数是集合的大小,默认为5,最大为无符号长中的位数):

#include <iostream>
template<typename UnsignedInteger>
std::ostream& show(std::ostream& out, UnsignedInteger comb) {
  out << '[';
  char a = 'a';
  for (UnsignedInteger i = 1; comb; i *= 2, ++a) {
    if (i & comb) {
      out << a;
      comb -= i;
    }
  }
  return out << ']';
}
int main(int argc, char** argv) {
  unsigned int n = 5;
  if (argc > 1) n = atoi(argv[1]);
  unsigned long mask = (1UL << n) - 1;
  unsigned long comb = 0;
  do {
    show(std::cout, comb) << std::endl;
    comb = next_combination(comb, mask);
  } while (comb);
  return 0;
}

考虑到枚举的大小,很难相信这个函数对一组超过64个元素有用,但它可能对枚举某些有限的部分有用,例如三个元素的所有子集。在这种情况下,只有当修饰适合一个单词时,bit hackery才真正有用。幸运的是,这很容易测试;您只需要对位集中的最后一个字进行如上所述的计算,直到测试last_zero为零。(在这种情况下,你不需要比特和mask,实际上你可能想选择一种不同的方式来指定集合大小。)如果last_zero为零(这实际上非常罕见),那么你需要用其他方式进行转换,但原理是相同的:找到在1之前的第一个0(注意0在单词的末尾而1在下一个单词的开头的情况);将01更改为10,计算出需要移动多少个1,然后将它们移动到最后。

考虑元素列表L = [a, b, c]L的幂集由以下公式给出:

P(L) = {
    [],
    [a], [b], [c],
    [a, b], [a, c], [b, c],
    [a, b, c]
}

将每个位置视为一个点,您将有映射:

id  | positions - integer | desired set
 0  |  [0 0 0]  -    0    |  []
 1  |  [1 0 0]  -    4    |  [a]
 2  |  [0 1 0]  -    2    |  [b]
 3  |  [0 0 1]  -    1    |  [c]
 4  |  [1 1 0]  -    6    |  [a, b]
 5  |  [1 0 1]  -    5    |  [a, c]
 6  |  [0 1 1]  -    3    |  [b, c]
 7  |  [1 1 1]  -    7    |  [a, b, c]

正如您所看到的,id并没有直接映射到整数。需要应用适当的映射,这样您就可以:

id  | positions - integer |  mapped  - integer
 0  |  [0 0 0]  -    0    |  [0 0 0] -    0
 1  |  [1 0 0]  -    4    |  [0 0 1] -    1
 2  |  [0 1 0]  -    2    |  [0 1 0] -    2
 3  |  [0 0 1]  -    1    |  [0 1 1] -    3
 4  |  [1 1 0]  -    6    |  [1 0 0] -    4
 5  |  [1 0 1]  -    5    |  [1 0 1] -    5
 6  |  [0 1 1]  -    3    |  [1 1 0] -    6
 7  |  [1 1 1]  -    7    |  [1 1 1] -    7

为了解决这个问题,我使用了一个二进制树来进行映射——我发布它是为了让人们可以从中看到一个解决方案:

                                        #
                          ______________|_____________
        a               /                             
                  _____|_____                   _______|______
        b        /                            /              
              __|__         __|__           __|__            __|__
        c    /            /              /               /     
           [ ]     [c]    [b]   [b, c]    [a]   [a, c]    [a, b]  [a, b, c]
index:      0       3      2       6       1      5         4         7

假设您的集合大小为N。

因此,有(N选择k)个大小为k的集合。你可以很快找到正确的k(即第N个集合的大小),只需从N中减去(N选择k),直到N即将变为负数。这将问题简化为找到N集的第N个k子集。

N集的第一个(N-1选择k-1)k-子集将包含其最小元素。因此,如果n小于(n-1选择k-1),则拾取第一个元素并在集合的其余部分上递归。否则,您有一个(N-1选择k)其他集合;丢弃第一个元素,从N中减去(N-1选择k-1),然后递归。

代码:

#include <stdio.h>
int ch[88][88];
int choose(int n, int k) {
 if (n<0||k<0||k>n) return 0;
 if (!k||n==k) return 1;
 if (ch[n][k]) return ch[n][k];
 return ch[n][k] = choose(n-1,k-1) + choose(n-1,k);
}
int nthkset(int N, int n, int k) {
 if (!n) return (1<<k)-1;
 if (choose(N-1,k-1) > n) return 1 | (nthkset(N-1,n,k-1) << 1);
 return nthkset(N-1,n-choose(N-1,k-1),k)<<1;
}
int nthset(int N, int n) {
 for (int k = 0; k <= N; k++)
  if (choose(N,k) > n) return nthkset(N,n,k);
  else n -= choose(N,k);
 return -1; // not enough subsets of [N].
}
int main() {
 int N,n;
 scanf("%i %i", &N, &n);
 int a = nthset(N,n);
 for (int i=0;i<N;i++) printf("%i", !!(a&1<<i));
 printf("n");
}