第三章、博弈论

一、Bash博弈

【巴什博奕】

题目背景:一共有n颗石子,两个人轮流拿,每次可以拿1~k颗石子,拿到最后一颗石子的人获胜。问:先手是否有必胜策略?

在回答这个问题之前,我们可以从题目背景中总结出来三个性质:

① n = 0时,必败。

② n %(k + 1)≠ 0,说明余数m在1~k之间,也就是说,我们必定能找到一个操作(取走m颗石子)使得剩余石子 a %(k + 1)= 0。

③ n %(k + 1)= 0,那么在取走一定数量的石子之后,剩余石子a只有两种情况,第一种:a %(k + 1)= 0;第二种:a % (k + 1)≠ 0。但是,第一种情况不可能成立!因为一开始的n是k+1的倍数,想要剩余石子a也是k+1的倍数的话,就只能取走k+1的整数倍数量的石子(最少也要取k+1颗石子),这就违背题目规则了,因为规则要求只能取1~k颗石子。也就是说,无论我们怎么操作,剩余石子a %(k + 1)≠ 0。

在得出上面三个性质之后,问题的证明也就很简单了:

结论:

当n %(k + 1)= 0 时,先手必败;

当n %(k + 1)≠ 0 时,先手必胜。

二、Nim博弈

【尼姆博弈】

题目背景:n堆物品,每堆分别有a1,a2,a3...an个,两个玩家轮流取走任意一堆的任意个物品,但不能不取。取走最后一个物品的人获胜。问:先手或者后手是否有必胜策略?

证明:

如果a1^a2^a2^...^an = s ≠ 0,设s的二进制表示中最高位为第k位。那么a1,a2,a3...an一定有奇数个ai的二进制第k位为1。那么随意拿出一个ai,用ai^s去替换ai,那么最终异或结果就是0,因为ai^s < ai,所以一定能够替换,且拿走石子的数量为ai - ai ^ s。

如果a1^a2^a3^...^an = s = 0,那么二进制表示中,同一位的1出现次数一定是偶数。此时执行一次操作之后,必定会使某一位的二进制由1变成0,那么这一位的异或结果一定是1,即新的a1^a2^...^an ≠ 0。

结论:

当a1^a2^a3^...^an = 0,先手必败;

当a1^a2^a3^...^an ≠ 0,先手必胜。

【阶梯型Nim游戏】

题目背景:有1~n级台阶,第 i 级台阶上放了a[i] 个石子,两名玩家轮流将任意级台阶的任意个石子移到下一级台阶上,移到地面上的石子不能继续移动,移走最后一个石子的玩家获胜。问:先手是否存在必胜策略?

性质:当某一位玩家遇到奇数台阶没有石子时,无论偶数台阶的石子如何分布,该玩家必败。

证明:

有了上面的性质,那么问题就变成了只考虑奇数台阶的经典Nim游戏。对于先手而言,面对的是奇数台阶异或结果不为0,那么总有一个操作使得异或结果为0,然后交给后手。对于后手而言,面对的是奇数台阶异或结果为0的情况。如果移动偶数台阶的石子,先手可以跟着模仿,依旧将异或为0的状态交给后手;如果移动奇数台阶的石子(相当于Nim博弈中从某一堆石子中拿走一些石子),先手再次遇到不等于0的情况,就可以再次重复上述步骤。因此,无论后手怎么操作,先手都会把奇数台阶异或为0的状态交给后手。

结论:

如果奇数台阶上的a1^a3^a5^... ≠ 0,则先手必胜;

否则,先手必败。

代码实现:

cpp 复制代码
#include <iostream>
using namespace std;

int main()
{
	int n;
	cin >> n;

	int ret = 0;
	for (int i = 1;i <= n;i++)
	{
		int x;
		cin >> x;

		if (i & 1)
			ret ^= x;
	}

	if (ret)
		cout << "win" << endl;
	else
		cout << "lose" << endl;

	return 0;
}

三、SG函数

【有向图游戏】

题目背景:给定一个有向无环图,图中只有一个起点,在起点上放上一个棋子,两个玩家轮流沿着有向边移动棋子,每次走一步,不能走的玩家失败。

