组合游戏定义:
- 有两个玩家
- 游戏的操作状态是一个有限的集合(比如:限定大小的棋盘)
- 游戏双方轮流操作
- 双方的每次操作必须符合游戏规定
- 当一方不能将游戏继续进行的时候,游戏结束,同时对方为获胜方
- 无论如何操作,游戏总能在有限次操作后结束
必败点和必胜点(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
导引输赢表(部分)
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|
| P | N | N | N | P | N | N | N | P | N |
巴什博弈
两个顶尖聪明的人在玩游戏,有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张
导引改编输赢表(部分)
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| P | N | P | N | N | N | N | P | N | P | N | N | N | N | P | N |
组合博弈图类型
- 给定一个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)为三堆牌所剩余的牌数
Nim-sum
Nim-sum从高位往低位找的第一个的1,往上对应有几个1则有几个方案。
SG函数
组合游戏的并
Nim游戏改编【三个小游戏组成一个大游戏】
玩家:2人
道具:有3堆扑克牌(分别为5,7,9)【可任意】
规则:
1、游戏双方轮流取牌
2、玩家的每次操作是选择其中的一堆牌,然后从中取走1~3张牌
3、扑克牌取光,则游戏结束
4、最后取牌的一方为胜者
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