博弈论相关问题

SG函数

适用范围

  • 两人、轮流操作
  • 信息公开透明
  • 没有随机因素
  • 有限步内必然结束
  • 不存在平局
  • 决策集合为空的游戏者输(即不能操作者输)
  • 可以将每个局面中的元素单独分析,元素之间不会有依赖关系(即一次可以选两个元素等)

策梅洛定理:对于这样的一个游戏,任何一个局面先手或者后手其中之一必然存在必胜策略

性质

  • 对于当前的局面X,定义其SG函数值为SG(X)。其中,先手必胜当且仅当SG(X)>0,反之后手必胜
  • X=X1+X2+...+Xn,则SG(X)=SG(X1)SG(X2) ... SG(Xn)。特别地,这里的a+b指的是局面上的组合(可理解为并集)而非数量加和。即,a+b=(a,b)
  • SG(X)=mex{SG(Y)|XY}XY表示X可以通过一步操作变换到Y)。从而一次博弈过程可以看成一棵有向无环树。那么结束节点为SG()=0(根据mex定义)

应用(ABC206F

注意到覆盖值域只有[1,99]N+,则初始局面S=(a1,a2,a3,...,a99)ai表示在当前局面中,第i个位置还没有被覆盖过。)

注意到覆盖或没覆盖过的地方一定是一段连续的位置。则我们用[l,r]来表示alar,即S=[1,99]

用记忆化搜索来求解SG(S)。设当前局面为X=[l,r],若l>rSG(X)=0

否则考虑SG(Y)|XY如何得来。在n个区间中,我们可以选择一个区间[p,q]满足[p,q][l,r]并将其覆盖。此时,Y=[l,p1][q+1,r],那么SG(Y)=SG([l,p1])SG([q+1,r]),可以递归求解。得到SG(Y)后,就可以通过SG(X)=mex{SG(Y)|XY}求出SG(X)了。

code

int dfs(int l, int r)
{
	if (l > r) return 0;
	if (sg[l][r] != -1) return sg[l][r];
	int bk[105]; memset(bk, 0, sizeof(bk));
	for (int i = l; i <= r; i ++ )
	{
		for (int o = 0; o < (int)p[i].size(); o ++ )
		{
			int j = p[i][o];
			if (j > r) continue;
			bk[dfs(l, i - 1) ^ dfs(j + 1, r)] = 1;
		}
	}
	for (int i = 0; ; i ++ ) if (!bk[i]) return sg[l][r] = i;
}

int main()
{
	t = read();
	while (t -- )
	{
		for (int i = 1; i <= 99; i ++ ) p[i].clear();
		n = read();
		for (int i = 1; i <= n; i ++ )
		{
			int x = read(), y = read();
			p[x].pb(y - 1);
		}
		memset(sg, -1, sizeof(sg));
		if (dfs(1, 99)) puts("Alice");
		else puts("Bob");
	}
	return 0;
}

Anti-SG函数

适用范围

  • 决策集合为空的游戏者赢(即不能操作者赢,也可以理解为操作最后一步者输)
  • 其他同SG函数

性质:SJ定理

设初始状态为S=(a1,a2,...,an),则先手必胜当且仅当下列两种情况的任意一种成立:(下面SG函数的定义没有改变)

  • SG(S)>0SG(ai)>1
  • SG(S)=0SG(ai)1

Nim博弈

Description

两个人玩取石子游戏:地上有n堆石子(每堆石子数量小于104),每人每次可从任意一堆石子里取出任意多枚石子扔掉,可以取完,不能不取。每次只能从一堆里取。最后没石子可取的人就输了。求是否存在先手必胜的策略。

Solution

设这n堆石子分别为a1,a2,...,an。则先手必胜当且仅当i=1nai>0,反之后手必胜

简要证明:SG(a1,a2,...,an)=SG(a1)SG(a2) ... SG(an)

SG(X)=mex{SG(Y)|XY}=mex{0,1,2,...,X1}=X(每次可以取1X1个石头),

SG(a1,a2,...,an)=i=1nai>0时先手必胜,反之后手必胜

阶梯Nim问题

Description

𝑛个位置,每个位置上有𝑎𝑖个石子。两个人轮流操作,步骤是:挑选1...𝑛中任意一个存在石子的位置𝑖,将至少1个石子移动至𝑖1位置(则最后所有石子都堆在在0这个位置),谁不能操作谁输。求先手必胜还是必败。

Solution

结论:该问题相当于所有奇数位置的石子做Nim博弈。

证明:假设两个人都只会移动奇数位置的石子(并移到偶数位置上去),这相当于所有奇数位置的石子做Nim博弈(移到偶数位置,等价于在奇数位置取石子,因为偶数位置的石子无论怎样都不会造成影响)

