ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

[学习笔记] 公平组合博弈全家桶:从 SG 本质到经典模型

2026/8/9 23:21:12 拓冰建站 浏览量
[学习笔记] 公平组合博弈全家桶:从 SG 本质到经典模型

[学习笔记] 公平组合博弈全家桶:从 SG 本质到经典模型

TAG: 博弈论, 公平组合博弈, SG函数, Sprague-Grundy定理, ACM

本文默认读者已经掌握公平组合博弈(Impartial Game)的基本概念、Nim 的异或结论以及 SG 函数的定义。本文不从零介绍,而更关注 SG 为什么成立、规则变化后应该看什么、常见模型如何快速识别,以及这些结论在竞赛题里怎样落地

记号约定:若无特殊说明,均为有限、无随机、Normal Play 的无偏博弈;P-position 表示先手必败态,N-position 表示先手必胜态。


目录

  • 题目总索引
  • Part 0. SG 函数的本质
    • 0.0 常见 SG 打表模板
    • 0.1 SG 值到底是什么
    • 0.2 为什么是 mex
    • 0.3 游戏和为什么做 xor
    • 0.4 什么规则改动会改变 SG
    • 0.5 猜出 SG 闭式以后怎么证明
  • Part 1. SG 扩展体系
    • 1.1 Anti-SG 与 SJ 定理
    • 1.2 SG 在 DAG、树、图上的落地
    • 1.3 Multi-SG
    • 1.4 Every-SG
  • Part 2. 线性移动博弈
    • 2.1 Nimble
    • 2.2 Welter Game
    • 2.3 Silver Dollar Game
    • 2.4 Staircase Nim
  • Part 3. 分裂型博弈
    • 3.1 统一结构
    • 3.2 Kayles
    • 3.3 Dawson's Kayles
  • Part 4. 数学结构型博弈
    • 4.1 Wythoff Nim
    • 4.2 同步减法类:Take Apples
  • Part 5. 结构性胜负与策略窃取
    • 5.1 不要见到博弈就硬算 SG
    • 5.2 Chomp 与 Strategy Stealing
    • 5.3 因子偏序上的高维 Chomp
    • 5.4 Independent Nim:一个“不能拆成独立子游戏”的反例
  • 最后:模型识别速查表

题目总索引

下面只保留 能够代表一个模型、能够体现关键转化、或者本身足够有训练价值 的题,不为了数量堆题。

模块 题目 关键词 定位
Part 0 Codeforces 2240C - Nim Game Is XOR Game P/N 刻画、xor、计数 综合题 / 个人做题记录筛选
Part 1 POJ 3480 - John Anti-Nim、Misère Nim 模板题
Part 1 POJ 2425 - A Chess Game DAG SG、多棋子 xor 模板题
Part 1 POJ 2311 - Cutting Game Multi-SG、切割 模板题
Part 1 HDU 3595 - GG and MM Every-SG、step 模板题
Part 1 Petrozavodsk Camp G - Remove the Prime 质因子拆分、连续段、Multi-SG 高质量转化题
Part 2 飞翔的甲鱼 Welter Function Welter 主例题,题面截图
Part 2 POJ 1704 - Georgia and Bob Silver Dollar、相邻配对 模板题
Part 2 LOJ 5096 - 鹅卵石 Pebbles / Luogu P3480 差分、Staircase Nim 高质量转化题
Part 3 POJ 3537 - Crosses and Crosses 区间分裂、SG 周期思想 模板题
Part 3 2025 CCPC Online F - 连线博弈 分裂 SG、周期 34、随机 Hash 综合题
Part 4 POJ 1067 - 取石子游戏 Wythoff、Beatty 序列 模板题
Part 4 Nowcoder NC26003 - Take Apples 同步减法、P-position 构造 结论题
Part 5 Chomp Strategy Stealing 经典模型
Part 5 因子偏序高维 Chomp 偏序格、质因数指数向量 拓展题
Part 5 AtCoder ARC225 B - Independent Nim 同一步跨多个区间、P-position 构造 综合题 / 个人做题记录筛选

公开做题记录中额外筛入了 CF 2240CARC225 B:它们不是为了凑“博弈题数量”,而是分别能很好体现 “SG 与单纯 P/N 判定不是一回事”“看起来被 0 分隔也未必是独立子游戏” 两个很容易犯的错误。


Part 0. SG 函数的本质

这一部分不重新讲“什么是公平博弈”,只把后文真正反复使用的 SG 思想压缩出来。

0.0 常见 SG 打表模板

竞赛里遇到陌生小状态博弈,最可靠的第一反应通常不是猜公式,而是:先把 SG 打出来,再观察规律。

模板 1:时间戳 mex

如果后继 SG 数量不大,用 vis + 时间戳 比每次开 set 更轻。

SG / mex 基础模板
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
const int M=4096;
int vis[M],tim;
int mex(const vector<int>&v){++tim;for(int x:v) if(x<M) vis[x]=tim;int g=0;while(vis[g]==tim) g++;return g;
}

M 只需要大于你能证明的最大 mex。若一个状态最多只有 d 种不同后继,则显然有 SG<=d,这通常能给出很小的数组上界。

模板 2:DAG 上记忆化 SG

状态 u├──> v1├──> v2└──> v3

直接:

\[SG(u)=\operatorname{mex}\{SG(v):u\to v\}. \]

DAG SG 模板
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N=2e5+10;
int sg[N],vis[N],tim;
vector<int>g[N];
int dfs(int u){if(sg[u]!=-1) return sg[u];vector<int>v;for(int x:g[u]) v.push_back(dfs(x));++tim;for(int x:v) if(x<N) vis[x]=tim;int t=0;while(vis[t]==tim) t++;return sg[u]=t;
}

模板 3:一次操作把游戏裂成多个部分

若一步操作得到:

\[G\longrightarrow G_1+G_2+\cdots+G_k, \]

那么这一种操作对应的后继 SG不是某一个子状态,而是:

\[SG(G_1)\oplus SG(G_2)\oplus\cdots\oplus SG(G_k). \]

所以常见区间递推长成:

for(int cut=...;cut...;cut++)nxt.push_back(sg[left]^sg[right]);
sg[len]=mex(nxt);

后面的 Multi-SG、Kayles、连线博弈本质都在反复用这一句。

模板 4:打表找周期

很多一维有限规则博弈会出现 SG 周期,但看见前 20 项重复绝不能直接当证明。竞赛里若题目本身允许经验找规律,至少应:

  1. 暴力算到几百 / 几千项;
  2. 找候选周期 p
  3. 在足够长的后缀上验证 sg[i]=sg[i-p]
  4. 若要写严谨题解,再补“为什么之后的转移窗口完全相同”的周期证明,或者引用已有经典结论。

0.1 SG 值到底是什么

SG 最容易被错误理解成“局面的强弱分数”。实际上:

\[\boxed{G\equiv *SG(G)} \]

