Loading

博弈论学习笔记

蒟蒻最近学习了一下博弈论,想着就发了一篇学习笔记。

什么是博弈论

博弈论,又称为对策论(Game Theory)、赛局理论等,既是现代数学的一个新分支,也是运筹学的一个重要学科。
(百度百科)

当然,以上说的十分正确,但在OI里没什么用。

简单来说,就是两个人玩一个小游戏的过程。

此类问题经常让我们求必胜决策或者必败决策。

几个性质

1所有的终止位置都是必败点P 我们认为这个是公理,即所有推导都在这个性质成立的基础上进行。

2 从任何一个必胜点N 操作,至少有一种方法可以达到一个
必败点P。

3 从一个必败点P 出发,只能够到达必胜点N。

举几个栗子

1.洛谷唯一一道入门博弈论

题目传送门

通过这一道题,大家初步了解一下什么是博弈论,这里就不讲了

#include<bits/stdc++.h>
using namespace std;
int t , n;

int read()
{
	long long X = 0 , w = 1;
	char c = getchar();
	while (c < '0' || c > '9')
	{
		if (c == '-')
		w = -1;
		c = getchar();
	}
	while (c >= '0' && c <= '9')
		X = X * 10 + c - '0' , c = getchar();
	return X * w;
}

int main()
{
	t = read();
	while(t--)
	{
		n = read();
		if(n % 2 == 0)
			cout << "pb wins" << endl;
		else
			cout << "zs wins" << endl;
	}
	return 0;
}
	

2. 巴什博奕Bash Game(你可以认为取石子游戏)

有一堆石子,总个数是n,两名玩家轮流在石子堆中
拿石子,每次至少取1 个,至多取m 个。取走最后一个石子的
玩家为胜者。判定先手和后手谁胜

博弈解题思路:

假设(m + 1)|n,那么假设先手拿走了x 个,那么后手必定可以拿走(m + 1) − x 个,这样子无论怎么拿,剩下的石头个数都将是m + 1 的倍数。

那么最后一次取的时候石头个数必定还剩下m + 1 个,无论先手拿多少个,都会剩下石头,此时后手必定可以将剩下的所有石头取光从而获胜。

否则的话,先手可以取走模m + 1 余数个数个石头,此时模型转换为了先手面对(m + 1)|n 个石头的情况,也就是后手必败,即先手必胜。(参考奆佬yyb的课件)

答案:

如果\((m + 1)|n\)则先手必败,否则先手必胜。

是不是感觉有一点复杂。

那么,就可以引出我们的第二个知识:

3. SG函数

基本定义

先讲一下SG函数中十分重要的mex函数

mex(a)表示最小的不属于这个集合的非负整数

例如:\(\operatorname{mex}({1,2})=0,\operatorname{mex}({0,1,2})=3,\operatorname{mex}({4})=0\)

而SG函数的定义是

\(\operatorname{sg}(x)=\operatorname{mex}(\operatorname{sg}(y))\) 其中,y是x的后继

也就是说,一个点的SG函数为在它所有后继中未出现的最小的值。

基本性质

  1. 对于已经没有出边的一个点(即出度为0),它的SG函数值为0.

  2. 对于一个有后继的点,若它的后继的SG值都不为0,则当前点的SG值为0。

  3. 对于一个有后继的点,若它的SG值不为0,则当前点的后继的SG值必有一个为0。

有没有发现,这个性质与我们最开始讲的性质很像。

是的,顶点x所代表局面是必败局面当且仅当\(\operatorname{sg}(x)=0\)

4.再探巴什博奕

由题我们可以知道,当场面上石子数量为0时,先手必败。

不妨设当前\(m=5\)

即,\(\operatorname{sg}(0)=0\)

那么 \(\operatorname{sg}(1) = \operatorname{mex}(\operatorname{sg}(0))=1\)

方便理解,我们多举几个例子

\(\operatorname{sg}(2) = \operatorname{mex}(\operatorname{sg}(0) , \operatorname{sg}(1))=2\)

\(\operatorname{sg}(3) = \operatorname{mex}(\operatorname{sg}(0) , \operatorname{sg}(1) , \operatorname{sg}(0))=3\)

\(\operatorname{sg}(4) = \operatorname{mex}(\operatorname{sg}(0) , \operatorname{sg}(1) , \operatorname{sg}(2) , \operatorname{sg}(3))=4\)

