目录
首先感谢2022.6.13杨超凡giegie带来的位运算算法讲解
位运算的常用几种方法
lowbit函数
int lowbit(int x){return x&(-x);}
lowbit函数可以用于查找一个数字的二进制表示形式当中的最右边的1,lowbit函数返回的值为x的最右边1所代表的十进制整数。例如10的二进制形式是1010,lowbit(10)返回的二进制形式也就是10,转化为十进制也就是2。因此,lowbit函数常常用来解决求某个十进制整数当中含有多少个1的问题
原码反码补码lowbit原理的补充
-x = ~x + 1
我们假设x为10,则可获得其原码反码补码
原码即为其二进制形式:1010
反码即为其二进制形式的相反:0101
补码则为反码加一:0110
那么lowbit函数的实质我们也就可以进行参透了
首次对于lowbit函数进行运算:(1010)&(0110)= 10(二进制形式)
减去第一次运算的结果之后,(1010) -> (1000) ,而后使用第二次lowbit运算
(1000) & (0111 + 1) -> (1000) & (1000) -> 1000(二进制形式)
以上即为lowbit函数原理的理解补充式子
具体应用
二进制当中1的个数
题目描述
给定一个长度为n的数列,请你求出数列中每个数的二进制表示中1的个数。
输入格式
第一行包含整数n。第二行包含n个整数,表示整个数列。
输出格式
共一行,包含n个整数,其中的第i个数表示数列中的第i个数的二进制表示中1的个数。
数据范围
1<=n<=100000
0≤数列中元素的值≤10^9
输入样例
5
1 2 3 4 5
输出样例
1 1 2 1 2
源代码
#include <iostream>
using namespace std;
const int N = 1000000+10;
int a[N];
int lowbit(int x)
{
return x&(-x);
}
int main()
{
int n;
cin >> n;
for(int i = 1;i <= n;i ++ )cin >> a[i];
for(int i = 1;i <= n;i ++ )
{
int ans = 0;
while(a[i])
{
a[i] -= lowbit(a[i]);
ans ++ ;
}
cout << ans <<' ';
}
return 0;
}
求取数的二进制的某一位的值与二进制枚举
求取n的二进制形式第k位的值:n>>k&1
例如10的二进制形式是1010
int a = n >> k & 1;a的值为0
int b = n >> k & 1;b的值为1
int c = n >> k & 1;c的值为0
int d = n >> k & 1;d的值为1
本方法常常与二进制枚举连用
用二进制枚举的情况往往都能够用DFS来解决,二者的共同特点都是暴力且有两个分支。在二进制枚举过程当中。用0和1来代表两种不同的情况,二进制枚举的模板如下所示
for(int i = 0;i < (1ll >> n);i ++ )
{
for(int j = 0;j < n;j ++ )
{
int num = i >> j & 1;
//分情况进行相应的处理
}
}
具体应用
凑数
题目描述
给定n个正整数,请判断能否用这n个数且每个数只能用一次,凑出来s
ps:建议使用二进制枚举
输入格式
第一行两个数n,s
第二行n个正整数
输出格式
如果能凑出输出YES,否则输出NO
样例输入
5 12
2 4 6 8 10
样例输出
YES
源代码
#include <iostream>
using namespace std;
typedef long long ll;
const int N = 1000000+10;
int a[N];
int main()
{
ll n,s,sum,flag;
cin >> n >> s;
for(int i = 0;i < n;i ++ )cin>>a[i];
for(int i = 0;i < (1ll << n);i ++ )
{
sum = 0,flag = 0;
for(int j = 0;j < n;j ++ )
{
int num = i >> j & 1;
if (num == 1)sum += a[j];
}
if(sum == s)
{
flag = 1;
break;
}
}
if(flag == 1)cout<<"YES";
else if(flag == 0)cout<<"NO";
return 0;
}
二进制的相关概念
只有0和1,逢二进一
二进制枚举的意义:利用二进制进行状态枚举,对于每一次的状态枚举0和1分别代表不同的状况
位运算的符号
按位与&
1&1=1 1&0=0 0&1=0 0&0=0 &&逻辑且常用于条件判断
例如6&7 实际为 110 和 111 的运算,根据按位与的运算法则结果为 110 也就是6
口诀为全一则一
常常用来判断奇偶,n&1
任何数按位与运算1结果是其本身
按位或|
1|1=1 1|0=1 0|1=1 0|0=0 ||逻辑或常用于条件判断
例如6|7 实际为 110 和 111 的运算,根据按位或的运算法则结果为 111 也就是7
口诀为有一则一
任何数按位或运算0结果是其本身
按位异或^
1^1=0 1^0=1 0^1=1 0^0=0
例如6^7 实际为 110 和 111 的运算,根据按位异或的运算法则结果为 001 也就是1
口诀为同零异一
任何数按位异或运算0结果是其本身
交换律结合律
按位与、按位或、按位异或遵守交换律与结合律
按位取反
按位取反可以求取一个数的负数减一,即~x=(-x-1)
左移右移
因c++运算符的重载,插入流也有着左移的功能,输出流也有着右移的功能
n>>k n除2的k次方 n<<k n乘2的k次方
规律记忆
同一个数对自身进行按位与和按位或都是其本身,对自身按位异或运算为0
a|(b&c) = (a|b)&(a|c) a^(b&c) = (a^b)&(a^c) a&(b^c) = (a&b)^(a&c)
根据交换率和结合律可自行继续推演,此处显示的公式仅仅为示例
知识巩固
麻烦的运算(按位&的规律题)
题目描述
小明刚刚学习了位运算中按位与的运算法则,想计算从n到m之间的数连续进行与运算之后的结果是多少,但是小明觉得一个一个的计算实在是太麻烦了,你能帮助他解决吗?
输入格式
两个整数n和m
输出格式
输出区间[n,m]之间的数进行与运算的结果
数据范围
0<=n<=m<=1e9
样例输入
11 13
样例输出
8
源代码
当从L到R连续进行按位与运算的话会使时间超限,因此我们需要找规律进行优化,显而易见的是L是一定小于等于R的,根据十进制整数转换为二进制整数的规律来看,当L的二进制形式位数只会小于等于R的二进制形式位数。因此问题简化为三个分支进行讨论:
若L与R的二进制位数不相等时,证明从L到R一定发生了进位,一旦有进位情况的发生那么就意味着在R的二进制形式下匹配不到L的二进制的位数全为0,而能够匹配到的也因为从L到R之间的连续按位与运算也为0,因此此情况只有一个答案那就是0
若L与R的二进制位数相等时
当L完全与R相等,根据运算律可以得到一个数对自身按位与运算还是那个数本身,因此答案为L(R)
当L与R不相等时,从最高位查找关键点(A[i] != B[i]时的下标i),关键点也就是发生进位的点,因此后面的数字全部换为0,再将处理过后的二进制数转化为十进制即为最终答案
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;
int main()
{
//数据记得开long long,因最后一个测试点是十位数
ll l,r;
cin>>l>>r;
//如果二者相等那么等于其本身
if(l == r)
{
cout<<l;//或者cout<<r;
return 0;
}
//A,B用于装l,r转化为二进制之后的字符串
vector<int> A,B;
//转换方法为不断对2取余再取倒
while(l > 0)
{
int num = l % 2;
l = l / 2;
A.push_back(num);
}
reverse(A.begin(),A.end());//注意取倒
while(r > 0)
{
int num = r % 2;
r = r / 2;
B.push_back(num);
}
reverse(B.begin(),B.end());//注意取倒
//当A的长度小于B的时候证明一定发生了进位,因此答案为0
if(A.size() < B.size())cout<<'0';
//二者长度相等,则从最高位开始查找关键点
else if(A.size() == B.size())
{
//关键点即为A[i] ! = B[i]是idx为i的坐标点
//说明i的下标位置发生了进位,因此从i往后的数全部为0
vector<int> C;
int flag = 0;
for(int i = 0;i < A.size();i ++ )//或者i < B.size()也可
{
if(A[i] == B[i] && flag == 0)C.push_back(A[i]);//未到关键点时正常存储
else if( A[i] != B[i] && flag == 0)//关键点时存0并打标记
{
flag = 1;
C.push_back(0);
}
else if(flag == 1)C.push_back(0);//关键点之后利用标记继续存0
}
//将C中存储的ans的二进制转换为十进制
ll ans = 0,w = 1;
for(int i = C.size() - 1;i >= 0;i -- )
{
ans += w * C[i];
w = w * 2;
}
cout<<ans;
return 0;
}
}
公式计算(交换律与结合律的推导)
题目描述
对于n个数a1,a2,a3.....an,计算公式[(a1&a1)|(a1&a2)|(a1&a3)|...(a1&an)]^[(a2&a1)|(a2&a2)|(a2&a3)...|(a2&an)]^...[(an&a1)|(an&a2)|(an&a3)...|(an&an)]
输入格式
第一行一个整数
第二行n个整数
输出格式
输出公式的结果
样例输入
2
1 1
样例输出
0
源代码
若是暴力运算会使时间超限,所以必然存在优化的方法,也就是对于公式的简化。
根据结合律将每一个按位异或运算的式子看作一个个模块
那么可以发现第一个模块为a1&(a1|a2|a3|a4|a5......|an)
由此推出第n个模块为an(a1|a2|a3|a4|a5......|an)
好了现在由于(a1|a2|a3|a4|a5......|an)太长我们把它简化为s1
那么公式经过第一次简化过后我们可以发现(a1&s1)^(a2&s1).......(an&s1)
再进行合并可得s1&(a1^a2^a3.........^an)
我们再把(a1^a2^a3.........^an)简化为s2,因此整个公式我们就进行简化完毕了
ans=s1&s2; s1=(a1|a2|a3|a4|a5......|an); s2=(a1^a2^a3.........^an);
#include <iostream>
using namespace std;
const int N = 1000000+10;
int a[N];
typedef long long ll;
int main()
{
int n;
cin >> n;
for(int i = 1;i <= n;i ++ )cin>>a[i];
ll head = a[1];
ll s = head;
ll s1 = s,s2 = s;
for(int i = 2;i <= n;i ++ )
{
s1 = (s1)|(a[i]);
s2 = (s2)^(a[i]);
}
ll ans = (s1)&(s2);
cout << ans;
return 0;
}
真题改编
改编自蓝桥杯省赛李白打酒题目
汽车加油
题目描述
陈老师经常开车大街上行走,假设刚开始油箱里有T升汽油,每看见加油站陈老师就要把汽油的总量
翻倍(就是乘2);每看见十字路口气油就要减少1升;最后的时候陈老师的车开到一个十字路口,然
后车就没油了------就熄火了,陈老师好痛苦啊~~~!
然后他就开始回忆,一路上一共遇到n个加油站,m个十字路口,问造成这种惨烈的境遇有多少种可能?
输入格式
三个正整数T,n,m;
输出格式
输出有多少种可能
数据范围
1<=T=100
0<n+m<=18
样例输入
1 5 10
样例输出
10
源代码
幂集:所谓幂集(Power Set), 就是原集合中所有的子集(包括全集和空集)构成的集族。可数集是最小的无限集; 它的幂集和实数集一一对应(也称同势),是不可数集。 不是所有不可数集都和实数集等势,集合的势可以无限的大。如实数集的幂集也是不可数集,但它的势比实数集大。 设X是一个有限集,|X| = k,根据二项式定理,X的幂集的势为2的k次方.
#include <iostream>
using namespace std;
typedef long long ll;
int main()
{
ll t,n,m,ans = 0;
cin >> t >> n >> m;
//总共会有m+n个元素,也就是2^(m+n)-1种情况
for(int i = 0;i < (1ll << (n + m - 1));i ++ )//开始对于相应幂集的情况进行暴力枚举
{
int sum = t;//临时变量转存t
int sumn = 0,summ = 0;//存路过加油站和路过十字路口的次数
for(int j = 0;j < n + m - 1;j ++ )//对于前m+n-1位的情况进行二进制枚举
{
int num = i >> j & 1;
if(num == 1)//加油站
{
sum = sum * 2;//油箱翻倍
sumn ++ ;//加油站次数加一
}
else if(num == 0)//十字路口
{
sum -= 1;//油减去一升
summ ++ ;//十字路口次数加一
}
}
//当在前m+n-1个情况当中,有n次路过加油站,有m-1次路过十字路口,且油箱仅有一升油,那么就意味着最后一次必是十字路口且油耗尽
if(sum == 1&&sumn == n&&summ == m - 1)ans ++;
}
cout << ans;//输出答案
return 0;
}