按格雷码顺序遍历所有组合

Iterate through all combinations in Gray code order

本文关键字:组合 遍历 顺序 雷码      更新时间:2023-10-16

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

我立即做的是创建一个位字段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 = ???;
}

但我不知道如何直接有效地计算哪个BitToFlip。所需的值基本上是序列A007814。我知道我可以使用公式(k>>1)^k计算格雷码,并将其与前一个进行异或,但随后我需要找到更改的比特的位置,这可能不会快得多。

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

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

否则,格雷码中更改的位对应标尺功能:

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

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

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

所以我想出了这个:

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);
}

它运行得非常好,将执行时间(与总是重新计算整个总和相比)从254秒降低到只有7秒,n=32。我还发现,由于rici提到的原因,使用for循环计算尾随零只比使用_BitScanForward64慢一点(~15%)。所以谢谢。