感觉博弈论ACM还蛮经常考,现在我只记得NIM结论似乎不太行:
博弈论就是要静下心一口气先把概念看完才懂啊(所以建议找个时间一口其总结完)
参考了https://www.cnblogs.com/Knuth/archive/2009/09/05/1561007.html
NIM简介
Nim游戏属于“Impartial Combinatorial Games”(以下简称ICG):
满足以下条件的游戏是ICG(可能不太严谨):
1、有两名选手;
2、两名选手交替对游戏进行移动(move),每次一步,选手可以在(一般而言)有限的合法移动集合中任选一种进行移动;
3、对于游戏的任何一种可能的局面,合法的移动集合只取决于这个局面本身,不取决于轮到哪名选手操作、以前的任何操作、骰子的点数或者其它什么因素;
4、如果轮到某名选手移动,且这个局面的合法的移动集合为空(也就是说此时无法进行移动),则这名选手负。根据这个定义,很多日常的游戏并非ICG。例如象棋就不满足条件3,因为红方只能移动红子,黑方只能移动黑子,合法的移动集合取决于轮到哪名选手操作。
就像我们玩的回合制游戏有木有
任何一个ICG游戏都 有不同的局面,而每一个局面都可以看成一个顶点,该局面到它的子局面 由有向边链接(类比一下下棋)
可以抽象为:给定一个有向无环图和一个起始顶点,两名选手交替的将这顶点沿有向边进行移动到它的子集,无法移动者判负。
显然:失败的条件就是该顶点没有子集(无法达到下一个局面)
现在定义:先手必胜局面(局面)和先手必败局面(顶点):
1.无法进行任何移动的局面(也就是terminal position 终局)是特殊的P-position(先手必败);2.可以移动到P-position的局面是N-position (先手必胜居局面);3.所有移动都导致N-position的局面是P-position 。
然后为了方便表示现在就要引进一个mex(minimal excludant)运算:表示mex{....}=最小的不属于这个集合的非负整数,
比如:mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。
现在再定义 SG函数: 对于一个给定的有向无环图,关于图的每个顶点的Sprague-Garundy(sg)函数如下:sg(x)=mex{sg(y) | y是x的后继(子集) }。
然后这个sg函数有个性质:所有的terminal position所对应的顶点(也就是没有出边的顶点|也是先手必败局面),其sg值为0,因为它的后继集合是空集(mex{}=0)
然后对于一个sg(x)=0的顶点x,它的所有后继y都满足g(y)!=0。对于一个sg(x)!=0的顶点,必定存在一个后继y满足sg(y)=0。(即能到达先手必败局面|当前局面就是先手必胜局面)
注意:
sg=0一定是先手必败局面,但是不一定没有子集,而 terminal-position (终局)没有子集且 sg=0。
sg函数的现实意义:
如果sg(x)=k,则可以从x到sg(i)=[0,k-1]的一个顶点y(局面):就是一堆有n石子,可以被拿到只剩[0,n-1]个。(是不是很像NIM游戏?)
但是如果是多堆石子呢,因为任何一个ICG都可以抽象成一个有向图游戏。所以“SG函数”和“游戏的和”的概念就不是局限于有向图游戏。所以说当我们面对由n个游戏组合成的一个游戏时,只需对于每个游戏找出求它的每个局面的SG值的方法,就可以把这些SG值全部看成Nim的石子堆,然后依照找Nim的必胜策略的方法来找这个游戏的必胜策略了!
发现还是只用NIM的结论
所以我们可以定义有向图游戏的和(Sum of Graph Games):设G1、G2、……、Gn是n个有向图游戏,定义游戏G是G1、G2、……、Gn的和(Sum),游戏G的移动规则是:任选一个子游戏Gi并移动上面的棋子。Sprague-Grundy Theorem就是:sg(G)=sg(G1)^sg(G2)^…^sg(Gn)。也就是说,游戏的和的SG函数值是它的所有子游戏的SG函数值的异或。
注:每一个子游戏最终都只有一个sg值 如 sg(n)而这个sg值要从sg(0),开始逆推,所以不要误以为是每个节点i的sg(i)值取xor
一般的求sg(x)函数的套路:
- 如果是可选步数为1~m的连续整数,直接取模即可,SG(x) = x % (m+1);
- 如果可选步数为任意步,SG(x) = x;
- 如果可选步数为一系列不连续的数,用模板计算(dp|dfs)。
例:有T堆石子,每次可以从第1堆石子里取1颗、2颗或3颗,可以从第2堆石子里取奇数颗,可以从第3堆及以后石子里取任意颗……假设第一堆有n个石子,第二堆有m个,其余的有a[1]~a[T-2]个石子,我们可以把它看作3个子游戏,第1个子游戏只有一堆石子,每次可以取1、2、3颗,可以得出sg1[n]值是n%4。第2个子游戏也是只有一堆石子,每次可以取奇数颗,经过简单的画图可以知道这个游戏的sg2[m]值是x%2。第3个游戏有T-2堆石子,就是一个Nim游戏即sg3=sg[a[i]]^sg[a[2]]...。对于原游戏的每个局面,把三个子游戏的SG值异或一下就得到了整个游戏的SG值,SG=sg1[n]^sg2[m]^sg3 然后就可以根据这个 if SG ==0 ==>必败,否则必胜。
下面贴一下可以取不连续的数的模板:
例1:
这题似乎交不了??!!还是贴一下代码吧
https://www.luogu.com.cn/problem/UVA1559
题目大意:若输入n=0代表结束,否则输入n(n<10),s(<2^13),n代表有n*2个人,s代表一共有s个石子 ,然后有n*2个数a1,a2....代表第i个人最多能拿的石子数,你拥有奇数的队员(1,3,5...),每次按顺序来取,不断循环,最后谁的组员把石堆拿走最后一个石子谁输(sg[1]=0,sg[0]=1),若这组数据你能能赢输出1,否则输出0。
因为只有一堆石子,直接判断正负状态就行了,用记忆化搜索|dp都可以
#include<cstdio> #include<algorithm> #include<vector> #include<cstring> #include<iostream> using namespace std; const int N=20,M=(2<<13)+1431; int n,S,x,sg[N*2][M],a[2*N];//这里的sg实际上是表示输赢0|1 int dfs(int x,int sum) { if(sum==1) { sg[x][1]=0; return 0; } if(sum<=0) { sg[x][sum]=1; return sg[x][sum]; } if(sg[x][sum]!=-1) return sg[x][sum]; sg[x][sum]=0; //预先假定现在是先手必输局面记 for(int i=1;i<=a[x];i++) { if(dfs((x+1)%2*n,sum-i)==0)//如果下一步必输 则现在是先手必赢局面与假设不成立 { sg[x][sum]=1; //因为要把所有状态全部枚举完,所以不能中途return //中途return 会输出 你输入的最末尾的数字 不知为什么 } } return sg[x][sum]; } int main() { while(1) { memset(sg,-1,sizeof(sg)); scanf("%d",&n); if(n==0) break; scanf("%d",&S); for(int i=0;i<n*2;i++) { scanf("%d",&a[i]); } printf("%d ",dfs(0,S)); } return 0; }
例2:
https://www.luogu.com.cn/problem/P2197
就是个nim模板
#include<cstdio> #include<algorithm> #include<vector> #include<cstring> #include<iostream> using namespace std; const int N=10006; int n,ans=0,sg[N],x,T,use[N]; void get_sg(int n)//得到每一个点的sg值 { memset(sg,0,sizeof(sg));//数组下标不能是负号 for(int i=0;i<=n;i++) { memset(use,0,sizeof(use)); for(int j=1;j<=i;j++)//这里是全部拿走 否则应该是枚举f[...]中的值 use[sg[i-j]]=1; //枚举i子集,并加入集合 for(int j=0;j<=n;j++)//找出i这一点的sg值 { if(use[j]==0) { sg[i]=j; break; } } } } int main() { scanf("%d",&T); get_sg(N-5); while(T--) { scanf("%d",&n); ans=0; for(int i=1;i<=n;i++) { scanf("%d",&x); ans=ans^sg[x]; } if(ans>0) //xor的结果不一定是1除非是上题那种直接判断当前sg是P|N局面才是0|1 printf("Yes "); else printf("No "); } return 0; }
如果上上题那种直接判断当前sg是P|N局面最后发现结果并不对,意思是sg值和输赢0|1各自的xor并不同 也就是说直接判断输赢只能用于一个游戏中
n个游戏就应该求出每一个游戏的sg值再xor判断