2020 ICPC 澳門站 G - Game on Sequence 題解
題面看這裡
題目大意
給你一個長度為 n 的陣列 a,Grammy 和 Alice 用這個陣列玩個小遊(bo)戲(yi),遊戲規則如下:
對於每局遊戲,先選定一個位置 \(k\) 為起點,\(\rm Grammy\) 先手,輪流操作,每次操作都可以從當前位置 \(i\) 跳到後面的某個位置 \(j\),\(j\) 滿足 \(a_i\) 和 \(a_j\) 在二進位制下最多隻有一位不同,誰的回合中不能操作了,誰就輸了。
然後有 m 次操作,每次操作給定兩個數 op 和 k,op 代表操作型別,有兩種:第一種型別,直接在陣列末尾新增一個數 k;第二種型別,詢問以 k 為起點開一局遊戲誰能贏。
題目分析
首先顯然這是一道博弈論的題目,於是我們從先手必勝態和先手必敗態的分析入手。(以下簡稱必勝態和必敗態)
必勝態的定義:可以跳到一個必敗態的即為必勝態。
必敗態的定義:沒有一個地方可以跳或者能跳的地方都是必勝態的即為必敗態。
打表,嗯推,推著推著發現一個有趣的性質:一個數如果在數組裡面多次出現,那麼除了最後出現的那個位置之外的其他位置都是必勝態。
設這個數為 \(x\),\(x\) 最後出現的那個位置為 \(j\),分兩種情況討論。
1、當 \(j\) 為必勝態時,說明 \(j\) 後面有個可以跳的位置是必敗態,則 \(j\) 前面的 \(x\) 也一定可以跳到這個位置從而達到必勝,此時 \(j\)
2、當 \(j\) 為必敗態時,\(j\) 前面的 \(x\) 只需要跳到 \(j\) 這個必敗態就可以達到必勝,此時 \(j\) 前面的 \(x\) 也都是必勝態。
綜上,無論 \(j\) 是什麼狀態,\(j\) 前面的 \(x\) 都存在至少一種的必勝策略。
有了這個性質,對於任意一個數 x,我們就不必關心非最後的位置 j 以外的其他位置了,遊戲開始位置為這些位置時直接輸出 "Grammy" 即可。
對於某個數的最後一個位置 j,我們則需要判斷一下這個究竟是必勝態還是必敗態。
顯然這個遊戲沒有平局,所以不是必勝態就是必敗態,同時根據上面的定義,感性地覺著必勝態更好判一些,我們來思考怎麼判斷必勝態。
起始位置為終點時先手必定跳不動,所以最初的必敗態是 k = n,初態在末尾,考慮逆著推。
想到一個正確性顯然的演算法:對於當前位置後面的每一個位置(逆著推的,後面的每一個位置的狀態肯定都是確定好了的),判斷其是否是必敗態,如果是,再判斷下能否跳過去,如果能跳過去,則當前的位置是必勝態,逆著推一直推到所求的位置 k 即可。
正確性顯然,但複雜度不咋地,考慮到 a 陣列很大,當起點 k 很小時,最壞時間複雜度 \(O(n^3),~n=2e5\),必 T。
考慮優化,怎麼優化捏,根據性質,易知只需判斷每個數字出現的最後一個位置即可,因為我們要判的是後面某個位置是否為必敗態,而除了每個數字的最後一個位置,其他位置都一定是必勝態,所以根本不用判,注意到資料範圍 \(0\leq a_i\leq255\),因此每次遊戲撐死也就判個 255 次,時間複雜度 \(O(n*255^2)\),當然還可以進一步優化,但這題時間開了 4s,且就算出題人特意造資料卡掉這個演算法,我們也可以通過加點離線和特判來黑掉出題人造的資料,所以直接嗯艹過去就行了。大力出奇跡
程式碼實現
結合程式碼來說明一下實現細節。
AC Code
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+6;
int a[maxn];
int vis[300],flag[300],v[300];
int main(){
int n,m,op,k;
cin >> n >> m;
for(int i=1;i<=n;i++) {
cin >> a[i];
vis[a[i]]=i;
}
while(m--){
cin >> op >> k;
if(op==1){
a[++n]=k;
vis[k]=n;
}
else {
if(vis[a[k]]>k){
cout << "Grammy\n";
}
else {
int cnt=0;
for(int i=0;i<256;i++){
flag[i]=0;
if(vis[i]){
v[cnt++]=vis[i];
}
}
sort(v,v+cnt);
for(int i=cnt-1;;i--){
for(int j=i+1;j<cnt;j++){
if(flag[a[v[j]]]==0){
int d=a[v[i]]^a[v[j]];
d&=d-1;
if(d==0){
flag[a[v[i]]]=1;
break;
}
}
}
if(a[v[i]]==a[k]) break;
}
if(flag[a[k]]) cout << "Grammy\n";
else cout << "Alice\n";
}
}
}
}
vis[x]
用於儲存數字 x 在陣列 a 中的最後一個位置。
把真正要判的位置存在陣列 v[]
中,排個序,然後 v[i]
就表示第 i 個真正要判的位置在原陣列 a 中的下標;用 flag[i]
來標記 i 這個位置是必勝態還是必敗態。
有個小技巧,判斷能否跳過去可以 \(O(1)\) 判,程式碼如下:
int d=a[v[i]]^a[v[j]];
d&=d-1;
if(d==0) ; //能跳
else ; //不能跳
解釋一下就是,兩個數如果二進位制下只有一位不同,那它們的異或和肯定是二的冪次,判斷一個數 \(d\) 是否是二的冪次只需判斷 \(d~\&~(d-1)\) 是否為 0 即可。(可以舉個栗子試試)