巴巴博弈
本文将竭尽所能带您一步步了解博弈论,如有错误,请各位大手子在评论区慷慨指出,谢谢!
正文
博弈论是个本质困难非常有趣的学科。
无特殊说明,本文均假设博弈者聪明绝顶。
1.博弈基础
在竞赛中,我们一般面对的都是组合博弈:
- 两名玩家,轮流操作
- 信息对双方公开,无隐藏信息(如军棋,扑克就不是)
- 无随机性
- 会在有限步数内终止
- 无平局
博弈分类
- 公平组合游戏:当前操作只与状态有关,与玩家无关的组合博弈(如象棋就不是)。
- 零和博弈:玩家收益之和恒不变。
反之则为非公平组合游戏、非零和博弈。
本文无特殊说明,都是公平组合游戏,且都至少取走 \(1\) 个石子。
\(N/P\)态
必胜态\((N)\):即对于当前操作的人来说,有可以必胜的决策。
必败态\((P)\):即对于当前操作的人来说,没有决策可以让他避免失败。
根据定义,我们可以推出以下结论:
- 当一个状态为必胜态\((N)\) ,他的后继状态(即他操作后的状态)中有一个必败态\((P)\)。
- 当一个状态为必败态\((P)\) ,他的所有 后继状态均是必胜态\((N)\)。
证明很容易,因为假设博弈者们是聪明绝顶,自行手推一下就懂了。
注:建议把 \(N/P\) 态定义弄懂,这是学懂博弈论最重要的。
2.常见公平组合游戏模型
接下来,我将讲解经典博弈模型,难度主观从易到难。
巴什博弈(bash game)
有两个人,他们面前有一堆石子,每次他们可以取 \(1,k\) 个石子,无法操作的人输
结论当石子数为 \(k+1\) 的倍数时先手必输,否则,先手必赢。
证明1显然,当此时的石子为 \(k+1\) 的倍数时,无论取多少石子,后手都能取一定石子使其还是 \(k+1\) 的倍数,最后一个石子必定会被后手取走。反之,先手可以先取一定石子使其数量变为 \(k+1\) 的倍数,此时后手变先手,后手必输。
由此,总数为 \(k+1\) 的倍数的状态为必败态。
我们来几张图加深理解:
可以看到这里有一共 \(13\) 个石子,假设一次至多拿 \(3\) 个,至少拿 \(1\) 个。
则,先手先拿 \(1\) 个,此时还剩 \(12\) 个,为 \(4(3+1)\) 的倍数。
若后手拿 \(2\) 个,则先手可拿 \(2\) 个,此时还是 \(4\) 的倍数。
以此类推,直至还剩 \(4\) 个...
此时到后手,而他至多拿 \(3\) 个,至少拿 \(1\) 个,即留给先手的一定至少 \(1\) 个,至多 \(3\) 个,先手一定可一次取完,于是先手必胜。
证明2我们尝试从 \(N/P\) 态的角度证明。
可以得知,\(0\) 一定是 \(P\) 态,因为无法操作。
而如此,\(1、2、...、k\) 便一定是 \(N\) 态,根据定义可知。
自然而然地,根据定义,\(k+1\) 便是 \(P\) 态,以此类推。
因此,\(k+1\) 的倍数均为 \(P\) 态。
对于例题,我们可以将其换个角度,即将 \(N\) 换成 \(N\) 个石子,那此题就变为了两人轮流取石子了,妥妥的板题。
代码
记得 \(N\) 要减 \(1\)!
cppwhile(cin>>n>>k&&n&&k){ n--; if(n%(k+1))cout<<"Tang\n"; else cout<<"Jiang\n"; }
Nim 游戏
还是两个人,但这次他们面前有 k 堆石子,每次他们可以从一堆 中取出若干 个(至少为 \(1\)),无法操作的人输。
结论设第 k 堆石子中有 \(a_k\) 个石子。
若 \(a_1\oplus a_2\oplus a_3\oplus...\oplus a_k=0\) 先手必输,反之,先手必胜
证明依旧从 \(N/P\) 态入手。
当无石子时,一定是 \(P\) 态。
从而得知,只有一堆石子时,一定是 \(N\) 态。
有奇数堆数量为 \(1\) 的石子时也是 \(N\) 态。
而偶数堆数量为 \(1\) 的石子是 \(P\) 态。
由此可依次推出 \((1,2)、(1,3)、(1,4)...\) 均为 \(N\) 态。
经过一系列推导,我们可以发现,当 \(a_1\oplus a_2\oplus a_3\oplus...\oplus a_k=0\) 时为 \(P\) 态,其余为 \(N\) 态。
为什么呢?
其实,对于任意异或和不为 \(0\) 时,此时操作的人可以轻易地取走一堆石子使其异或和为 \(0\)。
显然,一定会存在一个数,将其与异或和异或后,其会小于本身,此时异或和也变为 \(0\)。(可以从二进制角度思考,一定有一个数在异或和的最高 \(1\) 位的位数上也是 \(1\)。)
这样,到了最后是必胜的,让石子数异或和为 \(0\),这就是先手的必胜策略。
有一种证法是说对于异或和为 \(0\),操作后异或和一定不为 \(0\)。对于异或和不为 \(0\),一定有方案能使异或和为 \(0\)。
发现与 \(N/P\) 态关系符合,感性理解一下。
例题就是模板。
代码
cppwhile(t--){ cin>>n;int sum=0; for(int i=1,x;i<=n;i++)cin>>x,sum^=x; cout<<(sum?"Yes\n":"No\n"); }
阶梯 Nim 游戏(Moore`s Nim)---Nim 变式
在一个有 \(n\) 个阶梯的楼梯上,依次放着任意数量石子,每次可选一个阶梯上的若干个石子,使他们下到下一个阶梯,下到地面(\(0\) 阶梯)的石子不能移动。
结论对于所有的奇数阶梯,对其石子数做异或和,异或和为 \(0\) 则先手必败,反之,先手必胜。
证明我们考虑,阶梯上没有石子时,是 \(P\) 态,此时奇数异或和为 \(0\)。
而我们套用 Nim 游戏的第二套证法,则对于奇数异或和为 \(0\),不管移动哪里的石子,都会使异或和不为 \(0\),而对于异或和不为 \(0\),一定有方案能使异或和为 \(0\)。(自行推导到 Nim 游戏即可)
为什么不用考虑偶数?
因为若对方移动石子至偶数阶梯或从偶数阶梯移至奇数,我们都可以移动相同数量的石子来维护异或和。并且 \(0\) 地板是偶数。所以不用考虑偶数阶梯的。
例题大意:
有 \(n\) 堆数量非严格升序的石子,每次可选一堆取石子,但要满足数量依旧非严格升序,无法操作的人输。
对于例题,我们发现,一个堆可以移动的数量只与前一个数有关,若前一个堆移走一定数目,则后一个堆可移动的会增加相同数目。诶,这不就是相当于将前一堆的移至下一堆吗!不就是阶梯 Nim 吗!石子数量是当前堆数量减上一堆。记得是从 \(1\) 往 \(n\) 移。
代码
cppwhile(t--){ cin>>n;int ans=0; for(int i=1;i<=n;i++)cin>>a[i]; for(int i=1;i<=n;i++) if((i%2)^(n%2==0))ans^=a[i]-a[i-1]; if(ans==0)cout<<"NIE\n"; else cout<<"TAK\n"; }
Nim-k 游戏---Nim 扩展
游戏规则在 Nim 基础上变成变成了可任选不超过 \(k\) 堆石子,在这几堆石子中取走任意个。
结论将石子数转为二进制,若存在某个二进制位上的 \(1\) 的个数对 \(k+1\) 取模不为 \(0\),先手必胜,反之,先手必败。
证明考虑从 Nim 游戏中扩展。
当石子数为 \(0\) 时,为 \(P\) 态。
我们每次可以选 \(k\) 堆石子,也就是说我们可以改变 \(k\) 堆石子的状态,则我们可以参考 Nim 游戏,从二进制下手。
我们思考如何一步使取模为 \(0\)。
我们从高位开始,则对于已经选的堆,当前位可 \(0\) 可 \(1\)。
假设已经选 \(s\) 堆,剩下的有 \(m\) 堆为 \(1\)(对 \(k+1\) 取模)。
当 \(m\le k-s\) 时,直接全选。
当 \(m>k-s\) 时,我们让那 \(m\) 堆中有 \(k-m+1\) 个 \(1\) 就行。因为我们从高位开始,且 \(k-m+1<k-(k-s)+1=s+1\),即 \(k-m+1\le s\),所以一定可以做到。
举个例子,假设 \(k\) 为 \(3\),当前位 \(m\) 为 \(6\)
当 \(s\) 为 \(1\) 时,我们可以再选 \(2\) 个,这样 \(m\) 就变成了 \(4\),满足对 \(k+1\) 取模为 \(0\)。
当 \(s\) 为 \(2\) 时,我们使选的两堆中全为 \(1\),这样总 \(m\) 就是 \(8\),也满足要求。
例题大意:
在一个长度为 \(n\) 的棋盘上,有 \(k\) 个棋子,有黑白两种颜色,每种颜色数量相同,白棋子只能往右,黑棋子只能往左,最左边的一定是白棋子,最右边的一定是黑棋子。两人分别控制黑白棋子,每次可移动 \(1-d\) 个棋子任意步,但不能越过任何棋子。无法移动者输。
对于例题,显然不是模板而且似乎还是非公平组合游戏,但我们其实可以转为公平组合游戏,我们可以将一白一黑棋子之间的格子数看作石子数,这样他们的操作就类似移走这一堆中的石子,题目就变成 \(\frac{k}{2}\) 堆石子,每次可选 \(d\) 堆的 Nim-k 游戏了。
转化做完,开始想怎么来统计个数。
正着统计先手必胜不好做,那就统计先手必败吧!
考虑 \(dp\),\(dp_{i,j}\) 为前 \(i\) 二进制位中 \(1\) 的个数均为 \(d+1\) 倍数的方案数,则对于 \(i+1\) 位,我们假设有 \(x\times (d+1)\) 个 \(1\),方案数 \(\begin{pmatrix} \frac{k}{2} \\ x\times (d+1) \\ \end{pmatrix}\)。
显然需要一点组合数学细胞...
代码
cppvoid init(){ c[0][0]=1; for(int i=1;i<=10000;i++){ c[i][0]=1; for(int j=1;j<=min(1ll*i,k);j++)c[i][j]=(c[i-1][j-1]+c[i-1][j])%mod; } } int main(){ cin>>n>>k>>d;dp[0][0]=1; init(); for(int i=0;i<13;i++){ for(int j=0;j<=n-k;j++){ for(int x=0;x*(d+1)*(1<<i)+j<=n-k&&x*(d+1)*2ll<=k;x++){ (dp[i+1][j+(1<<i)*x*(d+1)]+=dp[i][j]*c[k/2][x*(d+1)]%mod)%=mod; } } } ll ans=0; for(int i=0;i<=n-k;i++)(ans+=dp[13][i]*c[n-i-k/2][k/2]%mod)%=mod; cout<<(c[n][k]-ans+mod)%mod; return 0; }
Anti-Nim(反尼姆游戏,简单提提)
与 Nim 游戏相差不大,但是是取走最后的人输。
证明与结论
我们先考虑全为 \(1\) 的情况,此时显然只与奇偶性有关。
再考虑不全为 \(1\) 的情况:
- 只有一堆 \(>1\):显然先手可根据 \(n\) 的奇偶来决定取空还是留一个,先手必胜。
- 两堆及以上:当异或和为 \(0\),可以变为上面情况或还是有两堆以上,但异或和不为\(0\);当异或和不为 \(0\),那一定有办法使其异或和为 \(0\),且还是有两堆及以上。第一种能变为第二种或必胜态,而第二种能变为第一种,所以第二种为必胜态,第一种为必败态。
所以,综合起来,我们发现:
全为一时异或和为 \(0\) 先手必胜,存在一堆不为 \(1\) 时,异或和不为 \(0\) 先手必胜。
例题即模板。
代码
cppcin>>n;bool f=0;sum=0; for(int i=1,x;i<=n;i++)cin>>x,sum^=x,f|=(x>1); if((sum>0)^f)cout<<"Brother\n"; else cout<<"John\n";
威佐夫博弈(Wythoff`s game)
两堆石子,每次可以选一堆取走若干个,或从两堆中取走相同数目的石子,无法操作的人败。
结论当两堆石子的差乘黄金分割比等于较小堆石子时,先手必败,反之,后手必败。
证明我们考虑必败态是什么。
我们设第 \(k\) 个 \(P\) 态是 \((a_k,b_k)\),其中 \(a_k<b_k\)。
为什么可以这样假设?
因为对于 \(a_k=b_k\) 时,我们可以一次取完,这一定是必胜态。那就只有两者不等了,我们调换一下,不会有影响。
那么,我们可以发现一个性质:若 \((a_k,b_k)\) 为必败态,那 \((a_k,x)\),\((y,b_k)\) 一定是必胜态,其中 \(x\ne b_k,y\ne a_k\),这很显然,根据必胜态,必败态的关系可知。
然后,根据这个,还有一个性质,就是每个数只能出现一次,我们接下来来证明每个数都存在。
假设存在最小缺失数 \(k\)
设 \(k\) 是最小的未在任何必败态中出现的正整数。则对所有 \(i>0\),\((k,k+i)\) 为必胜态。
必胜态的操作
\((k,k+i)\) 为先手必胜,故存在 \(0<j<k\) 使得下列之一为必败态:
- \((k,k+i-j)\)
- \((k-j,k+i)\)
- \((k-j,k+i-j)\)
但 \((k,k+i-j)\) 含较小数 \(k\),与 \(k\) 缺失矛盾,故只可能后两种。
每个 \(i\) 对应一个 \(j(i)\in\{1,\dots,k-1\}\)。因 \(i\) 有无穷多个而 \(j\) 只有 \(k-1\) 种,必存在某个 \(m\) 对应无穷多个 \(i\)。
从中取三个不同的 \(i_1,i_2,i_3\),至少有两个属于同一操作类型(要么全为 \((k-m,k+i)\) 型,要么全为 \((k-m,k+i-m)\) 型)。
- 若为 \((k-m,k+i)\) 型,则 \((k-m,k+i_1)\) 与 \((k-m,k+i_2)\) 均为必败态,较小数同为 \(k-m\),与只出现一次矛盾。
- 若为 \((k-m,k+i-m)\) 型,则 \((k-m,k+i_1-m)\) 与 \((k-m,k+i_2-m)\) 均为必败态,同样矛盾。
故假设不成立,不存在缺失的数 \(k\)。
所以每个数都会且仅会出现一次。
我们若将所有的必败态按 \(a_k\) 排序,会得到:
0 0 1 2 3 5 4 7 6 10 ...我们可以证明,\(a_k\) 是所有数中未出现的最小自然数:
\(b_k\) 比 \(a_k\) 大,且 \(a_k\) 递增,每个数又必须出现一次,故证。
我们可以发现,若 \(k\) 为他们的行数,那 \(a_k+k=b_k\)!
我们来证明一下:
对于第一、二行,显然成立。
假设对于前 \(i-1\) 行均成立,即所有 \((a_p,a_p+p)\) 均为必败态。
对于第 \(i\) 行,设其为 \((a_i,a_i+\delta)\)。
若 \(\delta<i\),则可以通过一次操作将 \((a_i,a_i+\delta)\) 变为前面已经出现的必败态(例如从较大堆中取出适当石子),从而该状态为必胜态,矛盾。因此必有 \(\delta\ge i\)。
考虑从第一堆中取出 \(p\) 个石子,得到状态 \((a_i-p,a_i-p+\delta)\)。
若 \(a_i-p\) 出现在某一行(设为第 \(k\) 行)的第一列,则由于存在必败态 \((a_i-p,a_i-p+k)\),且 \(k<\delta\)(因为 \(\delta\ge i>k\)),故 \((a_i-p,a_i-p+\delta)\) 可由该必败态通过增加第二堆石子得到,其为必胜态。
若 \(a_i-p\) 出现在某一行(设为第 \(l\) 行)的第二列,则其对应的第一列数 \(a_l<a_i+\delta\)(因第一列严格递增),同样可推出该状态为必胜态。
类似地,考虑从两堆石子中取出相同数目的情况,也容易看出所得状态均为必胜态。
因此,\((a_i,a_i+\delta)\) 的胜负性只与状态 \((a_i,a_i+d)\)(其中 \(d<\delta\))有关。而当 \(\delta=i\) 时,无论从第二堆中取出多少个石子,得到的另一堆石子(即第一堆)并未在之前出现过,故所得状态均为必胜态。从而 \((a_i,a_i+i)\) 本身无法一步到达任何必败态,故为必败态。
由此,我们可以得到:\(a_k\) 为最小未出现自然数,\(a_k+k=b_k\)。
那这个有什么用?
我们可以用一个名为 Beatty 定理 的东东。
设 \(a,b\) 是两个正无理数,且 \(\frac{1}{a}+\frac{1}{b}=1\)。
记 \(P=\{\lfloor a\times n \rfloor \mid n\in\mathbb{N}^+\},Q=\{\lfloor b\times n \rfloor \mid n\in\mathbb{N}^+\}\)。
则 \(P\cap Q=\varnothing,\ P\cup Q=\mathbb{N}^+\)。
我们发现这与我们的必败态的状态数有点像,尝试套一下。
\a_k+k=b_k\\to \\lfloor a\\times k\\rfloor+k=\\lfloor b\\times k\\rfloor,\\frac{1}{a}+\\frac{1}{b}=1 \\
接下来就是解方程。
由第一个,我们可以解出 \(a+1=b\)
代到 \(\frac{1}{a}+\frac{1}{b}=1\) 中,得 \(\frac{1}{a}+\frac{1}{a+1}=1\)
解出 \(a=\frac{1\pm \sqrt5}{2}\)
因为其为正有理数,故 \(a=\frac{1+\sqrt5}{2}\)。
我们便可得到
\(b_k-a_k)\\times \\frac{1+\\sqrt5}{2}=a_k \\
得证
好累
对于例题,是模板,直接用结论就行。
代码
cppcin>>x>>y; long double s=(sqrtl(5)+1.0)/2.0; if(floor(abs(x-y)*s)==min(x,y))cout<<0; else cout<<1;
斐波那契博弈(Fibonacci Nim)
一堆石子,每次至多取上一个人取的数目的两倍,先手第一次可以取任意个,但不能一次取完,无法操作的人败。
结论当石子数为斐波那契数时,先手必败,反之,后手必败。
证明尝试用归纳法(归纳法真是太好用了)
设 \(fi=\{1,2,3,5,8,13,21...\}\)
对于 \(i=2\),先手必败。
假设 \(i<k\) 成立。
对于 \(i=k\),\(fk=fk-1+fk-2\),将其分为 \(fk-1\) 与 \(fk-2\)。(因为若先手取 \(fk-2\),后手一定能一次取完)
由假设,对于 \(k-2\) 堆,后手一定能取最后一颗。
若先手取 \(y\ge \frac{fk-2}{3}\),则剩下的不会 \(>2\times y\),所以后手可一次取完这一堆。比较 \(\frac{4\times fk-2}{3}\) 与 \(fk-1\),可得到 \(fk-1\) 更大,因此先手无法一次取完,情况便来到了 \(fk-1\) 这堆,由假设,后手胜。
故对于 \(i=k\) 成立。
那如果不是斐波那契数呢?
有一个定理,名为 齐肯多夫定理
任何正整数可以表示为若干个不连续的斐波那契数之和。
如分解 \(53\),就可以分为 \(34+13+5+1\)
这样,先手就可以从分解后的最小堆入手,取完,因为不连续,所以倒数第二堆一定大于这一堆的两倍,后手不能一次取完。如此,后手对于每一堆都是必败,先手每次都能取走最后一颗。
例题就是典型模板。
代码
cppll n,k;cin>>n; f[1]=f[2]=1; for(int i=3;i<=85;i++){ f[i]=f[i-1]+f[i-2]; if(n>=f[i])k=i; } if(n==f[k])cout<<n; else{ ll tn=n,ans=1e18; for(int i=85;i>0;i--)if(tn>=f[i]&&!v[i+1])tn-=f[i],v[i]=1,ans=min(ans,f[i]); cout<<ans; }
3.SG 函数
SG 函数可以说是解决博弈论的一大利器,其巧妙的思想基本可以覆盖所有公平组合博弈问题(某些非公平组合博弈也可以,看具体情况)。
一些定义
后继状态:当前状态进行一步后的可达状态。
SG值 :当前状态的一个值,其为后继状态的 \(mex\)。
终止状态 :无后继状态,SG值为 \(0\)。
注:\(mex\) 为集合中最小未出现的自然数。
博弈与DAG
我们可以发现,公平组合博弈一般都能化成一个 DAG,因为每个状态都有确定的后继,且为了有限步数内终止,一定无环。所以我们求 SG 便可用拓扑了。
SG 运作
首先,终止状态的 SG 值为 \(0\)。然后递推回去求出每个状态的 SG 值。若非终止状态的 SG 值为 \(0\),则其后继节点一定无 SG 为 \(0\) 的状态,若不为 \(0\),则其后继节点一定有 SG 为 \(0\) 的状态。我们可以发现,终止状态一定是 \(P\) 态,而这样的话他的上一个状态均为 \(N\) 态,SG不为 \(0\)。
- 终止状态 :SG 为 \(0\),为 \(P\) 态。
- 归纳假设 :对于深度小于 \(d\) 的状态节点,SG 为 \(0\) 对应 \(P\) 态,SG 不为 \(0\) 对应 \(N\) 态。
- 对于深度为 \(d\) 的点:
- 若其 SG 为 \(0\),则其后继均为 \(N\) 态,则当前为 \(P\) 态。
- 若其 SG 不为 \(0\),则其后继中有 \(P\) 态,则当前为 \(N\) 态。
这就是 \(mex\) 的巧妙!
但这只适用于单个游戏,对应 bash
那对于 Nim 这种多堆的相当于多个游戏揉和的怎么办?
其实,SG 还有另一个定理,即 Sprague--Grundy 定理。
总游戏的 SG 为各个独立游戏的 SG值的异或和。
对于 Nim,我们可以将每一堆视为一个独立游戏,因为堆与堆之间是互不干扰的,而异或和就与 Nim 那里一样的意思。
对于其他博弈,我们也可以看作是在做 Nim,每一个独立游戏就相当于 Nim 中的一个堆。
或者我们也可以这样理解,Nim 中石子数量决定了它后缀状态的数量,SG 中的 SG 值也决定了它后缀状态的种类,这也是为什么 Nim 可以与 SG 结合。
这便是 Nim 与 SG 的关联。
SG 值在一些题目中通常带有规律,周期。所以在一些规模较大的题我们也可以用这种规律来求 SG 值。
SG 例题
例题 abc278_g Generalized Subtraction Game
例题讲解
对于例题,考虑 \(L=R\)、\(L<R\) 两种情况。
对于 \(L<R\),我们可以先手选走中间段,这样石子就分成两个相等段,那不管电脑选啥,我们都可以镜像地在另一段选,这样,我们肯定是最后一个取完的。
对于 \(L=R\),若 \(L\) 与 \(n\) 奇偶性相同,就如上操作。若不同,那就用 SG,处理连续的长度为 \(len\) 的 SG 值,按照 SG 值来决定我们的操作,细节请看代码。
cppcin>>n>>l>>r; if(l<r||((l^n)&1)==0){//将其分成两部分,与其做镜像 cout<<"First"<<endl; for(int i=l;i<=r;i++){ if(((i^n)&1)==0){ cout<<(n-i)/2+1<<" "<<i<<endl; break; } } while(1){ int x,y; cin>>x>>y; if(x==0&&y==0)return 0; if(x==-1&&y==-1)return 0; cout<<n-x-y+2<<" "<<y<<endl; } } for(int i=1;i<=n;i++){//计算长度为i的sg for(int j=1,k=l;k<=i;k++,j++)vs[sg[j-1]^sg[i-k]]=1;//将区间分为三份:1-j-1,j-1-k(取走),k+1-i for(int j=0;;j++){ if(!vs[j]){ sg[i]=j; break; } } for(int j=1,k=l;k<=i;k++,j++)vs[sg[j-1]^sg[i-k]]=0; } if(sg[n]==0){ cout<<"Second"<<endl; int x,y; cin>>x>>y; if(x==0&&y==0)return 0; if(x==-1&&y==-1)return 0; for(int i=x;i<=x+y-1;i++)vs[i]=1; }else cout<<"First"<<endl; while(1){ cnt=0; for(int i=1;i<=n;i++){ if(!vs[i]){ if(i==1)qj[++cnt][0]=1,qj[cnt][1]=1; else{ if(vs[i-1])qj[++cnt][0]=1,qj[cnt][1]=i; else qj[cnt][0]++; } } } int k1=0,f=0; for(int i=1;i<=cnt;i++)k1^=sg[qj[i][0]]; for(int i=1;i<=cnt;i++){ int k2=k1^sg[qj[i][0]]; for(int j=1,k=l;k<=qj[i][0];j++,k++){ if((sg[j-1]^sg[qj[i][0]-k]^k2)==0){//若处理这个能使达P态 cout<<qj[i][1]+j-1<<" "<<l<<endl; for(int p=qj[i][1]+j-1;p<=qj[i][1]+j-1+l-1;p++)vs[p]=1; f=1; break; } } if(f)break; } int x,y; cin>>x>>y; if(x==0&&y==0)return 0; if(x==-1&&y==-1)return 0; for(int i=x;i<=x+y-1;i++)vs[i]=1; }
浅提树删边
题目大概为给一颗有根树,每次可删一条边,然后这条边所连接的子树消失,无法操作的人输。
结论为:
\SG_x=\\begin{cases} 0\\quad x \\text{ 没有儿子}\\\\ \\oplus_{y\\in son(x)} SG_y+1\\quad \\text{ 其他情况} \\end{cases} \\
证明用归纳法:
-
\(x\) 无儿子,显然为 \(0\)。
-
\(x\) 有一个儿子
- 若删了这个儿子,SG 为 \(0\)。
- 删除这个儿子的子树,那根据归纳法,\(\{0,1,...,SG_x-1\}\) 是新树的后继 SG,那 \(x\) 的 SG 后继即为 \(\{1,2,...,SG_y\}\)
则 \(x\) 的 SG 值即为总集合的 \(mex\),即 \(SG_y+1\)。
-
\(x\) 有多个儿子,直接异或和就行
至于例题,可自行去找找,我这可以推荐一道 agc017d Game on Tree
浅提反 SG
反 SG 并不是很常见,本人也没怎么做过此类题目,所以简单了解一下就行。
其实跟 Anti-Nim 差不多,因为 Nim 与 SG 的关系就摆在这,所以 Anti-SG 与 Anti-Nim 的结论类似:
- SG 值异或和为 \(0\),且所有独立游戏的 SG 值都不大于 \(1\)。
- SG 值异或和不为 \(0\),且存在独立游戏的 SG 值大于 \(1\)。
5.非公平组合博弈
本版块主要通过下面练习题来引导。
对于非公平组合博弈,我们没有上面那些套路,只能靠思维碰撞。
我们对于此类题目,只能思考能不能将其保持平衡(即一个人取,另一个人可以通过一些操作将其状态与之前相差不多),或有没有什么特殊性质,以此来想出解法。
也有一些题,是有贪心性质的,这与其平衡性有关,如下面的 CF388C Fox and Card Game,这个题目就很好地体现了平衡。
但是,也不妨有些题,是可以用 SG 的,如下面的练习题 CF1704F Colouring Game。
当我们把题意化解后,题目的限制变成与公平组合游戏大差不差时,便可以用 SG。
6.DP+博弈论
主要讲一下对于 DP 与博弈论结合的题目的一些思路。
DP 与博弈论结合,DP 的状态设计一般是游戏状态。
然后,根据题目限制,转移状态,考虑有哪些可以转到这个状态,这个状态又能转到哪个。有时还能用 SG 来简化状态设计,转移。
零和博弈
遇到零和博弈,一般都会用到 DP。
讲解
我们尝试以谁是先手来 DP。
那么,我们倒序 DP 是可以保证准确性,因为我们无法知道之后的先手是谁,但知道第一个先手一定是 Bob。
考虑 DP 转移,如果把当前给对方,那先手及得分不变,状态直接从 \(dp_{i+1}\) 转来;如果给自己,那先手变了,得分也变,变成 \(sum-dp{i+1}+a_i\),其中 \(sum\) 为后缀和,不包括 \(a_i\)。
得到总转移式 \(dp_i=\max (dp_{i+1},sum-dp_{i+1})\),这里合并了一下,\(sum\) 包括 \(a_i\)。
cppcin>>n; for(int i=1;i<=n;i++)cin>>a[i]; for(int i=n;i>=1;i--)sum+=a[i],dp[i]=max(dp[i+1],sum-dp[i+1]); cout<<sum-dp[1]<<" "<<dp[1];
有些时候我们会遇到求极小化极大,即最大差值。这类题的 DP 定义与区间 DP 差不多,改改转移式就行。
7.题目汇总
样题
练习题(难度不递增,按类型排)
自主练习(难度很大)
练习题讲解
P17405 【MX-X31-T1】「FAOI-R14」四元博弈
很神的一道题,思维难度不低。
讲解
我们可以发现,每次操作都会操作到根,所以状态就与根有关。
当根的值不为 \(0\) 时,先手可以直接选根,让其变成 \(0\),此时后手只能选其他,此时根又会不为 \(0\),先手又可以选它。以此往复,因为游戏会在一定回合内结束,所以后手必败。
因此,根为 \(0\) 为必败态,反之为必胜态。
代码如下:
cpp#include<bits/stdc++.h> using namespace std; const int N=22; int n,a[N]; int main(){ int t;cin>>t; while(t--){ cin>>n; for(int i=1;i<=n;i++)cin>>a[i]; for(int i=1,u,v;i<n;i++)cin>>u>>v; if(a[1]==0)cout<<"Yuan\n"; else cout<<"Si\n"; } return 0; }
P2575 高手过招
挺好的一道博弈题。
讲解
实质上是 SG+阶梯 Nim
为什么是阶梯 Nim 呢?
因为我们能发现对于每个连续块,我们可以移动不同棋子,使其分成两个块,而我们可以看作是将这几个石子往下移。
诶,这不就是阶梯 Nim 吗!
行与行之间不干扰,可以直接异或。
cpp#include<bits/stdc++.h> using namespace std; int main(){ ios::sync_with_stdio(false),cin.tie(0); int t,n,m,v[22],sum; cin>>t; while(t--){ cin>>n;sum=0; for(int i=1;i<=n;i++){ cin>>m; memset(v,0,sizeof(v));int c=20-m+1,a=0,t=0; //为什么加1?因为我们统计的是一个空格后的连续棋子,在左边加一个空格能避免最左边的棋子没有被统计 for(int j=1,x;j<=m;j++)cin>>x,v[x]=1; for(int j=1;j<=20;j++){ if(!v[j]){ if((--c)&1)a^=t; t=0; }else t++; } sum^=a; } cout<<(sum>0?"YES":"NO")<<"\n"; } return 0; }
P6791 SNOI2020 取石子
与经典模型很好的结合。
讲解
可以发现如果不考虑 \(k\),此题的限制就变成了斐波那契游戏。(对于取走最后一个的输,我们可以将石子数减 \(1\),这不会有什么影响)
那么,我们考虑如果加了 \(k\),那只要保证分解后最小那堆不超过 \(k\) 就行,具体可以用数位 DP。
cpp#include<bits/stdc++.h> using namespace std; using ll=long long; using ld=long double; using i128=__int128; using PI=pair<int,int>; ll f[100],v[100],k,dp[100]; ll dfs(int p,bool vs,bool ls){ if(p<k)return 1; if(!vs&&!ls&&dp[p]!=-1)return dp[p]; ll r=dfs(p-1,vs&(v[p]==0),0); if(!ls&&(!vs||v[p]==1))r+=dfs(p-1,vs,1); if(!ls&&!vs)dp[p]=r; return r; } int main(){ ios::sync_with_stdio(0);cin.tie(0); int t;cin>>t; f[1]=1,f[2]=1; for(int i=3;i<=90;i++)f[i]=f[i-1]+f[i-2]; while(t--){ ll n;cin>>k>>n;n--;//转anti memset(v,0,sizeof(v)); for(int i=1;i<=90;i++)if(k<f[i]){k=i;break;} ll tn=n;for(int i=90;i>0;i--)if(tn>=f[i])tn-=f[i],v[i]=1; memset(dp,-1,sizeof(dp)); cout<<(n-dfs(90,1,0)+1)<<"\n"; } return 0; }
P2734 IOI 1996 / USACO3.3 游戏 A Game
来道比较经典的零和博弈。
讲解
题目比较简单,就是设计一个区间 DP,表示先手对于这个区间的最大收益。其实与上面的例题大差不差。
转移挺好想的,\(dp_{i,j}=sum_{i,j}-\min(dp_{i,j-1},dp_{i+1,j-1})\)。
cpp#include<bits/stdc++.h> using namespace std; int n,a[102],dp[102][102],s[102]; int main(){ cin>>n; for(int i=1;i<=n;i++)cin>>a[i],s[i]=s[i-1]+a[i]; for(int l=1;l<=n;l++) for(int i=1;i+l-1<=n;i++){ int j=i+l-1,sum=s[j]-s[i-1]; dp[i][j]=sum-min(dp[i+1][j],dp[i][j-1]); } cout<<dp[1][n]<<" "<<s[n]-dp[1][n]; return 0; }
P17128 ICPC 2025 Shanghai R AGI
开始进入非公平组合游戏。😃
讲解
说实话这题是有点难度的。
我们考虑每个数字出现次数的影响。
- 若某个数出现了偶数次,显然两者立场的不同会使其均分(这就是平衡),那影响就是确定的了。
- 若某个数出现了奇数次,就会有一给落单。落单的数的个数一定为偶数。
接下来我们考虑落单的数的影响。
- 当有两个落单的数时,若其中有先手需要的,即其与上面已计算的贡献相同,那先手是必赢的,反之,必输。
- 当大于两个时,后手完全可以取走先手需要的,所以先手必输。
cpp#include<bits/stdc++.h> using namespace std; using ll=long long; using ld=long double; using i128=__int128; using PI=pair<int,int>; const int N=2e5+3; int n,a[N<<1]; int main(){ // ios::sync_with_stdio(0);cin.tie(0); int t;cin>>t; while(t--){ cin>>n; int ans=0,cnt=0; for(int i=1;i<=2*n;i++)cin>>a[i]; sort(a+1,a+1+n+n); vector<int>v; for(int i=1,j=0;i<=2*n;i++){ if(a[i]!=a[i+1]||i==2*n){ int k=(i-j)/2; if(k%2)ans^=a[i]; if((i-j)%2)cnt++,v.push_back(a[i]); j=i; } } if(cnt>=4)cout<<"Bot\n"; else if(cnt==2){ if(ans==v[0]||ans==v[1])cout<<"Menji\n"; else cout<<"Bot\n"; }else{ if(ans)cout<<"Bot\n"; else cout<<"Menji\n"; } } return 0; }
CF388C Fox and Card Game
这道题挺简单,也很好体现了平衡。
讲解
首先,我们可以发现每一排数一定会均分(也就是平衡),因为两人的立场不同。
而至于多出来的数,两人可以轮流取最大。
cpp#include<bits/stdc++.h> using namespace std; using ll=long long; using ld=long double; using i128=__int128; using PI=pair<int,int>; int n,a[103][103],res,s,ans; int main(){ ios::sync_with_stdio(0);cin.tie(0); cin>>n; priority_queue<int>q; for(int i=1,x;i<=n;i++){ cin>>x; int sum=0; for(int j=1;j<=x;j++)cin>>a[i][j],sum+=a[i][j]*(j<=x/2),s+=a[i][j]; if(x%2)q.push(a[i][(x+1)/2]); res+=sum; } int f=1; while(!q.empty())ans+=q.top()*f,f^=1,q.pop(); cout<<res+ans<<" "<<s-res-ans; return 0; }
CF1704F Colouring Game
这道题可谓是无间道啊!
讲解
我们可以先将字符串拆成连续相同子串,那么,每个人的最优策略就是选交界,尽可能减少对手的操作次数,这其实也是在引导我们去找红蓝交叉段。
这样,我们就只剩怎么处理红蓝交叉段了。
可以发现,现在两人就相当与在选相邻的两个格子将其涂白,两人的操作集一样了!游戏变成了公平组合!于是我们便可考虑用 SG。
SG 的使用就十分粗暴,但这里用到了其循环节,打表可以发现其拥有长度为 \(34\) 的循环节。
cpp#include<bits/stdc++.h> using namespace std; using ll=long long; using ld=long double; using i128=__int128; using PI=pair<int,int>; const int N=5e5+3; int sg[N]; bool f[103]; void init(){ sg[2]=1; for(int i=3;i<1000;i++){ for(int j=1;j<i;j++)f[sg[j-1]^sg[i-j-1]]=1; for(int j=0;j<100;j++)if(!f[j]){sg[i]=j;break;} for(int j=0;j<100;j++)f[j]=0; } for(int i=1000;i<N;i++)sg[i]=sg[i-68]; } int main(){ init(); ios::sync_with_stdio(0);cin.tie(0); int t;cin>>t; while(t--){ int n; cin>>n; string s; cin>>s; int a=count(s.begin(),s.end(),'R'),b=count(s.begin(),s.end(),'B'); if(a>b){ cout<<"Alice\n"; continue; } if(b>a){ cout<<"Bob\n"; continue; } int S=0,len=0; for(int i=0;i<n;i++){ if(i&&s[i]==s[i-1]){ S^=sg[len];len=0; } len++; } S^=sg[len]; if(S)cout<<"Alice\n"; else cout<<"Bob\n"; } return 0; }
P4576 CQOI2013 棋盘游戏
一道与搜索结合的题,难度较为简单了。
讲解
显然,这游戏十分不公平,不是,非公平组合也没叫你不公平到这种地步啊。
我们可以发现,除非先手能直接吃,否则他必输,因为对方机动性更强。
那么,先手就只能尽量拖,后手就尽量追,于是我们可以记搜来找出答案。
cpp#include<bits/stdc++.h> using namespace std; const int inf=1e9+7,M=25; int f[2][60][M][M][M][M],n,ans; int dfs(int x,int y,int r1,int c1,int r2,int c2){ int ans; if(y>3*n)return inf;//游戏显然不会进行到这个步数 if(f[x][y][r1][c1][r2][c2])return f[x][y][r1][c1][r2][c2]; if(r1==r2&&c1==c2)return x?inf:0; if(!x){ ans=0; if(r1<n)ans=max(ans,dfs(1,y+1,r1+1,c1,r2,c2)); if(r1>1)ans=max(ans,dfs(1,y+1,r1-1,c1,r2,c2)); if(c1<n)ans=max(ans,dfs(1,y+1,r1,c1+1,r2,c2)); if(c1>1)ans=max(ans,dfs(1,y+1,r1,c1-1,r2,c2)); }else{ ans=inf; if(r2<n)ans=min(ans,dfs(0,y+1,r1,c1,r2+1,c2)); if(r2>1)ans=min(ans,dfs(0,y+1,r1,c1,r2-1,c2)); if(c2<n)ans=min(ans,dfs(0,y+1,r1,c1,r2,c2+1)); if(c2>1)ans=min(ans,dfs(0,y+1,r1,c1,r2,c2-1)); if(r2<n-1)ans=min(ans,dfs(0,y+1,r1,c1,r2+2,c2)); if(r2>2)ans=min(ans,dfs(0,y+1,r1,c1,r2-2,c2)); if(c2<n-1)ans=min(ans,dfs(0,y+1,r1,c1,r2,c2+2)); if(c2>2)ans=min(ans,dfs(0,y+1,r1,c1,r2,c2-2)); } return f[x][y][r1][c1][r2][c2]=++ans; } int main(){ int r1,c1,r2,c2; cin>>n>>r1>>c1>>r2>>c2; if(abs(r1-r2)+abs(c1-c2)<=1)puts("WHITE 1"); else printf("BLACK %d",dfs(0,0,r1,c1,r2,c2)); return 0; }
8.参考
鸣谢下面几篇讲解深刻的博客。
Wolfycz 的浅谈算法------博弈论(从零开始的博弈论)
😄