\(\operatorname{sg}(5) = \operatorname{mex}(\operatorname{sg}(0) , \operatorname{sg}(1) , \operatorname{sg}(2) , \operatorname{sg}(3) , \operatorname{sg}(4))=5\)

\(\operatorname{sg}(6) = \operatorname{mex}(\operatorname{sg}(1) , \operatorname{sg}(2) , \operatorname{sg}(3) , \operatorname{sg}(4) , \operatorname{sg}(5))=0\)

发现规律了吗?

同样,我们可以发现答案:

如果\((m + 1)|n\)则先手必败,否则先手必胜。

因此,用SG函数打表找规律也是不错的解法

5. 关于打表题

这里给一道SG函数打表找规律的题

P4860 Roy&October之取石子II

各位可以自己去尝试一下,多用一用SG函数

这里就直接给AC代码

#include<bits/stdc++.h>
using namespace std;
int t , n;
int read()
{
	long long X = 0 , w = 1;
	char c = getchar();
	while (c < '0' || c > '9')
	{
		if (c == '-')
		w = -1;
		c = getchar();
	}
	while (c >= '0' && c <= '9')
		X = X * 10 + c - '0' , c = getchar();
	return X * w;
}


int main()
{
	t = read();
	while(t--)
	{
		n = read();
		puts(n % 4?"October wins!" : "Roy wins!");
	}
	return 0;
}

6.尼姆博弈Nim Game(这里只讲带SG函数的做法)

题目传送门

大致题意:

地上有 n 堆石子,每人每次可从任意一堆石子里取出任意多枚石子扔掉,可以取完,不能不取。每次只能从一堆里取。最后没石子可取的人就输了。询问是否存在先手必胜的策略

分析:

我们可以把这道题分开来看,

因为一个显而易见的道理:若有奇数个必胜的局面,那么此盘游戏就是必胜。

所以我们最后仅要把每堆石子的SG值的异或和。

考虑分析每堆石子的SG值,

到了这一步就与我们上面讲的那道题很像,道理就不过多赘述了

直接上代码

#include<bits/stdc++.h>
using namespace std;
int vis[100010] , pw[100010] , sg[100010];

int read()
{
	long long X = 0 , w = 1;
	char c = getchar();
	while (c < '0' || c > '9')
	{
		if (c == '-')
		w = -1;
		c = getchar();
	}
	while (c >= '0' && c <= '9')
		X = X * 10 + c - '0' , c = getchar();
	return X * w;
}

void getsg()
{
	for(int i = 1;i <= 10000;++i)
	{
		for(int j = 0;pw[j] <= i;++j) vis[sg[i - pw[j]]] = true;  //标记出后继
		for(int j = 0;;++j) if(!vis[j]){sg[i] = j;break;}         //求出SG值
	}
}

int main()
{
    for(int i = 1;i <= 10001;i++) pw[i] = i;  //获得步数
    getsg();                                  //求SG值
    int t = read();
    while(t--)
    {
    	int n = read();
    	int ans = 0;
    	for(int i = 1;i <= n;i++)
    	{
    		int a = read();
    		ans ^= sg[a];                 //异或得答案
		}
    	if(ans)
    		cout << "Yes" << endl;
    	else
    		cout << "No" << endl;
	}
	return 0;
}

这里各位可以感性理解一下。

另外,还有大佬告诉我无需SG函数做法,这里也贴上来

#include<bits/stdc++.h>
using namespace std;

int read()
{
	long long X = 0 , w = 1;
	char c = getchar();
	while (c < '0' || c > '9')
	{
		if (c == '-')
		w = -1;
		c = getchar();
	}
	while (c >= '0' && c <= '9')
		X = X * 10 + c - '0' , c = getchar();
	return X * w;
}

int main()
{
    int t = read();
    while(t--)
    {
    	int n = read();
    	int ans = 0;
    	for(int i = 1;i <= n;i++)
    	{
    		int a = read();
    		ans ^= a;
		}
    	if(ans)
    		cout << "Yes" << endl;
    	else
    		cout << "No" << endl;
	}
	return 0;
}

7.小约翰的游戏(反nim游戏)

题目传送门

题意各位随便看看

