博弈论的理论和题目

博弈论问题大致分为,公平组合游戏、非公平组合游戏(绝大多数的棋类游戏)、反常游戏

只需要关注公平组合游戏,反常游戏是公平组合游戏的变形。

以下为公平组合游戏的特征:

1.两个玩家轮流行动且游戏方式一致。

2.两个玩家对状况完全了解

3.游戏一定会在有限步数内分出胜负

4.游戏以玩家无法行动结束

巴什博弈

一共有n颗石头,两个人轮流拿,每次可以拿1~m颗石子,拿到最后一颗石子的人获胜,根据n,m,返回谁赢。

分析:

显然,如果 n = m + 1那么由于一次最多只能取 m 个物品,所以无论先取者拿走多少个,后取者都能够一次拿走剩余的物品,故后者必然取胜。根据这样的规律,我们发现了如何取胜的法则。

如果 n = ( m + 1 ) r + s(r为任意自然数,0 ≤ s ≤ m,那么先取者首先拿走 s个物品,接下来若后取者拿走 k ( 1 ≤ k ≤ m ) 个,那么先取者再拿走 m + 1 − k个,结果剩下 ( m + 1 ) × ( r − 1 )个,以后都保持这样的取法,那么后取者最终会面临 ( m + 1 )  的局面,而先取者则必然获胜。总之,要保持给对手留下 ( m + 1 )的倍数,最后就一定能获胜。

质数次方版取石子(巴什博弈拓展)

一共有n颗石子,两个人轮流拿

每一轮当前选手可以拿 p 的 k 次方颗石子

当前选手可以随意决定p和k,但是要保证p是质数,k是自然数

拿到最后一颗石子的人获胜。

根据石子的个数返回谁赢

如果先手赢,返回0

如果后手赢,返回1

分析:

任意6的整数倍,它一定不是某一个整数的自然数次方

因为6的整数倍,一定由2和3两个质数组成,那么必然无法由某个整数的自然数次方得到。

因此我们就可以很快的得出结论

如果n为6的倍数,那么后手赢。

如果n不为6的倍数,那么先手赢。

因为当 n 为 6 的整数倍时,无论先手的人拿多少,后手的人都可以使其重新变为6的整数倍(因为1~5都可以拿),因此后手是必赢的。

如果 n 不为 6 的倍数,那么先手的人就可以先把它变为6的倍数,这样先手的人必赢

尼姆博弈

一共有 n 堆石头,两人轮流进行游戏

在每个玩家的回合中,玩家需要选择任何一个非空的石头堆,并从这堆石头中移除任意正数的石头数量

谁先拿走最后的石头就获胜,返回最终谁会获胜

分析:

如果所有的石堆进行异或和,然后再对石堆进行操作。

最后输的人一定是面对异或和等于0的情况,也就是没有石头了。

因此当异或和不为0时,先手赢。当异或和为0时,后手赢

证明:

反尼姆博弈(反常游戏)

一共有n堆石头,两人轮流进行游戏

在每个玩家的回合中,玩家需要选择任何一个非空的石头堆,并从这堆石头中移除任意正数的石头数量

谁先拿走最后的石头就失败,返回最终谁会获胜。

分析:

因为最后的情况是异或和不为0,所以不能直接用尼姆博弈

进行分类讨论

情况一:

情况二:

情况三:

结论:

当石子的个数都为1时,偶数则先手赢,奇数则后手赢。

当遇到其他情况,如果异或和不为0,则先手赢,如果异或和为0,则后手赢

sg函数

图游戏的概念

任何局面都认为是图种的点,每一个局面都可以通过一种行动,走向图中的下一个点,如果当前行动有若干个,那么后继结点就有若干个。最终,必败局面的点认为不再有后继结点。那么公平组合游戏,就可以对应成一张图。

后继结点:该节点在一次操作内能走向的下一个点

sg函数

如下是sg函数返回值的求解方式,俗称mex过程

最终必败点是A,规定sg(A) = 0

假设状态点是B,那么sg(B) = 查看B所有后继结点的sg值,找出其中没有出现过的最小自然数

sg(B)!= 0,那么状态 B 为必胜态,

sg(B)== 0,那么状态 B 为必败态。

证明:

sg函数的定理

如果一个公平组合游戏(总),由若干个独立的公平组合游戏构成(分1,分2,分3·····)那么:

sg(总)= sg(分1)^sg(分2)^sg(分3)……

当数据规模较大时,可以通过打印sg表观察。

如果sg(总)= 0,那么先手就是必败局面。

如果sg(总)!= 0,那么先手就是必胜局面

证明:

sg打表的代码

void bash(int n, int m)
{
	int* sg = (int*)malloc(sizeof(int)*(n+1));
	int* appear = (int*)malloc(sizeof(int)*(1000)); //记录出现过的自然数 
	sg[0] = 0;
	for (int i=1; i<=n; ++i)
	{
		memset(appear, 0, sizeof(appear)); //初始化为0,表示都没出现过。
		for (int j=1; j<=m && i-j>=0; ++j)
		{
			appear[sg[i-j]] = 1; // 出现过就为1
		}
		for (int j=0; j<=i; ++i)
		{
			if (appear[j] == 0)
			{
				sg[i] = j;
				break;
			}
		}
	}
}

通过sg打表,如果能找到规律,那就可以用O(1)的过程就可以得到sg(x)。

如果找不到规律,那就只能暴力求解所有的sg值

考试时要根据题目的数据量来决定是否优化

E&D游戏

桌子上有2n堆石子,编号为1,2,3,……2n

其中1,2为一组;3,4为一组;……2n-1和2n为一组

每组可以进行分割操作:

任取一堆石头,将其移走。

从同一组的另一堆石头中取出若干个石子放在被移走的位置,组成新的一堆

操作完成后,组内每堆的石子数必须保证大于0

显然,被分割的一堆的石子数至少要为2

两个人轮流进行分割操作,如果轮到某人进行操作时,所有堆的石子数都为1,判定此人输掉比赛

返回先手能不能赢

分析:

总游戏是对所有的石头进行操作。

分游戏是对一组里的两个石头进行操作

那么本题的就可以计算分游戏的sg值

然后进行异或和求出先手赢还是后手赢。

计算sg[a][b];

注意:本题的数据量很大,一堆石子的个数在1~2*10^9之间,所以要打表找规律求出sg

代码:

# include <stdio.h> 
# include <string.h>
int dp[100][100];
int sg(int a, int b)  //求sg[a][b]的值 
{
	if (a == 1 && b == 1)
		return 0;
	if (dp[a][b] != -1) //如果之前出现过,动态规划 
		return dp[a][b];
		
	int appear[1000];
	memset(appear, 0, sizeof(appear));
	if (a > 1)
	{
		//假设a = 5时
		//拆为 1 4
		//2 3
		//3 2
		//4 1 
		for (int l=1, r=a-1; l<a; ++l, --r)
		{
			appear[sg(l, r)] = 1;
		}
	}
	if (b > 1)
	{
		for (int l=1, r=b-1; l<b; ++l, --r)
		{
			appear[sg(l, r)] = 1;
		}
	}
	int ans = 0;
	for (int i=0; i<1000; ++i)
		if (appear[i] == 0)
		{
			ans = i;
			break;
		}
	dp[a][b] = ans;
	return ans;
	
}

int main()
{
	memset(dp, -1, sizeof(dp));
	printf("石子数9以内所有组合的sg值\n");
	printf("  ");
	for (int i=1; i<=9; ++i)
	{
		printf(" %d", i);
	}
	printf("\n");
	printf("\n");
	for (int a=1; a<=9; ++a)
	{
		printf("%d  ",a);
		for (int b=1; b<a; ++b)
			printf("X ");
		for (int b=a; b<=9; ++b)
		{
			int g = sg(a, b);
			printf("%d ", g);
		}
		printf("\n");
	}
}

打印结果:

找不出规律,于是我们把行列都减一

于是我们可以发现一个规律

该状态的sg值 = ((行 - 1)| (列 - 1)后的值,最低位0的索引)

例如:

sg[2][5] =

(2 - 1) | (5 - 1)= 0001 | 0100 = 0101,最低位0的索引为1。

因此sg[2][5] = 1;(和最上面那个表格对应)

分裂游戏

本题给的数据规模不大,因此可以直接把sg全部暴力解出

但是本题的sg求法很好

注意:

最后结果一定是所有的糖都在最后一个瓶子里,这样才判断输赢

因为选择的三个编号的 i ,j,k为 i < j <= k

所有这就是一个不断把糖豆放到最左边的过程。

分析:

并且本题以一个瓶子为分游戏是不合理的。

因为我们会从一个瓶子里拿出一个糖豆,分裂成两颗糖豆,再放入别的瓶子,因此瓶子和瓶子之间不是独立的,因此不能以一个瓶子作为分游戏

因此我们要以一个糖豆作为分游戏

以把所有的糖豆放到最后的瓶子作为总游戏

以单独的一块糖,双方进行操作,判断输赢。

假设我们把处在编号最大的瓶子里的糖豆设为编号为0(也就是把所有瓶子的编号反一反,方便理解)

因为这个糖豆无法进行操作了,也就是没有后继结点了,所以sg(0) = 0;

注意:

我们在这里是把一颗糖豆作为分游戏,把它分裂成两个糖豆后,相当于这两个糖豆是作为后继结点的分游戏,因此要进行异或求出后继结点。

后继结点的概念:该节点在一次操作内能走向的下一个点

代码:

# include <stdio.h>

//这里我们把编号0的瓶子放在最右边
//糖果还是同样的最终全部会放在编号为0的瓶子里 
int sg[21]; //假设有20个瓶子 ,说明有20种可能的糖果 
int appear[1000];
sg[0] = 0;
int num[100]; //记录每个瓶子里的糖果 
void build() //此函数直接把0~20瓶里糖豆的sg值全部求出来,因为相同瓶里的sg值相同 
{
	for (int i=1; i<=20; ++i)
	{
		memset(appear, -1, sizeof(appear));
		// i > j >= k 
		for (int j=0; j<i; ++j)
			for (int k=0; k<=j; ++k)
				appear[sg[j] ^ sg[k]] = 1;
		
		for (int j=0; j<1000; ++j)
		{
			if (appear[j] == 0)
			{
				sg[i] = j;
				break;
			}
		}
	}
} 


int main()
{
	n = 20;
	for (int i=0; i<n; ++i)
	{
		scanf("%d", &sum[i]);
	} 
	
	build();
	int eor = 0; //所有糖果,每个糖果都是独立游戏,所以把所有糖果的sg值异或起来 
	for (int i=0; i<n; ++i)
		if (num[i] % 2 == 1) //因为是对整体进行异或,并且处于同一个瓶子里的糖果sg值一样
							// 异或后就会为0,因此我们只计算个数为奇数个的糖果sg值 
			eor = eor ^ sg[i];
			
	if  (eor == 0)
	{
		printf("-1 -1 -1\n");
		printf("0");//零种方法 
	}
	else
	{
		int cnt = 0; //记录先手必胜的方法数 
		int a = -1, b = -1, c = -1; //讨论第一步该怎么行动 
		//要想先手第一步就获胜,那么就是要让sg(总)变为0,这样就是先手必胜的方法。
		//然后再记录第一步最小的字典序
		int pos; //记录总的sg值 
		for (int i=n-1; i>=1; --i)
		{
			if (num[i] > 0)
			{
				for (int j = i-1; j>=0; --j)
					for (int k = j; k>=0; --k)
					{
						pos = eor ^ sg[i] ^ sg[j] ^ sg[k];//相当于进行第一步之后的sg(总) 
						if (pos == 0)
							cnt = cnt + 1;
						if (a == -1)
						{
							a = i;
							b = j;
							c = k;
						}
					}
			}
		}
		  
	}
	
}

  • 28
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值