这里 *x 表示一堆大小为 x 的 Nim。也就是说,SG 值是在组合游戏意义下给局面划分 Nim 等价类的编号:无论以后把 G 和什么其他公平游戏相加,G 的作用都与一堆 SG(G) 个石子的 Nim 完全相同。

因此 SG=1SG=100 单独看都是先手必胜,却绝不是同一种局面。若再加一个 SG=1 的游戏:

\[1\oplus1=0,\qquad 100\oplus1\ne0. \]

前者变成必败,后者仍然必胜。P/N 只保留“是否为 0”这一位信息,SG 才保存了足以进行组合的信息。


0.2 为什么是 mex

定义:

\[SG(G)=\operatorname{mex}\{SG(H):G\to H\}. \]

SG(G)=g,mex 的真正信息只有两句:

\[0,1,\ldots,g-1\text{ 全部可达},\qquad g\text{ 不可达}. \]

而大小为 g 的 Nim 堆恰好能走到 0,1,...,g-1,却不能一步回到 g。因此普通游戏只要拥有相同的“nimber 可达结构”,在组合意义下就与 *g 等价。这也是后面证明 Staircase Nim、Welter Function 等闭式时最应该抓住的东西,而不是机械背 mex 三个字母。


0.3 游戏和为什么做 xor

若一次只能选择其中一个独立子游戏进行操作:

\[G=G_1+G_2+\cdots+G_k, \]

Sprague-Grundy 定理给出:

\[\boxed{SG(G)=SG(G_1)\oplus SG(G_2)\oplus\cdots\oplus SG(G_k)}. \]

原因并不是“SG 规定要异或”,而是每个 G_i 都先等价成 Nim 堆 *g_i,而 Nim 堆的和本身恰好满足 xor 运算。

这里最重要的前提是 独立:一步只能动一个子游戏,且对子游戏 A 的操作不会改变 B 的合法操作集合。很多题最难的并不是算 SG,而是判断“你以为的两个部分到底是不是独立子游戏”。


0.4 什么规则改动会改变 SG

设当前后继 SG 集合为 S,且:

\[g=\operatorname{mex}(S). \]

若只对当前节点增删边,并且那些后继节点自己的 SG 不变,那么:

  • 新增一个到 h<g 的操作:没影响,因为 h 本来就必须存在;
  • 新增一个到 h>g 的操作:没影响;
  • 新增一个到 h=g 的操作:一定改变当前 SG
  • 删除一个 h>g 的后继:没影响;
  • 删除 h<g 的后继:只要仍有其他操作能到 h 就没影响;若删掉了最后一个 h,SG 会改变。

所以:

\[\boxed{\text{SG 不关心有多少条走法,只关心后继 nimber 集合的 mex 结构。}} \]

这也解释了为什么 Staircase Nim 中“偶数层向奇数层搬、让有效堆增大”这种普通 Nim 没有的操作并不会破坏结论:它增加了额外后继,但始终没有破坏“所有小值可达、自己不可达”的结构。

注意:如果你修改的是全局规则,后继状态自己的 SG 也可能递归改变,此时不能只看当前节点新增的那一条边,而必须重新检查整个状态图。


0.5 猜出 SG 闭式以后怎么证明

以后若通过打表猜出:

\[SG(S)=F(S), \]

最通用、最短的证明模板就是:

对任意状态 S,先证明任何合法操作 S->T 都满足 F(T) != F(S);再证明对每个 0<=x<F(S),总能找到一个合法后继 T 使 F(T)=x。于是后继 F 值包含 0..F(S)-1 而不包含 F(S),根据 mex 定义即有 SG(S)=F(S)。按照游戏 DAG 的拓扑序归纳即可。

这一段几乎就是 Nim、Staircase Nim、Welter Function 等很多闭式 SG 证明的共同骨架


例题:Codeforces 2240C - Nim Game Is XOR Game

题目:https://codeforces.com/contest/2240/problem/C

这题不是标准 Nim:一次选择一个非零向量 b,满足 0<=b_i<=a_i 且所有 b_i 的 xor 为 0,再令 a_i-=b_i,问第一步有多少种能保证获胜的选择。

关键先不算 SG,而是刻画 P/N:局面中非零数不超过一个时为 P-position;非零数至少两个时为 N-position。 因此“第一步获胜”就是一步把局面变成只剩至多一个非零数。设总 xor 为 S:若 S=0,只有把全部数一次删完这一种;否则若最终只留下第 i 个数,则它必须变成 S xor a[i],合法条件正是 (S xor a[i])<a[i]。这题很适合提醒自己:有时题目只要求 P/N 或 winning move 数量,并不需要完整 SG

参考代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
void solve(){int n;cin>>n;vector<int>a(n);int s=0;for(int&i:a) cin>>i,s^=i;if(n==1){cout<<0<<endl;return;}if(s==0){cout<<1<<endl;return;}int ans=0;for(int x:a) if((s^x)<x) ans++;cout<<ans<<endl;
}
signed main(){ios::sync_with_stdio(false);cin.tie(nullptr);int T;cin>>T;while(T--) solve();return 0;
}

Part 1. SG 扩展体系

普通 SG 的核心前提是:Normal Play、一次只选择一个独立子游戏操作。规则一旦改变,首先应该问的不是“还能不能 xor”,而是 到底改坏了 SG 定理的哪一个前提

本部分主要参考:贾志豪《组合游戏略述——浅谈 SG 游戏的若干拓展及变形》:
GitHub PDF 镜像

1.1 Anti-SG 与 SJ 定理

Anti-SG

Anti-SG 把终局规则反过来:

\[\boxed{\text{轮到自己时无路可走的人获胜。}} \]

最熟悉的特例就是 Misère Nim(拿走最后一颗石子的人输)。

Misère Nim 结论

设石堆为 a_1,...,a_n

  • 若存在某一堆 a_i>1,结论与普通 Nim 的 P/N 判定一致:

\[\boxed{a_1\oplus\cdots\oplus a_n=0\iff\text{先手败}} \]

  • 若所有非空堆大小都为 1,则只看非空堆数量:

