1. 程式人生 > 其它 >2020 ICPC 澳門站 G - Game on Sequence 題解

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\)

前面的 \(x\) 都是必勝態。
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 即可。(可以舉個栗子試試)