【mex运算(minimum exclusion)】

mex(S) 表示找出不属于集合S的最小非负整数。

例如:S = {0,1,2,3,10},mex(S) = 4;S = {2,3,4},mex(S) = 0。

【SG函数】

在有向无环图中,当某一个状态 x 有 k 个后继 y1,y2,y3,...,yk,那么有:SG(x) = mex({SG(y1),SG(y2),...,SG(yk)}) 。

特殊的,如果 x 没有后继,SG(x) = 0。

结论:

在有向图游戏中,SG(x) ≠ 0 的状态为必胜态,SG(x) = 0 的状态为必败态。

证明:

根据mex的运算规则,如果SG(x) ≠ 0,说明x这个点的后继结点中,一定存在SG(y) = 0 的结点;如果SG(x) = 0,说明x这个点的后继结点中,全部都是SG(y) ≠ 0 的结点。

【利用SG函数解决Bash博弈】

实际上,大部分的公平组合游戏都可以转换为有向图游戏。

当Bash游戏中的 n 和 k 确定之后,就可以把所有的状态图画出来。因为游戏一定能结束,所以必定是一个有向无环图(见 图-1)。
图-1

那么,就可以用SG函数解决Bash博弈。

【利用SG函数,打表找规律】

对于上述Bash游戏,如果把所有的SG值打印出来,就能通过表格中数的规律,得到Bash游戏的结论(见 图-2)。
图-2

【SG定理】

由n个有向图游戏组成的组合游戏,设起点分别为s1,s2,s3,...,sn。

当SG(s1) ^ SG(s2) ^ SG(s3)^...^SG(sn) ≠ 0 时,先手必胜;反之,先手必败。

也就是说,如果一个游戏是由若干个ICG游戏组成的,那么把每一个ICG游戏的SG值异或在一起,就可以判断出所有游戏下的胜负情况。

【利用SG定理解决Nim游戏】

Nim游戏的每一堆石子,都可以看做是一个ICG游戏。因此Nim游戏本身就是由若干个ICG游戏组成的。

在每一个游戏中,SG值都是石子个数(见 图-3)。
图-3

因此,Nim游戏的结论与SG定理是对应的。

代码实现:

cpp 复制代码
#include <iostream>
#include <cstring>
#include <vector>
#include <unordered_set>
using namespace std;

const int N = 2010;

int n, m, k;
vector<int> edges[N];

int f[N];  // 存每一个点的SG值

int sg(int u)
{
	if (f[u] != -1)
		return f[u];

	unordered_set<int> mp;
	for (auto v : edges[u])
	{
		mp.insert(sg(v));
	}

	for (int i = 0;;i++)
	{
		if (!mp.count(i))
		{
			return f[u] = i;
		}
	}
}

int main()
{
	cin >> n >> m >> k;
	while (m--)
	{
		int x, y;
		cin >> x >> y;
		edges[x].push_back(y);
	}

	memset(f, -1, sizeof f);

	int ret = 0;
	for (int i = 1;i <= k;i++)
	{
		int x;
		cin >> x;
		ret ^= sg(x);
	}

	if (ret)
		cout << "win" << endl;
	else
		cout << "lose" << endl;

	return 0;
}

【利用SG函数,打表找规律】

对于很多ICG游戏,如果使用SG函数解决的话,时间复杂度会很高。

如果直接想结论很困难时,可以把所有的SG值从小到大打印出来,通过找规律,进而猜出游戏的结论。

【打表代码实现】

cpp 复制代码
#include <iostream>
#include <unordered_set>
using namespace std;

int a[] = {1, 2, 3, 4, 5, 7, 8, 9, 11, 13, 16, 17, 19};

int sg(int x)
{
	unordered_set<int> mp;
	
	for(auto y : a)
	{
		if(x - y < 0)
			break;
			
		mp.insert(sg(x - y));
	}
	
	for(int i = 0; ; i++)
	{
		if(!mp.count(i))
			return i;
	}
}

int main()
{
	for(int i = 0; i <= 20; i++)
	{
		cout << i << " -> " << sg(i) << endl;
	}
	
	return 0;
}

【程序运行结果】