这里参考了题解区第一位大佬的题解(手动点赞)

  1. :只有一堆石子,且石子数量为一,那么后手胜利

  2. :每一堆都是1,那么只需要判断奇偶性,奇数则先手败,偶数则后手败

  3. :只有一堆不是1,其余堆都是1,那么可以根据就行,先手可以选择是拿完或是那得只剩一个

  4. :一般情况,思考怎么转化成Case1-3

这里直接打上无需SG函数的做法(带了会玄学RE)

代码附上

#include<bits/stdc++.h>
using namespace std;

int read()
{
	long long X = 0 , w = 1;
	char c = getchar();
	while (c < '0' || c > '9')
	{
		if (c == '-')
		w = -1;
		c = getchar();
	}
	while (c >= '0' && c <= '9')
		X = X * 10 + c - '0' , c = getchar();
	return X * w;
}

int main()
{
    int t = read();
    while(t--)
    {
    	int n = read();
    	int ans = 0 , flag = 1;
    	for(int i = 1;i <= n;i++)
    	{
    		int a = read();
    		if(a != 1) flag = 0;
    		ans ^= a;
		}
		if(flag)
		{
			if(n & 1)
				cout << "Brother" << endl;
    		else
    			cout << "John" << endl;
		}
		else
		{
			if(ans)
    			cout << "John" << endl;
    		else
    			cout << "Brother" << endl;
		}
    	
	}
	return 0;
}

双倍经验

8.威佐夫博弈

题目传送门

大致题意:

有两堆石子,数量任意,可以不同。每次有两种不同的取法,一是可以在任意的一堆中取走任意多的石子;二是可以在两堆中同时取走相同数量的石子。最后把石子全部取完者为胜者,询问是否先手必胜。

至于思路的话,个人感觉十分奇妙(像我绝对想不到),可以参考题解区第一位大佬的题解,这里就不赘述了

代码还是要给的

#include<bits/stdc++.h>
using namespace std;
const double l = (sqrt(5.0) + 1.0) / 2.0;
int n, m;

int read()
{
	long long X = 0 , w = 1;
	char c = getchar();
	while (c < '0' || c > '9')
	{
		if (c == '-')
		w = -1;
		c = getchar();
	}
	while (c >= '0' && c <= '9')
		X = X * 10 + c - '0' , c = getchar();
	return X * w;
}

int main() 
{
    n = read() , m = read();
    if(n < m) swap(n, m);
    int ans = int(l * (double)(n - m));
    if(m == ans)
        cout << 0 << endl;
    else 
        cout << 1 << endl;
}

9.P3185 [HNOI2007]分裂游戏(Multi-SG)

题目传送门

蒟蒻决定不水字数了,好好讲几道题。

来看这道题,蒟蒻想了一个多小时,最后还去请同机房的大佬解答才做了出来。

这里就不讲题意了,各位可以自己去看看。

思路

与前几题相同,我们先来思考SG函数。

对于这道题的一个难点来说,

我们需要感性理解一下,以一粒豆子作为子游戏。

也就相当于一个取石子游戏的过程。

这里,蒟蒻也不是特别的理解,写的不好,勿喷

如果理解了这一点以后

不难看出,当前i节点的后继,应该是j , k

那么,一个板子就诞生了

void getsg()
{
	for(int i = n - 1;i;i--)
	{
		memset(vis , 0 , sizeof(vis)); //初始化
		for(int j = i + 1;j <= n;j++)
		{
			for(int k = j;k <= n;k++)
			{
				vis[sg[j] ^ sg[k]] = 1; //确定后继
			}
		}
		for(int j = 0;;j++)
			if(vis[j] == 0) //mex操作
			{
				sg[i] = j;
				break;
			}
	}
}

当然,肯定有人要问了,为什么后继是 xor 操作呢

这里蒟蒻也不是很清楚,是在看题解的时候,每篇都这么写,若是有神犇知道,请私信发给蒟蒻。

接着,SG函数都出来了,主函数就十分流畅了

注意到,对于每个瓶子里的巧克力豆,是可以在模2的意义下去考虑的,因为后手可以模仿先手的操作,所以就将巧克力豆个数转化为了0或1。(参考大佬题解)

对于输出字典序最小的一组解,因为
\(2≤n≤21\)所以直接\(O(n^3)\)遍历即可

AC代码:

#include<bits/stdc++.h>
using namespace std;
int t , n , ans , cnt , flag , vis[100] , sg[100] , a[100];

