【Learn】组合博弈入门

681 阅读6分钟

组合游戏定义:

  • 有两个玩家
  • 游戏的操作状态是一个有限的集合(比如:限定大小的棋盘)
  • 游戏双方轮流操作
  • 双方的每次操作必须符合游戏规定
  • 当一方不能将游戏继续进行的时候,游戏结束,同时对方为获胜方
  • 无论如何操作,游戏总能在有限次操作后结束

必败点和必胜点(P点&N点)

  • 必败点(P点):前一个选手(Previous player)将取胜的位置称为必败点
  • 必胜点(N点):下一个选手(Next player)将取胜的位置称为必胜点

必败(必胜)点属性

  • 所有终结点(即结束游戏的点)是必败点(P点)
  • 从任何必胜点(N点)操作,至少有一种方法可以进入必败点(P点)
  • 无论如何操作,从必败点(P点)都只能进入必胜点(N点)

导引题目

玩家:2人
道具:23张扑克牌
规则:
1、游戏双方轮流取牌
2、每人每次仅限于取1、2、3张牌
3、扑克牌取光,则游戏结束
4、最后取牌的一方为胜者

取子游戏算法实现

  • 将所有终结位置标记为必败点(P点)
  • 将所有一步操作能进入必败点(P点)的位置标记为必胜点(N点)
  • 如果从某个点开始的所有一步操作都只能进入必胜点(N点),则将该点标记为必败点(P点)
  • 如果在步骤三未能找到新的必败点(P点),则算法终止;否则,返回到步骤2

导引输赢表(部分)

0123456789
PNNNPNNNPN

巴什博弈

两个顶尖聪明的人在玩游戏,有n个石子,每人可以随便拿1−m个石子,不能拿的人为败者,问谁会胜利?

我们从最简单的情景开始分析
当石子有1−m1−m个时,毫无疑问,先手必胜
当石子有m+1m+1个时,先手无论拿几个,后手都可以拿干净,先手必败 当石子有m+2−2mm+2−2m时,先手可以拿走几个,剩下m+1m+1个,先手必胜
我们不难发现,面临m+1m+1个石子的人一定失败。
这样的话两个人的最优策略一定是通过拿走石子,使得对方拿石子时还有m+1m+1个
我们考虑往一般情况推广

  • 设当前的石子数为n=k∗(m+1)+rn=k∗(m+1)+r 先手会首先拿走rr个,接下来假设后手拿走xx个,先手会拿走m+1−xm+1−x个,这样博弈下去后手最终一定失败
  • 设当前的石子数为n=k∗(m+1)n=k∗(m+1) 假设先手拿xx个,后手一定会拿m+1−xm+1−x个,这样下去先手一定失败

代码实现

#include<cstdio>
int main()
{
	int n,m;
	scanf("%d%d",&n,&m);
	if(n % (m+1) !=0) printf("first win");//先手赢
	else printf("second win");//后手赢
    return  0;
}

导引游戏改编

其他不变,将可取的牌数变为1、3、4张

导引改编输赢表(部分)

0123456789101112131415
PNPNNNNPNPNNNNPN

组合博弈图类型

image.png

  • 给定一个N*M的矩阵,起点在右上角。棋子只能向左走、向下走、向左下走。二人轮流走一次,如果轮到某一个人,该棋子的位置使得他无路可走,那么他就输了。

此时可以通过找规律找出来,当n=4,m=6时,从下往上的输赢表为:PNPNPN、NNNNNN的交替。所以可知,当n和m都为奇数时,棋子所在的位置为必胜点,否则都必败点。

威佐夫博奕

有两堆各若干个物品,两个人轮流从某一堆或同时从两堆中取同样多的物品,规定每次至少取一个,多者不限,最后取光者得胜。

结论

必败点(P):(ai,bi)
ai=[i*(1+√5)/2](方括表示下取整),bi=ai+i
其余均为必胜点(N)

代码实现

#include<cstdio>
#include<algorithm>
#include<cmath>
#define int long long 
using namespace std;
main()
{
    int a,b;
    scanf("%lld%lld",&a,&b);
    if(a>b) swap(a,b);
    int temp=abs(a-b);
    int ans=temp*(1.0+sqrt(5.0))/2.0;
    if(ans==a) printf("0");
    else 	   printf("1");
    return 0;
}

Nim游戏

玩家:2人
道具:有3堆扑克牌(分别为5,7,9)
规则:
1、游戏双方轮流取牌
2、玩家的每次操作是选择其中的一堆牌,然后从中取走任意张牌
3、扑克牌取光,则游戏结束
4、最后取牌的一方为胜者

