ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

博弈论的理论和题目

博弈论的理论和题目

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

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

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

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;}}}}}}

返回列表