在 o(1) 中生成一个集合的所有组合

Generate all combinations of a collection in o(1)

int {1,2,3} 集合的组合是:

{},{1},{2},{1,2},{3},{1,3},{2,3},{1,2,3}

常见的解决方案是跟踪索引的代表性位掩码。

来自我的 0-7 数字示例:

000,001,010,011,100,101,110,111

此算法允许迭代解决方案, 每次生成器遍历每一位并决定是否插入该项目时, 所以它用 运行 时间的 O(n) 创建了下一个组合。

格雷码:(来自维基百科) 反射二进制码(RBC),也被称为弗兰克格雷之后的格雷码, 是一个二进制数字系统,其中两个连续的值只有一位不同

如果我们只使用灰色数字,我们会得到这个数字范围:

000,001,011,010,110,111,101,100

不同顺序结果相同:

{},{1},{1,2},{2},{2,3},{1,2,3},{1,3},{3}

但是在这里我们看到只有一项不同。 是否可以使用格雷码范围在 O​​(1) 中实现生成器。

我知道如果迭代,返回的列表无论如何都会有一个 O(n) 运行 时间。

增量解决方案和格雷码解决方案均摊销为 O(1),我相信这与您将获得的一样好。 “摊销 O(1)” 意味着您偶尔会在单次调用中获得更长的时间,但这种情况很少见 k 连续调用的总时间为 O(k),因此一次调用的平均时间是 O(1).

这比“预期的 O(1)”更有说服力,因为分摊执行时间是对足够调用的总计的保证,而如果您极度不吉利。 (在实践中,差异通常并不重要,但请参见,例如,针对“预期 O(1)”哈希表查找的 DoS 攻击。)

要了解增量算法为何摊销 O(1),请查看每次迭代中翻转的位数。每第二次迭代(数字以 0 结尾),正好翻转一位。每四次迭代,正好翻转两位。每八次迭代,正好翻转三个位。等等。很容易证明,在 k 次迭代后,最多 2k +log2 k 位将被翻转。

对于格雷码算法,每次迭代只翻转一位,但必须找到该位。格雷码递增的一个简单迭代实现是保持当前选择的奇偶性。由于奇偶校验在每次迭代中都会发生变化,因此无需重新计算;它只是倒置了。然后算法在两个规则之间交替:

  1. 如果奇偶校验,翻转低位。

  2. 如果奇偶校验为奇数,将最低位1位向左翻转

显然,选项 1 适用于一半的迭代,它显然只检查一位。对于选项 2,我们需要计算出每次迭代检查了多少位。由于每个可能的位组合恰好出现一次,因此尾随零的数量遵循与整数递增序列相同的频率:每隔两次,没有尾随零,每四次有一个,每八次有两个,等等。

所以,最后,我们检查了两种解决方案中相同数量的低阶位。但是,格雷码解决方案在每次迭代中仅更改集合中的一个元素,而增量解决方案会更改两个元素的摊销平均值。所以如果集合修改是昂贵的,格雷码算法可能更优越。


事后思考

  1. 您可以将格雷码算法直接应用于集合而不通过位串,但它的性能将取决于集合实现的一些细节。奇偶校验继续如上交替(它也是集合的模数 2 的大小,所以如果你的集合有一个 o(1) 大小的操作,你可以使用它而不是保持状态)。我们假设你有一些 o(1) 函数将每个可能的集合元素映射到它的后继者(在宇宙中,而不是选择),这可能类似于位映射到集合元素的方式。那么两种可能的算法动作是:

    1. 偶校验:如果集合中最小的元素是全域中最小的元素,则去掉那个元素;否则添加它。

    2. 奇校验:如果集合中最小元素的后继也存在,则移除;否则,添加它。

  2. 如果你需要在每次迭代时创建一个新的集合,而不是修改当前的选择集,那么你当然不能比 O(n) 做得更好,因为选择是 n/2,创建集合所需的时间不能少于其大小。