【问题标题】:Find n-th set of a powerset查找幂集的第 n 个集合
【发布时间】:2013-02-05 17:42:13
【问题描述】:

我正在尝试在 powerset 中找到 n-th 集。 n-th 我的意思是 powerset 是按以下顺序生成的——首先是大小,然后是字典顺序——所以,[a, b, c] 的 powerset 中的集合的索引是:

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

在寻找解决方案时,我只能找到一种算法来返回元素列表的第 n 个排列 - 例如,here

上下文

我正在尝试检索元素矢量V 的整个幂集,但我需要一次只使用一组。

要求

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

【问题讨论】:

  • 你应该包含你理解的by the n-th set in a powerset,因为集合没有顺序。在从您的回答中推断出什么意思之前,我从未听说过。
  • @phant0m 感谢您的评论!我会添加那个解释。

标签: c++ algorithm powerset


【解决方案1】:

我没有该函数的封闭形式,但我确实有一个有点破解的非循环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,并将所有后续的 1s 一直移动向右。

s/01(1*)(0*)$/10\2\1/

第 6 行执行此操作(仅当前一个失败时)再添加一个 1 并将 1s 一直向右移动:

s/(1*)0(0*)/\21\1/

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


这是一个快速而肮脏的驱动程序(命令行参数是集合的大小,默认为 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 才真正有用。幸运的是,这很容易测试。您只需要对 bitset 中的最后一个字进行上述计算,直到测试 last_zero 为零。 (在这种情况下,您不需要对mask 进行bitand 和mask,实际上您可能想要选择一种不同的方式来指定集合大小。)如果last_zero 变成零(这实际上非常罕见) ,那么你需要以其他方式进行转换,但原理是一样的:找到1之前的第一个0(注意0在单词末尾的情况和下一个开头的1);将01更改为10,计算出需要移动多少个1,然后将它们移动到最后。

【讨论】:

  • 还没有仔细阅读,但肯定有很大帮助(:我会在理解后立即回复。另外,感谢您指出定义的问题;我'会立即改变它!我也会考虑一下,因为实际上我不确定哪种排序是合适的^^
  • 您介意添加一个使用示例吗?我不是很熟悉你使用过的这种bit hacking mastering技术(:
  • 你去。请注意,由于迭代是循环的,以 0 开始和结束,因此您不能使用 for-loop 样式的预测试/后增量。如果不方便使用,您可以将其调整为不同的界面。
  • 好吧,除了非常感谢你之外,我只能说你应该因为这种巫术而被捕!真的非常感谢! (:
  • @Rubens:我草拟了多词程序。但老实说,如果您有数千个值,您将不会在枚举中走得太远,您可能希望使用基于列表的方法。至于“我从哪里挖到这些表达式”,它们是标准的位操作;要记住的主要事情是1s 传输的连续字符串携带,0s 传输借用。 (另外,在 2 的补码中,-a == ~a + 1。虽然 gcc 也知道这一点,所以您不必这样做。)
【解决方案2】:

考虑到元素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

【讨论】:

  • 对于第四组,这将为您提供 [1,0,0] = [a],而第三组将是 [0,1,1] = [b,c]。您需要找到一种将数字映射到字典顺序的方法,因为使用您的答案中描述的方法表示集合的数字顺序与您的问题中描述的集合的字典顺序不匹配。
  • @G.Bach 感谢您的评论。我做了映射,但我并没有真正注意到解决方案还没有准备好^^。我已经编辑了我的答案。
  • 我怀疑反转位会给你想要的东西。您链接到的链接 - 尽管那里的整个讨论忽略了这一点 - 谈论“重复排列”,因为这些字母可能不止一次出现。另一方面,您的问题需要没有重复的排列(在群论和组合学中简称为排列),因为您需要子集,并且集合很简单(意味着它们不包含多次相同的元素)。
  • 这导致了一个问题,虽然长度为 k 的重复排列有 n^k 种可能性,但只有 n 个选择 k 排列,从而使计数发生偏差。不幸的是,我只能指出这一点,因为我想不出一个有效的方法来解决这个问题。
  • @G.Bach 天哪,巴赫,我以为我的问题已经解决了,现在我完全迷路了^^
【解决方案3】:

假设你的集合大小为 N。

所以,有 (N 选择 k) 个大小为 k 的集合。只需从 n 中减去(N 选择 k),直到 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");
}

【讨论】:

  • 听起来不错(没有检查递归的东西),但我不会谈论“很快”。计算一个二项式系数将花费 O(n^2) 时间,最坏的情况是,您可能必须计算其中的 n/2,因此其中存在 O(n^3) 复杂度(肯定比天真的蛮力枚举方法更好) ,但仍然)。假设将考虑其功率集的大型集,这可能不会很好地扩展。不过,我自己也想不出更好的办法。
  • 肯定不是立方的。
  • 你是对的,不知道为什么我虽然计算二项式会在 O(n^2) 中。
  • @G.Bach:比这稍微微妙一些。从scratch计算单个二项式系数确实是二次的。从头开始计算其中的 n 个将是三次方。但我不会从头开始计算它们。我记。因此,您可以获得计算相关二项式系数的总体二次复杂度。
  • hm 就像我看到的那样,如果您将一个二项式系数计算为 (n!)/(k!(nk)!) 这是不必要的复杂,因为我们可以将其中一个阶乘留在分母中和分子的较大“一半”,因为它们抵消了,你在分子中得到 n 乘法,在分母中得到 k + n -k,所以总共有 2n 个乘法和一个除法。我错过了什么吗?
猜你喜欢
  • 2021-03-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-16
  • 1970-01-01
  • 2016-01-25
  • 1970-01-01
  • 2013-02-10
相关资源
最近更新 更多