下面的所有知识均属博弈论范畴。
SG \operatorname{SG} SG 函数
Nim \operatorname{Nim} Nim 博弈
在介绍 SG \operatorname{SG} SG 函数前,我们先介绍一种经典的博弈—— Nim \operatorname{Nim} Nim 博弈,并从中获得一定的启发。
-
问题:有 n n n 堆石子,第 i i i 堆石子里面有 A i A_i Ai 枚石子,每人每次可从任意且至多一堆石子里取出任意多枚石子扔掉,可以取完,不能不取,最后无石子可取的人判负。假如甲是先手,且告诉你这 n n n 堆石子的数量,他想知道是否存在先手必胜的策略。
-
结论:若 xor i = 1 n A i ≠ 0 \operatorname{xor}_{i = 1}^n A_i \ne 0 xori=1nAi=0,先手必胜,否则后手必胜。
我们发现,当甲做完最后一步操作,将所有石子取完时,甲就赢了。此时,说明每堆石子均为 0 0 0 是一个必胜态。
所以,甲要进行的操作一定要使得自己最终一定要到达必胜态,并且对方永远不可能到达必胜态。
可以发现,如果甲操作后 xor i = 1 n A i = 0 \operatorname{xor}_{i = 1}^n A_i = 0 xori=1nAi=0,那么对方操作一次一定会使得 xor i = 1 n A i ≠ 0 \operatorname{xor}_{i = 1}^n A_i \ne 0 xori=1nAi=0。
所以,甲只要保持自己每次操作后 xor i = 1 n A i = 0 \operatorname{xor}_{i = 1}^n A_i = 0 xori=1nAi=0,就会使得对方永远到达不了必胜态,同时若干次操作后甲一定能够到达必胜态(因为石子数只会减少)。
实际上,我们可以称所有 xor i = 1 n A i = 0 \operatorname{xor}_{i = 1}^n A_i = 0 xori=1nAi=0 的状态为必胜态,所有 xor i = 1 n A i ≠ 0 \operatorname{xor}_{i = 1}^n A_i \ne 0 xori=1nAi=0 的状态为必负态。
最后一个问题:能不能保证每一次操作都能使得甲从对方操作后形成的必负态走向一个必胜态?答案是能的。
我们假设对方操作完后的必负态为 xor i = 1 n A i = x \operatorname{xor}_{i = 1}^n A_i = x xori=1nAi=x,设 x x x 的最高位为 k k k,那么必定存在至少一个 A i A_i Ai 满足第 k k k 位为 1 1 1,我们只要任意选择这样的一个 A i A_i Ai,进行操作 A i ← A i xor x A_i \leftarrow A_i \operatorname{xor} x Ai←Aixorx,就能使得甲到达必胜态。
可以发现, A i > A i xor x A_i > A_i \operatorname{xor} x Ai>Aixorx,所以这样的操作是合法的。
有向图游戏
可以发现, Nim \operatorname{Nim} Nim 游戏满足如下三条定则:
- 两名玩家交替行动;
- 任意时刻,可以执行的合法行动与轮到哪名玩家无关;
- 不能够行动的玩家判负。
满足它们的游戏被称为公平组合游戏。
同时我们还可以发现,如果把所有的状态看作是图上的一个个节点,那么 Nim \operatorname{Nim} Nim 博弈的过程非常像一个有向图。
所以,我们完全可以把 Nim \operatorname{Nim} Nim 游戏转化为:
- 在初始节点摆一枚棋子,两人轮流拿棋子在有向图上移动一条边,无法移动的人判负。
这种游戏被称为有向图游戏。
上述内容证明了:如果把状态看作节点,所有的公平组合游戏都能够转化为有向图游戏。
正文—— SG \operatorname{SG} SG 函数
我们把先手必胜的节点称为必胜态,反之称为必负态。
那么在这个有向图中,终点显然为必负态,因为在这里无法移动。
否则,我们该如何判定一个状态是否为必胜态或必负态呢?
这里就要用到 SG \operatorname{SG} SG 函数了。注意:它只能够在有向图游戏中使用。
对于终点,我们设它为 T T T。像这样的必负态,我们设它的 SG ( T ) = 0 \operatorname{SG}(T) = 0 SG(T)=0。
如果一个节点 U U U 存在后继节点 V V V 的 SG ( V ) = 0 \operatorname{SG}(V) = 0 SG(V)=0,那么我们可以选择走这个状态,把这个必负态抛给对手。所以, U U U 节点即为必胜态,有 SG ( U ) > 0 \operatorname{SG}(U) > 0 SG(U)>0。
反之,如果一个节点 U U U 不存在后继节点 V V V 的 SG ( V ) = 0 \operatorname{SG}(V) = 0 SG(V)=0,那么这个节点无论怎么走都将给对方抛一个必胜态,所以 SG ( U ) = 0 \operatorname{SG}(U) = 0 SG(U)=0。
把以上两条公式结合起来,就有:
SG
(
U
)
=
mex
{
SG
(
V
)
}
\operatorname{SG}(U) = \operatorname{mex} \{\operatorname{SG}(V)\}
SG(U)=mex{SG(V)}
其中
mex
\operatorname{mex}
mex 表示的是集合中没有出现过的最小非负整数。
由此,我们只要从终点 T T T 开始,把整个有向图每一个节点的 SG ( U ) \operatorname{SG}(U) SG(U) 推出来,那么起点 S S S 的 SG ( S ) \operatorname{SG}(S) SG(S) 即决定了整个有向图游戏先手是必胜还是必负。
SG \operatorname{SG} SG 定理
并不是每一个公平组合游戏都能够转化为一个有向图游戏 ,因为可以有多个。
如果一个游戏存在多个有向图游戏,那么整个游戏的 SG ( G ) \operatorname{SG}(G) SG(G) 等于每一个有向图游戏的 SG ( S k ) \operatorname{SG}(S_k) SG(Sk) 的异或和。
即:
SG
(
G
)
=
xor
k
=
1
n
SG
(
S
i
)
\operatorname{SG}(G) = \operatorname{xor}_{k = 1}^n \operatorname{SG}(S_i)
SG(G)=xork=1nSG(Si)
这被称为
SG
\operatorname{SG}
SG 定理,它给了我们把整个游戏进行分治的可能。证明略去,因为笔者不是很会。
由此,我们可以将 Nim \operatorname{Nim} Nim 博弈转化为 n n n 个有向图游戏,那么每一堆石子都代表了一个有向图游戏。可以推出,第 i i i 堆的 SG \operatorname{SG} SG 函数值即为 A i A_i Ai,那么总的答案即为 xor k = 1 n A i \operatorname{xor}_{k = 1}^n A_i xork=1nAi。
比较好像的博弈论动态规划 ,不过笔者第一眼认为是贪心然后就狂吃 WA。
设状态 f i , j f_{i, j} fi,j 表示当前数对为 ( i , j ) (i, j) (i,j) 时我们的选择。注意 i i i 的特点是只含有 2 , 3 2,3 2,3 这两个质因子,所以可以预处理出所有合法的 i i i 进行一定的状态压缩。
我们可以通过 SG \operatorname{SG} SG 函数预处理出所有的必胜态和必败态。当然,这里不需要严格的 mex \operatorname{mex} mex 操作,只要判断是否为必胜态或者必败态即可,这也是大多数 SG \operatorname{SG} SG 函数题的套路。
可以发现,当 i + j < n i + j < n i+j<n 并且三种操作中存在一种操作使得操作后的 i ′ + j ′ ≥ n i'+j' \ge n i′+j′≥n,那么我们可以直接进行这个操作,使得数对变为 ( i ′ , j ′ ) (i', j') (i′,j′),这样对手会恰好进行一个 1 1 1 操作,然后输掉游戏。因此,满足上述条件的 ( i , j ) (i, j) (i,j) 为必胜态, f i , j = 1 f_{i, j} = 1 fi,j=1。
否则,我们模仿 SG \operatorname{SG} SG 函数的运算法则,如果三个操作中存在一个操作使得操作后的 ( i ′ , j ′ ) (i', j') (i′,j′) 为必败态,那么我们可以把这个必败态抛给对手。换言之,此时的 ( i , j ) (i, j) (i,j) 为必胜态, f i , j = 1 f_{i, j} = 1 fi,j=1。
如果上述条件均不满足,那么 ( i , j ) (i, j) (i,j) 为必败态, f i , j = 0 f_{i, j}=0 fi,j=0。
在 _opt \operatorname{\_opt} _opt 函数中,我们每次选择一个必败态,将其抛给对手即可。
代码:
#define MAXN 30005
int cnt, mp[MAXN], seq[MAXN], f[MAXN >> 8][MAXN];
bool Flag = false;
inline void Init(int n)
{
for(register int i = 1; i < n; i++)
{
int t = i;
while(t % 2 == 0) t /= 2;
while(t % 3 == 0) t /= 3;
if(t == 1) seq[cnt] = i, mp[i] = cnt++;
}
for(register int yi = n - 1; yi >= 0; yi--)
{
for(register int t = cnt - 1; t >= 0; t--)
{
int xi = seq[t];
if(xi + yi >= n) continue;
if(xi + yi + 1 >= n || xi * 2 + yi >= n || xi * 3 + yi >= n) f[t][yi] = 1;
else if(!f[mp[1]][xi + yi] || !f[mp[xi * 2]][yi] || !f[mp[xi * 3]][yi]) f[t][yi] = 1;
}
}
return;
}
extern "C" int _opt(int n, int xi, int yi)
{
if(!Flag) Init(n), Flag = true;
if(xi + yi + 1 >= n || !f[mp[1]][xi + yi]) return 1;
if(xi * 2 + yi >= n || !f[mp[xi * 2]][yi]) return 2;
if(xi * 3 + yi >= n || !f[mp[xi * 3]][yi]) return 3;
}