首先發出題目鏈接:
鏈接: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;
}