【博弈论】组合游戏及SG函数浅析
目录
预备知识
普通的Nim游戏
SG函数
预备知识
公平组合游戏(ICG)
若一个游戏满足:
- 由两名玩家交替行动;
- 游戏中任意时刻,合法操作集合只取决于这个局面本身;
- 若轮到某位选手时,若该选手无合法操作,则这名选手判负;
则称该游戏为一个公平组合游戏。
Nim游戏
有若干堆石子,每堆石子的数量都是有限的,合法的移动是“选择一堆石子并拿走若干颗(不能不拿)”,如果轮到某个人时所有的石子堆都已经被拿空了,则判负(因为他此刻没有任何合法的移动)。
mex(minimal exdudant)函数
设 表示一个非负整数集合。定义 为求出不属于集合 的最小非负整数的运算。
如:{ } 对应的 就是 。
SG函数简介
定义 ,其中 指 的后继状态对应的 的集合。
在 函数板块对应的模板题中(见下), 代表着该堆石子的数量。
普通的Nim游戏
题目传送门:https://www.acwing.com/problem/content/893/
题面:给定n堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
分析
这题的结论十分地简洁,就是:
若 ,则先手必负,否则先手必胜。
证明:
我们记 为数列a的异或和,以下简记为异或和。
先给出两条引理:
-
时,必可以从一堆石子中拿走若干个石子,使得异或和为 。
证明: 的最高位(记为第 位)是 , 中必然存在 满足 第 位是 ,那么我们将 变为 (因为 ,所以这样操作一定合法),那么变换后的异或和即为 。 -
时,不存在合法操作,使得异或和仍为 。
证明:假设将 变为 后异或和为
即 ,我们将这个式子与上式 联立,即得 ,意味着 ,即 不变,不是合法操作,故矛盾。
证明完引理后就不难了:
若轮到先手时,异或和为 ,那么无论先手如何行动,后手都可以进行操作,使再次轮到先手时异或和仍为 ,而游戏结束时异或和必然为 ,故先手必败。
反之(即若轮到先手时,异或和不为 )后手必败。
代码:
#include<bits/stdc++.h>
using namespace std;
int main(){
int n;
cin>>n;
int res=0;
for(int i=1;i<=n;i++){
int k; cin>>k;
res^=k;
}
if(res) puts("Yes");
else puts("No");
return 0;
}
SG函数
利用一道模板题引入:
题目传送门:https://www.acwing.com/problem/content/description/895/
题面:
给定 堆石子以及一个由 个不同正整数构成的数字集合 。
现在有两位玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合 ,最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
分析
先从一堆石子分析开始:
例如:该堆石子有 个,每次可取 或 个,求 。
我们可以画出一棵树,代表着两人的决策树。
注意到
根据 函数的定义,对于决策树上的点对应 函数值为:
除了上面的例子外,
我们还可以自己构造一棵 函数值构成的树:
从中我们可以直观地看出 的两个重要性质:
- 非 结点可以到 结点
- 结点一定不可以到非 结点
根据 函数的性质以及游戏规则, 时意味着相应的玩家必负。
分析多堆石子的情况:
我们规定,对于每堆石子 ,对应的 ,其中 是该堆石子最初的数量。
结合这棵树:
从 函数可以看出,当先手进行决策后,对应的的 函数值可以为 (根据 函数,虽然可能会有函数值 的结点,但是如果先手选择转移到这样的结点,那么后手可以紧接着转移到函数值 的结点),因此这恰好就像我们最初讨论的普通的Nim问题中取石子的规则!
在这里,我们将 函数值看成是普通的Nim问题中石子的数量就可以用相同的方法解决了。
求 函数的办法
我采取的是记忆化搜索的办法,见下:
int f[M]; // SG函数的值
int s[N]; // 可以取多少石子
int sg(int x){
if(f[x]!=-1) return f[x]; // 当已经更新过就直接返回。
unordered_set<int> S;
for(int i=1;i<=k;i++)
if(x-s[i]>=0) S.insert(sg(x-s[i]));
for(int i=0;;i++)
if(!S.count(i)) return f[x]=i;
}
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=105 ,M=1e4+5;
int n,k;
int f[M]; // SG函数的值
int s[N]; // 可以取多少石子
int sg(int x){
if(f[x]!=-1) return f[x]; // 当已经更新过就直接返回。
unordered_set<int> S;
for(int i=1;i<=k;i++)
if(x-s[i]>=0) S.insert(sg(x-s[i]));
for(int i=0;;i++)
if(!S.count(i)) return f[x]=i;
}
int main(){
memset(f,-1,sizeof f); // init
cin>>k;
for(int i=1;i<=k;i++) cin>>s[i];
cin>>n;
int res=0;
for(int i=1;i<=n;i++){
int t; cin>>t;
res^=sg(t);
}
if(res) puts("Yes");
else puts("No");
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探