本篇摘要

本篇介绍一个非常给力的求组合的算法!上一篇“c_c++刁钻问题各个击破之位运算及其实例(2)”介绍了6个比较复杂的位操作,但是没有给出任何应用实例,本篇就之前谈到的位操作进行应用,其主要内容是用位操作来实现求组合

引例

先来看一道题目,这个题目是理解利用位操作求组合的关键。它是POJ的2453。英文原题就不贴了,我用中文描述一下吧:给定一个正整数N,求最小的、比N大的正整数M,使得M与N的二进制表示中有相同数目的1。

上面的题目描述或许有点拗口,举个例子把,假如给定的N为78,其二进制表示为1001110,包含4个1,那么最小的比N大的并且二进制表示中只包含4个1的数是83,其二进制是1010011,因此83就是答案。那么如何求解这个问题呢?这里给出两个思路:

(1) 最直观的思路:枚举

思路是从N+1开始枚举,对每个数都测试其二进制表示中的1的个数是否与N的二进制表示中1的个数相等,遇到第一次相等时就停止。其算法流程如下:

1、  求得N的二进制表示中1的个数k;

2、  N++;

3、  测试N的二进制表示中所包含的1的个数是否等于k,如果是,则输出N后结束,否则转2;

上面的过程可以用如下的流程图来表示:

用代码实现上述流程如下:

int GetNextN(unsigned int N)
{
int k = count1Bits(N);
do
{
N++;
}while(count1Bits(N)!=k);
return N;
}

上面的代码中还有一个问题没有解决,那就是“求整数的二进制表示中有多少个1”即count1Bits()函数。这个问题在我之前的《c/c++刁钻问题各个击破之位运算及其实例(1)》中有详细阐述,它应用了n&=(n-1)能将n的二进制表示中的最右边的1翻转为0的事实。只需要不停地执行n&=(n-1),直到n变成0为止,那么翻转的次数就是原来的n的二进制表示中1的个数,其代码如下:

int count1Bits(unsigned int n)
{
int count = 0;
while(n)
{
count++;
n&=(n-1);
}
return count;
}

至此,第一个枚举方法介绍完毕,你只需要写如下的main()函数就能测试本方法:

void main()
{
int N;
cin>>N;
cout<<GetNextN(N)<<endl;
}

上面的方法即直观,效率也还不错,但是还有没有更好效率的算法呢?通过查阅资料,我找到了一个“非常给力的”、“通过位操作的”、“只需要一步就能求得答案的”方法。是的,我用了如此之多的形容词来美化这个方法,其实一点不为过,接下我将介绍它。

(2) 非常给力的方法

即将要介绍的方法到底有多给力呢?它神奇到只用如下几行代码(事实上可以合并为一行代码)就能实现上述所有代码的功能,这几行代码是:

int NextN(int N)
{
int x = N&(-N);
int t = N+x;
int ans = t | ((N^t)/x)>>2;
return ans;
}

看到了不,这就是所有代码,如果省去临时变量,它就只包含一行代码,但是为了后面讲述方便,我将它写成三行代码。它的强大之处并非只是代码少,如你所见,它无需调用count1Bits函数(效率!),也没有前面枚举法中GetNextN函数中的while循环(效率!)。是的,我们这里的NextN函数比之前的GetNextN函数效率要高很多,它的时间复杂度是O(1)!

如果您是大牛,请不要笑话我在这里的大惊小怪,如果你跟两三天前的我一样不懂这几句代码的话,那么请往下看,我将尝试着把我的理解详细的表述出来,力求细致,简单,易懂。

以N=78为例(其二进制表示为1001110),我们的任务是求得最小的比N大,二进制表示中1的个数与N相同的数:83(其二进制表示为1010011)。首先我们要总结出来从78变成83的规律,为了方便,将78和83的二进制写成竖式形式:

78:1 0 0 1 1 1 0

83:1 0 1 0 0 1 1

