主题:[讨论]位运算 大牛进来下 看的我理解对不
小小宇宙
[专家分:30] 发布于 2010-07-31 21:37:00
[code=c]
#include <cstdio>
#include <iostream>
using namespace std;
#define FOR(i,l,h) for(i=(l);i<=(h);++i)
#define longlong __int64
int cal(int n){
int s=0,i;
FOR(i,0,30)
[b]s+=(n&(1<<i))>0;[/b]
return s;
}
longlong getMinBottles(int N, int K){
longlong ans=0;
while (cal(N)>K){
[b] ans+=N&-N;
N+=N&-N;[/b] }
return ans;
}
int main(){
//freopen("water5.in","r",stdin);
//freopen("water5.out","w",stdout);
int n,k;
int t;
scanf("%d",&t);
while (t--)
{
scanf("%d%d",&n,&k);
cout<<getMinBottles(n,k)<<endl;
}
return 0;
}[/code]
[b]s+=(n&(1<<i))>0;[/b](n&(1<<i))是n%(2^i) ?
(n&(1<<i))>0,判断(n%(2^i))>0 ? 如果不大于0,程序会怎么执行?
s+=(n&(1<<i))>0;整句话就是 s+=(n%(2^i))>0 ?
[b]ans+=N&-N;
N+=N&-N;[/b]
这两句就完全去不懂了。
回复列表 (共8个回复)
沙发
bruceteen [专家分:42660] 发布于 2010-08-01 11:13:00
(n&(1<<i))>0 是判断n的第i位是否为1
N&-N 是取得最右一个1,比如N是二进制 ?????10000,结果就是10000
板凳
bruceteen [专家分:42660] 发布于 2010-08-01 11:19:00
n&(1<<i)
--- 1<<i 就是 1000……0,共i个连续的0
N&-N
--- -N 等同于 ~N + 1,就是 各位取反后再加一
假设N是 010100110000000 (1)
各位取反后是 101011001111111
再加一后是 101011010000000 (2)
你对比一下(1)和(2)看看,是不是只有最后那个1是两者都有的
3 楼
eastcowboy [专家分:25370] 发布于 2010-08-01 12:23:00
先看calc这个函数,主要的语句就是循环中的那一句“s+=(n&(1<<i))>0;”
一点一点的理解。
(1<<i)就是1左移i次,实际结果是2的i次方。
n&(1<<i),这里&是“与”运算,实际的效果是取得n的二进制第i位。(并不是楼主所说的n%(2^i),两者结果是不同的)
(n&(1<<i))>0,判断n的二进制第i位是否大于零。判断的结果只有两种可能,一种是“成立”,一种是“不成立”。在C语言中,这个结果可以被转化为整数。“成立”转化为1,“不成立”转化为0。在C++语言中,因为有bool的概念,所以“成立”就是true,“不成立”就是false,但bool也可以转化为整数,true转化为1,false转化为0。综合起来就是:如果n的二进制第i位大于0,则值为1,否则值为0。
s+=(n&(1<<i))>0,结合前面的叙述知道,这条语句的效果是:如果n的二进制第i位大于0,则s的值增加1,否则不变。
这样一来就清楚了,calc这个函数的效果是:计算n的所有二进制位之中,一共有多少个1(第31位是符号位,所以只计算0~30位)。
然后看getMinBottles这个函数。
同样是一点一点的理解。
while (cal(N) > K),意思是只要N的二进制位之中1的数量超过K个,就一直循环。
N&-N这一句。因为现在计算机一般都采用补码,所以-N其实就是N的所有二进制位全部取反,然后加上1。它的实际效果是:把N的二进制位中最低一个为1的位保留,其它为1的位全部修改为0。
ans+=N&-N,意思是把N的最低一个不是0的二进制位加到ans。
N+=N&-N,意思是把N的最低一个不是0的二进制位加到ans。
为什么要这样循环?我也看得云里雾里,于是在百度输入了“getMinBottles”,开始搜索,得到了题目:
http://hi.baidu.com/cn_rigel/blog/item/921d73ac0191a5004a36d617.html
题目大意如下:
有N个瓶子,每个瓶子容量无限,并且初始时装有一升水,这些瓶子中的水需要全部运走。
你一次最多只能带走K个瓶子,但你希望只跑一趟就能完工。所以必须把一些瓶子里的水倒进另一些瓶子,然后空瓶子就不用带走了。
倒水的规则是:只能取任意两个装有等量水的瓶子,然后把其中一个瓶子的水全部倒进另一个瓶子。
由于规则限制,有些时候将无法完成任务。此时可以再买一些瓶子(同样是容量无限,并且初始时装有一升水),协助完成任务。
问题:输入N和K(其中N的范围是[1, 10000000],K的范围是[1, 10000]),计算需要再买多少瓶子,输出购买的瓶子数量。(如果无法完成,输出-1)
解决方法自然是位操作。如果瓶子数N的二进制位数小于等于K,那就表示不再需要购买瓶子了。所以代码写出来就是:while (cal(N) > K)
若需要购买瓶子,则取N的二进制位中最低一个为1的位,将其作为购买的瓶子数。(也就是,需要再购买N&-N个瓶子)
于是,购买的瓶子数:ans += N&-N;
当前瓶子总数:N += N&-N;
这样就把整个代码解释清楚了。
一开始我想,如果输入的K等于0,则N += N&-N这一句会使N无限的增大。但看了题目之后明白这是不会发生的。如果不知道题目的话,只看代码就难以理解了。
另外,cal这个函数还可以有更好一点的实现方法:
while (n != 0) {
++s;
n -= n & -n;
}
因为n&-n表示了n的最低一个二进制位,如果n的值减少n&-n,则实际效果就是n少了一个二进制位。
楼主贴出的代码不论n的值是多少,都需要循环31次。而改进的代码则很可能少一些循环次数,平均性能会得到改进。
从题目给出的数据范围来看,用64位整数也是没有必要的。
4 楼
小小宇宙 [专家分:30] 发布于 2010-08-01 20:56:00
果断给分啦,源代码是答案代码,先一开始自己写了for 循环的来mod 2,超时,晕倒。
只好直接看测试数据的代码,位运算又晕倒,真搞懂,怎么那么齐欢位运算。
后来在网上找了代码,pascal的,还好我坚强,学过点,哈哈
http://hi.baidu.com/xuexixiaozhu/blog/item/a29997ed3a865ae6ce1b3eb0.html
其实K为几就是 2^k1+2^k2+…+2^k(k-1)+2^kk中的2次幂的个数,当然还要考虑出现数字自己本身仅靠分解就能符合要求的情况,就比如说n=7 k=4
谢谢啦,有不懂我再问啦
5 楼
windy0will [专家分:2300] 发布于 2010-08-02 14:08:00
cal可不可写成下面这样:
int cal(int n){
int s = 0 ;
if ( 0==(n&=0x7fffffff) ) return 0;
while( ++s,n&=n-1 )
;
return s;
}
希望大牛们能分析下。
6 楼
eastcowboy [专家分:25370] 发布于 2010-08-02 19:07:00
n &= n - 1
这样的写法确实更好一些。比起我在3楼最后的那几行代码,有两个方面的优势:
(1) 效率。一次减法,一次与运算,就可以让n的最后一个1变成0。而原来的代码需要一次取反,一次与运算,一次减法。
(2) 适用性。我在3楼的代码实际上是沿袭了楼主所贴代码的写法,如果计算机采用的是补码来表示负数,则可以运行正确,否则就不能运行正确。而“n &= n - 1”这样的写法,就没有这个限制。即使计算机采用的不是补码表示,也可以正确运行。这个写法也可以顺利的推广到unsigned int,而原来的代码,由于unsigned int类型的取反操作并不安全,所以适用性不足。
不过也有一点小小的不足,那就是使用了常数0x7fffffff,这限制了这个代码只能用于32位整数。对于16位、64位整数,必须修改代码,才能确保运行无误。如果能够避免这个缺陷的话,就更好了。
如果n为零,则返回零。如果n是正数,则利用那个while循环就可以得出正确结果。如果n是负数,那就看着办吧……本题不会出现负数的情况,所以即使不考虑也没问题。
7 楼
eastcowboy [专家分:25370] 发布于 2010-08-02 19:23:00
网上流传一段更奇怪的代码,功能同样是计算n的二进制位中1的个数。不过,它不需要循环就能计算出结果,效率更高。代码如下:
[code=c]unsigned cal(unsigned int x)
{
const unsigned MASK1 = 0x55555555;
const unsigned MASK2 = 0x33333333;
const unsigned MASK4 = 0x0f0f0f0f;
const unsigned MASK8 = 0x00ff00ff;
const unsigned MASK16 = 0x0000ffff;
x = (x & MASK1 ) + (x >> 1 & MASK1 );
x = (x & MASK2 ) + (x >> 2 & MASK2 );
x = (x & MASK4 ) + (x >> 4 & MASK4 );
x = (x & MASK8 ) + (x >> 8 & MASK8 );
x = (x & MASK16) + (x >> 16 & MASK16);
return x;
}[/code]
原来我们的思路都是:一个一个的加,直到所有的位都判断完毕。
但这段代码的原理是:相邻的相加,逐步统一。
假设二进制数据本身有32位,每2个位为一组,计算每一组有多少个1。(因为一共有16组,每组的计算结果只需要2个bit就能保存,因此16个结果总共32位,计算好之后保存到变量x中)
然后每4个位为一组,计算每一组有多少个1。
然后每8位、每16位、每32位,于是得到最终结果。
8 楼
windy0will [专家分:2300] 发布于 2010-08-03 09:33:00
恩,还是上面那段代码牛!!!
不仅仅是它的高效,理解起来也更容易。
我来回复