这样可以求出是先手必胜/后手必胜。不妨令先手必胜,则先手一定能通过只移动奇数位置上的石子获胜。因为若后手也只移动奇数位置上的石子,先手必胜;若后手某一步移动了偶数位置的石子,先手可以紧接着把被移动的石子再移动到下一个偶数位置。这样,所有奇数位置的石子数量没有变,只有偶数位置的改变了,但这是没有影响的,故新问题与原问题等价。

应用(P2575

注意到一次只能对一行操作,而整个棋盘相当于这n行的组合。所以对每一行算出SG值并异或,即可判断。

因为某一行的石子之间是会相互影响的,不好直接算SG函数,考虑模型转化。

阶梯Nim问题要考虑不变量来划分阶段。注意到空格的数目是不变的。于是我们想到把每个空格右边有多少个连续的石子,作为该位置的石子数。这样就转化成了阶梯Nim问题。

code

#include <bits/stdc++.h>

using namespace std;

int t, n, v[25];

int read()
{
	int x = 0, fl = 1; char ch = getchar();
	while (ch < '0' || ch > '9') { if (ch == '-') fl = -1; ch = getchar();}
	while (ch >= '0' && ch <= '9') {x = (x << 1) + (x << 3) + ch - '0'; ch = getchar();}
	return x * fl;
}

int main()
{
	t = read();
	while (t -- )
	{
		n = read(); int sg = 0;
		while (n -- )
		{
			int m = read(), sg0 = 0, s = 20 - m + 1, tt = 0; memset(v, 0, sizeof(v));
			while (m -- ) { int x = read(); v[x] = 1;}
			for (int i = 1; i <= 20; i ++ )
			{
				if (v[i]) {tt ++ ; continue;}
				s -- ; if (s & 1) sg0 ^= tt; tt = 0;
			}
			sg ^= sg0;
		}
		if (sg) puts("YES"); else puts("NO");
	}
	return 0;
}

威佐夫博弈

Description

有两堆石子,由两个人轮流取。游戏规定,每次有两种不同的取法,一是可以在任意的一堆中取走任意多的石子;二是可以在两堆中同时取走相同数量的石子(都至少要取一个)。最后把石子全部取完者为胜者。现在给出初始的两堆石子的数目,求是否存在先手必胜的策略。

Solution

本题不满足SG函数的性质,故要换一种方式求解。

设这两堆石子分别有x,y个且x<y,那么后手必胜当且仅当5+12(yx)=x

不会证,但是学习了Beatty定理:

对于两个无理数x,y,若其满足1x+1y=1,令两个集合P={p|p=nx,nN+},Q={q|q=my,mN+},其中N+为正整数集。

则有PQ=,PQ=N+

用dp​解决博弈问题

Description

有时博弈问题不是某种基本模型,且不能用SG函数求解,这时可以考虑dp(通常是要最大化/最小化答案)

Solution

抓住“当前这个人(能得到的)最大/最小值,等于总和减去该人进行这步操作后,另一人的操作值(所有可能情况里的最大/最小,因为另一人也想使答案最优)”

应用(P2964

f[i][j]为当前剩下第i至第n个硬币,当前这个人从第i个硬币开始取j个硬币所能获得的最大价值。

由于取完后就是另一个人取,他肯定会取价值最大的方案,因此可以列出状态转移方程:

f[i][j]=s[n]s[i1]maxk=1min(2j,nij+1)f[i+j][k]

其中s[i]=j=1ic[i]

通过维护g[i][j]=maxk=1jf[i][k],将时间复杂度优化到O(n2)

code

#include <bits/stdc++.h>

using namespace std;

int n, s[2005], f, g[2005][2005];

int read()
{
	int x = 0, fl = 1; char ch = getchar();
	while (ch < '0' || ch > '9') { if (ch == '-') fl = -1; ch = getchar();}
	while (ch >= '0' && ch <= '9') {x = (x << 1) + (x << 3) + ch - '0'; ch = getchar();}
	return x * fl;
}

int main()
{
	n = read(); for (int i = 1; i <= n; i ++ ) {int x = read(); s[i] = s[i - 1] + x;}
	for (int i = n; i >= 1; i -- )
	{
		for (int j = 1; j <= n - i + 1; j ++ )
		{
			f = s[n] - s[i - 1] - g[i + j][min(j << 1, n - i - j + 1)];
			g[i][j] = max(g[i][j - 1], f);
		}
	}
	printf("%d\n", g[1][2]);
	return 0;
}
posted @   andysj  阅读(170)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示