可以看出,为了得到83,我们只需要对N(78)的二进制中最右边的连续的1位串(加粗标红)进行操作!其过程是:将连续的1位串中最左边的1向左“移动”一位,其他的1位串“移动”到最右边!这即保证了二进制表示中1的个数不变,又保证了新得到的数比原来的数大,并且是最小的。其过程可以用如下图示表示:

在上面的描述中我用引号把两个“移动”引起来了,原因是,具体实现时,我们并不是对这些二进制位进行移动,而是通过位操作来达到同样的目的,而这些位操作就是本问题的关键。接下来我将分析前面那个“非常给力的代码”看看它是如何用位操作来实现对这些位的“移动”的。

首先来看语句int x = N&(-N);它的功能是找到N(即78)的二进制表示中最右边的1(这个1必定是N的二进制表示中最右边的连续的1位串的开始)。该过程图示如下:

接下来看看int t = N+x;该语句实现了“将连续的1位串中最左边的1向左“移动”一位”的功能,当然它带来了副作用:使得连续的1位串中其他的1丢失了!其过程如下:

最后的任务就是要将上面丢失的1补上,并放到最右边,这就是语句int ans = t | ((N^t)/x)>>2;的功能。首先,要知道需要补多少个1,通过分析可以知道需要补上的1的个数等于N的二进制表示中最右边的连续的1位串中1的个数减1,然而如果通过位操作来求得呢?这就是N^t的功能了,如下图所示,N^t的二进制表示只包含1个连续的1串,并且1的个数正好等于N的二进制表示中最右边的连续的1位串中1的个数加1:

由上面的分析可知,N^t中的1的个数实际上比我们需要补的1的个数多2!这就使得我们可以通过N^t求得需要补的1的个数,接下来的任务就是如何补上这些1了。

进步一分析得知,N^t的二进制表示中最低位的1正好与x中那个1对应,因此我们就可以通过(N^t)/x将这些1全部移到最右边了,然而此时1的个数比我们要补的个数多了2,没关系我们在把结果右移2位就可以了,也就是((N^t)/x)>>2。如此一来我们求得了要补的1的个数和其位置。本段的描述可以用下图形象地表示:

最后我们只需要用t | ((N^t)/x)>>2;就能得到所求之数了!其过程如下图:

以上就是我对这个非常给力的代码的分析。短短3句代码(省去中间变量的话,就一句代码)居然包含了如此之多的东西,这就是位运算的强大之处,也是位运算的难学之处。本人以前也很少关注位运算,像这样给力的代码我是写不出来的,因此我也只能按照如上步骤那么去读懂这个代码。至此,本篇的引例算是完成了,不可思议吧,为了求组合,我居然用了这么多篇幅来讲一个引例,这会不会本末倒置啊?我自信不会的,因为有了这个引例,下面求组合就太easy了。

本篇主题:利用位操作求组合

组合就是从N各对象中选取m个对象,问有多少种选法,并且要求输出每次的选法。比如给定1,2,3,4四个数,从中选择2个的选法有:{1,2},{1,3},{1,4},{2,3},{2,4},{3,4}共6种选法。当然求组合的方法非常之多,我这里只介绍如何利用位操作来求,其思路是:用2进制bit位来标识某个对象是否被选中,1代表选中,0代表没选中。比如前面的例子的组合可以用下图来表示(最低位为1表示选中第一对象,以此类推)。

根据上面的分析,我们可以用一个包含N个bit位的数C来求N个对象中选取m个对象的组合:首先让C的最低m位全部为1(对应到从N个对象中选择前m个对象的组合情况),然后用我们引例的方法求出最小的比C大并且二进制表示中包含的1的个数与C相同的数K,就能求得下一个组合情况。其流程如下:

1、  初始化C=(1<<m)-1;(选择N个对象中的前m个作为第一个组合);

2、  根据C的二进制表示输出其所对应的组合;

3、  调用C=NextN(C);

4、  通过判断C是否小于等于(1<<N)-(1<<(N-m))来确定是不是输出了所有的组合(注意,当C==(1<<N)-(1<<(N-m))时,C就对应着从N个对象中选择后m个对象的组合情况,也就是最后一个组合),如果C小于等于(1<<N)-(1<<(N-m)),则转2,否则转5;

