算法基础之数学知识——公平组合游戏ICG

概念:

公平组合游戏ICG
若一个游戏满足:

1.由两名玩家交替行动;

2.在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;

3.不能行动的玩家判负;

则称该游戏为一个公平组合游戏。
NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。

介绍:

1.Mex运算

设S表示一个非负整数集合。定义mex(S)为求出不属于集合S的最小非负整数的运算,即:

mex(S) = min{x}, x属于自然数,且x不属于S

2.SG函数

在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点y1, y2, …, yk,定义SG(x)为x的后继节点y1, y2, …, yk 的SG函数值构成的集合再执行mex(S)运算的结果,即:

SG(x) = mex({SG(y1), SG(y2), …, SG(yk)})
特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点s的SG函数值,即SG(G) = SG(s)。

经典Nim游戏也是公平组合游戏,每一堆石子算一个有向图,这个有向图的起点就是每一堆石子初始的数量,假设一堆石子数量为3个,那么说明这个有向图起点就是3,又因为经典Nim游戏中可以拿1——3(这一堆石子所有的数量),所以我们画出这个有向图为下图:
在这里插入图片描述
图中的点3就是这个有向图的SG值。 因为点3可以拿1,2,3个石子,所以3号点可以连接到0,1,2这三个点,然后2号点可以拿1,2两个数量的石子,所以2号点可以连接0,1这两个点。然后计算出3号点的SG函数值为3,所以这个有向图的SG函数值为3
总的来说,有向图的建立就是当前状态能转移到哪种状态,那么它就可以连接那些状态的点。并且如果出边为0的话SG函数值就是0.

定理:

设G1, G2, …, Gm 是m个有向图游戏。定义有向图游戏G,它的行动规则是任选某个有向图游戏Gi,并在Gi上行动一步。G被称为有向图游戏G1, G2, …, Gm的和。
有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数值的异或和,即:

SG(G) = SG(G1) ^ SG(G2) ^ … ^ SG(Gm)

有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于0。

有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于0。

上面说了,经典Nim游戏是一个有向图游戏,并且可以得出每一堆石子对应的有向图的SG函数就是这堆石子的数量(因为可以任意拿这堆石子),所以有向图要想先手必赢的话就得让每一堆石子的数量异或和不为0。也就是所有有向图的SG函数异或和不为0。

例题:
给定 n 堆石子以及一个由 k 个不同正整数构成的数字集合 S。

现在有两位玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合 S,最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

输入格式
第一行包含整数 k,表示数字集合 S 中数字的个数。

第二行包含 k 个整数,其中第 i 个整数表示数字集合 S 中的第 i 个数 si。

第三行包含整数 n。

第四行包含 n 个整数,其中第 i 个整数表示第 i 堆石子的数量 hi。

输出格式
如果先手方必胜,则输出 Yes。

否则,输出 No。

数据范围
1≤n,k≤100,
1≤si,hi≤10000

输入样例:
2
2 5
3
2 4 7

输出样例:
Yes

思想:
由上面的说明我们可以得出这是一个公平组合游戏,所以我们可以将这个游戏变成一个有向图游戏。但是每一堆只能拿集合里面数量的游戏,所以我们按照要求做出有向图。例如某一堆有10个石子,集合里面的元素为2,5,那么我们就可以从这堆石子里面每次拿2个或者5个。有向图如下:
在这里插入图片描述
所以这个有向图的SG为0。每一个堆都按照这种方法得出SG,然后将之异或起来就可以得出结果。

代码如下:

#include<iostream>
#include<cstring>
#include<unordered_set>

using namespace std;

int k, n, res;
//f数组保存一下SG值,减少重复计算 ,a数组保存的是集合里面的元素
int f[10005], a[105];

int sg(int x)
{
	//如果已经求过x的sg值了,那么就会保存在f函数里面,这时候就可以直接返回了
    if(f[x] != -1)    return f[x];
    //unordered_set保存的是当前节点的后继节点的SG值,这样方便计算当前节点的SG值
    unordered_set<int> m;
    for(int i = 0; i < k; i ++){
    	//如果当前的石子数量大于集合中某些元素,那么就说明可以拿这些元素数量的石子。
    	//也就说明当前节点的后继节点就是x - a[i]
        if(x - a[i] >= 0){
        	//求得后继节点的SG值,并存储至set
        	//注意,此时当前节点的set不包括后继节点的set。
            m.insert(sg(x - a[i]));
        }
    }
    //计算当前节点的SG值
    for(int i = 0; ; i ++){
        if(!m.count(i)){
            return f[x] = i;
        }
    }
}

int main()
{
    cin >> k;
    //输入集合a,存储每一次可以拿多少石子
    for(int i = 0; i < k; i ++){
        cin >> a[i];
    }
    
    cin >> n;
    //将f数组初始化为-1
    memset(f, -1, sizeof f);
    for(int i = 1; i <= n; i ++){
        int x;
        cin >> x;
        //计算每一堆的sg函数并且将之异或起来
        res ^= sg(x);
    }
    if(res) cout << "Yes";
    else    cout << "No";
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值