首先发出题目链接:
链接:https://ac.nowcoder.com/acm/contest/847/D
来源:牛客网
涉及:按位运算
题目如下:
说实话,按位运算挺重要的,快速幂有了按位运算才降低了复杂度,有和无本身就是对立的,有了0和1所代表的按位运算,才使得我们能够从一个数的结构上分析或者改变数字
我们先来说如何初步地得到一个最大的k–or–and的值
要让k-or-and值最大,首先是先让分段后每一段的or值最大,那么and后的值当然就是最大的,要想找到这个每一段中or的最大值,可以通过枚举的方式,由于这个最大or数的二进制位最大只有30位,可以从二进制位的最高位开始枚举每一段or的最大值。
举个例子:
我们假设第30位是1,就得到100…000(共29个0),然后贪心的验证,存不存在k段,使得每一段的or值的二进制位第30位为1,如果存在,那么最大or值二进制位的第30位一定是1,否则就是0.
然后,假如第30位是1,再来假设第29位也是1,就得到1100…000,在来贪心的验证能不能将序列分成k段,使得每一段的or值的二进制位第30位为1且二进制第29位为1,如果存在,那么最大or值二进制位的第29位也是1,否则就是0.
再然后,假设第29位不能为1,那么再假设第28位是1,就得到10100…0000,在来贪心的验证能不能将序列分成k段,使得每一段的or值的二进制位第30位为1,二进制第29位为0,二进制第28位为1(以此类推)…
问题:如何来贪心得进行验证?
加入当前得到的数是101000…000(ans),从序列第一个数开始往后一直或,直到或到一个数,使得这一系列的数或之后的值(cnt)与ans的与值等于ans(表示ans的二进制位为1,cnt在这个位置也是1,即**(cnt&ans)==ans**),于是刚刚或的一系列的数即为这个序列的一段,然后这样再继续找下一段,满足了每一段的or值都是ans,然后看一看是否分成有k段,如果有,表示验证成功,否则验证失败。
按照以上的方法,判断完30位的每一位,就得到了一个数,这个数就是最大的k-or-and值
bool check(ll ans){
ll cnt=0,ant=0;//cnt是当前一系列的或值,ant代表当前已分段数
for(int i=1;i<=n;i++){
cnt|=num[i];
if((cnt&ans)==ans) ant++,cnt=0;//满足条件,分段数加一,cnt清零,从新开始新一段的或
}
//再判断是否已经分了至少k段
if(ant>=k) return true;
else return false;
}
下面来说一说如何修改
不多说,直接把序列中每一个值进行修改,再套用上面求k-or-and最大值的方法肯定超市。
所以得找技巧!
根据或和与的确定性可知:
1.当序列所有的数或上一个x,当x二进制第i位是1,那么序列中所有数二进制的第i位都会变成1,最后k-or-and值得二进制位的第i位就锁定为1;
2.当序列所有的数与上一个x,当x二进制第i位是0,那么序列中所有数二进制的第i位都会变成0,最后k-or-and值得二进制位的第i位就锁定为0;
但是,不一定最后k-or-and值二进制每一位的数都发生变化,也就是说,只有那些被锁定的值是确定的,没有被锁定的值仍然是未知的
ps:每一次或上或者与上一个数,可能出现新的位置被锁定了,也有可能没有出现新的被锁定的位置。
于是,求最大k-or-and的方法就会发生变化
不需要再一个个判断所有的30和位置,我们只用讨论那些没有被锁定的位置
最后的k-or-and的最大值就是没有锁定位置的值加上锁定位置的值
但是注意,在这里需要剪枝:
如果上一次或上或者与上一个值,没有出现新的锁定的位置,那么,我们可以跳过判断没有锁定位置值得这一步骤!可以用一个数组来判断每一个位置是否被锁定,然后用一个bool类型标记,来判断是否出现新的锁定位置
举个例子:
比如题目例子(这里只讨论求k-or-and值,由于一开始没有任何位置被锁定,所以要全部位都要讨论)
n=3,k=2,序列为{11,30,4}
序列 | 二进制数 |
---|---|
11 | 1011 |
30 | 11110 |
4 | 100 |
可以从10000开始讨论:
1.当ans=10000时
序列 | 二进制数 | 满足条件分段后的or值 |
---|---|---|
11 | 1011 | — |
30 | 11110 | 11,30为一段,or值为11111 |
4 | 100 | — |
只能分成一段{11,30,4},不满足条件,故ans第五位为0
2.当ans=1000时
序列 | 二进制数 | 满足条件分段后的or值 |
---|---|---|
11 | 1011 | 11为一段,or值为1011 |
30 | 11110 | 30为一段,or值为11110 |
4 | 100 | — |
可以分成两段{11}{30,4},满足条件,故ans第四位为1
3.当ans=1100时
序列 | 二进制数 | 满足条件分段后的or值 |
---|---|---|
11 | 1011 | — |
30 | 11110 | 11,30为一段,or值为11111 |
4 | 100 | — |
只能分成一段{11,30,4},不满足条件,故ans第三位为0
4.当ans=1010时
序列 | 二进制数 | 满足条件分段后的or值 |
---|---|---|
11 | 1011 | 11为一段,or值为1011 |
30 | 11110 | 30为一段,or值为11110 |
4 | 100 | — |
可以分成两段{11}{30,4},满足条件,故ans第二位为1
5.当ans=1011时
序列 | 二进制数 | 满足条件分段后的or值 |
---|---|---|
11 | 1011 | 11为一段,or值为1011 |
30 | 11110 | — |
4 | 100 | — |
只能分成一段{11,30,4},不满足条件,故ans第一位为0
所以最大的k-or-and值为1010(10)。
代码如下:
#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstring>
#define INF 200005
using namespace std;
typedef long long ll;
int n,k,q;//题目定义的变量
int flag;//flag存题目定义的操作类型数
ll num[INF],x;//num数组存题目中的A数组
int bit[32];//bit数组存被锁定的位置,以及锁定位置对应的锁定值,当bit[I]=-1表示这一位未被锁定
bool updata=true;//updata判断于或者或之后是否出现新的锁定位
bool check(ll ans){
ll cnt=0,ant=0;//ant存当前已经分段数,ant存当前一段的or值
for(int i=1;i<=n;i++){//贪心的判断能不能分成k段
cnt|=num[i];
if((cnt&ans)==ans) ant++,cnt=0;//能,则重新开始分段
}
if(ant>=k) return true;//能贪心分成k段
else return false;
}
int main(){
cin>>n>>k;
ll ans;//在查询最大K-OR-AND事使用,用来贪心枚举未锁定位置的的最大分段or值
memset(bit,-1,sizeof(bit));//把bit数组初始化为-1,表示所有位置都没有被锁定
int i;
for(i=1;i<=n;i++) cin>>num[i];
cin>>q;
while(q--){
cin>>flag;
if(flag==3){
ll final=0;//final存所有被锁定位置中,锁定值为1的贡献
for(i=0;i<=30;i++)
if(bit[i]==1) final+=(1<<i);
if(updata){//如果没有出现新的所定值,就不用再次对未锁定的位置进行贪心
ans=0;//初始化为0
updata=false;
for(i=30;i>=0;i--){
if(bit[i]!=-1) continue;//跳过锁定位置
ans^=1<<i;//假设二进制这一位为1
if(!check(ans)) ans^=1<<i;//如果不能为1,则为0
}
}
cout<<final+ans<<endl;//锁定位置的值加未锁定位置的值就是答案
}
else if(flag==2){//与
cin>>x;
for(i=0;i<=30;i++)
if(!(x&1<<i)){//判断x的二进制的第i位是不是为0
if(bit[i]==-1) updata=true;//如果是0,且k-or-and这一位未被锁定,则最大k-or-and这一位锁定
bit[i]=0;//锁定为0
}
}
else{//或
cin>>x;
for(i=0;i<=30;i++)
if(x&1<<i){//判断x的二进制的第i位是不是为1
if(bit[i]==-1) updata=true;//如果是1,且k-or-and这一位未被锁定,则最大k-or-and这一位锁定
bit[i]=1;//锁定为1
}
}
}
return 0;
}