【笔记】博弈论

· · 个人记录

\color{red}{\text{我正是来访者,六轩岛上的第 18 个人类!!!}}

\text{不好意思,}\color{red}{\text{就算迎来了汝,也是 17 人。}}

\text{—— 海猫鸣泣之时}

博客链接

博弈论主要分为公平组合游戏、非公平组合游戏、反常游戏。本文将讲解公平组合游戏。

公平组合游戏

公平组合游戏(Impartial Game)的定义如下:

有向图游戏是一个经典的公平组合游戏,它的定义如下:

在一个有向无环图中,只有一个起点,上面有一个棋子,两个玩家轮流沿着有向边推动棋子,不能走的玩家判负。

事实上,大部分公平组合游戏都可以转化为有向图游戏,将每一个游戏状态作为一个节点,若一个状态 A 能够通过一次决策变为状态 B,则连边 A\to B,称 BA 的后继状态。下面都在有向图游戏的基础上讨论,以下的讨论都假设先后手都采取最优策略。

定义 必胜状态先手在该状态下能够通过一系列决策获得胜利(即先手必胜);必败状态 为 在该状态下无论先手怎么决策,后手都能够通过一系列决策获得胜利(即后手必胜)。

通过推理,我们可以得出下面三条定理:

SG 函数

为了更准确地描述胜败状态,我们引入 SG 函数。

定义 \operatorname{mex} 函数为不属于集合 S 的最小非负整数:

\operatorname{mex}\{S\}=\min\{x\}\quad (x\notin S,x\in N)

例如 \operatorname{mex}\{ 0,1,2,5,7 \}=3

设状态 xk 个后继状态 y_1,y_2,\dots,y_k,则有:

SG(x)=\operatorname{mex}\{SG(y_1),SG(y_2),\dots,SG(y_k)\}

显然,一个状态为必败状态的充要条件为该状态的 SG 值为 0,必胜状态的充要条件为 SG 值不为 0

假如只有一个游戏,SG 函数好像没什么用,因为我们只需要知道 SG 值是否为 0,而不需要知道具体数值,但如果有多个游戏,情况就不一样了。

考虑由两个有向图游戏组成的组合游戏,其中,这两个游戏相互独立,即其中任意一个游戏的任意决策都不会影响其他游戏。这个组合游戏的规则是每次要选择一个有向图游戏进行一次移动,最终无法移动的玩家失败。

定义一个有向图游戏的 SG 值为起点的 SG 值。设这两个游戏的起点分别为 A,B。下面我们先假设 SG 值只能降低,详细解释稍后再说。

显然当这两个游戏的 SG 值都为 0 时为必败状态。

SG(A)=0,SG(B)\ne 0,先手可以将 B 移动到 SG 值为 0 的后继状态(因为根据 SG 函数的定义,B 的所有后继状态的 SG 集合包含所有比 SG(B) 小的非负整数),此时为必败状态,因此原状态为必胜状态。

SG(A)=SG(B)\ne 0,先手降低任意一个游戏的 SG 值,后手总是可以将另一个游戏的 SG 值降低到与之相等,直到两个游戏的 SG 值为 0,此时这个状态由先手拿到,因此后手必胜,原状态为必败状态。

SG(A)\ne SG(B),SG(A)\ne 0,SG(B)\ne 0,先手只需将 SG 值较大的游戏降低到与另一个游戏相等,就回到了上面的状态,因此原状态为必胜状态。

在上面的讨论中,我们总是让 SG 值降低,事实上,一个状态还可能转移到 SG 值比它大的后继状态,如果一个玩家这样做,那么另一个玩家只需将该状态降回原来即可(SG 值大的总是可以降低为 SG 值小的),因此升高 SG 值是无用的。

SG 定理

A+B 表示两个相互独立的游戏组合而成的游戏,\oplus 表示异或。基于上面的讨论,我们发现当 SG(A)\oplus SG(B)=0 时,组合游戏为必败状态,即 SG(A+B)=0;当 SG(A)\oplus SG(B)\ne 0 时,组合游戏为必胜状态,即 SG(A+B)\ne 0。我们猜测 SG(A+B)=SG(A)\oplus SG(B),证明如下。

C,D 分别表示 A,B 的任意后继状态,即 A\to C,B\to D。则 A+B 的局面要么移动 A,要么移动 B,即 (A+B)\to\{C+B\}\cup \{A+D\}

由定义可得,SG(A+B)=\operatorname{mex}\big\{\{SG(C+B)\}\cup \{SG(A+D)\}\big\}