可以看到,SG函数的值的变化周期为6。因此,这道题就是n % 6 = 0的时候为必败态。

四、经典例题

【题目描述】

【算法原理】

【代码实现】

cpp 复制代码
#include <iostream>
#include <cstring>
#include <unordered_set>
using namespace std;

const int N = 15, M = 1010;

int n, m;
int a[N], b[N];
int f[M];

int sg(int x)
{
	if (f[x] != -1)
		return f[x];

	unordered_set<int> mp;
	for (int j = 1;j <= m && x - b[j] >= 0;j++)
		mp.insert(sg(x - b[j]));

	for (int i = 0;;i++)
	{
		if (!mp.count(i))
		{
			return f[x] = i;
		}
	}
}

int main()
{
	cin >> n;
	for (int i = 1;i <= n;i++)
		cin >> a[i];

	cin >> m;
	for (int i = 1;i <= m;i++)
		cin >> b[i];

	memset(f, -1, sizeof f);

	int s = 0;
	for (int i = 1;i <= n;i++)
		s ^= sg(a[i]);

	if (s == 0)
		cout << "NO" << endl;
	else
	{
		cout << "YES" << endl;
		for (int i = 1;i <= n;i++)
		{
			for (int j = 1;j <= m && a[i] - b[j] >= 0;j++)
			{
				if ((s ^ sg(a[i] - b[j]) ^ sg(a[i])) == 0)
				{
					cout << i << " " << b[j] << endl;
					return 0;
				}
			}
		}
	}

	return 0;
}

【题目描述】

【算法原理】

这道题并不是公平组合游戏,而是一个反常游戏(胜者为第一个无法行动的玩家),因此不能直接按照有向图游戏来解决。我们可以先找出必败态,然后把必败态当成有向图的终点,那么反常游戏就变成了公平组合游戏。

如果状态为"不得不切出边长为1的长条",那么就是必败态。(比如2×2,2×3,3×2,3×3的矩形)

【代码实现】

cpp 复制代码
#include <iostream>
#include <cstring>
#include <unordered_set>
using namespace std;

const int N = 210;

int n, m;
int f[N][N];

int sg(int n, int m)
{
	if (f[n][m] != -1)
		return f[n][m];

	unordered_set<int> mp;
	for (int i = 2;i <= n - 2;i++)
		mp.insert(sg(i, m) ^ sg(n - i, m));
	for (int j = 2;j <= m - 2;j++)
		mp.insert(sg(n, j) ^ sg(n, m - j));

	for (int i = 0;;i++)
	{
		if (!mp.count(i))
			return f[n][m] = f[m][n] = i;
	}
}

int main()
{
	memset(f, -1, sizeof f);
	while (cin >> n >> m)
	{
		int ret = sg(n, m);
		if (ret)
			cout << "WIN" << endl;
		else
			cout << "LOSE" << endl;
	}

	return 0;
}
相关推荐
xvhao20133 小时前
P4084 [USACO17DEC] Barn Painting G 题解
数据结构·c++·算法·深度优先·动态规划
云栖梦泽3 小时前
Linux内核与驱动:5.并发与竞争
linux·c++
We་ct3 小时前
LeetCode 190. 颠倒二进制位:两种解法详解
前端·算法·leetcode·typescript
wangchunting3 小时前
算法-二分查找
java·数据结构·算法
月落归舟3 小时前
帮你从算法的角度来认识二叉树---(一)
数据结构·算法·二叉树
不想看见4043 小时前
在AI时代下,刷LeetCode题的价值与意义
开发语言·c++·qt
南境十里·墨染春水4 小时前
C++ 笔记 多重继承 菱形继承(面向对象)
开发语言·c++·笔记
龙文浩_4 小时前
AI深度学习演进之路:从机器学习到大模型的范式变革
人工智能·深度学习·神经网络·算法·回归·线性回归
LTphy4 小时前
P3131 [USACO16JAN] Subsequences Summing to Sevens S
算法·前缀和·蓝桥杯
cpp_25014 小时前
P1569 [USACO ?] Generic Cow Protests【来源请求】
数据结构·c++·算法·题解·洛谷·线性dp