炮姐v587:
有一个排列,我们可以轻易得到下一个排列:
http://www.cnblogs.com/threef/p/3200507.html
怎么枚举下一个组合呢:
科普:
x - 1 : 100 后缀反转
m100 -> m011
x&(x - 1)消除最后一个有效位
x^(x - 1):100后缀掩码
x + 1: 011 后缀反转
m011 -> m100
x&(x + 1)消除后缀连续连续有效位
x^(x + 1):011后缀掩码
-x: 求反加1
x& -x: 得到最低有次位掩码(有木有想到树状数组)
x + (x& -x): m0111000 -> m1000000
x^(x + (x & -x)): m0111000 -> 01111000
通常,我们使用位运算来枚举组合的时候,对那个int自加,然后判断它的1的个数(判断1的个数用(n&(n-1))循环得到),而当那个int的1的个数,等于选取的个数的时候,我们就得到下一个组合了。
不过这样做,显然太笨,因为如果是30选1,那你的程序要循环多少次呢。。。2^30已经能感觉到明显的延迟了。。。
好了,现在我们来改进这个算法,我们最好是以O(1)的时间直接得到下一个组合,而不要一个个枚举尝试。
这个想法很不错,不过代码要怎么写呢?
见以下代码,运行时,输入"5 3"(不包含引号),再回车,看看结果:
#include <stdio.h> int next_combination(int n, int k) //根据前一组合枚举下一组合 { int ret, b = k & -k, t = (k + b); ret = (((t ^ k) >> 2) / b) | t; if ((1 << n) < ret) return 0; return ret; } int main() { int n, k; while (scanf("%d%d", &n, &k) != EOF && n >= k && k>0 && n<30) { int ik = (1 << k) - 1, i; //初始化 do { // 输出组合 for (i = 0; i < n; ++i) { if (ik & (1 << i)) printf("%d ", i); } //输出空行分隔之 puts(""); } while (ik = next_combination(n, ik)); } return 0; }
输出结果是:
5 3 5 4 3 2 1 0
0 1 2 0 0 1 1 1 init
0 1 3 0 1 0 1 1
0 2 3 0 1 1 0 1
1 2 3 0 1 1 1 0
0 1 4 1 0 0 1 1
0 2 4 1 0 1 0 1
1 2 4 1 0 1 1 0
0 3 4 1 1 0 0 1
1 3 4 1 1 0 1 0
2 3 4 1 1 1 0 0
1 0 0 1 1 1 > (1 >> 5) 溢出
嗯,很完美的得到了所有的组合
在这里,main函数没什么好说的,很容易看明白,关键是那个next_combination函数,这个函数是什么意思?
其实那个next_combination函数,是根据k,得到比k大,并且二进制下有相同个数的'1',而且是当中最小的数
比如把1011,变成1101,这个到底是怎么算的呢?
如果你笔算一下,不难发现规律,并且是不太复杂的,首先,因为1的个数要相同,并且比原数要大,那么,先要找出右起第一次出现1的位置,对这个数加上1,然后在最右边补上少了的1就可以了。
找出右起第一次出现1的位置的算法很简单,就是(n & -n),这里的b就是得到这个位置,t就是加了以后的结果,这个应该不难明白,关键的,是后面的计算。
后面的计算,主要是针对右边补1的个数,细心想一下,你就知道,要补的1的个数,等于原数右起第一个1向左数,连续的1的个数减1,然后,t^k是什么意思呢?这个就非常有技巧了,它的效果其实和x ^ (x - 1)很类似。
而x ^ (x - 1)的作用,是保留右起第一个“1”,同时把右起第1个“1”右边全部变为“1”,类似1101000 -> 1111
逆向过来说,就是k = 1100111,对它 +1后,得到t = 1101000,用这个运算可以得到1111,位数和少掉的1成常数差的关系
事实上,这样我们变相地得到少掉的1的个数(这个例子中是少了两个1),我们只需要对运算结果中1的个数减2即可,用>>2解决之
不过,在当k最右边不是1,有若干个0的时候,前一个步骤得到的数的最右边,就会有同样多的0,如何去掉这些0?
这时候,我们最初计算的b,即(n & -n)就太有作用了,只要除以这个b,0就没有了,最后,就是最右边应该补上的值,和前面的t求和,或者求并均可。
这一篇也是这个玩意:讲的挺好的!