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

巴巴博弈

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

正文

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

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

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

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\) 态关系符合,感性理解一下。

例题就是模板。

代码

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\) 移。

代码

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}\)

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

代码

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\) 先手必胜。

例题即模板。

代码

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 \]

得证

好累

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

代码

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

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

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

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

\(f[i]=\{1,2,3,5,8,13,21...\}\)

对于 \(i=2\),先手必败。

假设 \(i<k\) 成立。

对于 \(i=k\)\(f[k]=f[k-1]+f[k-2]\),将其分为 \(f[k-1]\)\(f[k-2]\)。(因为若先手取 \(f[k-2]\),后手一定能一次取完)

由假设,对于 \(k-2\) 堆,后手一定能取最后一颗。

若先手取 \(y\ge \frac{f[k-2]}{3}\),则剩下的不会 \(>2\times y\),所以后手可一次取完这一堆。比较 \(\frac{4\times f[k-2]}{3}\)\(f[k-1]\),可得到 \(f[k-1]\) 更大,因此先手无法一次取完,情况便来到了 \(f[k-1]\) 这堆,由假设,后手胜。

故对于 \(i=k\) 成立。

那如果不是斐波那契数呢?

有一个定理,名为 齐肯多夫定理

任何正整数可以表示为若干个不连续的斐波那契数之和。

如分解 \(53\),就可以分为 \(34+13+5+1\)

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

例题就是典型模板。

代码

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

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

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\) 为必败态,反之为必胜态。

代码如下:

#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 吗!

行与行之间不干扰,可以直接异或。

#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。

#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})\)

#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

开始进入非公平组合游戏。😃

讲解
说实话这题是有点难度的。

我们考虑每个数字出现次数的影响。

  • 若某个数出现了偶数次,显然两者立场的不同会使其均分(这就是平衡),那影响就是确定的了。
  • 若某个数出现了奇数次,就会有一给落单。落单的数的个数一定为偶数。

接下来我们考虑落单的数的影响。

  • 当有两个落单的数时,若其中有先手需要的,即其与上面已计算的贡献相同,那先手是必赢的,反之,必输。
  • 当大于两个时,后手完全可以取走先手需要的,所以先手必输。
#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

这道题挺简单,也很好体现了平衡。

讲解
首先,我们可以发现每一排数一定会均分(也就是平衡),因为两人的立场不同。

而至于多出来的数,两人可以轮流取最大。

#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\) 的循环节。

#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] 棋盘游戏

一道与搜索结合的题,难度较为简单了。

讲解
显然,这游戏十分不公平,不是,非公平组合也没叫你不公平到这种地步啊。

我们可以发现,除非先手能直接吃,否则他必输,因为对方机动性更强。

那么,先手就只能尽量拖,后手就尽量追,于是我们可以记搜来找出答案。

#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

😄

posted @ 2026-09-14 15:46  yjimf  阅读(140)  评论(2)    收藏  举报