考虑数学归纳法,对两个有向图进行拓扑排序,拓扑序最大的点为出度为 0 的点。当 A,B 都为拓扑序最大的点时,SG(A+B)=SG(A)\oplus SG(B) 显然成立。假设拓扑序比 A,B 大的点都满足定理,则有 SG(C+B)=SG(C)\oplus SG(B),SG(A+D)=SG(A)\oplus SG(D)

于是有 SG(A+B)=\operatorname{mex}\big\{\{SG(C)\oplus SG(B)\}\cup \{SG(A)\oplus SG(D)\}\big\}。下面将证明 SG(A)\oplus SG(B) 不属于右边两个集合的任何一个,但比它小的非负整数都属于右边两个集合的某一个。

因为 A\to C,B\to D,所以 SG(A)\ne SG(C),SG(B)\ne SG(D),则 SG(A)\oplus SG(B) 不等于 SG(C)\oplus SG(B)SG(A)\oplus SG(D)

e 是比 SG(A)\oplus SG(B) 小的任意非负整数,k=SG(A)\oplus SG(B)\oplus e。令 SG(A),SG(B),e 三者分别与 k 异或,则至少有一者异或后会减小(因为 k 的二进制最高位的 1 一定来自这三者之一),但 e\oplus k=SG(A)\oplus SG(B)>e,因此这一者不会是 e。不妨设这一者为 SG(A),即 SG(A)\oplus k=SG(B)\oplus e<SG(A)。由 SG 函数的定义可知,A 一定存在一个后继状态 C^{'},满足 SG(C^{'})=SG(B)\oplus e,则此时有 SG(C^{'})\oplus SG(B)=e,因此 e 属于右边两个集合的某一个。原命题得证。

显然这个定理可以推广至多个游戏,设 Xn 个独立的有向图游戏的组合游戏,它们的起点分别为 s_1,s_2,\dots,s_n,则有 SG(X)=SG(s_1)\oplus SG(s_2)\oplus \dots \oplus SG(s_n)

经典模型

博弈论中有很多经典的游戏,很多博弈都可以转化为这些游戏。

巴什博弈

有一堆石子,数量为 n,两个人轮流拿石子,每个人每次至少拿 1 个,至多拿 m 个,最终拿完石子的人获胜。

SG(k) 表示 k 个石子的 SG 值,显然有 SG(0)=0

k\le m 时,k 可以转移到 0k-1 任意一个局面,因此 SG(k)=k,(k\le m)

k=m+1 时,可以转移到 1m 任意一个局面,则 SG(m+1)=0

k=m+2 时,可以转移到 2m+1 任意一个局面,则 SG(m+2)=1\dots\dots

以此类推,可以得到 SG(n)=n\bmod (m+1)。当且仅当 (m+1) \mid n 时先手必败,否则先手必胜。

Nim 游戏

n 堆石子,第 i 堆有 a_i 个石子,两个玩家轮流取走任意一堆的任意个石子,但不能不取,拿完石子的人获胜。

n 堆石子相互独立,因此可以看作 n 个独立的游戏。对于一堆有 k 个石子的游戏,显然有 SG(k)=k。因此 Nim 游戏的 SG 值为 a_i\oplus a_2\oplus \dots \oplus a_n,称其为 Nim 和。

考虑先手必胜时要如何操作,当 Nim 和为 k(k>0) 时,设 k 的二进制最高位的 1 在第 d 位,则一定存在 a_i 满足其二进制第 d 位为 1,于是 a_i\oplus k <a_i,因此只需将 a_i 取到 a_i\oplus k 即可。

阶梯 Nim

n 堆石子,第 i(i>0) 堆有 a_i 个石子,两人轮流操作,每人每次可以任选一个 k(k>0),将第 k 堆石子中的任意个石子移动到第 k-1 堆,第 0 堆的石子无法移动,最后无法操作的人输。

结论:先手必败当且仅当奇数堆中的石子数异或和为 0

证明:当奇数堆异或和为 0 时,无论怎么移动都会使奇数堆的异或和大于 0。如果先手移动偶数堆的石子到奇数堆,后手可以将移动到奇数堆的石子再往下移动到偶数堆,异或和就变回了 0;如果先手移动奇数堆,可以看作两人在奇数堆上玩 Nim 游戏,所以后手可以通过移动奇数堆的石子使异或和变回 0

综上,奇数堆异或和为 0 的局面都由先手拿到,异或和不为 0 的局面都由后手拿到。无法移动的局面为所有石子都在第 0 堆,此时奇数堆的异或和为 0,因此该局面由先手拿到,故先手必败。

k-Nim

n 堆石子,第 i 堆有 a_i 个石子,每次可以从不超过 k 堆中各取走任意个石子,不能操作的人输。

结论:设 s_i 表示 a_1,a_2\dots ,a_n 二进制下第 i1 的个数。则先手必败当且仅当对所有的 s_i,都有 s_i\equiv 0 \pmod{(k+1)}

证明:

s_i^{'}=s_i\bmod (k+1)

若所有的 $s_i^{'}$ 为 $0$,由于最多选择 $k$ 堆,因此 $s_i$ 的变化量的绝对值不会超过 $k$,并且至少有一个 $s_i$ 产生变化,所以该操作结束后一定会使得某些 $s_i^{'}$ 不为 $0$。 若 $s_i^{'}$ 不全为 $0$,下面将证明一定存在一种合法的方案使得所有的 $s_i^{'}$ 变回 $0$。 考虑数学归纳法,假设当前在二进制下的第 $i$ 位,满足对所有的 $j>i$,$s_j^{'}$ 都已经变为了 $0$,并且已经改变了 $m(m\le k)$ 堆石子。 显然对于已经改变的 $m$ 堆石子,我们任意改变其二进制下第 $i$ 位的值都是合法的。设这 $m$ 堆石子的第 $i$ 位上有 $a$ 个 $1$ 和 $b$ 个 $0$。 下面分情况讨论, 1. $a\ge s_i^{'}$,只需要从这 $a$ 个中选择 $s_i^{'}$ 个将其变为 $0$ 即可。 2. $b\ge (k+1)-s_i^{'}$,只需要从这 $b$ 个中选择 $(k+1)-s_i^{'}$ 个将其变为 $1$ 即可。 3. 上述两种情况都不满足,即 $a<s_i^{'}$ 且 $b<(k+1)-s_i^{'}$。那么先将这 $a$ 个 $1$ 变为 $0$,然后再从这 $m$ 堆之外选 $s_i^{'}-a$ 堆第 $i$ 位为 $1$ 的,并将其变为 $0$ 即可,此时选择的总堆数变为了 $m+s_i^{'}-a=b+s_i^{'}$,由于 $b<(k+1)-s_i^{'}$,所以 $b+s_i^{'}<k+1$,这种取法是合法的。 综上,一定存在一种合法的方案使得所有的 $s_i^{'}$ 变回 $0$。 故 $s_i^{'}$ 全为 $0$ 为必败状态,$s_i^{'}$ 不全为 $0$ 为必胜状态,结论得证。 ### 威佐夫博弈 有两堆石子,数量任意,可以不同。游戏开始由两个人轮流取石子。游戏规定,每次有两种不同的取法:一是可以在任意的一堆中取走任意多的石子;二是可以在两堆中同时取走相同数量的石子。最后把石子全部取完者为胜者。 结论:设石子数量为 $a,b(a<b)$,当且仅当 $a=\lfloor \frac{\sqrt{5}+1}{2} \rfloor\times (b-a)$ 时先手必败。 证明太抽象了,[自己看题解吧](https://www.luogu.com.cn/problem/solution/P2252)。 ## 例题 博弈论常用解题方法有:转化为经典模型;手算几个寻找结论;打表 SG 找规律;通过一些方法维护 SG 函数;将原游戏转化为一个新的游戏。 ### 【模板】Nim 游戏 [题目链接](https://www.luogu.com.cn/problem/P2197) 模板题。 ```cpp #include <bits/stdc++.h> typedef long long ll; using namespace std; const int N=1e3+7; int main() { int T; cin>>T; while(T--) { int n,ans=0; scanf("%d",&n); for(int i=1;i<=n;++i) { int x; scanf("%d",&x); ans^=x; } if(ans!=0) puts("Yes"); else puts("No"); } return 0; } ``` ### 【模板】威佐夫博弈 [题目链接](https://www.luogu.com.cn/problem/P2252) 模板题。 ```cpp #include <bits/stdc++.h> typedef long long ll; using namespace std; const int N=5e5+7; int main() { double k=(sqrt(5.0)+1)/2.0; int a,b; cin>>a>>b; if(a>b) swap(a,b); if(a==int((b-a)*k)) puts("0"); else puts("1"); return 0; } ``` ### CF388C Fox and Card Game [题目链接](https://www.luogu.com.cn/problem/CF388C) 桌子上有 $n(n\le 100)$ 堆牌,每堆有 $s_i(s_i\le 100)$ 张牌。每张牌上都有一个正整数。C 可以从任何非空牌堆的顶部取出一张牌,J 可以从任何非空牌堆的底部取出一张牌。C 先取,当所有的牌堆都变空时游戏结束。他们都想最大化他所拿牌的分数(即每张牌上正整数的和)。问他们所拿牌的分数分别是多少? 先考虑所有牌堆都为偶数的情况,结论为对于每堆牌,先手会取走顶部的一半,后手会取走底部的一半,将这个局面记为稳定局面,设此时先手有 $a$ 分,后手有 $b$ 分。 证明:设先后手最终分别获得 $x,y$ 分。无论一方怎么拿牌,另一方都可以与对方对照着拿牌来迫使最终达到稳定局面,因此他们获得的分数一定不会低于稳定局面,即 $x\ge a,y\ge b$,又因为两人拿到的分数之和不变,即 $x+y=a+b$,因此 $x=a,y=b$。 接下来考虑奇数牌堆,对于某一个奇数牌堆,某一方取走一张牌后,这个牌堆就变为了偶数堆,两人各分一半,因此,谁先取了奇数牌堆,谁就多拿走了中间那张牌。所以先手会取中间牌最大的奇数牌堆,后手会取第二大的,先手再取第三大的,就这样一直取下去,直到没有奇数牌堆。剩下就一人一半就行了。 用大根堆将奇数牌堆按中间牌从大到小排序,依次取出即可。时间复杂度 $O(n\log n)$。 ```cpp #include <bits/stdc++.h> typedef long long ll; using namespace std; const int N=1e2+7; int a[N][N],b[N][N]; priority_queue<int> q; int main() { int n,ans1=0,ans2=0; cin>>n; for(int i=1;i<=n;++i) { int m; scanf("%d",&m); for(int j=1;j<=m;++j) { scanf("%d",&a[i][j]); b[i][j]=b[i][j-1]+a[i][j]; } ans1+=b[i][m/2];ans2+=b[i][m]-b[i][(m+1)/2]; if(m&1) q.push(a[i][(m+1)/2]); } bool flag=0; while(!q.empty()) { if(!flag) ans1+=q.top(); else ans2+=q.top(); flag^=1;q.pop(); } cout<<ans1<<" "<<ans2; return 0; } ``` ### CF794E Choosing Carrot [题目链接](https://www.luogu.com.cn/problem/CF794E) 有一排 $n(n\le 3\times 10^5)$ 个数,甲乙轮流取,每次可以选择两端中的一端取走一个数,直到剩下一个数,甲希望剩下的数最大,乙希望最小。同时,**在游戏开始前**,甲可以提前操作 $k(0\le k\le n-1)$ 次(操作规则和上面一样),**游戏开始后**甲是先手。问对于每一个 $k$,最后剩下来的数是几。 先考虑 $k=0$ 的情况,如果 $n$ 是偶数,设 $mid=\frac{n}{2}$,与上题类似,无论先手怎么取,后手都可以与先手相对着取,则可以保证最后留下来的数不大于 $\max\{ a_{mid},a_{mid+1}\}$。而对于先手,如果 $a_{mid}>a_{mid+1}$,先手就先取右端,之后无论后手怎么取,先手都和对方相对着取,这样就留下了 $a_{mid}$,当 $a_{mid}<a_{mid+1}$ 时也同理,因此先手可以保证最后留下来的数不小于 $\max\{ a_{mid},a_{mid+1}\}$。综上,最后留下来的数一定为 $\max\{ a_{mid},a_{mid+1}\}$。 如果 $n$ 是奇数,设 $mid=\frac{n+1}{2}$先手任取一端后就变为了偶数的情况,但此时是后手拿到偶数的情况,因此后手会留下中间两个中较小的一个,而先手希望最后留下来的最大,因此答案为 $\max\left\{ \min\{ a_{mid-1},a_{mid}\},\min\{ a_{mid},a_{mid+1}\} \right\}$。 接下来考虑 $k>0$ 的情况,当留下的区间确定后该情况的答案也确定了,先手会选择所有情况中最大的答案。假设 $n$ 为偶数,设 $mid=\frac{n}{2}$,当 $k=0$ 时答案为 $\max\{ a_{mid},a_{mid+1}\}$; 当 $k=2$ 时答案为 $\max\left\{ \max\{ a_{mid-1},a_{mid}\},\max\{ a_{mid},a_{mid+1}\},\max\{ a_{mid+1},a_{mid+2}\} \right\}$; 当 $k=4$ 时答案为 $\max\left\{ \max\{ a_{mid-2},a_{mid-1}\},\dots,\max\{ a_{mid+2},a_{mid+3}\} \right\}$。 可以发现当 $n$ 为偶数时,$k+2$ 相当于在 $k$ 的答案的基础上左右扩展一个。同理可以推得 $n$ 为奇数时也符合这个结论。注意要特判 $k=n-1$ 的情况。 时间复杂度为 $O(n)$。 ```cpp #include <bits/stdc++.h> typedef long long ll; using namespace std; const int N=3e5+7,inf=1e9+7; int a[N],b[N]; int main() { int n; cin>>n; for(int i=1;i<=n;++i) scanf("%d",&a[i]); if(n==1) {cout<<a[1];return 0;} for(int i=2;i<n;++i) b[i]=max(min(a[i-1],a[i]),min(a[i],a[i+1])); int max0=-inf,max1=-inf,h=n/2,t; if(n&1) t=h+2,max1=b[h+1],printf("%d ",max1),max0=a[h+1]; else t=h+1,max0=max(a[h],a[t]),printf("%d ",max0); for(int i=n-1;i>=2;--i) { if(i&1) max1=max(max1,max(b[h],b[t])),--h,++t,printf("%d ",max1); else max0=max(max0,max(a[h],a[t])),printf("%d ",max0); } printf("%d ",max0); return 0; } ``` ### [AGC002E] Candy Piles [题目链接](https://www.luogu.com.cn/problem/AT_agc002_e) 桌上有 $n(n\le 10^5)$ 堆糖果,第 $i$ 堆糖果有 $a_i$ 个糖。两人在玩游戏,轮流进行,每次可以将当前最大的那堆糖果全部吃完,或者将每堆糖果吃掉一个。吃完的人输,假设两人足够聪明,问谁有必胜策略? 将这 $n$ 堆糖果按数量从多到少依次排序,每堆糖果排成一排,设左上角为 $(1,1)$,如图, ![](https://cdn.luogu.com.cn/upload/image_hosting/gyy37iq0.png) 如果将当前最大的那堆糖果全部吃完(操作一),就会变成 $(2,1)$, ![](https://cdn.luogu.com.cn/upload/image_hosting/5pjaxoyp.png) 如果将每堆糖果吃掉一个(操作二),就会变成 $(1,2)$, ![](https://cdn.luogu.com.cn/upload/image_hosting/u53ij6pc.png) 于是发现,设当前在 $(i,j)$,则执行一次操作一会变成 $(i+1,j)$,相当于向右走一步;执行一次操作二会变成 $(i,j+1)$,相当于向下走一步。 显然如果一个点的右边和下边都没有点,那该点为必败点。一个点为必胜点当且仅当它的右边和下边至少有一个必败点,一个点为必败点当且仅当它的右边和下边都为必胜点。 则有结论:$(i,j)$ 和 $(i+1,j+1)$ 的胜败状态相同。 证明:设 $0$ 表示必败,$1$ 表示必胜。若 $(i,j)$ 为 $1$,则 $(i+1,j)$ 和 $(i,j+1)$ 中至少有一个为 $0$,因此 $(i+1,j+1)$ 一定为 $1$。 若 $(i,j)$ 为 $0$,则 $(i+1,j)$ 和 $(i,j+1)$ 都为 $1$。考虑反证法,假设 $(i+1,j+1)$ 为 $1$,则 $(i+2,j)$ 和 $(i,j+2)$ 必须为 $0$,进而有 $(i+2,j+1)$ 和 $(i+1,j+2)$ 都为 $1$,由此可推出 $(i+1,j+1)$ 为 $0$,与假设矛盾,因此 $(i+1,j+1)$ 为 $0$。 综上,$(i,j)$ 和 $(i+1,j+1)$ 的胜败状态相同。 因此只需从 $(1,1)$ 一直往右下方走,直到右下方没有点,此时只能一直向右走或者一直向下走,则只需判断右边和下边剩余点的奇偶即可。 时间复杂度 $O(n)$。 ```cpp #include <bits/stdc++.h> typedef long long ll; using namespace std; const int N=1e5+7; int a[N]; bool cmp(int x,int y) {return x>y;} int main() { int n; cin>>n; for(int i=1;i<=n;++i) scanf("%d",&a[i]); sort(a+1,a+1+n,cmp); int mx=0; for(int i=1;i<=n;++i) if(a[i]>=i) mx=i; if((a[mx]-mx)&1) puts("First"); else { int cnt=0; for(int i=mx+1;i<=n;++i) { if(a[i]<mx) break; ++cnt; } if(cnt&1) puts("First"); else puts("Second"); } return 0; } ``` ### SP11414 COT3 - Combat on a tree [题目链接](https://www.luogu.com.cn/problem/CF1110G) 两人在一棵 $n(n\le 10^5)$ 个节点的树上玩游戏,根节点为 $1$,每个节点最初都是黑色或白色,他们轮流执行以下操作:从当前树中选择一个白色节点 $v$,将路径 $(1,v)$ 上的所有白色节点都变为黑色。无法操作的玩家失败,问先手是否能够必胜,并求出第一步的方案。 考虑维护每个点的 SG 值,设 $SG(i)$ 表示以 $i$ 为根的子树的 SG 值,$SG(i)(k)$ 表示在 $i$ 子树内选择点 $k$ 进行操作后的 SG 值,$son_i$ 表示 $i$ 的儿子数量,$i_j$ 表示 $i$ 的第 $j$ 个儿子。 如果 $i$ 是黑点,可以发现 $i$ 的儿子间都是独立的,因此 $SG(i)=SG(i_1)\oplus SG(i_2)\oplus \dots \oplus SG(i_{son_i})$, 但只求 SG 值是不够的,因为题目要求第一步选哪些点可以必胜,也就是从 $i$ 的所有后继状态中找出 SG 值为 $0$ 的状态,因此我们还需要维护 $i$ 的所有后继状态,考虑到每个点都至多有 $n$ 个后继状态,因此这是可行的。 设 $s_i=SG(i_1)\oplus SG(i_2)\oplus \dots \oplus SG(i_{son_i})$,若任选 $i$ 子树中的一点 $k$,设 $k$ 在 $i_j$ 子树内,则当前局面的 SG 值为 $SG(i)(k)=SG(i_1)\oplus \dots \oplus SG(i_{j-1})\oplus SG(i_j)(k)\oplus SG(i_{j+1})\oplus \dots \oplus SG(i_{son_i})$,即 $SG(i)(k)=s_i\oplus SG(i_j)\oplus SG(i_j)(k)$, 这是 $i$ 的一个后继状态。我们发现 $SG(i_j)(k)$ 是 $i_j$ 的一个后继状态,由于 $i_j$ 能选的点,$i$ 也可以选,因此 $i_j$ 的任意一个后继状态异或上 $s_i\oplus SG(i_j)$ 就变成了 $i$ 的后继状态。 这样就得到了一种解法,但一个个异或复杂度太高,我们需要一个能够区间异或的数据结构,自然想到了 01Trie,对每个节点都建一个 01Trie 来保存该子树的所有后继状态的 SG 值,要求 $i$ 的 01Trie,只需将 $i$ 的每一个儿子 $i_j$ 的 01Trie 整体异或上 $s_i\oplus SG(i_j)$,然后将所有儿子的 01Trie 合并就得到了 $i$ 的 01Trie,合并时可以用启发式合并或者线段树合并。需要注意由于要求第一步的方案,因此还要维护每个 SG 值是由哪些点产生的,合并时用启发式合并。 上面是 $i$ 为黑点的情况,而当 $i$ 为白点时,任选一点后,$i$ 就变成了黑点,就和上面一样了,唯一不同的是多了一个选择点 $i$ 的状态。因此白点就是在黑点的 01Trie 上再插入一个 $s_i$。 如果使用线段树合并,时间复杂度为 $O(n\log n)$;使用启发式合并为 $O(n\log^2 n)$。 ```cpp //线段树合并 #include <bits/stdc++.h> typedef long long ll; using namespace std; const int N=1e5+7,LEN=17; struct node { int ch[2],tag,id,siz; }tr[N*18]; int c[N],tot=0,rt[N],sg[N],tot_id=0,ANS[N]; vector<int> nx[N],num[N]; void push(int x,int k) { if(k==0) return ; if((tr[x].tag)&(1<<(k-1))) swap(tr[x].ch[0],tr[x].ch[1]); if(tr[x].ch[0]) tr[tr[x].ch[0]].tag^=tr[x].tag; if(tr[x].ch[1]) tr[tr[x].ch[1]].tag^=tr[x].tag; tr[x].tag=0; } void merge(int &now,int re,int k) { if(!now||!re) {now=now+re;return ;} if(k==0) { tr[now].tag=0; int x=tr[now].id,y=tr[re].id; if(!x||!y) tr[now].id+=tr[re].id; else { if(num[x].size()<num[y].size()) swap(x,y),swap(tr[now].id,tr[re].id); for(int i=0;i<num[y].size();++i) num[x].push_back(num[y][i]); } return ; } if(tr[now].tag!=0) push(now,k); if(tr[re].tag!=0) push(re,k); merge(tr[now].ch[0],tr[re].ch[0],k-1); merge(tr[now].ch[1],tr[re].ch[1],k-1); tr[now].siz=tr[tr[now].ch[0]].siz+tr[tr[now].ch[1]].siz+1; } void ins(int &now,int v,int k,int ID) { now=++tot; if(k==0) { tr[now].id=++tot_id; num[tot_id].push_back(ID); tr[now].siz=1; return ; } int tmp=(v>>(k-1))&1; ins(tr[now].ch[tmp],v,k-1,ID); tr[now].siz=tr[tr[now].ch[tmp]].siz+1; } int mex(int now,int k) { if(!now) return 0; int ans=0; while(1) { if(!now) return ans; if(tr[now].tag!=0) push(now,k); if(tr[now].siz==(1<<(k+1))-1) return ans+(1<<k); if(tr[tr[now].ch[0]].siz==(1<<k)-1) ans+=(1<<(k-1)),now=tr[now].ch[1]; else now=tr[now].ch[0]; --k; } return ans; } void dfs(int x,int fa) { int tmp=0; for(int i=0;i<nx[x].size();++i) { int y=nx[x][i]; if(y==fa) continue; dfs(y,x); tmp^=sg[y]; } if(!c[x]) ins(rt[x],tmp,LEN,x); for(int i=0;i<nx[x].size();++i) { int y=nx[x][i]; if(y==fa) continue; if(rt[y]) tr[rt[y]].tag^=tmp^sg[y]; merge(rt[x],rt[y],LEN); } sg[x]=mex(rt[x],LEN); } int main() { int n; cin>>n; for(int i=1;i<=n;++i) scanf("%d",&c[i]); for(int i=1;i<n;++i) { int x,y; scanf("%d%d",&x,&y); nx[x].push_back(y); nx[y].push_back(x); } dfs(1,0); if(sg[1]==0) puts("-1"); else { int now=rt[1]; for(int i=LEN;i>0;--i) { if(tr[now].tag!=0) push(now,i); now=tr[now].ch[0]; } int cnt=num[tr[now].id].size(); for(int i=0;i<cnt;++i) ANS[i]=num[tr[now].id][i]; sort(ANS,ANS+cnt); for(int i=0;i<cnt;++i) printf("%d ",ANS[i]); } return 0; } ``` ### CF1451F Nullify The Matrix [题目链接](https://www.luogu.com.cn/problem/CF1451F) 有一个 $n\times m$ 的棋盘,每个格子有一个非负整数的权值。 两个人轮流操作,每次操作选择两个权值非 $0$ 的格子,左上的点为起点,右下的为终点,每次移动只能向右走一格或者向下走一格,首先给起点的权值减一个正整数,然后从起点移动到终点,路径上每个点的权值随意加减(也可以不改变),但是都不能改成负数。 不能操作(全变成 $0$)的人就输了,问谁必胜。 设当前在 $(i,j)$,则每走一步都会使 $i+j$ 加上 $1$。将 $i+j$ 相同的分为一组,记为第 $i+j$ 组,也就是将每一斜行分为一组,设 $s_{i}$ 表示 $i$ 组内所有格子权值的异或和,则有结论:当且仅当所有的 $s_i$ 都为 $0$ 时先手必败。 证明:当所有的 $s_i$ 都为 $0$ 时,由于起点的权值只能减小,因此起点所在的组的异或和在操作后一定不为 $0$。 当存在 $s_i$ 不为 $0$ 时,则找到最小的 $i$ 使得 $s_i\ne 0$,将第 $i$ 组看作一个 Nim 游戏,则一定存在一种合法方案能够降低某个格子的值并使得 $s_i$ 变为 $0$,然后从该点出发,以 $(n,m)$ 为结尾,就可以从 $s_{i+1}$ 遍历到 $s_{n+m}$。对于每一个 $s_j (i+1\le j \le n+m)$,若 $s_j=0$ 则保持不变;若 $s_j\ne 0$,则修改其中任意一个格子的权值使得 $s_j=0$ 即可。 时间复杂度 $O(nm)$。 ```cpp #include <bits/stdc++.h> typedef long long ll; using namespace std; const int N=1e2+7; int a[N][N]; int main() { int T; cin>>T; while(T--) { int n,m; scanf("%d%d",&n,&m); for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) scanf("%d",&a[i][j]); bool flag=0; for(int i=2;i<=n+m;++i) { int sum=0; for(int j=1,k=i-1;j<=n&&k>=1;++j,--k) if(k<=m) sum^=a[j][k]; if(sum) {flag=1;break;} } if(flag) puts("Ashish"); else puts("Jeel"); } return 0; } ``` ### CF1149E Election Promises [题目链接](https://www.luogu.com.cn/problem/CF1149E) 给定一张有向无环图,每个点有一个非负整数点权。两人轮流操作,每次操作为选择一个权值为不为 $0$ 的点,并将它的值**降低**为一个非负整数,对于它的出点(即走一步能到达的点),可以将其值任意修改为一个非负整数(可以保持不变)。最终无法操作的人输,问先手能否必胜,如果必胜请输出第一步的方案。 和上题类似,我们可以将每个点分组,使得它们满足:任意操作都会使必败状态变为必胜状态,必胜状态一定能够通过合法的操作变为必败状态。 我们令每个点和它的所有出点都不在同一组,设每个组的权值 $s_i$ 为组内所有点权值的异或和,当且仅当所有组都为 $0$ 时为必败状态,这样就满足了任意操作都会使其变为必胜状态。接下来考虑如何分组才能使得必胜状态能够转移到必败状态。 在上题中选择一个点后可以修改其后的所有组,类似地,我们希望对于每一组,选择其中任意一点后可以修改该组前面的所有组。也就是该组前面的每一组都有该点的出点,又因为当前组没有出点,自然想到了 $\operatorname{mex}$ 操作,一个点所在的组为:对其所有出点所在的组的编号进行 $\operatorname{mex}$ 操作所得到的数。于是对原图进行拓扑排序,按拓扑序的逆序进行分组即可。 这样对于必胜状态,只需找到编号最大的权值不为 $0$ 的组,该组的点的出点所在的组一定包含了前面所有组,就和上一题一样修改就行了,输出方案也比较简单。 时间复杂度 $O(n)$。 ```cpp #include <bits/stdc++.h> typedef long long ll; using namespace std; const int N=2e5+7; vector<int> nx[N],last[N],num[N]; ll a[N],sum[N]; int deg[N],col[N]; deque<int> q; bool vis[N]; int main() { int n,m; cin>>n>>m; for(int i=1;i<=n;++i) scanf("%lld",&a[i]); for(int i=1;i<=m;++i) { int x,y; scanf("%d%d",&x,&y); nx[x].push_back(y); last[y].push_back(x); ++deg[x]; } for(int i=1;i<=n;++i) if(!deg[i]) { q.push_back(i); col[i]=1; sum[1]^=a[i]; num[1].push_back(i); } int mx=1; while(!q.empty()) { int x=q.front(); q.pop_front(); for(int i=0;i<last[x].size();++i) { int y=last[x][i]; --deg[y]; if(!deg[y]) { q.push_back(y); for(int j=0;j<nx[y].size();++j) vis[col[nx[y][j]]]=1; int mex=1; while(vis[mex]) ++mex; col[y]=mex;sum[mex]^=a[y]; mx=max(mx,mex); num[mex].push_back(y); for(int j=0;j<nx[y].size();++j) vis[col[nx[y][j]]]=0; } } } int flag=0; for(int i=mx;i;--i) { if(!sum[i]) continue; int cnt=1; while(sum[i]>>cnt) ++cnt; for(int j=0;j<num[i].size();++j) { int x=num[i][j]; if(a[x]&(1<<(cnt-1))) {a[x]^=sum[i];flag=x;break;} } for(int j=0;j<nx[flag].size();++j) { int x=nx[flag][j]; if(sum[col[x]]) a[x]^=sum[col[x]],sum[col[x]]=0; } } if(!flag) puts("LOSE"); else { puts("WIN"); for(int i=1;i<=n;++i) printf("%lld ",a[i]); } return 0; } ``` ### P3235 [HNOI2014] 江南乐 [题目链接](https://www.luogu.com.cn/problem/P3235) [写过题解了](https://a154051.gitee.io/2023/12/06/sol-P3235-jiang-nan-le/) ## 参考资料 [公平组合游戏 - OI Wiki](https://oi-wiki.org/math/game-theory/impartial-game/) [10170 Sprague-Grundy定理是怎么想出来的 - 王赟 Maigo](https://zhuanlan.zhihu.com/p/20611132?utm_id=0)