\[\boxed{\#1\text{ 为奇数}\iff\text{先手败}}. \]

原理只需记一句:当存在大堆时,胜手可以把游戏控制到“奇数个 1 交给对方”的尾局;一旦全部进入 1 的区域,普通 xor 已经退化成数量奇偶,而 Misère 正好把终局奇偶翻转。

SJ 定理

Anti-Nim 的结论不能直接无条件推广到任意 Anti-SG 游戏和。SJ 定理需要额外终止条件:

当所有单一游戏的 SG 值都变成 0 时,整个游戏结束。

在此前提下,先手必胜当且仅当满足下面两种情况之一:

  1. 总 xor 不为 0,并且至少有一个子游戏 SG>1
  2. 总 xor 为 0,并且所有子游戏 SG<=1

也就是:

\[\boxed{ \text{win}\iff \begin{cases} X\ne0 \text{ 且存在 }g_i>1,\\ \text{或 }X=0 \text{ 且所有 }g_i\le1. \end{cases}} \]

其中:

\[X=g_1\oplus g_2\oplus\cdots\oplus g_n. \]

易错点:不要看到“最后一步输”就把所有子游戏 SG 算出来后机械套上面两条。SJ 定理的附加终止条件是结论成立的关键;普通 Misère Nim 恰好满足更强的特殊结构,所以有大家熟悉的简洁结论。

例题:POJ 3480 - John

题目:https://poj.org/problem?id=3480

标准 Misère Nim。若至少有一堆大于 1,按普通 Nim 看 xor;否则所有堆都是 1,看堆数奇偶即可。

参考代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
void solve(){int n;cin>>n;int xr=0;bool big=0;for(int i=1,x;i<=n;i++){cin>>x;xr^=x;if(x>1) big=1;}if(big) cout<<(xr?"John":"Brother")<<endl;else cout<<(n%2==0?"John":"Brother")<<endl;
}
signed main(){ios::sync_with_stdio(false);cin.tie(nullptr);int T;cin>>T;while(T--) solve();return 0;
}

1.2 SG 在 DAG、树、图上的落地

DAG:最标准的 SG 状态图

只要每个状态的合法转移组成 DAG,就直接按拓扑关系定义:

\[SG(u)=\operatorname{mex}\{SG(v):u\to v\}. \]

若有多个互不影响的棋子分别位于 u_1,...,u_k,总局面就是这些单棋子游戏的和:

\[SG=SG(u_1)\oplus\cdots\oplus SG(u_k). \]

树:根树删边 / Green Hackenbush 的经典形式

一棵以 u 为根的树,每次删一条边,所有与根断开的部分同时消失。设 vu 的儿子,则:

\[\boxed{SG(u)=\bigoplus_{v\in son(u)}(SG(v)+1)}. \]

为什么是 +1?从 u 到某个儿子 v 的整条分支,相当于在 v 的游戏上方再串了一条可以直接砍断的边;不同儿子分支之间独立,于是最后 xor。

一般图要谨慎

“图上博弈”不等于“直接对顶点做 SG”。如果原图有环,状态可能出现回到旧状态、和局、重复局面等,普通 SG(u)=mex(...) 的 DAG 递归未必成立。竞赛中常见的正确做法是:

  • 原图本身就是 DAG;或
  • 虽然棋盘有环,但完整游戏状态按某个势函数严格下降,因此状态图仍是 DAG;或
  • 题目另有专门的环缩并 / 图博弈定理。

不要只因为题目出现“图”就机械套 SG。

例题:POJ 2425 - A Chess Game

题目:https://poj.org/problem?id=2425

给定 DAG,多枚棋子可以重合,每次只选择一枚棋子沿一条有向边移动。单枚棋子位于 u 的 SG 就是 sg[u];每枚棋子互不影响,因此查询时把所有起点 SG xor 即可。

参考代码
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N=1005;
vector<int>g[N];
int sg[N];
int dfs(int u){if(sg[u]!=-1) return sg[u];bool vis[N]={0};for(int v:g[u]) vis[dfs(v)]=1;int x=0;while(vis[x]) x++;return sg[u]=x;
}
int main(){ios::sync_with_stdio(false);cin.tie(nullptr);int n;while(cin>>n){for(int i=0;i<n;i++){g[i].clear();int k;cin>>k;while(k--){int v;cin>>v;g[i].push_back(v);}}memset(sg,-1,sizeof(sg));int m;while(cin>>m&&m){int xr=0;while(m--){int u;cin>>u;xr^=dfs(u);}cout<<(xr?"WIN":"LOSE")<<endl;}}return 0;
}

1.3 Multi-SG

普通 SG 中,一个单一游戏一步后还是一个单一游戏。Multi-SG 允许:

\[G\longrightarrow G_1+G_2+\cdots+G_k. \]

那么这一次操作对应的后继 nimber 就是:

\[SG(G_1)\oplus SG(G_2)\oplus\cdots\oplus SG(G_k), \]

父状态仍然对所有合法操作的后继 nimber 做 mex。换句话说,Multi-SG 并没有推翻 Sprague-Grundy,而只是允许“一次操作产生多个独立子游戏”。

这和普通 DAG SG 要区分:DAG 上的 u->v 只是一个状态变成另一个状态;Multi-SG 的关键是 u -> v_1+v_2+...

例题:POJ 2311 - Cutting Game

题目:https://poj.org/problem?id=2311

切一块 w*h 的纸,一刀以后产生两块独立矩形,于是某个竖切位置 k 的后继为:

\[SG(k,h)\oplus SG(w-k,h). \]

横切同理。题目中若一刀直接得到 1*1 当前玩家立即获胜,所以在转换成普通 SG 时,只枚举两边宽度都至少为 2 的“继续游戏”切法;能直接制造 1*1 的情况已经属于即时胜利,不应该再当普通后继继续递归。

参考代码
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
int sg[205][205];
int dfs(int n,int m){if(n>m) swap(n,m);if(sg[n][m]!=-1) return sg[n][m];bool vis[512]={0};for(int i=2;i<=n-2;i++) vis[dfs(i,m)^dfs(n-i,m)]=1;for(int i=2;i<=m-2;i++) vis[dfs(n,i)^dfs(n,m-i)]=1;int g=0;while(vis[g]) g++;return sg[n][m]=g;
}
int main(){ios::sync_with_stdio(false);cin.tie(nullptr);memset(sg,-1,sizeof(sg));int n,m;while(cin>>n>>m) cout<<(dfs(n,m)?"WIN":"LOSE")<<endl;return 0;
}

例题:G. Remove the Prime

题目:2020-2021 Winter Petrozavodsk Camp, Day 5, G - Remove the Prime

一次选一个质数 p 和一段连续区间,要求区间内每个数都能被 p 整除,然后把这一段中所有数的 p 因子全部去掉。

最关键的拆分是:不同质数互不影响。 固定一个质数 p,只看哪些位置当前含有 p。每个极大连续 1 段就是一个独立游戏;一次选子段把它删掉,会把长度 L 的段裂成左右两个段。这个“任取一个非空子段删除”的游戏打表可得且能直接证明:

\[\boxed{SG(L)=L}. \]

于是答案就是:对每个质数,把它出现位置的所有极大连续段长度 xor 起来,再把所有质数的结果继续 xor。真正的工程难点反而变成 a_i<=10^{18} 的快速质因数分解,因此使用 Miller-Rabin + Pollard-Rho。

参考代码(Miller-Rabin + Pollard-Rho)
#include<bits/stdc++.h>
using namespace std;
using ull=unsigned long long;
using u128=__uint128_t;
#define endl '\n'
mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count());
ull mul(ull a,ull b,ull mod){return (u128)a*b%mod;}
ull qpow(ull a,ull b,ull mod){ull ans=1;while(b){if(b&1) ans=mul(ans,a,mod);a=mul(a,a,mod);b>>=1;}return ans;
}
bool isprime(ull n){if(n<2) return 0;for(ull p:{2ULL,3ULL,5ULL,7ULL,11ULL,13ULL,17ULL,19ULL,23ULL,29ULL,31ULL,37ULL}){if(n%p==0) return n==p;}ull d=n-1,s=0;while(!(d&1)) d>>=1,s++;for(ull a:{2ULL,325ULL,9375ULL,28178ULL,450775ULL,9780504ULL,1795265022ULL}){if(a%n==0) continue;ull x=qpow(a%n,d,n);if(x==1||x==n-1) continue;bool ok=0;for(ull r=1;r<s;r++){x=mul(x,x,n);if(x==n-1){ok=1;break;}}if(!ok) return 0;}return 1;
}
ull nxt(ull x,ull c,ull mod){return (mul(x,x,mod)+c)%mod;}
ull rho(ull n){if(n%2==0) return 2;if(n%3==0) return 3;while(1){ull c=rng()%(n-1)+1;ull x=rng()%(n-2)+2,y=x,d=1;while(d==1){x=nxt(x,c,n);y=nxt(nxt(y,c,n),c,n);ull z=x>y?x-y:y-x;d=gcd(z,n);}if(d!=n) return d;}
}
void factor(ull n,vector<ull>&v){if(n==1) return;if(isprime(n)){v.push_back(n);return;}ull d=rho(n);factor(d,v);factor(n/d,v);
}
void solve(){int n;cin>>n;unordered_map<ull,int> last,len;ull xr=0;for(int i=1;i<=n;i++){ull x;cin>>x;vector<ull>fac;factor(x,fac);sort(fac.begin(),fac.end());fac.erase(unique(fac.begin(),fac.end()),fac.end());for(ull p:fac){if(last[p]==i-1) len[p]++;else{if(last[p]) xr^=(ull)len[p];len[p]=1;}last[p]=i;}}for(auto [p,l]:len) xr^=(ull)l;cout<<(xr?"First":"Second")<<endl;
}
int main(){ios::sync_with_stdio(false);cin.tie(nullptr);solve();return 0;
}

这题真正值得留下来的不是 Pollard-Rho,而是第一步:把“操作某个质数”识别成按质数完全独立的子游戏,再把每个质数拆成连续段游戏。


1.4 Every-SG

Every-SG 的规则与普通游戏和恰好相反:

对于所有还没有结束的单一游戏,当前玩家这一回合都必须各走一步。

因此 xor 不再是核心。现在真正决定整体何时结束的是:哪个子游戏拖得最久,以及胜手/败手分别希望它拖长还是尽快结束。

对一个单一状态 v 定义 step(v)

\[step(v)= \begin{cases} 0,&v\text{ 为终止状态},\\ \max\limits_{v\to u,\ SG(u)=0}step(u)+1,&SG(v)>0,\\ \min\limits_{v\to u}step(u)+1,&SG(v)=0. \end{cases} \]

含义是:

  • 当前是必胜态时,胜手会在能走向必败态的方案中尽量拖长
  • 当前是必败态时,对手最终能控制结果,因此当前一方只能按最短结束来衡量。

Every-SG 定理:

\[\boxed{\text{整个游戏先手必胜}\iff \max_i step(G_i)\text{ 为奇数}.} \]

并且单个游戏中,N-position 的 step 必为奇数,P-position 的 step 必为偶数。直觉上,最长的那个子游戏最后一个结束,它的步数奇偶直接决定谁做最后一次全局操作。

例题:HDU 3595 - GG and MM

题目:https://acm.hdu.edu.cn/showproblem.php?pid=3595

每个单一游戏给两个数 (x,y),一次从大数中减去小数的正整数倍;全局每一回合必须对所有未结束的单一游戏各操作一次,正是 Every-SG。

对单局做欧几里得递归。若 y/x=1,当前只有一种商层级,胜负翻转且 step+1;若 y/x>1,当前玩家可以通过选择减几倍来控制后继奇偶,因此当前一定是 N-position,并能由后继信息求出最长 step。最后只取所有单局 step 的最大值判奇偶。

参考代码
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N=1005;
int sg[N][N],st[N][N];
int dfs(int x,int y){if(x>y) swap(x,y);if(sg[x][y]!=-1) return sg[x][y];if(x==0||y==0) return sg[x][y]=st[x][y]=0;int r=y%x,k=y/x;if(k==1){sg[x][y]=dfs(r,x)^1;st[x][y]=st[min(r,x)][max(r,x)]+1;}else{int t=dfs(r,x);st[x][y]=t+st[min(r,x)][max(r,x)]+1;sg[x][y]=1;}return sg[x][y];
}
int main(){ios::sync_with_stdio(false);cin.tie(nullptr);memset(sg,-1,sizeof(sg));int n;while(cin>>n){int ans=0;for(int i=1;i<=n;i++){int x,y;cin>>x>>y;if(x>y) swap(x,y);dfs(x,y);ans=max(ans,st[x][y]);}cout<<(ans&1?"MM":"GG")<<endl;}return 0;
}

Part 2. 线性移动博弈

这一类题最值得形成“规则辨认链”:

可以任意向左,允许跨越、允许重合       -> Nimble
可以任意向左,允许跨越、禁止重合       -> Welter
可以任意向左,禁止跨越、禁止重合       -> Silver Dollar
按相邻层向出口移动                     -> Staircase Nim

规则只改一条,SG 结构可能完全不同。

2.1 Nimble

棋子位于 x_1,...,x_n,一次任选一枚向左移动任意距离,允许跨过其他棋子,也允许落在同一位置。棋子之间完全独立,所以每枚位置 x_i 就是一堆大小为 x_i 的 Nim:

\[\boxed{SG=x_1\oplus x_2\oplus\cdots\oplus x_n}. \]

这个模型本身没必要展开,真正值得记的是下面两次“加限制”会发生什么。


2.2 Welter Game

模型

格子编号:

\[0,1,2,3,\ldots \]

若干硬币占据互不相同的位置 a_1,...,a_n。每次选择一个硬币移动到任意更小的空位置:允许跨越其他硬币,但不允许重合。

它可以理解成:

Nimble + “所有 Nim 堆大小必须互不相同”。

正是这一个“禁止重合”,让单纯的位置 xor 失效。

Welter Function

对不同的 x,y,定义 mating function:

\[(x\mid y)=2^{v_2(|x-y|)+1}-1. \]

因为若 d=|x-y|,则:

\[\boxed{(x\mid y)=d\oplus(d-1)}. \]

于是 Welter Function 为:

\[\boxed{ W(a_1,\ldots,a_n)= \bigoplus_i a_i \oplus \bigoplus_{i<j}(a_i\mid a_j) } \]

Welter 定理:

\[\boxed{SG(a_1,\ldots,a_n)=W(a_1,\ldots,a_n)}. \]

怎么记这个修正项

Nimble 原来只有:

\[\bigoplus_i a_i. \]

Welter 多出来的是所有棋子对的“碰撞修正”。若:

\[v_2(|a_i-a_j|)=t, \]

说明两位置的二进制低 t 位相同、第 t 位才第一次不同,于是这一对贡献:

\[2^{t+1}-1=(111\cdots111)_2, \]

恰好翻转第 0..t 位。Welter 的修正只关心两位置在二进制低位上“粘了多久”。 这比死背一个 d xor (d-1) 更容易记。

最精髓的证明思路

W 当成候选 SG。Welter Function 对任意一个坐标都是所谓 animating function:固定其他硬币后,目标 nimber 唯一决定这个坐标应该变到哪里;若目标 s'<s=W,总能找到至少一个坐标需要向左减小,从而覆盖所有 0..W-1。另一方面合法地只移动一枚硬币不可能保持 Welter Function 不变,因此 W 自己不可达。正好满足 mex 的两个条件,所以 W=SG

论文 / 资料

Welter 部分建议直接看下面几篇:

  1. Tomoaki Abuku, Transfinite Version of Welter's Game
    arXiv 页面 / PDF
    其中第 1.3 节完整整理了普通 Welter Game、mating function、Welter Function,并在 Theorem 1.17 给出 Grundy = Welter Function
  2. Yuki Irie, p-Saturations of Welter's Game and the Irreducible Representations of Symmetric Groups
    arXiv 页面 / PDF
  3. Yuki Irie, A p-calm game and Welter's game
    arXiv 页面
  4. C. P. Welter, The theory of a class of games on a sequence of squares, in terms of the advancing operation in a special group, 1954(原始论文)
    DOI
    现代阅读建议以上面的 Abuku 论文为主:原论文记号较老,而 Abuku 的第 1.3 节已经把普通 Welter Game 与 Welter Function 用现代 SG 语言重新整理。

例题:飞翔的甲鱼

题意就是标准 Welter:格子从 1 开始编号,每只甲鱼可以飞到任意更小且没有甲鱼的位置,可以跨过其他甲鱼。先把位置全部减一变成 Welter 标准的 0 起点,然后计算 Welter Function,非零即先手胜。

直接按所有棋子对计算是 O(n^2)。还可以利用:修正项的第 b 位为 1 当且仅当一对位置满足:

\[a_i\equiv a_j\pmod {2^b}. \]

因此第 b 位只需统计“模 2^b 相同的数对个数的奇偶”。把 32 位整数按位反转后排序,原数低 b 位相同就变成排序后公共前缀相同;对每个 b 扫描所有组,C(cnt,2) 的奇偶即 (cnt>>1)&1。这样可以把公式计算到 O(n\log n+31n)

说明:截图中的 Σn<=2*10^8、64MB 是非常极端的工程约束;下面代码给出的是正确的 Welter Function 计算与一个远优于 O(n^2) 的实现,用于本节模型总结。若必须严格卡截图所示最坏上限,还需要针对原题评测数据继续做 IO / 内存乃至算法工程优化,这里不把未经验证的实现冒充原题最坏界 AC。

参考代码:O(n log n + 31n) 计算 Welter Function
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
using u32=uint32_t;
u32 rev32(u32 x){x=((x>>1)&0x55555555u)|((x&0x55555555u)<<1);x=((x>>2)&0x33333333u)|((x&0x33333333u)<<2);x=((x>>4)&0x0f0f0f0fu)|((x&0x0f0f0f0fu)<<4);x=((x>>8)&0x00ff00ffu)|((x&0x00ff00ffu)<<8);return (x>>16)|(x<<16);
}
void solve(){int n;cin>>n;vector<u32>r(n);u32 base=0;for(int i=0;i<n;i++){u32 x;cin>>x;--x;base^=x;r[i]=rev32(x);}sort(r.begin(),r.end());u32 corr=0;for(int b=0;b<=30;b++){int parity=0;for(int l=0;l<n;){int rr=l+1;u32 key=b==0?0:r[l]>>(32-b);while(rr<n&&(b==0?0:r[rr]>>(32-b))==key) rr++;int cnt=rr-l;parity^=(cnt>>1)&1;l=rr;}if(parity) corr^=(1u<<b);}cout<<((base^corr)?"YES":"NO")<<endl;
}
int main(){ios::sync_with_stdio(false);cin.tie(nullptr);int T;cin>>T;while(T--) solve();return 0;
}

2.3 Silver Dollar Game

模型

若干硬币在一维格子上,只能向左移动,并且:

  • 不能落到已有硬币上;
  • 不能跨越其他硬币。

设从左到右:

\[x_1<x_2<\cdots<x_n. \]

与 Welter 相比,只多了“不能跨越”,但整个结构反而从二进制碰撞修正变成了非常干净的相邻配对

结论

n 为偶数:

\[\boxed{SG=(x_2-x_1-1)\oplus(x_4-x_3-1)\oplus\cdots\oplus(x_n-x_{n-1}-1)}. \]

n 为奇数,并且格子从 0 开始:

\[\boxed{SG=x_1\oplus(x_3-x_2-1)\oplus\cdots\oplus(x_n-x_{n-1}-1)}. \]

也就是:从最右边开始两个两个配对,每对只看两枚硬币之间的空格数;若最左边剩一枚,再把它到出口的距离当成一堆。

原理

从右向左把硬币配成 (x_{n-1},x_n)(x_{n-3},x_{n-2})……。一对中真正可自由改变的是两枚硬币之间的 gap,它恰好像一堆 Nim;左侧那枚硬币移动时虽然会改变相邻空间,却相当于把自由度传递给更左边的“缓冲部分”。按这个配对顺序做 mex,可证明每个有效 gap 独立贡献一个 Nim 堆。最值得记的是:禁止跨越带来了顺序不变,于是相邻棋子可以固定配对。

例题:POJ 1704 - Georgia and Bob

题目:https://poj.org/problem?id=1704

这是最标准的 Silver Dollar。题目位置从 1 开始,因此若 n 为奇数,最左边单独那一堆大小应为 x_1-1

参考代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
void solve(){int n;cin>>n;vector<int>a(n);for(int&i:a) cin>>i;sort(a.begin(),a.end());int xr=0;for(int i=n-1;i>=1;i-=2) xr^=a[i]-a[i-1]-1;if(n&1) xr^=a[0]-1;cout<<(xr?"Georgia will win":"Bob will win")<<endl;
}
signed main(){ios::sync_with_stdio(false);cin.tie(nullptr);int T;cin>>T;while(T--) solve();return 0;
}

2.4 Staircase Nim

模型

从出口向上编号 1..n,第 i 层有 a_i 个石子。一次选择第 i 层若干石子移动到第 i-1 层,第 0 层视为直接离开游戏。

结论

\[\boxed{SG=a_1\oplus a_3\oplus a_5\oplus\cdots}. \]

这里的“奇数层”本质不是输入编号奇偶,而是:

\[\boxed{\text{距离出口为 }1,3,5,\ldots\text{ 的层。}} \]

原理

定义候选值 X=a_1 xor a_3 xor ...。任意一步只在相邻两层之间搬石子,而相邻层必定一奇一偶,因此一次操作恰好改变一个参与 X 的量,所以不能保持 X 不变;另一方面,对任意 0<=Y<X,利用普通 Nim 的最高位性质,总能找到某个奇数层把它减少到合适值,使 xor 变成 Y,搬下去的石子只进入偶数缓冲层。于是所有小于 X 的值可达、X 本身不可达,mex 正好为 X

这和 Silver Dollar 的共同直觉是:

相邻结构发生配对,其中一个量是真正的 Nim 自由度,另一个只承担缓冲 / 传递作用。

例题:LOJ 5096 / Luogu P3480 - 鹅卵石 Pebbles

题目:
https://loj.ac/p/5096
https://www.luogu.com.cn/problem/P3480

给一个非降序列:

\[a_1\le a_2\le\cdots\le a_n. \]

把它差分:

\[d_i=a_i-a_{i-1},\qquad a_0=0. \]

若原操作让第 i 堆减少 x,差分上恰好表现成:

\[d_i\leftarrow d_i-x,\qquad d_{i+1}\leftarrow d_{i+1}+x, \]

这就是一个出口在右边的 Staircase Nim。因此从右端开始隔一个差分 xor:

\[\boxed{d_n\oplus d_{n-2}\oplus d_{n-4}\oplus\cdots}. \]

参考代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
void solve(){int n;cin>>n;vector<int>a(n+1);for(int i=1;i<=n;i++) cin>>a[i];int xr=0;for(int i=n;i>=1;i-=2) xr^=a[i]-a[i-1];cout<<(xr?"TAK":"NIE")<<endl;
}
signed main(){ios::sync_with_stdio(false);cin.tie(nullptr);int T;cin>>T;while(T--) solve();return 0;
}

Part 3. 分裂型博弈

3.1 统一结构

这一类题的识别关键词是:

一次操作发生在中间,之后左右 / 若干块再也互相影响不到。

于是:

原局面|| 一次操作v
左子游戏 + 右子游戏 (+ ...)

如果状态只由长度 n 决定,典型递推就是:

\[\boxed{SG(n)=\operatorname{mex}\{SG(L)\oplus SG(R)\}}. \]

注意:这其实就是 Multi-SG 最常见的落地形式。Part 1 讲的是抽象规则,这里讲的是最常见的“区间被切开”模型。


3.2 Kayles

Kayles 有一排 n 个瓶柱,一次可以击倒:

  • 一个瓶柱;或
  • 两个相邻瓶柱。

击倒中间瓶柱后,左右两段完全独立。因此:

\[SG(n)=\operatorname{mex}\left( \left\{SG(i)\oplus SG(n-i-1)\mid 0\le i<n\right\} \cup \left\{SG(i)\oplus SG(n-i-2)\mid 0\le i<n-1\right\} \right), \]

其中分别枚举删一个和删相邻两个的位置,越界部分视作长度 0

它真正值得记的不是一串 SG 值,而是:

\[\boxed{\text{take-and-break:一次删掉局部,剩余部分裂成多个独立游戏。}} \]

经典 Kayles 的 nim-sequence 最终周期为 12(preperiod 为 71)。在竞赛中如果题目规模极大而局部规则固定,“先写分裂递推、再打表观察周期”是非常常见的路线。


3.3 Dawson's Kayles

Dawson's Kayles 可以定义为:一排棋子,每次必须拿走两个相邻棋子,剩余左右两段独立。它仍然是完全相同的 split-SG:

\[SG(n)=\operatorname{mex}_{0\le i\le n-2} \{SG(i)\oplus SG(n-i-2)\}. \]

这个经典 octal game 0.07 的 normal-play Grundy 序列从 n=53 起进入长度为 34 的周期。这里不展开周期证明;更重要的是认识到 “删一段 -> 左右 xor -> mex -> 打表/周期” 这一整套套路。

例题:POJ 3537 - Crosses and Crosses

题目:https://poj.org/problem?id=3537

1*n 棋盘轮流放 X,谁先造出连续三个 X 谁赢。分析安全状态时,一旦在位置 i 放下 X,它左右距离不超过 2 的位置都不能再作为“不会立刻送对手胜利”的独立安全区域,于是剩余可继续博弈的部分被切成:

\[L=\max(0,i-3),\qquad R=\max(0,n-i-2). \]

因此:

\[SG(n)=\operatorname{mex}_{i=1}^{n}\{SG(L)\oplus SG(R)\}. \]

参考代码
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
const int N=2005;
int sg[N];
int dfs(int n){if(sg[n]!=-1) return sg[n];bool vis[N]={0};for(int i=1;i<=n;i++){int l=max(0,i-3),r=max(0,n-i-2);vis[dfs(l)^dfs(r)]=1;}int g=0;while(vis[g]) g++;return sg[n]=g;
}
int main(){ios::sync_with_stdio(false);cin.tie(nullptr);memset(sg,-1,sizeof(sg));sg[0]=0;int n;cin>>n;cout<<(dfs(n)?1:2)<<endl;return 0;
}

例题:2025 CCPC Online F - 连线博弈

题目:https://qoj.ac/contest/2534/problem/14552

这题很适合放在 Kayles 后面,因为它把“分裂 SG”包装得更深了一层。

先看一个连通块。若其中有 x 个当前可用点,一次连线会消耗两个点,并把剩余点分成两个互不影响的子块,所以:

\[\boxed{SG(x)=\operatorname{mex}_{a=0}^{x-2}\{SG(a)\oplus SG(x-2-a)\}}. \]

打表后可发现从足够大的位置开始周期为 34,实现中预处理到 1000,对 x>500 使用:

\[SG(x)=SG\left(500+(x-500)\bmod34\right). \]

真正麻烦的是“哪些点属于同一连通块”。对每条已有线段,给线段内部点集 xor 一个随机 A,给线段外部点集 xor 一个随机 B;两个未占用点若对所有线段都处于完全相同的相对区域,最终 Hash 就相同,可以视作同一个连通块。区间 xor 用差分事件离线维护,最后统计每个 Hash 对应多少自由点,再 xor 各块 SG。这里使用 64 位随机 Hash,属于概率正确算法,碰撞概率可以忽略。

参考代码
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
using ull=unsigned long long;
const int K=1005;
int sg[K];
mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count());
void init(){sg[0]=sg[1]=0;for(int x=2;x<=1000;x++){bool vis[K]={0};for(int a=0;a<=x-2;a++) vis[sg[a]^sg[x-2-a]]=1;int g=0;while(vis[g]) g++;sg[x]=g;}
}
int getsg(int x){if(x<=500) return sg[x];return sg[(x-500)%34+500];
}
void add(map<int,ull>&d,int l,int r,ull v){if(l>r) return;d[l]^=v;d[r+1]^=v;
}
void solve(){int n,m;cin>>n>>m;if(m==0){cout<<(getsg(n)?"YES":"NO")<<endl;return;}map<int,ull>d;vector<int>ed;for(int i=1;i<=m;i++){int x,y;cin>>x>>y;++x;++y;if(x>y) swap(x,y);ed.push_back(x);ed.push_back(y);ull A=rng(),B=rng();add(d,x+1,y-1,A);add(d,1,x-1,B);add(d,y+1,n,B);}sort(ed.begin(),ed.end());ed.erase(unique(ed.begin(),ed.end()),ed.end());d[n+1]^=0;map<ull,int>cnt;int last=1;ull cur=0;for(auto [pos,val]:d){if(last<=pos-1){int occ=upper_bound(ed.begin(),ed.end(),pos-1)-lower_bound(ed.begin(),ed.end(),last);cnt[cur]+=pos-last-occ;}cur^=val;last=pos;}int ans=0;for(auto [h,c]:cnt) ans^=getsg(c);cout<<(ans?"YES":"NO")<<endl;
}
int main(){ios::sync_with_stdio(false);cin.tie(nullptr);init();int T;cin>>T;while(T--) solve();return 0;
}

