巴巴博弈——博弈论模块浅谈

巴巴博弈

本文将竭尽所能带您一步步了解博弈论,如有错误,请各位大手子在评论区慷慨指出,谢谢!

正文

博弈论是个本质困难非常有趣的学科。

无特殊说明,本文均假设博弈者聪明绝顶。

1.博弈基础

在竞赛中,我们一般面对的都是组合博弈:

  • 两名玩家,轮流操作
  • 信息对双方公开,无隐藏信息(如军棋,扑克就不是)
  • 无随机性
  • 会在有限步数内终止
  • 无平局

博弈分类

  • 公平组合游戏:当前操作只与状态有关,与玩家无关的组合博弈(如象棋就不是)。
  • 零和博弈:玩家收益之和恒不变。

反之则为非公平组合游戏、非零和博弈。

本文无特殊说明,都是公平组合游戏,且都至少取走 \(1\) 个石子。

\(N/P\)态

必胜态\((N)\):即对于当前操作的人来说,有可以必胜的决策。

必败态\((P)\):即对于当前操作的人来说,没有决策可以让他避免失败。

根据定义,我们可以推出以下结论:

  • 当一个状态为必胜态\((N)\) ,他的后继状态(即他操作后的状态)中有一个必败态\((P)\)。
  • 当一个状态为必败态\((P)\) ,他的所有 后继状态均是必胜态\((N)\)

证明很容易,因为假设博弈者们是聪明绝顶,自行手推一下就懂了。

注:建议把 \(N/P\) 态定义弄懂,这是学懂博弈论最重要的。

2.常见公平组合游戏模型

接下来,我将讲解经典博弈模型,难度主观从易到难。

巴什博弈(bash game)

例题-HDU4764 vj

有两个人,他们面前有一堆石子,每次他们可以取 \(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\)!

cpp 复制代码
while(cin>>n>>k&&n&&k){
	n--; 
	if(n%(k+1))cout<<"Tang\n";
	else cout<<"Jiang\n";
}

Nim 游戏

例题-P2197

还是两个人,但这次他们面前有 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\) 态关系符合,感性理解一下。

例题就是模板。

代码

cpp 复制代码
while(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 变式

例题-P3480

在一个有 \(n\) 个阶梯的楼梯上,依次放着任意数量石子,每次可选一个阶梯上的若干个石子,使他们下到下一个阶梯,下到地面(\(0\) 阶梯)的石子不能移动。
结论

对于所有的奇数阶梯,对其石子数做异或和,异或和为 \(0\) 则先手必败,反之,先手必胜。
证明

我们考虑,阶梯上没有石子时,是 \(P\) 态,此时奇数异或和为 \(0\)。

而我们套用 Nim 游戏的第二套证法,则对于奇数异或和为 \(0\),不管移动哪里的石子,都会使异或和不为 \(0\),而对于异或和不为 \(0\),一定有方案能使异或和为 \(0\)。(自行推导到 Nim 游戏即可)

为什么不用考虑偶数?

因为若对方移动石子至偶数阶梯或从偶数阶梯移至奇数,我们都可以移动相同数量的石子来维护异或和。并且 \(0\) 地板是偶数。所以不用考虑偶数阶梯的。

例题大意:

有 \(n\) 堆数量非严格升序的石子,每次可选一堆取石子,但要满足数量依旧非严格升序,无法操作的人输。

对于例题,我们发现,一个堆可以移动的数量只与前一个数有关,若前一个堆移走一定数目,则后一个堆可移动的会增加相同数目。诶,这不就是相当于将前一堆的移至下一堆吗!不就是阶梯 Nim 吗!石子数量是当前堆数量减上一堆。记得是从 \(1\) 往 \(n\) 移。

代码

cpp 复制代码
while(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 扩展

例题-P2490

游戏规则在 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}\)。

显然需要一点组合数学细胞...

代码

cpp 复制代码
void 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(反尼姆游戏,简单提提)

例题-P4279

与 Nim 游戏相差不大,但是是取走最后的人输。

证明与结论

我们先考虑全为 \(1\) 的情况,此时显然只与奇偶性有关。

再考虑不全为 \(1\) 的情况:

  • 只有一堆 \(>1\):显然先手可根据 \(n\) 的奇偶来决定取空还是留一个,先手必胜。
  • 两堆及以上:当异或和为 \(0\),可以变为上面情况或还是有两堆以上,但异或和不为\(0\);当异或和不为 \(0\),那一定有办法使其异或和为 \(0\),且还是有两堆及以上。第一种能变为第二种或必胜态,而第二种能变为第一种,所以第二种为必胜态,第一种为必败态。

所以,综合起来,我们发现:

全为一时异或和为 \(0\) 先手必胜,存在一堆不为 \(1\) 时,异或和不为 \(0\) 先手必胜。

例题即模板。

代码

cpp 复制代码
cin>>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)

例题-P2252

两堆石子,每次可以选一堆取走若干个,或从两堆中取走相同数目的石子,无法操作的人败。
结论

当两堆石子的差乘黄金分割比等于较小堆石子时,先手必败,反之,后手必败。
证明

我们考虑必败态是什么。

我们设第 \(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 \\

得证

好累

对于例题,是模板,直接用结论就行。

代码

cpp 复制代码
cin>>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)

例题-P6487

一堆石子,每次至多取上一个人取的数目的两倍,先手第一次可以取任意个,但不能一次取完,无法操作的人败。
结论

当石子数为斐波那契数时,先手必败,反之,后手必败。
证明

尝试用归纳法(归纳法真是太好用了)

设 \(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\)

这样,先手就可以从分解后的最小堆入手,取完,因为不连续,所以倒数第二堆一定大于这一堆的两倍,后手不能一次取完。如此,后手对于每一堆都是必败,先手每次都能取走最后一颗。

例题就是典型模板。

代码

cpp 复制代码
ll 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 值来决定我们的操作,细节请看代码。

cpp 复制代码
	cin>>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。

CF859C Pie Rules

讲解

我们尝试以谁是先手来 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\)。

cpp 复制代码
cin>>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.题目汇总

样题

HDU4764

P2197

P3480

P2490

P2252

P6487

abc278_g

CF859C


练习题(难度不递增,按类型排)

P17405(神)

P2575(SG)

P6791(DP)

P2734(零)

P17128(非)

CF388C(非)

CF1704F(非)

P4576(非)


自主练习(难度很大)

P9170 省选联考 2023 填数游戏(非)

P3210 HNOI2010 取石头游戏(零)

P3179 HAOI2015 数组游戏(DP)

练习题讲解

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 的浅谈算法------博弈论(从零开始的博弈论)

wjyppm1403 的博弈论半家桶-从入门到门入从

Moya_Rao 的浅谈博弈 DP

😄