08. 位运算&快速幂
08. 位运算&快速幂
位运算
对于计算机而言,只能处理二进制数据,所以无论多么高级的语言最后都会被计算机转为二进制数据 01 的格式。
比如:十进制的 10,我们会将它记为 10D,D 表示 DEC
我们知道 \(10 =8+2 = 2^3 + 2^1 = 1*2^3+0*2^2+1*2^1+0*2^0\)
所以可以转为二进制:1010B
如果我们使用2Byte,也就是8bit 来储存这个数据,由于是正数,符号位为0,所以10D的二进制原码为:0000 1010B。
学习位运算前,需要掌握二进制,那么我们这里就将进制转换在复习一下
常用进制:2,8,10,16
计算公式:结果 = sum(基数*位数权重)
二进制 bin,Binary :1001B = 8+1 =9D
八进制 oct,Octal :1005O = 1*8^3+5*8^0=8^3+5
十进制 dec,Decimal:1289D = 1289D
十六进制 hex,Hexadecimal:0x119f = 1*16^3+1*16^2+9*16^1 +15*16^0
#include<iostream>
#include<iomanip>
using namespace std;
int main(){
int n; cin>>n; // 10
cout<<oct<<n<<endl; // 12,以八进制格式输出
cout<<dec<<n<<endl; // 10,以十进制格式输出
cout<<hex<<n<<endl; // a ,以十六进制格式输出
cout<<setiosflags(ios::uppercase)<<hex<<n<<endl; // A
return 0;
}
& : 按位与,同真为真,其余为假
| : 按位或,同假为假,其余为真
^ : 异或运算,相异为真,相同为假
~ : 取反,真假颠倒
<<: 左移:按二进制把数字向左移动对应位数,高位移出(舍弃),低位的空位补0。
>>: 右移:按二进制把数字向右移动对应位数,低位移出(舍弃),高位的空位补符号位,即正数补0,负数补1
>>左移运算符:num << n; 相当于num乘以2的n次方(低位补0)
>>左移运算符:num >> n; 相当于num除以2的n次方
10D = 8+2 --> 1010B<<2 = 101000B = 32+8=40 = 10*2^2
10D = 8+2 --> 1010B>>2 = 0010B = 2 = 10/(2^2)=2
原码,反码,补码是机器存储一个具体数字的编码方式
原码就是符号位加上真值的绝对值,即用第一位表示符号,其余位表示值
比如如果是8位二进制:
+2 的原码 = 0000 0010
-2 的原码 = 1000 0010
第一位为符号位,所以8位二进制取值范围为[1111 1111, 0111 1111] 也就是[-127, 127]
反码:正数的反码就是原码,负数的反码是在原码的基础上,符号位不变,其余位取反
+2的反码 = 0000 0010
-2的反码 = 1111 1101
补码:正数的补码就是原码,负数的补码是在反码的基础上+1
+2的反码 = 0000 0010
-2的反码 = 1111 1110
总结:
正数的原码,反码,补码都是原数的二进制;
负数的原码就是符号位为1,其余位按正数计算
负数的反码就是符号位为1,其余位按正数取反
负数的补码就是符号位为1,其余位按正数取反后+1
原码:10D=0000 1010B, -10D=1000 1010B
反码:10D=0000 1010B, -10D=1111 0101B
补码:10D=0000 1010B, -10D=1111 0110B
位运算的一些使用
我们知道一个数的二进制组成一定是 01 格式,那么
对于偶数而言:其末位一定为 0;// 如 8D = 1000B
对于奇数而言:其末位一定为 1;// 如 7D = 0111B
【例】 输入一个数,判断它是否是2的整数幂,如果是就输出Yes,如果不是就输出No。
方法1:判断n是否是2的整数幂,最简单的办法就是在数字大于0且能被2整除时,就反复除以2。
退出循环时,若商不是1,n就不是2的整数幂,否则就是。
#include<iostream>
using namespace std;
int main() {
int n; cin>>n;
while(n>0 && n%2==0) n>>=1;
if(n!=1) cout<<"No";
else cout<<"Yes";
return 0 ;
}
方法2:利用位运算进行计算。
举例:8D = 1*2^3 = 1000B,如果是 2^k 次,则一定只有首位为 1,其余位为 0;
则将其原码-1,应当出现全部按位取反的情况,则此时将二者进行按位与,会得到 0。
#include<iostream>
using namespace std;
int main() {
int n; cin>>n;
if(n&(n-1)) cout<<"No";
else cout<<"Yes";
return 0 ;
}
【例】不及格人数
学习了一段C++程序设计后,老师组织了验收考核。
考核分为笔试和上机两种形式,每位同学也便有了两个成绩,
现在老师想知道,参加考核的n人中有多少人只有一个成绩不及格(每场考核满分为100分,不低于60算及格)。
#include <iostream>
using namespace std;
int main() {
double x,y,n,ans=0; cin>>n;
for(int i=1; i<=n; i++) {
cin>>x>>y;
if(x<60 ^ y<60) ans++;
}
cout<<ans;
return 0;
}
【例】街灯
在一条笔直的街道上,有无数的街灯,每盏灯有自己的独立开关。
为了检验灯的质量,管理员想出了一个有趣的办法,找若干人按顺序一个一个地从街道的一侧进入,每个人看到亮着的灯就熄灭,直到看到第一盏关着的灯,将其点亮,任务完成。如果所有灯都质量完好,且初始时都关闭,那么第m个人走过后,有多少灯被点亮过?
输入样例:4
输出样例:3
#include<iostream>
using namespace std;
int main() {
int n,num=1; cin>>n;
n>>=1;
while(n!=0) {
num++;
n>>=1;
}
cout<<num;
return 0;
}
P1226 【模板】快速幂||取余运算
给你三个整数 a,b,p,求 a^b mod p。
输入格式:一行三个整数,分别代表 a,b,p;
输出格式:一行一个字符串 a^b mod p=s,a,b,p 分别为题目给定的值, s 为运算结果。
输入样例:2 10 9
输出样例:2^10 mod 9=7
对于 100% 的数据,保证 0 ≤ a,b < 2^31, a+b > 0, 2 ≤ p < 2^31
分析:顾名思义,快速幂就是快速计算底数的 n 次幂。
其时间复杂度为O(logN),与朴素的O(N)相比效率有了极大的提高。
快速幂有两种理解方式:二分递归和二进制,但是其本质都是二进制。
二分递归证明如下:
当 n 为偶数的时候:\(a^{n} = a^{n/2} * a^{n/2}\)
当 n 为奇数的时候:\(a^n = a^{n/2} * a^{n/2} * a\) ;
注意:快速幂的运算结果是呈几何倍数的增加,所以很容易爆范围,故而开 long long
经常对于这样的题目,会说对某个数取模,这里我们可以记住以下几个取余运算相关性质:
(a + b) % p = (a % p + b % p) % p
(a - b) % p = (a % p - b % p) % p
(a * b) % p = (a % p * b % p) % p
除法不满足
这里我们选择第一个进行证明。
由于减法其实就是加上一个负数,乘法就是多个加法,所以 也可以伪证另外两条性质。
ll halfpow(ll a, ll n){
if(n==0) return 1;
ll ans=halfpow(a, n/2);
ans = ans*ans%mod;
if(n&1LL) ans=ans*a%mod;
return ans;
}
-
进制原理实现快速幂(二分快速幂思想不变,但是从进制原理的角度去理解)
-
\(a^{n} = a^{XB}\)
-
\(a^{11} = a^{8+2+1} = a^{2^{3} +2^{1} + 2^{0}} = a^{1011B}\)
发现仅当二进制位为 1 时,才会对其进行累乘,而累乘的数值为该进制位的权值次幂(基数)。
比如:1011,从右向左,仅当第 0 位,第 1 位,第 3 位时才进行累乘计算。
算法流程:
初始值:ans=1, a=a, X=1011
如果X的二进制最低位为1,即 lowbit(X)=1,则 ans = ans*a;
基数一直: a = a*a;
主要在于:a = a*a 的理解
次数:数值
0:a = a^(2^0)
1:a^2 = a^(2^1)
2:a^4 = a^(2^2)
3:a^8 = a^(2^3)
4:a^16 = a^(2^4)
ll fastpow(ll a, ll n){
ll ans=1;
while(n){
if(n&1LL) ans = ans*a%mod;
a = a*a%mod;
n >>= 1; // n/=2;
}
return ans;
}