这题有两个很值得留下的坑:第一,不能看见线段就想当然认为每条线段对应一个独立子游戏,真正独立的是连通块;第二,SG 周期只是解决了“一个块有多少点”,划分块本身还需要另一套 Hash 技巧。


Part 4. 数学结构型博弈

这类题仍然是公平组合博弈,但答案不再表现为“把几个显然独立的量 xor 一下”,而是出现 Beatty 序列、黄金分割、特殊 P-position 构造等数学结构。

4.1 Wythoff Nim

模型

两堆石子 a<=b,一次可以:

  1. 只减少第一堆;
  2. 只减少第二堆;
  3. 两堆同时减少相同的正整数。

结论

令:

\[\varphi=\frac{1+\sqrt5}{2}. \]

所有 P-position 恰好是:

\[\boxed{ (a_k,b_k)= (\lfloor k\varphi\rfloor,\lfloor k\varphi^2\rfloor) } \]

又因为:

\[\varphi^2=\varphi+1, \]

所以:

\[\boxed{b_k=a_k+k}. \]

于是给定 a<=b,设:

\[k=b-a, \]

只需判断:

\[\boxed{a=\lfloor k\varphi\rfloor}. \]

原理

两列 Beatty 序列 floor(k*phi)floor(k*phi^2) 恰好把所有正整数不重不漏地划分掉。不同 P-position 的差值 k 不同,因此不能通过“两堆同减”互达;Beatty 序列的互补性又保证不能通过只减一堆从一个 P-position 到另一个 P-position。反过来,每个非 P-position 都能通过只减一堆或同减两堆落到唯一合适的 P-position。于是满足“P 不到 P、N 必到 P”的标准刻画。

