其他分享
首页 > 其他分享> > [提高组集训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(' ');
	}
}

标签:温故而知新,int,复杂度,石子,while,异或,2021,include,集训
来源: https://www.cnblogs.com/C202044zxy/p/15369329.html