相信积累的力量

下一个组合,位运算,bits,combination,next_combination

炮姐v587: 

http://misakamm.com/blog/25


 

有一个排列,我们可以轻易得到下一个排列:

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求和,或者求并均可。


 

 这一篇也是这个玩意:讲的挺好的!

http://blog.csdn.net/w57w57w57/article/details/6657547

 

posted @ 2013-07-20 14:34  ThreeF  阅读(569)  评论(0编辑  收藏  举报

相信积累的力量