博弈论学习笔记
借鉴/感谢:自为风月马前卒,Candy,贾志豪《组合游戏略述——浅谈SG游戏的若干拓展及变形》(发现前两位竟然都是我省神仙
由于不写博客就容易颓废(其实写了也还是颓废 所以开一个博客记录一下 这样有点动力...
应该先是看贾志豪的《组合游戏略述——浅谈SG游戏的若干拓展及变形》然后后面会接一些题 这里会放一些笔记
零、前置
我们考虑的是一类组合游戏问题,前提是两人都以最优策略进行,不存在平局现象且在有限步内结束。
任意确定状态可以做出的策略集合只与状态有关,与游戏者无关。
一、定义
游戏图:把状态抽象成点,转移抽象成边的DAG
SG函数:是对游戏图每一个点的一个评估函数。定义式为SG(x)=mex{SG(y)|(x,y)∈E}。
根据拓扑序转移SG 先手必胜的条件SG不等于0。
两个显然的性质:
1)对于任意的局面,如果它的SG值为0,那么它的后继一定没有0。
2)对于任意的局面,如果它的SG值为0,那么它至少有一个后继SG值等于0。
Nim模型:取石子游戏 $f_i$表示一堆石子的个数,每次选择一个$f_i$减少一个$x$($x<=f_i$)。
$f_{tot} = f_1 \oplus f_2 \oplus ... \oplus f_n$ 先手必胜当且仅当f_{tot}不等于0。
我们看到SG函数和nim游戏的定义十分相似,考虑两者之间的关系。
可以发现,每个简单SG游戏可以完全等效成一堆数目为SG(x)的石子。
归纳证明:
1.最终状态SG=0
2.对于任意必胜态(异或和不为0),必定存在一个后继必败状态(异或和为0)
即一定有一种取法使Nim和再变成0(具体考虑类似于线性基 一位一位往下取即可)
3.对于任意必败态(异或和为0),必定不存在任意一个后继必败状态(异或和为0)
显然,需要取走非零颗石子,异或和必定改变。
引入游戏的和概念
考虑任意多个同时进行的SG-组合游戏,这些SG-组合游戏的和是这样一个SG-组合游戏,在它进行的过程中,游戏者可以任意挑选其中的一个单一游戏进行决策,最终,没有办法进行决策的人输。
发现SG和Nim的定义类似 我们就可以把游戏的和转换为Nim模型处理,即$SG(x)=SG(x_1) \oplus SG(x_2) \oplus ... \oplus SG(x_n)$。
注意:游戏的和与SG函数的递推不同,前者是多个同时进行的SG游戏选择其中一个进行决策,最终需要每一个都决策,后者只需要决策一步就达到后继状态,而没有多次决策的过程。比如说前者就是多个游戏图上选择一个进行游戏,后者就是单独的一个游戏图上决策。需要区分两个概念。
二、模型拓展——Anti-SG游戏和SJ定理
走了最后一步的人输,那么怎么判断先手胜负呢?
我们回到最开始的Nim游戏 类似的定义Anti-Nim,规定取走最后一枚石子的人输。
我们一步一步推理出结论。游戏分为两种情况。
1)每一堆石子都为1。先手必胜当且仅当N是偶数。相当于SG=0。
2)
其它情况。
{
1)当SG为0时。
至少还有两堆石子的数目>1,先手决策完至少还有一堆石子数目>1,且SG一定不等于0,带入到第二种情况。最后可以得到结论一定可以走到第二种情况的前半部分,所以说先手必败。
2)
若至少只有一堆石子的数目>1,先手总可以将石子变成奇数堆1。那么先手必胜。若至少有两堆石子的数目>1,我们一定可以把SG变为0,带入回第一种情况SG=0。而SG为0一定最终带入回前者情况,所以先手必胜。
}
结论:
先手必胜当且仅当
1)所有堆石子数都为1,且SG=0。
2)有至少一堆石子数量大于1,且SG!=0。
我们有了最初的模型,可以如下定义Anti-SG游戏:
Anti-SG游戏规定,决策集合为空的游戏者胜利。
也可以得到SJ定理:
先手必胜当且仅当:
1)$SG_{tot}$不为0,且存在某个单一的SG游戏其SG值大于1
2)$SG_{tot}$等于0,且不存在单一的SG游戏其SG值等于1
证明在此略去。类似于Anti-Nim的证明。
三、模型拓展——MultiSG
MultiSG就是类似于前面介绍的游戏的和的概率。
举个例子:MultiNim
还是很多石子堆,但每次除了可以选择拿走石子,还可以选择把一堆石子分成n堆石子(n>=2)
发现SG(3)的后继转移就有{0}{1}{2}{1,2}这4种,而{1,2}这种的SG就相当于SG(1)^SG(2)(游戏的和)。然后SG(3)的转移就是这四种的mex啦。
MultiNim游戏具有结论性质如下:
(x%4==0) SG(x)=x-1
(x%4==1/2) SG(x)=x
(x%4==3) SG(x)=x+1
得,全网没找到靠谱证明。先咕着吧。
现在考虑MultiSG
类似于MultiNim,MultiSG后继的状态有分裂的那些取mex,而分裂出的局面的SG是xor。
四、模型拓展——EverySG
如果每个棋子都需要移动,那么我们就来到了EverySG。
对于一个先手必胜的局面,先手一定要尽可能让这个游戏玩的时间久一点,让它成为最后一次游戏,来保证先手必胜。
而他的对手则希望这个游戏尽快结束,不至于让输的局面是最后一次游戏。
根据我们上面的推论我们用一个$step(x)$来作为评估函数 它具体的转移如下
$step(x) = 0 $ x为终止状态
$step(x) = max(step(y))+1$ sg(x)>0 sg(y)=0
$step(x) = min(step(y))+1$ sg(x)=0
定理:对于EverySG游戏,先手必胜当且仅当单一游戏中最大的step为奇数
什么?你要证明?好像很显然啊...你可以选择分类讨论一下吧(其实是step已经是一个确定性函数了
五、实例——翻硬币游戏
1-N共N枚硬币,每次翻可以选择翻连续几个,但要保证最右的那一枚是正面翻到反面。
结论:局面的SG值是局面中每个正面向上的硬币单一存在(只有它正面)时的SG值的异或。
具体证明其实就可以考虑和Nim完全等价,每次正到反的位置都是向左移。
六、实例——无向图删边游戏
1.树的删边游戏
这是一道常见题 也有相应的结论
结论:叶子节点的SG为0,其余节点的SG为它的儿子节点SG+1后的异或。
理解:与+1对应的是砍掉整个子树,而每个子树是独立的,因此是异或。
2.简化的无向图删边
我们现在考虑这样一个无向图,它现在是一棵树,有一些边,保证形成的环不存在公共边,且只与原树有一个交点。
我们发现,这个图上的所有环都是单独连出去又连回来的。
结论:
对于奇环来说,去掉任意一条边,剩下的两段同奇偶,异或后不会出现奇数,所以它的SG值为1。
对于偶环来说,去掉任意一条边,剩下的两段一定不相同,异或后不会出现0,它的SG值为0。
把奇环变成长度为1的链,偶环变成一个点,我们就回到了之前的问题。
3.无向图删边
结论:对于任意的奇环都可以缩成一个新点+一个新边,偶环缩成新点,原来与其相连的边全部连到新点上。直到变成一棵树就是我们之前的做法了。
证明很神仙,我也不会,脑补一下还是很科学的(逃。
七、实例——阶梯Nim
阶梯Nim:每次可以选择一堆石子把其中的x枚石子移到左边的一堆(从第一堆往左移相当于取走)。不可操作者输。
结论:等价于所有奇数层石子的Nim。
证明很简单,考虑每次从奇数层往下移,后手一定可以复制先手的操作使Nim继续。
理论到这里就结束了,我们来看题吧。
八、习题
解法和代码一起放在ViewCode里了,读者可以先自行思考在看。
POJ2848
这真的是想不到啊。。。 自己的思路是考虑第一次拿走了以后就变成序列操作了,然后一顿SG推理得出结论,我不会做。 实际上解法很简单,除了n=1/n=2是先手必胜(一次取走),其余都是后手必胜。因为考虑当n=偶数的时候,后手只需要中心对称的模仿先手的操作即可。n=奇数的时候,如果先手取了1,后手只需要对称取走2就变回第一种情况;先手取了2,后手对称取1即可。好神仙啊。。 //Love and Freedom. #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #define ll long long #define inf 20021225 using namespace std; int read() { int s=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-') f=-1; ch=getchar();} while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar(); return f*s; } int n; int main() { while(n=read()) if(n==1||n==2) printf("Alice\n"); else printf("Bob\n"); return 0; }
BZOJ2463
鉴于第一题的机智经验和之前好像看到过这个题。。。 顺利切掉了QAQ 具体证明可以考虑1*2骨牌覆盖,先手位于骨牌开始位置,它只需要移动到骨牌的另一位置,而后手需要寻找新的骨牌。对于偶数一定存在合法的骨牌覆盖,但是奇数的话相当于先手陷入了寻找新骨牌的困境,所以判断奇偶就可以了。 //Love and Freedom. #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #define ll long long #define inf 20021225 using namespace std; int read() { int s=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-') f=-1; ch=getchar();} while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar(); return f*s; } int main() { while(int n=read()) if(n&1) printf("Bob\n"); else printf("Alice\n"); return 0; }
BZOJ3895
我太难了 尝试推结论未遂。。。 打开百度:记忆化搜索??? 大概是这个样子,我们发现一堆里头如果只有一个石子,它比较特殊,因为它取走了其实相当于进行了两个操作。。。所以我们设f[a][b]表示有a个1石子堆,其余石子需要操作b次的答案,然后这样的话是O(n*n*m)跑记忆化搜索刚刚好。 //Love and Freedom. #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #define ll long long #define inf 20021225 #define N 50100 using namespace std; int read() { int s=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-') f=-1; ch=getchar();} while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar(); return f*s; } int f[51][N]; int dfs(int a,int b) { if(a==0) return b&1; if(b==1) return dfs(a+1,0); if(~f[a][b]) return f[a][b]; if(a&&!dfs(a-1,b)) return f[a][b]=1; if(a&&b&&!dfs(a-1,b+1)) return f[a][b]=1; if(b&&!dfs(a,b-1)) return f[a][b]=1; if(a>=2&&!dfs(a-2,b+2+(b?1:0))) return f[a][b]=1; return f[a][b]=0; } int main() { memset(f,-1,sizeof(f)); int T=read(),n,x; while(T--) { n=read(); int cnt=0,val=-1; for(int i=1;i<=n;i++) { x=read(); if(x==1) cnt++; else val+=x+1; } if(val==-1) val=0; puts(dfs(cnt,val)?"YES":"NO"); } return 0; }
POJ1740
感觉智商疯狂被碾压... 考虑有两堆相等的,后手只需要复制先手的操作就行了。 推广到每堆都有与之相等的(也就是一对一对的),后手还是复制先手的操作就行了。 接下来考虑别的情况,我们发现先手只需要把每一组都补齐,就可以使后手陷入必败的局面。为什么一定能补齐呢?我们考虑将石子排序。 对于n为奇数,显然第n组一定是大于其它相邻一对的差的和的。 对于n为偶数,第n个和第1个配对,剩下的也一定大于相邻一对的差的和的。 //Love and Freedom. #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #define ll long long #define inf 20021225 using namespace std; int read() { int s=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-') f=-1; ch=getchar();} while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar(); return f*s; } int buc[101]; int main() { while(int n=read()) { int pr=0; for(int i=1;i<=n;i++) { int x=read(); if(buc[x]) buc[x]--,pr--; else buc[x]++,pr++; } if(!pr) printf("0\n"); else printf("1\n"),memset(buc,0,sizeof(buc)); } return 0; }
BZOJ1982
这个题怎么和上个题一样啊... //Love and Freedom. #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #define ll long long #define inf 20021225 using namespace std; int read() { int s=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-') f=-1; ch=getchar();} while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar(); return f*s; } int a[100001],n; int main() { while(~scanf("%d",&n)) { int pr=0; for(int i=1;i<=n;i++) a[i]=read(); sort(a+1,a+n+1); for(int i=1;i<=n;i+=2) if(a[i]!=a[i+1]){pr=1; break;} if(!(n&1) && !pr) printf("second player\n"); else printf("first player\n"); } return 0; }
POJ2505
我没有脑子... 首先[1,9]是Stan胜,[10,18]是Ollie胜。 Stan要越接近N取胜,而Ollie要尽量拖住Stan,所以每次的区间就是这样的:[2*9*...*2*9+1,2*9*...*2*9*9]Stan胜,其余Ollie胜。 //Love and Freedom. #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #define ll long long #define inf 20021225 #define db double using namespace std; int read() { int s=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-') f=-1; ch=getchar();} while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar(); return f*s; } int main() { db n; while(scanf("%lf",&n)!=EOF) { while(n>18) n/=18; if(n<=9) printf("Stan "); else printf("Ollie "); printf("wins.\n"); } return 0; }
POJ2975
这个还是会做的嘤嘤嘤。 我们考虑从一个Ai的异或中改变一个数使得新的异或=0 只需要判断一下大小关系就可以啦w //Love and Freedom. #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #define ll long long #define inf 20021225 #define N 1001 using namespace std; int read() { int s=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-') f=-1; ch=getchar();} while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar(); return f*s; } int a[N]; int main() { while(int n=read()) { int s=0; for(int i=1;i<=n;i++) a[i]=read(),s^=a[i]; int ans=0; for(int i=1;i<=n;i++) if((s^a[i])<a[i]) ans++; printf("%d\n",ans); } return 0; }
BZOJ1299
发现先手一旦可以找到一个Nim=0的,后手就没了。所以说只需要跑一遍就行了。 //Love and Freedom. #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #define ll long long #define inf 20021225 #define N 15 #define lowbit(x) (x&-x) using namespace std; int read() { int s=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-') f=-1; ch=getchar();} while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar(); return f*s; } int n,f[1<<N]; int main() { int T=10; while(T--) { n=read(); for(int i=0;i<n;i++) f[1<<i]=read(); int i,top=1<<n; for(i=1;i<top;i++) { f[i]=f[i^lowbit(i)]^f[lowbit(i)]; if(!f[i]) break; } puts(i==top?"YES":"NO"); } return 0; }
POJ2425
SG函数裸题。应该看懂博客了的都能会= = //Love and Freedom. #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #define ll long long #define inf 20021225 #define N 1001 using namespace std; int read() { int s=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-') f=-1; ch=getchar();} while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar(); return f*s; } struct edge{int to,lt;}e[N*N]; int in[N],cnt,f[N],stk[N],n; bool vis[N]; void add(int x,int y) { e[++cnt].to=y; e[cnt].lt=in[x]; in[x]=cnt; } int mex(int *a,int n) { sort(a+1,a+n+1); if(a[1]) return 0; for(int i=2;i<=n;i++) if(a[i]>a[i-1] && a[i]!=a[i-1]+1) return a[i-1]+1; return a[n]+1; } void dfs(int x) { if(vis[x]) return; vis[x]=1; int top=0; for(int i=in[x];i;i=e[i].lt) dfs(e[i].to); for(int i=in[x];i;i=e[i].lt) stk[++top]=f[e[i].to]; f[x]=mex(stk,top); if(!in[x]) f[x]=0; } int main() { while(~scanf("%d",&n)) { memset(vis,0,sizeof(vis)); memset(in,0,sizeof(in)); cnt=0; int x,ans; for(int i=0;i<n;i++) { int d=read(); while(d--) x=read(),add(i,x); } while(int m=read()) { ans=0; while(m--) { x=read(); dfs(x); ans^=f[x]; } puts(ans?"WIN":"LOSE"); } } return 0; }
POJ2960
还是这种套路一点的比较适合我... //Love and Freedom. #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #include<vector> #define ll long long #define inf 20021225 #define N 10010 #define vec vector<int> #define pb push_back using namespace std; int read() { int s=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-') f=-1; ch=getchar();} while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar(); return f*s; } int f[N],buc[N],k,s[N]; vec to[N]; int mex(vec a) { if(!a.size()) return 0; int ans=a.size(); for(int i=0;i<a.size();i++) buc[a[i]]++; for(int i=0;i<a.size();i++) if(!buc[i]){ans=i;break;} for(int i=0;i<a.size();i++) buc[a[i]]--; return ans; } int dfs(int x) { if(~f[x]) return f[x]; to[x].clear(); for(int i=1;i<=k;i++) if(x-s[i]>=0) to[x].pb(dfs(x-s[i])); return f[x]=mex(to[x]); } int main() { while(k=read()) { memset(f,-1,sizeof(f)); for(int i=1;i<=k;i++) s[i]=read(); sort(s+1,s+k+1); int m=read(),ans,w; while(m--) { int x=read(); ans=0; while(x--) w=read(),ans^=dfs(w); putchar(ans?'W':'L'); } printf("\n"); } return 0; }
BZOJ1874
喵的我会做这个题TAT 题意杀我 它的意思是每次取的石子数必须是给出的其中一个,不是要按顺序取... 所以做法就是暴力SG啦... //Love and Freedom. #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #define ll long long #define inf 20021225 #define N 1001 using namespace std; int read() { int s=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-') f=-1; ch=getchar();} while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar(); return f*s; } int f[N],buc[12],st[12],b[12],n,m; int mex(int a[],int top) { for(int i=1;i<=top;i++) buc[a[i]]++; int ans=12; for(int i=0;i<12;i++) if(!buc[i]) { ans=i; break; } for(int i=1;i<=top;i++) buc[a[i]]--; return ans; } int get(int x) { if(~f[x]) return f[x]; int to[11],top=0; for(int i=1;x>=b[i]&&i<=m;i++) to[++top]=get(x-b[i]); return f[x]=mex(to,top); } int main() { n=read(); for(int i=1;i<=n;i++) st[i]=read(); m=read(); for(int i=1;i<=m;i++) b[i]=read(); memset(f,-1,sizeof(f)); int ans=0; for(int i=1;i<=n;i++) ans^=get(st[i]); if(!ans) return puts("NO"),0; for(int i=1;i<=n;i++) for(int j=1;b[j]<=st[i]&&j<=m;j++) { if((ans^get(st[i])^get(st[i]-b[j]))==0) { puts("YES"); printf("%d %d\n",i,b[j]); return 0; } } return 0; }
BZOJ1115
发现要求相邻的大小关系,根据常见套路我们可以通过差分来做。发现差分了以后就变成了倒着的阶梯Nim。 //Love and Freedom. #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #define ll long long #define inf 20021225 #define N 1001 using namespace std; int read() { int s=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-') f=-1; ch=getchar();} while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar(); return f*s; } int a[N],d[N]; int main() { int T=read(); while(T--) { int n=read(); int ans=0; for(int i=1;i<=n;i++) a[i]=read(),d[i]=a[i]-a[i-1]; for(int i=n;i>0;i-=2) ans^=d[i]; puts(ans?"TAK":"NIE"); } return 0; }
HDU5996
这个我会!直接把结论搬到树上就好啦。 //Love and Freedom. #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #define ll long long #define inf 20021225 #define N 100010 using namespace std; int read() { int s=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-') f=-1; ch=getchar();} while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar(); return f*s; } int dep[N],fa[N],a[N]; int main() { int T=read(); while(T--) { int n=read(),ans=0; for(int i=1;i<n;i++) fa[i]=read(),dep[i]=dep[fa[i]]+1; for(int i=0;i<n;i++) a[i]=read(),ans^=(dep[i]&1)?a[i]:0; puts(ans?"win":"lose"); } return 0; }
POJ1704
这个我也会!差分就可以了qwq 倒着的阶梯nim 最后注意给出的不一定有序QAQ! //Love and Freedom. #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #define ll long long #define inf 20021225 #define N 1001 using namespace std; int read() { int s=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-') f=-1; ch=getchar();} while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar(); return f*s; } int d[N],p[N],n; int main() { int T=read(); while(T--) { int n=read(),ans=0; for(int i=1;i<=n;i++) p[i]=read(); sort(p+1,p+n+1); for(int i=1;i<=n;i++) d[i]=p[i]-p[i-1]-1; for(int i=n;i>0;i-=2) ans^=d[i]; if(ans) printf("Georgia will win\n"); else printf("Bob will win\n"); } return 0; }
做博弈论让我日常怀疑我有没有脑子这个问题...