sg函数:
Mex运算
设S表示一个非负整数集合。定义mex(S)为求出不属于集合S的最小非负整数(自然数)的运算,即:mex(S)= min{x},x属于自然数,且x不属于S
Sg函数定义:
Sg[终点] = 0;
Sg[x] = mex(Sg[y1],Sg[y2],……Sg[yn]),其中y1……y2是x能够到达的点。
规则:
如果Sg[x] == 0,就是必败,!=0就是必胜,因为Sg==0,就一定不能走到0,如果Sg!=0,就一定能够走到0。
定理:
有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于0。
有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于0。
证明方式:
终点的Sg[xi] ^ 起来一定是0.
如果所有起点的Sg[xi]异或==x,那一定能够找到一个Sg[xi],使得Sg[xi]走到Sg[xi] ^ x(小于Sg[xi]) 这个节点,从而使
所有节点的异或值为0。
如果所有起点的Sg[xi]异或==0,那一定不能走到一个全零的局面,否则的话(反证法),就能够得到两个相邻节点的Sg值相等的情况,矛盾,因为Sg[x]能走到了。
思考:
其实如果只有一个图,其实Sg只需要具有两个值即可,但是本题中的集合nim游戏,指的是集合中的所有元素都对应一个有向图,如果最终所有集合起点的Sg值异或起来依然是0,就说明是先手必败状态。
本题中,不是说最后必须拿完,即使不拿完,如果最后没有能拿的了,也算输。
最后如果所有起点的Sg异或起来是0,即使有两个起点的Sg相同,实际含义就是在一堆集合中选完之后,后手可以在另一个集合中选,使得先手面对异或都是0的局面。
状态数量是10000(就是每一个有向图最多有10000(每一堆石子的最大值就是1000,拿走1个,最多也就是从0~10000右10001个状态)个节点),但是一共有100个有向图(有100堆石子),所以时间复杂度就是o(10^6);
#include<iostream>
#include<cstring>
#include<unordered_set>
#include<algorithm>
using namespace std;
int n,m;
const int N =110,M=10010;
int t[N],f[M];//t表示所有可能取走的石子数量,f表示所有可能出现的sg的值(可能取到很小的数,小于t数组的最小值,所以取值在1~n之间)
int sg(int x)
{
if(f[x]!=-1) return f[x];//记忆化搜索,只要t集合一定,那么某一个元素的sg的值就是确定的,如果某一个元素的sg的值求过了,就直接返回即可。
unordered_set<int> C;//注意每一次递归C都被重新定义,每一次C都不一样。
//用集合(可以删掉重复元素)来存储所有能到的状态的sg的值,来计算本状态的sg的值。
for(int i=0;i<n;i++)//dfs,先可t[0]来,一条道走到头(t[i]全都>x)
{
int sum=t[i];
if(sum<=x) //这一步就是找到所有状态,其中状态数一定<=10000
{
C.insert(sg(x-sum));
}
}
for(int i=0;;i++)//当t[i]全都>x时,说明到末状态了,末状态由于C没有被辐值,所以末状态的值都是0
{
if(!C.count(i)) return f[x]=i; //表面上是for遍历,但是实际上if的时间效率比return 的时间效率小很多,所以可以近似看成是o(1)的时间复杂度。
}
}
int main()
{
memset(f,-1,sizeof f);//sg函数值最小也是0,不可能是-1,所以可以用-1表示f值没有被计算过。
scanf("%d",&n);
for(int i=0;i<n;i++)
{
scanf("%d",&t[i]);
}
int res=0;
scanf("%d",&m);
while(m--)
{
int x;
scanf("%d",&x);
res^=sg(x);//所有sg(x)的值做异或,相当于nim游戏。
}
if(res) puts("Yes");
else puts("No");
return 0;
}