可知:(x,y,z)为三堆牌所剩余的牌数

image.png

Nim-sum

image.png

image.png

image.png Nim-sum从高位往低位找的第一个的1,往上对应有几个1则有几个方案。

SG函数

image.png

image.png

组合游戏的并

Nim游戏改编【三个小游戏组成一个大游戏】

玩家:2人
道具:有3堆扑克牌(分别为5,7,9)【可任意】
规则:
1、游戏双方轮流取牌
2、玩家的每次操作是选择其中的一堆牌,然后从中取走1~3张牌
3、扑克牌取光,则游戏结束
4、最后取牌的一方为胜者

image.png

g(5,7,9)=??

由于取的是1-3张牌,输赢表为PNNNPNNN以此循环,四个为一组,所以此时g(x)=x%4;

所以:g(5)=1;g(7)=3;g(9)=1; 再将三者分别按位异或。
001
011
001
异或----------
011=>3=g(5,7,9)

Nim游戏改编的改编

玩家:2人
道具:有3堆扑克牌(分别为5,7,9)【可任意】
规则:
1、游戏双方轮流取牌
2、玩家的每次操作是选择其中的一堆牌,对于第一堆牌每次只能取出1-2张牌,对于第二堆牌每次只能取出1-3张牌,对于第三堆牌每次只能取出2-3张牌
3、扑克牌取光,则游戏结束
4、最后取牌的一方为胜者

做法相同,一样都是求出每个小游戏的sg值,然后再按位异或求出大游戏的sg值。

例题

输入一个k,代表规则的个数,紧随其后输入k的整数,分别代表每次取牌的规则(例如规则为2 5,则每次取牌只能取2张或者5张)。
在下一行,输入一个整数t,代表情况数量,在接下来的n行中,每行输入一个整数n代表牌堆数量,和n个整数,分别代表每个牌堆所拥有的牌数。
当输入k为0的时候结束输入。
输出先手的人是输还是赢,如果赢则输出“W”否则输出“L”。

样例输入

2 2 5
3
2 5 12
3 2 4 7
4 2 3 7 12
5 1 2 3 4 5
3
2 5 12
3 2 4 7
4 2 3 7 12
0

样例输出

LWW
WWL

AC代码

//记忆化DFS求SG值
#include <bits/stdc++.h>
using namespace std;
int k,a[100],f[10001];
//参考sg函数定义,可更好理解
int sg(int p)
{
    int i,t;
    bool g[101]={0};//标记数组
    for(int i=0;i<k;i++)//枚举所有规则
    {
        t=p-a[i];//跳转状态,即后继状态
        if(t<0)//小于0代表不合法
            break;//如果main函数不写sort函数,可将此处改为continue
        if(f[t]==-1)//如果后继状态还没算出来,那我就算出来,放到f[t]中
            f[t]=sg(t);
        g[f[t]]=1;//标记一下算出来的状态
    }
    for(int i=0;;i++)//从小往大找,遍历g[i],找一个g[i]未记录的最小的非负整数
    {
        if(!g[i])//如果没被记录
            return i;//返回i
    }
}

int main()
{
    int n,i,m,t,s;
    while(scanf("%d",&k),k)//规则数量
    {
        for(int i=0;i<k;i++)
            scanf("%d",&a[i]);//将规则读到a数组里
        sort(a,a+k);//对规则a进行排序,升序
        memset(f,-1,sizeof(f));//将f初始化
        f[0]=0;
        scanf("%d",&n);//情况数量
        while(n--)
        {
            scanf("%d",&m);//牌堆数量
            s=0;
            while(m--)
            {
                scanf("%d",&t);//每个牌堆的卡牌数量
                if(f[t]==-1)//如果f[t]还没求过【记忆化:可提高效率】
                    f[t]=sg(t);//f[t]保存的是t这个状态的sg值
                s=s^f[t];//将每个小游戏的sg值进行按位异或,逐渐得到大游戏的sg值
            }
            if(s==0)//由之前定理可知,如果sg值为0,则先手必败
                printf("L");
            else//否则,先手必胜
                printf("W");
        }
        printf("\n");
    }
    return 0;
}

博弈总结 - 自为风月马前卒 - 博客园 (cnblogs.com)
博弈论学习笔记 - One_Zzz の 小窝qwq - 洛谷博客 (luogu.com.cn) //2022.5.9