以格雷码顺序迭代所有组合

时间:2014-09-27 23:23:27

标签: c++ algorithm

假设我在数组 a 中有 n 整数,我想迭代这些整数的所有可能子集,找到 sum ,然后用它做点什么。

我立即做的是创建一个位域 b ,它指示哪些数字包含在子集中,并使用++b迭代其可能的值。然后,为了计算每一步中的总和,我不得不迭代所有这些位:

int sum = 0;
for (int i = 0; i < n; i++)
    if (b&1<<i)
        sum += a[i];

然后我意识到,如果我以格雷码顺序迭代 b 的可能值,那么每次只翻转一个比特,我就不必完全重建总和,但只需要添加或减去从子集中添加或删除的单个值。它应该像这样工作:

int sum = 0;
int whichBitToFlip = 0;
bool isBitSet = false;
for (int k = 0; whichBitToFlip < n; k++) {
    sum += (isBitSet ? -1 : 1)*a[whichBitToFlip];

    // do something with sum here

    whichBitToFlip = ???;
    bool isBitSet = ???;
}

但我无法弄清楚如何直接有效地计算 whichBitToFlip 。期望的值基本上是序列A007814。我知道我可以使用公式(k>>1)^k计算格雷码并使用前一个计算格雷码,但是我需要找到更改位的位置,这可能不会更快。

那么有没有更好的方法来确定这些值(翻转位的索引),最好是没有一个循环,比每次重新计算总和(最多64个值)更快?

2 个答案:

答案 0 :(得分:1)

要将位掩码转换为位索引,可以使用ffs函数(如果有的话),这对应于某些机器上的机器操作码。

否则,格雷码中的位改变对应于标尺函数:

0, 1, 0, 2, 0, 1, 0, 3, 0, 1...

有一个简单的递归。您可以使用堆栈模拟递归(它将具有最大深度O(log N),因此它的空间不大),但可能ffs可能要快得多。

(顺便说一句,即使你从右到左一次计数一位,增量函数平均为O(1),因为整数中的尾随0的总数从1开始到2 k 是2 k -1。)

答案 1 :(得分:0)

所以我想出了这个:

int sum = 0;
unsigned long grayPos = 0;
int graySign = 1;
for (uint64 k = 2; grayPos < n; k++) {
    sum += graySign*a[grayPos];

    // Do something with sum

    #ifdef _M_X64
        grayPos = n;
        _BitScanForward64(&grayPos, k);
    #else
        for (grayPos = 0; !(k&1ull<<grayPos); grayPos++);
    #endif
    graySign = 2-(k>>grayPos&0x3);
}

它的效果非常好,将执行时间(与总是重新计算总和相比)从 n = 32降低到254秒。我还发现用尾数计算尾随零由于rici提到的原因,周期仅比使用_BitScanForward64稍微(~15%)。谢谢。