1. 程式人生 > 其它 >Codeforces 1511G - Chips on a Board(01trie/倍增)

Codeforces 1511G - Chips on a Board(01trie/倍增)

01-trie/分塊/倍增,hot tea

Codeforces 題面傳送門 & 洛谷題面傳送門

一道名副其實的 hot tea

首先顯然可以發現這倆人在玩 Nim 遊戲,因此對於一個 \(c_i\in[l,r]\) 其 SG 值就是 \(c_i-l\),最終遊戲的 SG 值就是 \(\oplus_{c_i\in[l,r]}(c_i-l)\),如果該值為 \(0\) 答案就是 B,否則答案是 A

從這一步開始就有好幾種不同複雜度的做法了,下面將一一介紹它們。

下視 \(n,m,q\) 同階。

做法一:暴力

迫真 · \(2\times 10^5\)\(n^2\) 暴力踩過去。

你看這 \(2\times 10^5\) 的資料範圍,你不寫 \(n^2\)

寫啥呢,難道還傻傻地想 \(n\log^2n\) 或是 \(n\log n\) 的做法?

不要笑,現場 rk1 那位大哥就是暴力碾標算還抵過了 100 多發 hack。不得不說 CF 機子是真的快(

就沒啥好說的了,暴力從第一個 \(\ge l\) 的位置開始列舉把它們異或起來即可。

時間複雜度 \(n^2\)

做法二:莫隊+01-trie

允許離線+靜態區間問題,這無不啟發我們往莫隊的方向想。又因為此題涉及異或,因此考慮 01-trie,具體來說我們建立一棵 01-trie 維護當前莫隊表示的區間中所有元素與 \(l\),那麼向左/右移動右端點時 01-trie 時刪除/插入元素,這個維護起來異常容易的不再贅述。向左/右移動左端點時需要先將 01-trie 中所有元素 \(+1/-1\)

再插入/刪除新的元素,全域性 \(\pm 1\) 這個操作的維護方法算是經典套路了,考慮從低到高將序列中的元素插入 01-trie,那麼全域性 \(+1\) 相當於從根節點開始 DFS,每次 DFS 時交換當前節點兩個兒子再 DFS 左兒子,如此進行下去,每次交換都實時更新答案即可。

時間複雜度 \(n\sqrt{n}\log n\)

做法三:分塊+01-trie

一個我自己 yy 出來的演算法(

上面莫隊的做法每次向右移動都要插入新的元素,複雜度就總要多一個 log,並且莫隊移動複雜度本身就帶個根號導致這個 log 不太好攤,因此考慮將莫隊換成分塊。我們考慮設一個閾值 \(B\) 然後每 \(B\)

個元素一塊,在下文中方便起見,設 \(L_i,R_i\) 分別表示每一塊的左右端點。那麼對於每一塊 \(i\),我們設 \(v_{i,j}\) 表示這一塊中所有 \(c_k\ne 0\) 的元素 \(k\)\(k-j\) 的異或和,其中 \(j\) 小於第 \(i\) 塊的左端點,那麼每次查詢就整塊呼叫預處理求得的值,散塊暴力即可。那麼怎麼求 \(v_{i,j}\) 呢?我們考慮將這一塊中所有 \(c_k\ne 0\)\(k\)\(k-L_i+1\) 插入 01-trie,然後從 \(L_i-1\) 開始往前遍歷,每次進行全域性 \(+1\) 操作,那麼每次 \(+1\) 後得到的異或和就分別是我們要求的 \(v_{i,L_i-1},v_{i,L_i-2},\cdots\)

顯然取 \(B=\sqrt{n\log n}\) 時複雜度最優。

時間複雜度 \(n\sqrt{n\log n}\)

const int MAXN=2e5;
const int BLK=300;
const int MAXP=1e5;
const int LOG_N=20;
int n,m,qu,cnt[MAXN+5],v[BLK+5][MAXN+5];
int bel[MAXN+5],L[BLK+5],R[BLK+5];
int ch[MAXP+5][2],siz[MAXP+5],ncnt=0,rt=0,val[MAXP+5];
void clear(){
	memset(ch,0,sizeof(ch));ncnt=rt=0;
	memset(siz,0,sizeof(siz));memset(val,0,sizeof(val));
}
void pushup(int k,int d){val[k]=val[ch[k][0]]^val[ch[k][1]]^((siz[ch[k][1]]&1)?(1<<d):0);}
void insert(int &k,int v,int d){
	if(!k) k=++ncnt;siz[k]++;if(d==LOG_N) return;
	insert(ch[k][v>>d&1],v,d+1);pushup(k,d);
}
void add1(int k,int d){
	if(!k) return;swap(ch[k][0],ch[k][1]);
	add1(ch[k][0],d+1);pushup(k,d);
}
int query(int l,int r){
	if(bel[l]==bel[r]){
		int res=0;
		for(int i=l;i<=r;i++) if(cnt[i]) res^=(i-l);
		return res;
	} else {
		int res=0;
		for(int i=l;i<=R[bel[l]];i++) if(cnt[i]) res^=(i-l);
		for(int i=L[bel[r]];i<=r;i++) if(cnt[i]) res^=(i-l);
		for(int i=bel[l]+1;i<bel[r];i++) res^=v[i][l];
		return res;
	}
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1,x;i<=n;i++) scanf("%d",&x),cnt[x]^=1;
	int blk_sz=max((int)sqrt(m*log2(m)),1),blk_cnt=(m-1)/blk_sz+1;
	for(int i=1;i<=blk_cnt;i++){
		L[i]=(i-1)*blk_sz+1;R[i]=min(i*blk_sz,m);
		for(int j=L[i];j<=R[i];j++) bel[j]=i;
		clear();
		for(int j=L[i];j<=R[i];j++) if(cnt[j]) insert(rt,j-L[i],0);
		for(int j=L[i]-1;j;j--) add1(rt,0),v[i][j]=val[rt];
	} int qu;scanf("%d",&qu);
	while(qu--){
		int l,r;scanf("%d%d",&l,&r);
		printf("%s",(query(l,r))?"A":"B");
	}
	return 0;
}

做法四:根分+BIT

這是官方題解的做法(

考慮設一個閾值 \(K\) 並將詢問離線下來。那麼我們將所有位分為 \(<2^K\) 的位和 \(\ge 2^K\) 的位,對於 \(<2^K\) 的位就暴力存下所有數對 \(2^K\) 取模的值,查詢時用個桶維護一下即可。對於 \(\ge 2^K\) 的位我們考慮掃描線,將詢問掛在左端點處,可以注意到隨著左端點的增大,每個 \(c_i-l\)\(\ge 2^K\) 的位最多變化 \(\dfrac{m}{2^K}\) 次。我們就用 BIT 維護這些 \(c_i-l\)\(\ge 2^K\) 位的異或值即可。取 \(K=\log_2(n\log n)\) 時複雜度最優。

時間複雜度 \(n\sqrt{n\log n}\)

有本事強制線上啊

做法 499122181:根分+分塊

根據根號平衡的思想,我們把上面做法中 BIT 換成 \(\mathcal O(1)\) 單點加,\(\mathcal O(\sqrt{n})\) 求異或和的分塊即可實現 \(n\sqrt{n}\) 的複雜度。

u1s1 官方題解為啥沒想到根號平衡啊,不太懂

做法五:倍增

這是一個 nb 至極的倍增做法,不僅可以強制線上,而且複雜度還是 \(n\log n\) 的 %%%%%%%%

由於倍增本就基於二進位制,而這題恰好涉及與下標有關的二進位制運算,因此我們應嘗試往倍增的方向考慮。我們設 \(f_{i,j}=\oplus_{c_k\in[j,j+2^i-1]}(c_k-j)\)。首先考慮怎麼求 \(f_{i,j}\):考慮首先拿 \(f_{i-1,j}\) 去異或 \(f_{i-1,j+2^{i-1}}\),那麼顯然這兩個東西異或在一起恰好包含了 \(2^0\sim 2^{i-2}\) 位的所有貢獻,而顯然 \(2^{i-1}\) 位會產生貢獻,當且僅當有奇數個 \(c_k\in[j+2^{i-1},j+2^i)\),因此我們可以得到以下轉移方程:

f[i][j]=f[i-1][j]^f[i-1][j+(1<<i-1)]^(((a[j+(1<<i)-1]-a[j+(1<<i-1)-1])&1)<<i-1);

其中 \(a\) 為桶的字首和。

得出了 \(f\) 陣列之後求解答案就非常 easy 了,考慮從大到小遍歷每一位 \(i\),如果 \(l+2^i-1\le r\) 那麼我們就將答案異或上 \(f_{i,l}\),即將 \([l,l+2^i-1]\) 的貢獻計入答案,同時對於 \(c_k\in[l+2^i,r]\),它們也應對答案產生 \(2^i\) 的貢獻,額外加上即可。

時間複雜度 \(n\log n\)

const int LOG_N=18;
const int MAXN=2e5;
int n,m,qu,a[MAXN+5],f[LOG_N+2][MAXN+5];
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1,x;i<=n;i++) scanf("%d",&x),a[x]++;
	for(int i=1;i<=m;i++) a[i]+=a[i-1];
	for(int i=1;i<=LOG_N;i++) for(int j=1;j+(1<<i)-1<=m;j++)
		f[i][j]=f[i-1][j]^f[i-1][j+(1<<i-1)]^(((a[j+(1<<i)-1]-a[j+(1<<i-1)-1])&1)<<i-1);
	scanf("%d",&qu);while(qu--){
		int l,r,cur,res=0;scanf("%d%d",&l,&r);cur=l;
		for(int i=LOG_N;~i;i--){
			if(r-cur+1>=(1<<i)) res^=f[i][cur],cur+=(1<<i),res^=((a[r]-a[cur-1])&1)<<i;
		} printf("%s",(res)?"A":"B");
	}
	return 0;
}