例题:POJ 1067 - 取石子游戏

题目:https://poj.org/problem?id=1067

标准 Wythoff,直接按差值 k=b-a 判断黄金分割下取整即可。

参考代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
signed main(){ios::sync_with_stdio(false);cin.tie(nullptr);int a,b;cin>>a>>b;if(a>b) swap(a,b);int k=b-a;long double phi=(1.0L+sqrtl(5.0L))/2.0L;int x=(int)floorl(k*phi);cout<<(a==x?0:1)<<endl;return 0;
}

若数据被放大到接近 1e18 甚至更高,不要无脑依赖浮点取整;这时应考虑高精度或等价整数判定。经典 POJ 数据范围使用 long double 足够。


4.2 同步减法类:Take Apples

例题:Nowcoder NC26003 - Take Apples

题目:https://ac.nowcoder.com/acm/problem/26003

初始三堆苹果为:

\[(M,N,N). \]

一次可以:

  1. 选一堆拿 1..S 个;
  2. 三堆同时拿相同的正数,且这个数可以大于 S

最后拿完者胜。

针对题目规定的这个对称初态,结论非常简洁:

\[\boxed{\text{Bob 胜}\iff N\le S\ \text{且}\ M\equiv0\pmod{S+1}.} \]

N<=S 时,两堆相同的 N 可以用对称应对理解:若对手只动其中一堆,就在另一堆做同样操作;第一堆 M 则出现经典“每两步合计拿 S+1”的模结构。对三堆同步操作再配合第一堆补到 S+1,可以维持这个标准败态。若 M mod(S+1)!=0,先手直接修正第一堆即可。N>S 的区域则都属于先手必胜,完整证明需要对后继分类讨论;这题更适合把上述条件当作特定 (M,N,N) 初态的结论记忆,而不要误写成任意三堆游戏的完整 P-position 分类。

