位运算
二进制数的位进行操作。对二进制的每一位进行逻辑操作,一般情况下位运算的每一位相互独立,各自运算出结果。
- 用途:优化算法,位掩码操作,位字段处理等领域。
- 竞赛中的位运算:异或的性质,状态压缩,与位运算有关的特殊数据结构(树状数组,01tire,01线性基),构造题。
- 常见的位运算:
-
按位与运算:只有当两位都为1时,结果为才为1,否则为0.(逻辑乘法)。
一条规则:当两个数字做与运算时,结果不会变大。 -
按位或运算:当两位只要有一位为1时,结果就为1,否则为0。
一条规则:当两个数字做与运算时,结果不会变小。 -
异或运算:^ 两位结果不同为1,否则为0.
该运算不会进位。
性质: -
按位取反(~):通常用于无符号整数。
-
按位左移(<<):移动时低位补0,注意移动时不要移动到无符号位上,或者干脆使用无符号整理,1会移动到符号位上。
左移操作相当于对原数进行乘以2的幂次方的操作。例如:5左移三次==5*(2^3) -
按位右移(>>):高位补0,如果数据类型为有符号整型,注意移动时让符号位为0,或者干脆使用无符号整型。如果符号位上有1不会被移走,这是负数位移的规则。
右移操作相当于对原数进行除以2的幂次方的操作。
13>>2==13/(2^2)向下取整。 -
例子:
-
#include<bits/stdc++.h>
using namespace std;
const int N=1050;
int main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cout<<bitset<32>((1 << 31) >> 2)<<'\n';
//bitset<32> 创建一个32位的二进制数
//{1<<31}:将数字1左移31位,得到一个32位的二进制数,其中第32位为1,其余位为0
return 0;
}
- 位运算的技巧:
- 判断数字的奇偶性:
x&1;
结果为1说明是奇数,结果为0说明是偶数。 - 获取二进制的某一位:
x>>i&1
结果必然为0或1,表示x的二进制表示中的第i位。 - 修改二进制的某一位为1或0
x|(1<<i) 将x的第i位或上1,则第i位变为1,其他位上或上0没有影响
x&~(1<<i) - 快速判断一个数字是否是2的幂次方:
x&(x-1)
结果为0证明x为2的幂次方。(说明该x的二进制表示中只有一个1,x-1就有很多个连续的1并且和x的1没有交集,两者的运算一定为0,可以证明其他情况一定不为0)
如何计算x-1:当我们从x减去1时,我们需要从最低位开始找到第一个1,并将该位改为0,然后将所有更低位的0改为1。如果最低位是0,我们就从最低位开始,将所有的0都改为1。 - 获取二进制位中最低位的1:常用于数据结构树状数组中。
lowbit(x)=x&-x;
补:二进制数负数的二进制:
- 求原码
- 取反码
- 求补码(反码加1)。加1:从最右一位加一直到不产生进位。
- 判断数字的奇偶性:
- 例题:
- 求二进制中1的个数:
unsigned int x;重中之重的一句话
//大神的写法:
#include<bits/stdc++.h>using namespace std;
int main(){
unsigned int x;
cin>>x;
int ans=0;
while(x){
if(x%2==1) ans++;
x/=2;
}
cout<<ans;
return 0;
}
//我的挣扎:(入栈和出栈)
#include<bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
stack<int>a;
int count=0;
unsigned int m;cin>>m;
//求二进制的算法
while(m > 0)
{
if(m%2==0)
a.push(0);
else
a.push(1);
m = m / 2;
}
while(!a.empty())
{
if(a.top()==1)
count++;
a.pop();
}
cout<<count<<endl;
return 0;
}
//老师的解法
#include<bits/stdc++.h>
using namespace std;
int main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int count=0;
unsigned int m;cin>>m;
while(m)
{
if(m&1) count++;//x的最低位为1
m>>=1;//x不断向右移动1位
}
cout<<count<<endl;
return 0;
}
- 3691:区间或–难理解
重要思想:拆位做贡献。
将整个问题划分为31个部分,分别表示数组元素二进制表示的第0位,第一位,,第30位。
计算每一部分的前缀和,对于每次询问,判断这一位是否是1(即前缀和>0),如果是就算上这一位对答案的(求和的)贡献。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+9;
int a[N];
int prefix[35][N];//前缀和
int main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int n,q;cin>>n>>q;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
for(int w=0;w<=30;w++)
{
for(int i=1;i<=n;i++)
{
prefix[w][i]=prefix[w][i-1]+(a[i]>>w&1);
//将当前元素的第w位的值(0或1)加到前缀和数组prefix[w]中对应的位置上。这里使用了右移操作符>>和按位与操作符&来判断当前元素的第w位是否为1。
}
}
while(q--)
{
int l,r;cin>>l>>r;//输入区间l到r
int ans=0;
for(int w=0;w<=30;w++)
{
ans+=(1<<w)*(prefix[w][r]-prefix[w][l-1]?1:0);
//对于每个二进制位的位置w,根据前缀和数组prefix计算出区间l到r内该位为1的个数,并将其乘以2的w次方后累加到ans中
}
cout<<ans<<'\n';
}
return 0;
}
- 3400异或森林:
题目:子数组所有元素异或的运算结果的因数个数为偶数。
因数个数为偶数–不是完全平方数–根号x为整数。
什么是完全平方数? - 非负性:完全平方数是非负数,包括零和所有正整数的平方。
- 乘法定义:如果一个数可以表示为某个整数n的平方,即 ( n^2 ),那么这个数就是一个完全平方数。例如,( 1^2 = 1 ), ( 2^2 = 4 ), ( 3^2 = 9 ) 等。
- 平方根为整数:完全平方数的平方根是一个整数,这是判断一个数是否为完全平方数的重要依据。
- 数字特征:完全平方数的个位数字只能是0, 1, 4, 5, 6, 或 9。此外,如果完全平方数的个位是奇数,则其十位上的数字必为偶数;如果个位是6,则十位上的数字必为奇数。
试除法:x 根号下x
正难则反:计算有多少个子数组的异或和为完全平方数,用总区间减去即可。
前缀和
它不是已实现的数组,是需要额外定义的。
- 前缀和的原理和特点:
prefix表示前缀和,前缀和由一个用户输入的数组生成。
可理解 为数组前i个元素之和。
prefix的重要特性:快速获得prefix
prefix[i]=prefix[i-1]+a[i];
求静态数组一段区间的和
sum(l,r)=prefix[r]-prefix[l-1];
注:如果要实现“先区间修改,再区间查询”可以使用差分数组,如果需要“一边修改,一边查询”需要使用树状数组或线段树等数据结构。 - 实现前缀和:
int a[N],prefix[N];
for(int i=1;i<=n;i++)//注意数组下标从1开始
{
prefix[i]=prefix[i-1]+a[i];
}
- 例题:
- 区间次方和:我的想法是先找出区间求k次方的和。老师的想法是存放5个数组,第一个数组放1次方,第二个数组放而次方,再求区间和。这样能更方便的利用前缀和的特性
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=1e5+9;
const ll p=1e9+7;
ll a[6][N];
ll prefix[6][N];//前缀和
int main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int n,m;cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[1][i];
}
//求k次方
for(int i=2;i<=5;i++)//1次方不用求,从2开始
{
for(int j=1;j<=n;j++)
{
a[i][j]=a[i-1][j]*a[1][j] % p;//条件意义是a[j]的i次方等于。。
// a[i][j]表示第j个元素的i次方的结果,a[i-1][j]表示第j个元素的(i-1)次方的结果,a[1][j]表示第j个元素的1次方的结果。
}
}
//定义前缀和
for(int i=1;i<=5;i++)
{
for(int j=1;j<=n;j++)
{
prefix[i][j]=(prefix[i][j-1]+a[i][j])%p;
}
}
while(m--)
{
int l,r,k;
cin>>l>>r>>k;
cout<<(prefix[k][r]-prefix[k][l-1]+p)%p<<'\n';//+p是为了防止出现负数
}
return 0;
}
- 3419:最长平衡串的长度,出现了读题错误。我的想法是找到出现次数最少的字符串的个数*2.这种做法只能求出最小字符串,不能求出最长字符串,要想求出最长字符串需要考虑“最少要满足什么条件”。
可以这样做:
把L看做1,Q看做-1,只有当某个区间的和为0时,字符串是平衡的。
可以预处理出前缀和,枚举所有区间,得到所有平衡区间的长度最后取大输出。
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
char s[N];
int prefix[N];
int main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>s+1;
int n=strlen(s+1);
for(int i=1;i<=n;i++)
{
prefix[i]=prefix[i-1]+(s[i]=='L'?1:-1);
}
int ans=0;
for(int i=1;i<=n;i++)
{
for(int j=i;j<=n;j++)
{
if(prefix[j]-prefix[i-1]==0)//如果区间和为0
ans=max(ans,j-i+1);//取最大的区间
}
}
cout<<ans<<'\n';
return 0;
}
差分
- 定义
diff[i]=a[i]-a[i-1];
对差分数组求前缀和可以还原为原数组。 - 特点
可以实现快速的区间修改。
将区间[L,R]都加上x
diff[l]+=x;
diff[r+1]-=x;
- 需要最终用前缀和还原为原数组
- 差分的实现
直接用循环
for(int i=0;i<=n;i++)
{
diff[i]=a[i]-a[i-1];
}
- 例题
- 区间更新
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+3;
int a[N],diff[N];
void solve(int n,int m)
{
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)diff[i]=a[i]-a[i-1];
while(m--)
{
int l,r,k;
cin>>l>>r>>k;
diff[l]+=k;diff[r+1]-=k;
}
for(int i=1;i<=n;i++)a[i]=diff[i]+a[i-1];//还原数组
for(int i=1;i<=n;i++)cout<<a[i]<<(i==n?"\n":" "); //控制换行
}
int main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int n,m;
while(cin>>n>>m)
solve(n,m);
return 0;
}
- 小明的彩灯1276
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=1e5+5;
ll a[N],diff[N];
int main()
{
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int n,q;cin>>n>>q;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)diff[i]=a[i]-a[i-1];
while(q--)
{
int l,r,k;
cin>>l>>r>>k;
diff[l]+=k;
diff[r+1]-=k;
}
for(int i=1;i<=n;i++)a[i]=diff[i]+a[i-1];
for(int i=1;i<=n;i++)cout<<max(0ll,a[i])<<(i==n?"\n":" ");
return 0;
}
说明:这段代码中的max函数是C++标准库中的一个函数,用于比较两个值并返回较大的那个。在这个代码中,它被用来确保数组a中的元素不会小于0。如果a[i]小于0,那么max(0ll, a[i])将返回0,否则返回a[i]本身。
递归
- 递归的介绍
函数直接或间接的调用自身的过程。 - 递归的实现
-
主要:出口+分治
[图片] -
递归与循环的比较
- 递归:直观。递归调用不断减少,常用于树和图的遍历,有栈溢出的风险。
补充:栈一般只有8MB,所以递归层数不宜过深,不超过1e6层。 - 循环:常数小,效率高,有特定的迭代次数,适合大部分的动态规划。
补:dfs和栈可