1. 程式人生 > 其它 >[提高組集訓2021] 溫故而知新

[提高組集訓2021] 溫故而知新

一、題目

\(n\) 堆石子,第 \(i\) 堆石子有 \(a_i\) 個,當前取石子的人可以任取一堆還沒有取完的石子,從中取 \([1,x]\) 個。

對於所有 \(x\in[1,n]\),你都需要告訴是先手必勝還是後手必勝。

\(n\leq 5\cdot 10^5\)

二、解法

利用 \(\tt sg\) 函式,把題目做一個簡單的轉化:

\[\forall x\in[1,n],sg=\oplus_{i=1}^n a_i\bmod (x+1) \]

根據套路,可以用調和級數的複雜度來優化,我們列舉 \(x\) 和左端點 \(i\),那麼 \([i,i+x]\) 這一段的模運算可以轉化成減去 \(x\)

問題變成了求一個區間的異或值,所有元素從其左端點開始

複雜的區間問題考慮倍增,設 \(f[i][j]\) 表示從 \(i\) 開始走 \(2^j\) 步的異或和,轉移:

\[f[i][j]=f[i][j-1]\oplus f[i+2^{j-1}][j-1] \]

顯然這個轉移是錯的,因為後面那一段全部少加了一個 \(2^{j-1}\),但考慮到後面的元素值都小於 \(2^{j-1}\),所以我們只需要考察後面那一段的奇偶性就可以知道需不需要添上這樣一個 \(2^{j-1}\)

詢問的時候也是類似的思路,考慮少算的那個 \(2^i\) 即可,時間複雜度 \(O(n\log^2n )\)

三、總結

奇怪區間問題考慮倍增!

#pragma GCC optimize(2)
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 500005;
#define p putchar
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,a[M],f[M][20],g[M][20];
int work(int l,int r)
{
	int res=0,nw=0;
	for(int i=19;i>=0;i--)
	{
		if(l+(1<<i)-1>r) continue;
		res^=f[l][i];l+=(1<<i);
		if(a[r]^a[l-1]) res^=(1<<i);
	}
	return res;
}
int main()
{
	freopen("stone.in","r",stdin);
	freopen("stone.out","w",stdout);
	n=read();
	for(int i=1;i<=n;i++)
		a[read()]^=1;
	for(int i=1;i<=n;i++)
		a[i]^=a[i-1];
	for(int j=1;(1<<j)<=n+1;j++)
		for(int i=0;i+(1<<j)-1<=n;i++)
		{
			f[i][j]=f[i][j-1]^f[i+(1<<j-1)][j-1];
			if(a[i+(1<<j)-1]^a[i+(1<<j-1)-1])
				f[i][j]^=(1<<j-1);
		}
	for(int i=2;i<=n+1;i++)
	{
		int ans=0;
		for(int j=0;j<n;j+=i)
			ans^=work(j,min(n,i+j-1));
		if(ans) p('A'),p('l'),p('i'),p('c'),p('e'),p(' ');
		else p('B'),p('o'),p('b'),p(' ');
	}
}