导语:
大家都知道计算机的所有的运行操作和程序最终都是根据二进制来实现的,在我们写算法程序的时候,在编译器内部,也是将我们的程序转化为二进制序列,进行程序操作的。而位运算就是基于二进制的程序运算操作,相比于正常的运算操作,位运算不仅速度快、效率高,而且在算法设计中,还可以简化时间复杂度和空间复杂度,本篇给出位运算的两个常见的应用,通过位运算来大幅度简化时间复杂度和空间复杂度。
一、位运算基本操作
与运算 &
或运算 |
异或运算 ^
非运算(求补) ~
右移运算 >>
左移运算 <<
(1)与运算
双目运算符,当前后两个都为真的时候,返回为真,反之为假
1 & 1 == 1
1 & 0 == 0
0 & 1 == 0
0 & 0 == 0
(2) 或运算
双目运算符,当前后两个有一个为真的时候,返回为真,反之为假
1 | 1 == 1
1 | 0 == 1
0 | 1 == 1
0 | 0 == 0
(3)异或运算
双目运算符,当前后两个真值不相等的时候,返回为真,反之为假
1 ^ 1 == 0
1 ^ 0 == 1
0 ^ 1 == 1
0 ^ 0 == 0
(4)非运算
单目运算符,真值取反
~1 == 0
~0 == 1
(5)左移运算符
a<<b表示把a转为二进制后左移b位(在后面添b个0)
a<<b实际上就是a乘2的b次方(在二进制后面加0就相当于乘2的次方)
(6)右移运算符
a>>b表示把a转为二进制后右移b位(在后面减b个位数)
a>>b实际上就是a除以2的b次方(在二进制后面减位数就相当于除以2的次方)
二、常见的位运算操作
-
取出整数 n 在二进制下的第k位
(n>>k)&1 说明:n>>k,右移k位,对1取与运算
右移k位->使第k位变为第一位,对1取与运算,得到第k位 -
把 n 在二进制条件下的第k位取反
n ^ (1<<k) -
对 n 在二进制表示下的第 k 位 赋值为 1
n | (1<<k) -
对 n 在二进制下的第 k 为赋值为 0
n & ( ~ ( 1 << k ) )
三、位运算的例题应用
例一、快速幂
链接:ACwing 89
题目描述:
给出三个整数 a, b, c
求 a^b%p的值
数据范围:
0≤a,b,p≤10^18
分析:
如题所示,求a的b次方对p取模的值。
数据范围都是1e9次方,如果手动模拟累乘则是O(n)的算法,1e9一定超时,其实一般1e8~1e7就差不多超时了。
所以这里优化一下算法。
对于一个a^b
假设a等于3,b等于11
3^11
可以转化为
3^(1011)
也就是
3 ^ (2 ^ 3+2 ^ 1+ 2 ^ 0)
类比推导a^b
a^1… a^( 2^0)
a^2… a^( 2^1)
a^4… a^( 2^2)
a^8… a^( 2^3)
…
a^k… a^( 2^(log2 k))
累计k+1项,如此优化,复杂度化为O(logn),此算法又称快速幂,采用位运算的思想
代码如下:
#include<iostream>
using namespace std;
int main()
{
int a,b,p;
cin>>a>>b>>p;
int res=1%p;//初始化res,因为是幂积运算,所以初始化为1
while(b)
{
if(b&1)//看b的最后一位是不是1。
res=res*1ll*a%p;
a=a*1ll*a%p;
b>>=1;//对b右移
}
cout<<res<<endl;
return 0;
}
模拟一下代码操作,假设a=3,b=11,p=1000;
b=11在二进制下表示是1011即2^0 + 2 ^ 1 + 2 ^ 3
刚开始res=0,当b!=0的时候开始执行循环操作
if(b&1)等价于if(b&1!=0)也就是说b的二进制下的第一位不等于0.我们知道b刚开始是1011,满足条件,也就是说应该取第一位。即res=res * a%p,即res=3*1%1000=3;
操作结束后,a=a * a%p == 9, b>>=1等价于b=b>>1也就是说b变为了101
继续循环,满足if条件,res=res * a%p
也就是res=3*9%1000=27
a=a * a=81,b=b>>1=10,然后继续循环,此时不满足if条件,不进行res操作,a变为81*81,b变为1,一直到最后,计算出结果
a=a * a % p是模拟a的1、2、4、8次幂
b=b>>1是对b逐位运算。
res是最终计算结果
例二、64位整数乘法
链接:64位整数乘法
题目描述:
给出 a, b, p 三个整数,求a * b 对 p 的取模
数据范围:
1 ≤ a,b,p ≤ 10^18
分析:
数据范围,最大1e18,很显然,模拟不可靠,可以考虑位运算。
由刚刚的ACwing_89可以看出来位运算可以将O(n)的算法简化到O(logn)进行处理。
a*b可以转化为
a + a + a + a +…+ a(一共b个a相加)
a^b可以转化为
a * a * a * a * … * a(一共b个a相乘)
所以对于a * b也可以采用上题的思想
假设a等于3,b等于11
3 * 11可以转化为3 * (1101) 可以转化为 3 * 2 ^ 3 + 3 * 2 ^ 2+3 * 2 ^ 0
代码如下:
#include<iostream>
typedef unsigned long long LL;//考虑到最大单数为1e18,有可能会到达1e19的数量级
//所以开了unsigned long long
using namespace std;
int main()
{
LL a,b,p;
cin>>a>>b>>p;
LL res=0;//初始化为0
while(b)
{
if(b&1)//如果b的最后一位是1
res=(res+a)%p;//进行加和操作
a=(a+a)%p;
b=b>>1;//b右移一位
}
cout<<res<<endl;
return 0;
}
以上便是位运算对时间复杂度的优化,可以将O(n)的算法优化为O(logn),大大加快了程序的运行速度,所以,有的时候如果没有了解题思路,可以尝试一下考虑位运算,或许会有不一样的天地~