5、  结束(已经输出所有组合)。

上面流程中的关键部分我都标粗了,如果对该流程有疑问,可以与我联系(w57w57w57@163.com)。下面我将根据上面的流程给出代码:

#include <stdio.h>
#include"iostream"
using namespace std;
//定义包含4个元素的集合
char set[] ={'a','b','c','d','e','f','g','h','i'};
//根据C的二进制表示输出一个组合
void print(char* set,int C)
{
int i = 0;
int k;
while((k=1<<i)<=C)
{//循环测试每个bit是否为1
if((C&k)!=0)
{
cout<<set[i];
}
i++;
}
}
//这个NextN跟之前我们讨论的是一样的,只不过省去了临时变量
int NextN(int N)
{
return (N+(N&(-N))) | ((N^(N+(N&(-N))))/(N&(-N)))>>2;
}
//求从set中前N个元素 中选择m个的组合
void Combination(char* set,int N,int m)
{
int C = (1<<m)-1;
while(C<=((1<<N)-(1<<(N-m))))
{
print(set,C);
cout<<endl;
C = NextN(C);
}
}
void main()
{
Combination(set,4,2);
}

上面的代码在VC6.0中测试通过,其运行结果如下:

最后,或许您对Combination函数中的while中的条件表达式:C<=((1<<N)-(1<<(N-m)))不理解,那么请看下图,该图示意了最后一个组合所对应的C,其值正好等于(1<<N)-(1<<(N-m))

分析

由于我这里没有给出求组合数的其他算法,因此无法对该算法与其他算法的性能做对比,有兴趣的朋友可以做一个对比。事实上这个算法的效率相当之高,因为它直接根据前一个组合一步就能求得后面一个组合。当然它也并非没有一点瑕疵,我个人认为它有两点不足:

1、  由于它需要用一个整数的二进制位来标识哪些对象被选中,而整数是有范围的,因此如果N比较大(大于32),那么该算法就不能直接利用了。

2、  得到的组合并非有序的,如上面的结果所示输出ac之后并非输出ad,而是bc,其原因是NextN(N)函数,它返回的是“最小的、比N大的、二进制表示中1的个数与N相同的数”然而这个约束并不能保证根据它求得的组合是有序的。如果一定要求有序的组合,那么,可以修改NextN这个函数,但本文的核心就是它,因此修改它很可能就失去了意义,当然您可以想出另外一个位运算,在不损失效率的情况下改变NextN的功能,从而得到有序的组合,这个也是我在思考的问题。

结束语

到此本篇即将结束,个人感觉对引例说得比较清晰透彻,如果您有不清楚的地方或者发现有不妥之处,请留言,最后感谢您的阅读!

申明

本人的所有原著博文的版权均归本人和CSDN所有。除侵权行为外,欢迎各位朋友评论指正转载分享,分享时请标明作者和出处,本人昵称:语过添情(w57w57w57)。其中语过添情”是我一直用的QQ昵称,从2001年至今,它跟我已经10个年头了。后面那一串古怪的字符的来历是:字母w和数字5分别是本人姓(伍)的拼音首字母和谐音数字,7是因为我的启蒙女友(初一时的,非初恋女友,因为那时我完全不懂恋爱)的姓(柒)的谐音数字。以前天真的以为以后生个小孩可以取名伍陆柒(567)的,可惜一学期后她就辍学了……

作者:    语过添情 (w57w57w57)

Email:  w57w57w57@163.com

QQ:       11335457

2011-08-03

