博弈论学习笔记

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

什么是博弈论#

博弈论,又称为对策论(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)表示最小的不属于这个集合的非负整数

例如:mex(1,2)=0,mex(0,1,2)=3,mex(4)=0

而SG函数的定义是

sg(x)=mex(sg(y)) 其中,y是x的后继

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

基本性质#

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

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

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

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

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

4.再探巴什博奕#

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

不妨设当前m=5

即,sg(0)=0

那么 sg(1)=mex(sg(0))=1

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

sg(2)=mex(sg(0),sg(1))=2

sg(3)=mex(sg(0),sg(1),sg(0))=3

sg(4)=mex(sg(0),sg(1),sg(2),sg(3))=4

sg(5)=mex(sg(0),sg(1),sg(2),sg(3),sg(4))=5

sg(6)=mex(sg(1),sg(2),sg(3),sg(4),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。(参考大佬题解)

对于输出字典序最小的一组解,因为
2n21所以直接O(n3)遍历即可

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(n2)的直接处理即可,

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

再看看数据范围 <100000

wow!好开心啊

正经思路#

分析题意,我们可以发现分成的 mm 堆的石子数最多有两种取值,分别是: i/m , (i/m)+1。这两种取值的个数分别是: i%m , mi%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;
}
	

作者:JiaY19

出处:https://www.cnblogs.com/JiaY19/p/15549808.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   JiaY19  阅读(71)  评论(2编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示