代码和内容参考至 Gosper’s Hack Explained
算法描述
问题:对于n位的数值,得到其k个比特位为1的所有排列组合 (n >= k):
比如 k = 3, n = 6, 我们可以得到:
朴素实现:
- 枚举所有二进制情况:
int mask = (1 << k) - 1; //具有k位1 的最小值
int limit = mask << (n - k); //具有k位1 的最大值
while(mask != limit){
if(Integer.bitCount(mask) != k){
mask++;
continue;
}
save(mask);
mask++;
}
- 时间复杂度 O ( 2 n ) O(2 ^n ) O(2n)
GosperHack 算法
- 可以在O(1) 的时间内就得到下一个有k个1-bit的 数值
- 时间复杂度 O ( C n k ) O(C_n ^k ) O(Cnk)
void GospersHack(int k, int n)
{
int set = (1 << k) - 1;
int limit = (1 << n);
while (set < limit)
{
save(mask);
// Gosper's hack:
int c = set & - set;
int r = set + c;
set = (((r ^ set) >> 2) / c) | r;
//java 也可以写为 set = ((r ^ set) >> ( Integer.numberOfTrailingZeros(c) + 2) )| r;
}
}
原理
- 首先手工计算,我们要遵循的规则有两条:
- rule1 : 找到可以左移到0-bit 的最右侧 1-bit, 并把1-bit左移一位
- rule2:讲右侧所有1-bits 一直移动到最右侧(没有尾部0-bit)
-
举例
比如开始时二进制为 000111
rule1 : 000111 -> 001011
rule2 : 001011 -> 001011
结果 : 001011rule1 : 001011 -> 001101
rule2 : 001101 -> 001101
结果 : 001101
-
那么如何利用程序计算呢?
解析程序:int c = set & - set;
set & -set 的作用是获取set 最右侧的1, 比如
set = 001110 -set = 110010 set&-set = 000010
int r = set + c;
set 和c 相加,其实就是实现了规则1,把set可以移动的1-bit左移
set = (((r ^ set) >> 2) / c) | r;
r ^ set 的结果包括原来set右侧连续的1-bits 和刚刚左移的1-bit
现在开始进行规则2, 需要将右侧连续1-bits 右移。那么移动几位呢?可以通过c, 也就是set最右侧的1-bit判断位数,表现在运算上就是:(r ^ set) / c
但现在(r^set)/ c 是包含原本需要左移的1-bit 和已经左移的1-bit, 所以我们需要右移两位把它们去掉: (r ^ set)/ c)>> 2
最后在与之前计算的r相与,得到最终结果:((r ^ set)/ c)>> 2)| r
- 为什么用 ((r ^ set)>> 2) / c)而不是((r ^ set)/ c)>> 2)?
作者可能是考虑到cpu 除法的一些特性:
Gosper chose to use ((r ^ set) >> 2) / c) instead of ((r ^ set) / c) >> 2. Why? I believe he did so because some CPUs use the shift-and-subtract method to perform division and stop as soon as the remainder is 0, so dividing by a small number is faster. Thus, in some CPUs, we can expect ((r ^ set) >> 2) / c) to be marginally faster than ((r ^ set) / c) >> 2.
相关题目
力扣2397. Maximum Rows Covered by Columns
核心思想是二进制枚举,可以试一试