给力!高效!易懂!位运算求组合相关推荐

  1. 位运算与组合搜索(二)

    People who play with bits should expect to get bitten. -- Jurg Nievergelt I failed math twice, never ...

  2. 【飞秋】位运算与组合搜索(二)

    这篇文章接着讲怎样高效地遍历所有的组合.同样,假定全集的大小不大于机器字长,计算模型为 word-RAM,即诸如 +, –, *, /, %, &, |, >>, << ...

  3. 位运算求整数中二进制1的个数

    package _位运算;public class _位运算求整数中1的个数 {public static void main(String[] args) {int n = 4;int ans = ...

  4. 位运算求两个数的平均值

    一直不理解位运算求两个数的平均值.参考网上资料后终于明白. 如下: 求两个数的平均值的算法:Avg = (ValueA & ValueB) + (ValueA ^ ValueB) >&g ...

  5. 用位运算实现四则运算之加减乘除(用位运算求一个数的1/3)

    听同学百度二面中,不准用四则运算操作符来实现四则运算.一想就想到了计算机组成原理上学过的.位运算的思想可以应用到很多地方,这里简单的总结一下用位运算来实现整数的四则运算. 加法运算: int AddW ...

  6. java位运算求幂,程序员必学:快速幂算法

    前阵子,有小伙伴在我B站的算法教程底下留言 小伙伴们有任何疑问或者希望我解说任何内容,都可以在我的小我私家B站或民众号(xmg_mj)留言哦,我会尽我最大能力.只管抽时间去写文章\录视频来回应人人. ...

  7. java中取整数绝对值_Java之——位运算求整数绝对值

    通过下面的位运算可以得到一个整数的绝对值 public int abs( int a ) { return (a + (a >> 31)) ^ (a >> 31) ;//前半部 ...

  8. 求实数的整数次幂(循环版)(高效)(位运算解题)

    求实数的整数次幂(循环版)(高效) (10 分) 原理图: 请编写函数,用循环语句以最快的方法求任意实数的任意整数次幂. 函数原型 double Power(double x, int n); 说明: ...

  9. c通过位运算求绝对值_初中数学归类总结(四)有理数的乘除乘方及混合运算...

    学习了有理数的加减运算以后,再来进行有理数的乘除,就比较容易理解和运算了. 首先我们来看有理数的乘法法则:两数相乘,同号得正,异号得负,并把绝对值相乘:任何数与零相乘,积仍为0.有理数乘法法则和有理数 ...

最新文章

  1. lua的table+setfenv+setmetatable陷阱
  2. 华为云家庭视频监控帮你一起守护家
  3. Oracle 记录插入时“Invalid parameter binding ”错误
  4. 高斯平稳随机过程仿真
  5. (网页)中的简单的遮罩层
  6. weui 加载提示_WeUI与WeUI.JS配合切换进入页面显示加载动画
  7. 最详细移动硬盘安装linux过程,装在移动硬盘上的linux系统不能在另一台电脑启动的解决办法
  8. word单页(或中间几页)横向显示
  9. 人物画像及“七步人物角色法”
  10. python numpy dtype object_python – 创建numpy数组时dtype = object意味着什么?
  11. 电商业务Alipay支付实战(当面付实现)
  12. 连续被特斯拉碾压的国产车终于成功反击,五菱宏光月销超2万
  13. Android工程师进阶第一课 夯实Java基础 JVM内存模型和GC回收机制
  14. uniapp公共测试证书签名
  15. c学习笔记 文件输入/ 输出 20210314
  16. 天亮说晚安,我们回家
  17. 题解 P4460 【[CQOI2018]解锁屏幕】
  18. 结合泛函极值_泛函的极值
  19. 杜邦分析模型 java_如何用java报表工具Style Report 制作财务分析杜邦分析
  20. java文件预览_java 在线预览doc,pdf

热门文章

  1. 慢慢做一个模仿天猫网站-3
  2. “SaaS加速器”赋能开发者-以产业平台助燃生态布局
  3. Excel如何同时查找多个数据
  4. 分布式技术与实战第五课 分布式-消息中间件选型
  5. 【神经网络压缩加速之剪枝一】Filter Pruning via Geometric Median for Deep Convolutional Neural Network Acceleration
  6. 探究#define SQR(x) (x*x) 结果
  7. 【matlab】:matlab如何写函数并且调用函数?
  8. idea 解决报错 Artifact web:war exploded: Error during artifact deployment. See server log for details.
  9. 升级GLIBC至2.17及系统崩溃解决方案
  10. cesium实现添加在线地图的偏移纠正