参考代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
signed main(){ios::sync_with_stdio(false);cin.tie(nullptr);int S,M,N;while(cin>>S>>M>>N){if(N<=S&&M%(S+1)==0) cout<<"Bob"<<endl;else cout<<"Alice"<<endl;}return 0;
}

它和 Wythoff 的共同点是“允许同步减少多个堆”,但结论并不是黄金分割,所以更适合单独归到“同步减法类”,而不是强行叫作 Wythoff 变形。


Part 5. 结构性胜负与策略窃取

5.1 不要见到博弈就硬算 SG

SG 很强,但竞赛里还有一类题,真正要求的只是:

\[\text{找 P-position / 证明先手一定存在获胜策略}. \]

如果题目:

  • 状态巨大,根本不像有限小状态 DP;
  • 操作之间高度耦合,很难拆成游戏和;
  • 只问谁赢,不要求 winning move;
  • 存在明显对称、偏序、最小/最大“无害操作”;

那么应该优先尝试:

  • 配对策略;
  • 对称策略;
  • 不变量;
  • P/N 状态直接构造;
  • Strategy Stealing(策略窃取)。

SG 是工具,不是博弈题的唯一入口。


5.2 Chomp 与 Strategy Stealing

经典 Chomp:https://cariboutests.com/games/chomp.php?lang=cn

