在 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位向左翻转
显然,选项 1 适用于一半的迭代,它显然只检查一位。对于选项 2,我们需要计算出每次迭代检查了多少位。由于每个可能的位组合恰好出现一次,因此尾随零的数量遵循与整数递增序列相同的频率:每隔两次,没有尾随零,每四次有一个,每八次有两个,等等。
所以,最后,我们检查了两种解决方案中相同数量的低阶位。但是,格雷码解决方案在每次迭代中仅更改集合中的一个元素,而增量解决方案会更改两个元素的摊销平均值。所以如果集合修改是昂贵的,格雷码算法可能更优越。
事后思考
您可以将格雷码算法直接应用于集合而不通过位串,但它的性能将取决于集合实现的一些细节。奇偶校验继续如上交替(它也是集合的模数 2 的大小,所以如果你的集合有一个 o(1) 大小的操作,你可以使用它而不是保持状态)。我们假设你有一些 o(1) 函数将每个可能的集合元素映射到它的后继者(在宇宙中,而不是选择),这可能类似于位映射到集合元素的方式。那么两种可能的算法动作是:
偶校验:如果集合中最小的元素是全域中最小的元素,则去掉那个元素;否则添加它。
奇校验:如果集合中最小元素的后继也存在,则移除;否则,添加它。
如果你需要在每次迭代时创建一个新的集合,而不是修改当前的选择集,那么你当然不能比 O(n) 做得更好,因为选择是 n/2,创建集合所需的时间不能少于其大小。
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位向左翻转
显然,选项 1 适用于一半的迭代,它显然只检查一位。对于选项 2,我们需要计算出每次迭代检查了多少位。由于每个可能的位组合恰好出现一次,因此尾随零的数量遵循与整数递增序列相同的频率:每隔两次,没有尾随零,每四次有一个,每八次有两个,等等。
所以,最后,我们检查了两种解决方案中相同数量的低阶位。但是,格雷码解决方案在每次迭代中仅更改集合中的一个元素,而增量解决方案会更改两个元素的摊销平均值。所以如果集合修改是昂贵的,格雷码算法可能更优越。
事后思考
您可以将格雷码算法直接应用于集合而不通过位串,但它的性能将取决于集合实现的一些细节。奇偶校验继续如上交替(它也是集合的模数 2 的大小,所以如果你的集合有一个 o(1) 大小的操作,你可以使用它而不是保持状态)。我们假设你有一些 o(1) 函数将每个可能的集合元素映射到它的后继者(在宇宙中,而不是选择),这可能类似于位映射到集合元素的方式。那么两种可能的算法动作是:
偶校验:如果集合中最小的元素是全域中最小的元素,则去掉那个元素;否则添加它。
奇校验:如果集合中最小元素的后继也存在,则移除;否则,添加它。
如果你需要在每次迭代时创建一个新的集合,而不是修改当前的选择集,那么你当然不能比 O(n) 做得更好,因为选择是 n/2,创建集合所需的时间不能少于其大小。