(注:二进制值中1位的个数一般称为population count——popcount,简称popcount——或Hamming weight。)
有一个众所周知的 bit-hack 循环遍历具有相同人口计数的所有二进制字,它基本上执行以下操作:
找出单词的最长后缀,包括一个 0、一个非空的 1 序列,最后是一个可能为空的 0 序列。
将第一个0改为1;接下来的 1 到 0,然后将所有其他 1(如果有)移到单词的末尾。
例子:
00010010111100
^-------- beginning of the suffix
00010011 0 becomes 1
0 1 becomes 0
00111 remaining 1s right-shifted to the end
这可以通过使用x 中的最低设置位是x & -x(其中- 表示x 的2s 补码负数)这一事实来快速完成。要找到后缀的开头,只需将最低位设置位添加到数字中,然后找到新的最低位设置位即可。 (用几个数字试试这个,你应该会看到它是如何工作的。)
最大的问题是执行右移,因为我们实际上并不知道位数。传统的解决方案是用除法(原始的低位 1 位)进行右移,但事实证明,现代硬件上的除法相对于其他操作数来说确实很慢。循环移位通常比除法快,但在下面的代码中,我使用 gcc 的__builtin_ffsll,如果目标硬件上存在操作码,它通常会编译成适当的操作码。 (详情请参阅man ffs;我使用内置函数来避免功能测试宏,但它有点难看,并且限制了您可以使用的编译器范围。OTOH,ffsll 也是一个扩展。)
为了便携性,我还包括了基于部门的解决方案;但是,在我的 i5 笔记本电脑上花费的时间几乎是我的三倍。
template<typename UInt>
static inline UInt last_one(UInt ui) { return ui & -ui; }
// next_with_same_popcount(ui) finds the next larger integer with the same
// number of 1-bits as ui. If there isn't one (within the range
// of the unsigned type), it returns 0.
template<typename UInt>
UInt next_with_same_popcount(UInt ui) {
UInt lo = last_one(ui);
UInt next = ui + lo;
UInt hi = last_one(next);
if (next) next += (hi >> __builtin_ffsll(lo)) - 1;
return next;
}
/*
template<typename UInt>
UInt next_with_same_popcount(UInt ui) {
UInt lo = last_one(ui);
UInt next = ui + lo;
UInt hi = last_one(next) >> 1;
if (next) next += hi/lo - 1;
return next;
}
*/
剩下的唯一问题是在给定范围内找到第一个具有正确 popcount 的数字。为了解决这个问题,可以使用以下简单算法:
从范围内的第一个值开始。
只要值的 popcount 太高,通过将低位 1 位添加到数字来消除最后一次运行的 1(使用与上面完全相同的 x&-x 技巧)。由于这是从右到左工作的,它不能循环超过 64 次,每比特一次。
1234563 @times(其中k 是目标popcount),不需要像第一步那样重新计算每个循环的population count。
在下面的实现中,我再次使用 GCC 内置函数 __builtin_popcountll。这个没有对应的 Posix 函数。请参阅Wikipedia page 了解替代实现和支持该操作的硬件列表。请注意,找到的值可能会超出范围的末尾;此外,该函数可能返回一个小于提供的参数的值,表明没有合适的值。所以你需要在使用前检查结果是否在期望的范围内。
// next_with_popcount_k returns the smallest integer >= ui whose popcnt
// is exactly k. If ui has exactly k bits set, it is returned. If there
// is no such value, returns the smallest integer with exactly k bits.
template<typename UInt>
UInt next_with_popcount_k(UInt ui, int k) {
int count;
while ((count = __builtin_popcountll(ui)) > k)
ui += last_one(ui);
for (int i = count; i < k; ++i)
ui += last_one(~ui);
return ui;
}
可以通过将第一个循环更改为:
while ((count = __builtin_popcountll(ui)) > k) {
UInt lo = last_one(ui);
ui += last_one(ui - lo) - lo;
}
这节省了大约 10% 的执行时间,但我怀疑该函数是否会被频繁调用以使其值得。根据您的 CPU 实现 POPCOUNT 操作码的效率,使用单个位扫描执行第一个循环可能会更快,以便能够跟踪 popcount 而不是重新计算它。在没有 POPCOUNT 操作码的硬件上几乎肯定会出现这种情况。
一旦有了这两个函数,迭代一个范围内的所有 k 位值就变得微不足道了:
void all_k_bits(uint64_t lo, uint64_t hi, int k) {
uint64_t i = next_with_popcount_k(lo, k);
if (i >= lo) {
for (; i > 0 && i < hi; i = next_with_same_popcount(i)) {
// Do what needs to be done
}
}
}