有一个 n*m 矩形点阵 / 巧克力,每次选择仍存在的一个位置,并删除它及其右下方的所有位置;左上角是毒点,谁取到毒点谁输。

结论

除了只有一个毒点的 1*1 棋盘:

\[\boxed{n\cdot m>1\Longrightarrow\text{先手一定存在必胜策略}.} \]

注意这里是“存在”,经典 Strategy Stealing 一般并不会告诉你具体第一步应该下在哪里。

最精髓的策略窃取证明

假设某个非平凡矩形是先手必败。先手先吃掉最右下角那个安全格,它只删除自己。若此后局面对后手是必胜的,设后手存在某个获胜第一步 x;但 x 的删除区域必然也包含刚才那个最右下角格,于是原来的先手完全可以一开始就直接走 x,得到与“先吃右下角、再由对手走 x”相同的剩余局面,却把行动权交换给了对手,矛盾。因此原局面不可能是必败态。

结论判定代码(若题目只问胜负)
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
signed main(){ios::sync_with_stdio(false);cin.tie(nullptr);int n,m;cin>>n>>m;cout<<(n==1&&m==1?"Bob":"Alice")<<endl;return 0;
}

Chomp 很适合提醒自己:能证明先手必胜,不代表能高效构造具体必胜着。 这和 SG 算出非零后通常还能进一步找 SG=0 后继的情况很不一样。