int read()
{
	long long X = 0 , w = 1;
	char c = getchar();
	while (c < '0' || c > '9')
	{
		if (c == '-')
		w = -1;
		c = getchar();
	}
	while (c >= '0' && c <= '9')
		X = X * 10 + c - '0' , c = getchar();
	return X * w;
}

void getsg()
{
	for(int i = n - 1;i;i--)
	{
		memset(vis , 0 , sizeof(vis));
		for(int j = i + 1;j <= n;j++)
		{
			for(int k = j;k <= n;k++)
			{
				vis[sg[j] ^ sg[k]] = 1;
			}
		}
		for(int j = 0;;j++)
			if(vis[j] == 0)
			{
				sg[i] = j;
				break;
			}
	}
}

int main()
{
	t = read();
	while(t--)
	{
		n = read() , sg[n] = 0 , ans = 0 , cnt = 0 , flag = 0;
		for(int i= 1;i <= n;i++) a[i] = read();
		getsg();
		for(int i = 1;i <= n;i++)
			if(a[i] % 2) ans ^= sg[i];			
		for(int i = 1;i < n;i++)
		{
			if(!a[i]) continue;
			for(int j = i + 1;j <= n;j++)
			{
				for(int k = j;k <= n;k++)
				{
					if((ans ^ sg[i] ^ sg[j] ^ sg[k]) == 0)
					{
						cnt++;
						if(!flag)
						{
							flag = 1;
							cout << i - 1 << " " << j - 1 << " " << k - 1 << endl;
						}
					}
				}
			}
		}
		if(!flag) cout << "-1 -1 -1" << endl;
		cout << cnt << endl;
	}
	return 0;
}
	

10.P3235 [HNOI2014]江南乐(Multi-SG)

题目传送门

这是今天的最后一道题目,已经是蒟蒻的极限了,讲得可能不好,勿喷

可以看见这道题,题解区大多都是记忆化搜索的SG,既然我们讲了这么久的预处理,我们就继续这么做。

首先,看见这道题,就有了一个朴实的做法

一个\(O(n^2)\)的直接处理即可,

很遗憾,你将会拿到爆TLE的成绩

再看看数据范围 \(每堆石子数量<100000\)

wow!好开心啊

正经思路

分析题意,我们可以发现分成的 mm 堆的石子数最多有两种取值,分别是: \(i/m\) , \((i/m)+1\)。这两种取值的个数分别是: \(i\%m\) , \(m-i\%m\)

这里应该可以理解

又因为SG函数维护的是异或和

所以只有奇数的个数,才能有贡献

分析一下奇偶性

对于分析奇偶性,各位可以看看题解区第一位大佬的题解,个人认为十分详细,我因为不会就不赘述了

有始有终,上代码

#include<bits/stdc++.h>
using namespace std;
int t , f , n , sg[100010] , vis[100010];

int read()
{
	long long X = 0 , w = 1;
	char c = getchar();
	while (c < '0' || c > '9')
	{
		if (c == '-')
		w = -1;
		c = getchar();
	}
	while (c >= '0' && c <= '9')
		X = X * 10 + c - '0' , c = getchar();
	return X * w;
}

void getsg()
{
	for(int i = f;i <= 100000;i++)
	{
		int a;
		for(int j = 2;j <= i;j = a + 1)
		{
			int b = i / j , tot = 0, cnt = 0 , flag;
			a = i / (i / j);
                        if(j == a) flag = 1;
                        else flag = 2;
                        while(tot < flag) 
	                {
                		tot++;
                		cnt = 0;
                		if((i % j) & 1) 
               		   	     cnt ^= sg[b + 1];
                		if((j - i % j) & 1) 
                    	             cnt ^= sg[b];
                		j++;
                		vis[cnt] = i;
        		}
		}
		for(int j = 0; ; j++) 
            		if(vis[j] != i) 
			{ 
          		        sg[i] = j; 
				break;
            		}
	}
}

int main()
{
	t = read() , f = read();
	getsg();
	while(t--)
	{
		n = read();
		int ans = 0;
		for(int i = 1;i <= n;i++)
		{
			int a = read();
			ans ^= sg[a];
		}
		if(ans == 0) cout << 0 << " ";
		else cout << 1 << " ";
	}
	return 0;
}
	
posted @ 2021-11-13 21:16  JiaY19  阅读(67)  评论(2编辑  收藏  举报