5.3 因子偏序上的高维 Chomp

拓展题:给定正整数 n。双方轮流选择一个还可以拿的因子 d,拿走 d 后,d 的所有因子都不能再拿;谁拿到 n 谁输。

把:

\[n=p_1^{a_1}p_2^{a_2}\cdots p_k^{a_k} \]

的任意因子写成:

\[d=p_1^{b_1}p_2^{b_2}\cdots p_k^{b_k},\qquad0\le b_i\le a_i. \]

于是每个因子就是一个高维格点:

\[(b_1,b_2,\ldots,b_k), \]

而:

\[d_1\mid d_2 \iff b_i^{(1)}\le b_i^{(2)}\quad\forall i. \]

因此整个因子集合就是若干条链的直积偏序,选 d 后删除它的所有因子,就是高维 Chomp 的一个方向版本;n 对应最高角,是毒点。

结论

\[\boxed{n=1\Rightarrow\text{Alice 败};\qquad n>1\Rightarrow\text{Alice 胜}.} \]

证明直接复制策略窃取:1 是最小安全元素,先拿 1 只会删掉自己。若之后 Bob 有某个获胜回应 d,由于 1|d,Alice 原本就可以第一步直接拿 d,得到完全相同的剩余局面并交换行动权,矛盾。

参考代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
signed main(){ios::sync_with_stdio(false);cin.tie(nullptr);int n;cin>>n;cout<<(n==1?"Bob":"Alice")<<endl;return 0;
}

这道题最漂亮的点不是代码,而是:因数关系 -> 质因数指数向量 -> 逐维偏序 -> 高维 Chomp。


5.4 Independent Nim:一个“不能拆成独立子游戏”的反例

题目:AtCoder ARC225 B - Independent Nim

给一个 01 串。一次可以选择若干个当前为 1 的位置改成 0,但同一次选中的位置两两不能相邻;无法操作者输。

最容易犯的错

看到:

111 0 11111 0 11

很容易把每个连续 1 段当成独立子游戏,然后 xor 每段 SG。

这是错的,因为同一回合允许同时从多个连续段各删一些位置。一步操作可以同时作用于多个“段”,所以这些段根本不是 disjunctive sum,普通 SG xor 前提不成立。

P-position 结论

\[\boxed{\text{每一个极长连续 }1\text{ 段长度都恰好为 }2\iff\text{Bob 胜}.} \]

0 也满足这个条件,因此同样是 Bob 胜。

原理

把“所有 1 段都是 11”叫标准形。标准形中一次合法操作在每个 11 中最多删一个,因此只要操作就必然破坏至少一对 11,走到非标准形;反过来,对任意长度不为 2 的连续段,都能按周期结构留下若干个 11,删除位置之间至少隔两个 1,所以所有非标准段可以在同一回合一起整理成标准形。于是标准形的任何后继都是 N-position,而任何非标准形都有一步走到标准形。

一个方便记的构造是:长度 L 的段可以留下:

L = 3k     : (110)^k
L = 3k + 1 : 0(110)^k
L = 3k + 2 : (110)^k11

其中 0 表示这一回合删除的位置。所有被删位置两两不相邻。

参考代码
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
void solve(){int n;cin>>n;bool ok=1;int cnt=0,x=0;for(int i=1;i<=n;i++){cin>>x;if(x) cnt++;else{if(cnt&&cnt!=2) ok=0;cnt=0;}}if(cnt&&cnt!=2) ok=0;cout<<(ok?"Bob":"Alice")<<endl;
}
int main(){ios::sync_with_stdio(false);cin.tie(nullptr);int T;cin>>T;while(T--) solve();return 0;
}

这题非常适合作为全文最后的反例:

\[\boxed{\text{“空间上分开”不等于“组合博弈意义下独立”。}} \]

判断能否 xor,永远要回到定义:一次是否只能操作一个子游戏?一个子游戏的操作是否完全不影响另一个?


最后:模型识别速查表

题面特征 第一反应 核心结论 / 工具
普通有限无偏博弈,小状态 SG 打表 mex
多个完全独立部分,一次只能动一个 游戏和 子 SG xor
最后一步输 Anti-SG / Misère 先检查是否满足 SJ 条件;Misère Nim 单独记
DAG 上棋子沿边走 DAG SG sg[u]=mex(sg[v])
树根删边,断开部分消失 Green Hackenbush 树 sg[u]=xor(sg[v]+1)
一步把一个状态裂成多个独立状态 Multi-SG 后继 nimber 先 xor,再 mex
每回合所有未结束子游戏都必须动 Every-SG 最大 step 奇偶
棋子任意向左,可跨越、可重合 Nimble 所有位置 xor
可跨越但不能重合 Welter Welter Function / v2(ai-aj)
不能跨越也不能重合 Silver Dollar 从右向左相邻配对,gap xor
石子只能逐层向出口移动 Staircase Nim 距出口奇数层 xor
一步删除局部后左右独立 Kayles / split game mex(sg[L]^sg[R])
一维固定局部规则、n 巨大 SG 周期 打表 + 验证周期
两堆可单减或同步等量减 Wythoff 黄金分割 / Beatty 序列
规则有同步操作但不完全是 Wythoff 特殊 P-position 不要强行套黄金分割
矩形 / 偏序中选点删除一个方向区域 Chomp Strategy Stealing
看似分成多段,但一步能同时动多段 不能直接 xor 先重新检查“独立子游戏”前提

最后真正该记的几条

  1. SG 不是胜负分数,而是 Nim 等价类。
  2. mex 真正表达的是:所有小值可达,自己不可达。
  3. xor 的前提不是“有多个部分”,而是这些部分构成 disjunctive sum。
  4. 一个操作产生多个独立部分:先 xor 新部分,再对所有操作做 mex。
  5. 规则只改一点,模型可能彻底改变:Nimble -> Welter -> Silver Dollar 就是最典型的例子。
  6. 打表不是不会做时的最后手段,而是猜 SG 闭式、周期、P-position 的第一实验工具。
  7. 不是所有博弈都值得求完整 SG。只问胜负时,配对、不变量、P/N 构造、策略窃取可能更直接。

参考资料

  • 贾志豪:《组合游戏略述——浅谈 SG 游戏的若干拓展及变形》
    GitHub PDF 镜像
  • C. P. Welter, The theory of a class of games on a sequence of squares, in terms of the advancing operation in a special group
    DOI
  • Tomoaki Abuku, Transfinite Version of Welter's Game
    arXiv
  • Yuki Irie, p-Saturations of Welter's Game and the Irreducible Representations of Symmetric Groups
    arXiv
  • Yuki Irie, A p-calm game and Welter's game
    arXiv
  • Aaron N. Siegel, Misère Games and Misère Quotients(其中也整理了 Kayles / Dawson's Kayles 的 normal-play 周期结论)
    arXiv PDF