博弈论
相关概念
公平组合游戏
公平组合游戏(ICG 游戏)定义为:
双人(游戏有两个人参与),回合制(二者轮流做出决策),信息完全公开(双方均知道游戏的完整信息)。
任意一个游戏者在某一确定状态可以作出的决策集合只与当前的状态有关,而与游戏者无关。
游戏中的同一个状态不可能多次抵达,游戏以玩家无法行动为结束,且游戏一定会在有限步后以非平局结束。
因此 ICG 游戏可以转化成 DAG,一个状态是一个点,一个决策是一条边,而终止状态指的就是出度为 0 0 的点,称其为博弈状态图。
这样,对于组合游戏中的每一对弈,我们都可以将其抽象成图中的一条从某一顶点到出度为 0 0 的点的路径。
必胜态与必败态
在公平组合游戏中,定义:
(先手)必胜状态:从当前状态开始操作的玩家有必胜策略。
(先手)必败状态:无论从当前状态开始操作的玩家如何操作,另外一位玩家在之后都有相对应必胜策略。
通过推理可得下面两条定理:
一个状态是必胜状态,当且仅当它存在至少一个必败的后继状态。
一个状态是必败状态,当且仅当它的所有后继状态均为必胜状态。
如果博弈图是一个 DAG ,则通过这两个定理,我们可以在绘出博弈图的情况下用 O ( n + m ) O ( n + m ) 的时间复杂度 DP 出每个态是必胜状态还是必败状态。
有向图游戏
有向图游戏:在一个有向无环图中,只有一个起点,上面有一个棋子,两个玩家轮流沿着有向边推动棋子,不能走的玩家判负。
有向图游戏的和:一个游戏可能是由多个子游戏组成的(将其记作 G 1 ∼ n G 1 ∼ n ),如果整个游戏进行时,玩家可以任意选取一个子游戏进行上面的合法操作,所有子游戏均无法进行合法操作时判负。整个游戏称为子游戏的游戏和,记作 G = G 1 + G 2 + ⋯ + G n G = G 1 + G 2 + ⋯ + G n 。
SG 定理
对于状态 x x 和它的所有 k k 个后继状态 y 1 ∼ k y 1 ∼ k ,定义 SG 函数:
SG ( x ) = mex { SG ( y 1 ) , SG ( y 2 ) , ⋯ , SG ( y k ) } SG ( x ) = mex { SG ( y 1 ) , SG ( y 2 ) , ⋯ , SG ( y k ) }
而对于由 n n 个有向图游戏组成的组合游戏,设它们的起点分别为 s 1 ∼ n s 1 ∼ n ,则有 SG 定理(Sprague–Grundy Theorem):当且仅当
SG ( s 1 ) ⊕ SG ( s 2 ) ⊕ ⋯ ⊕ SG ( s n ) ≠ 0 SG ( s 1 ) ⊕ SG ( s 2 ) ⊕ ⋯ ⊕ SG ( s n ) ≠ 0
时,这个游戏是先手必胜的。同时,这是这一个组合游戏的游戏状态 x x 的 SG 值。
证明:
使用数学归纳法,假设对于游戏状态 x ′ x ′ ,其当前节点 s ′ 1 ∼ n s 1 ∼ n ′ (∀ i , s ′ i < s i ∀ i , s i ′ < s i )符合 SG 定理,SG 定理便成立。
事实上这一个状态可以看作一个 Nim 游戏,对于某个节点 s i s i ,它可以移动到任意一个 SG 值比它小或比它大的节点。
在有向图游戏中,当一方将某一节点 s i s i 移动到 SG 值比它大的节点时,另一方可以移动回和 SG 值和 s i s i 一样的节点,所以向 SG 值较大节点移动是无效操作。
当移动到 SG 值较小的节点时,情况则会和 Nim 游戏一样,能够到达任何一个游戏状态 x ′ x ′ 使得 SG ( x ′ ) = SG ( s ′ 1 ) ⊕ SG ( s 2 ) ⊕ ⋯ ⊕ SG ( s ′ n ) < SG ( X ) SG ( x ′ ) = SG ( s 1 ′ ) ⊕ SG ( s 2 ) ⊕ ⋯ ⊕ SG ( s n ′ ) < SG ( X ) (注意到前文已经假设 x ′ x ′ 满足 SG 定理),但到达不了 SG 值为 SG ( s 1 ) ⊕ SG ( s 2 ) ⊕ ⋯ ⊕ SG ( s n ) SG ( s 1 ) ⊕ SG ( s 2 ) ⊕ ⋯ ⊕ SG ( s n ) 的节点。
所以状态 x x 符合 SG 定理。
SG 定理适用于任何公平的两人游戏,它常被用于决定游戏的输赢结果。
计算给定状态的 SG 值的步骤一般包括:
获取从此状态所有可能的转换。
每个转换都可以导致一系列独立的博弈(退化情况下只有一个)。计算每个独立博弈的 SG 值并对它们进行异或求和。
在为每个转换计算了 SG 值之后,状态的值是这些数字的 mex mex 。
如果该值为零,则当前状态为输,否则为赢。
事实上还有很多非 ICG 游戏,不能够直接用 SG 函数解决,例如:
非回合制游戏:可能可以通过收益矩阵等方法解决。
不平等回合制博弈:可能可以通过对抗搜索、超现实数等方法解决。
博弈图带环的回合制博弈:可能会出现无法终止的情况;
Nim 博弈及变种
Nim 游戏
P2197 【模板】Nim 游戏
有 n n 堆石子,第 i i 堆有 a i a i 个石子。
游戏双方轮流取石子,每次只能从其中一堆中取出任意数目的石子,不能不取。
取完者获胜,求先手是否有必胜策略。
n , a i ≤ 10 4 n , a i ≤ 10 4
Bouton 定理:定义 Nim 和为 a 1 ⊕ a 2 ⊕ ⋯ ⊕ a n a 1 ⊕ a 2 ⊕ ⋯ ⊕ a n ,当且仅当 Nim 和为 0 0 时先手必败。
证明:只需要证明:
没有后继状态的状态是必败状态。
没有后继状态的状态只有一个,即全 0 0 局面,此时 Nim 和为 0 0 。
对于 Nim 和不为 0 0 的局面,一定存在某种移动使得 Nim 和为 0 0 。
不妨假设 Nim 和为 k k ,若要将 a i a i 改为 a ′ i a i ′ ,则 a ′ i = a i ⊕ k a i ′ = a i ⊕ k 。设 k k 的二进制最高位 1 1 为 d d ,根据异或定义,一定有奇数个 a i a i 的二进制第 d d 位为 1 1 。满足这个条件的 a i a i 一定也满足 a i > a i ⊕ k a i > a i ⊕ k ,因而这是合法的移动。
定理三:对于 Nim 和为 0 0 的局面,一定不存在某种移动使得 Nim 和为 0 0 。
如果我们要将 a i a i 改为 a ′ i a i ′ ,则根据异或运算律可以得出 a i = a ′ i a i = a i ′ ,这不是合法的移动。
view code
#include <bits/stdc++.h>
using namespace std ;
signed main () {
int T;
scanf ("%d" , &T);
while (T--) {
int n, ans = 0 ;
scanf ("%d" , &n);
for (int i = 1 ; i <= n; ++i) {
int x;
scanf ("%d" , &x);
ans ^= x;
}
puts (ans ? "Yes" : "No" );
}
return 0 ;
}
应用
HDU1730 Northcott Game
有一个 n n 行 m m 列的棋盘,每行有一个黑棋(先手)和一个白棋(后手)。每次可以把某一行自己的棋子移动到同一行任意一格,但不能越过对方的棋子。不能移动的人输,求先手是否必胜。
n ≤ 1000 n ≤ 1000 ,m ≤ 100 m ≤ 100
考虑黑棋在左边时黑棋的移动情况,其余情况是对称的。若黑棋往左移动,则白棋可以模仿其进行同样的移动。
那么一行就可以抽象为有 | a − b | − 1 | a − b | − 1 个石子( a , b a , b 为横坐标),于是可以转化为 Nim 游戏。
view code
#include <bits/stdc++.h>
using namespace std ;
int n, m;
signed main () {
while (~scanf ("%d%d" , &n, &m)) {
int ans = 0 ;
for (int i = 1 ; i <= n; ++i) {
int a, b;
scanf ("%d%d" , &a, &b);
ans ^= abs (a - b) - 1 ;
}
puts (ans ? "I WIN!" : "BAD LUCK!" );
}
return 0 ;
}
Anti-Nim
P4279 [SHOI2008] 小约翰的游戏
有 n n 堆石子,第 i i 堆有 a i a i 个石子。
游戏双方轮流取石子,每次只能从其中一堆中取出任意数目的石子,不能不取。
取完者失败,求先手是否有必胜策略。
这与 Nim 游戏唯一不同的地方就是胜负判定与其相反。
结论:
全部 a i = 1 a i = 1 时,当且仅当石子堆数为奇数时先手必败。
至少一个 a i > 1 a i > 1 时,当且仅当 Nim 和为 0 0 时先手必败。
证明:第一种情况显然,考虑证明第二种情况。
Nim 和非 0 0 。
若还有至少两堆石子数量 > 1 > 1 ,直接将 Nim 和变为 0 0 即可。
否则直接考虑取 > 1 > 1 的那一堆,并且可以控制剩下 1 1 的堆数,因此此时先手必胜。
Nim 和为 0 0 。
若还有至少两堆石子数量 > 1 > 1 ,决策完后 Nim 和一定非 0 0 。
否则显然先手必败。
注:Anti-Nim 的结论不能直接搬到一般的 ICG 游戏的 SG 函数上。因为之前的推导中运用到了必败态为 0 0 的条件,但这在 Anti-Nim 中不成立。
view code
#include <bits/stdc++.h>
using namespace std ;
const int N = 5e1 + 7 ;
int a[N];
int n;
signed main () {
int T;
scanf ("%d" , &T);
while (T--) {
scanf ("%d" , &n);
int all = 0 ;
for (int i = 1 ; i <= n; ++i)
scanf ("%d" , a + i), all ^= a[i];
puts ((count(a + 1 , a + n + 1 , 1 ) == n ? ~n & 1 : all) ? "John" : "Brother" );
}
return 0 ;
}
Staircase-Nim
给定 n n 个阶梯,每一个阶梯有若干个石子。
游戏双方轮流任意选取一个有石子的阶梯,将任意数量(至少为 1 1 )石子移动到下一个阶梯上。
具体地,从编号为 i i 的阶梯拿到 i − 1 i − 1 阶梯上。若 i = 1 i = 1 ,则直接拿走石子。
取完者获胜,求先手是否有必胜策略。
结论:Staircase-Nim 游戏的性质与在奇数编号阶梯上做 Nim 游戏的性质相同。
证明:若在奇数阶梯上 Nim 和非 0 0 ,那么可以采取以下策略:
按照 Nim 游戏的必胜策略将某一奇数阶梯上的一些石子移动到其下的偶数阶梯上(相当于丢弃)。
若对手也移动奇数阶梯上的石子,相当于配合你做 Nim 游戏,否则只要将这些石子再移动到再其下的偶数堆即可。
不难发现这样操作始终能保证自己必胜,必败情况的等价性质类似证明即可。
Nim-K 游戏
有 n n 堆石子,每堆有 a i a i 个。
游戏双方轮流取石子,每次可以选堆数 ∈ [ 1 , m ] ∈ [ 1 , m ] 的若干堆,并从这几堆中各取若干石子。
取完者获胜,求先手是否有必胜策略。
结论:当前状态必败,当且仅当对于每一个二进制位,这一位 1 1 的数量 mod ( m + 1 ) mod ( m + 1 ) 为 0 0 。
证明:类似 Nim 博弈,只需证明:
结论一:全 0 0 的局面一定是必败态。
结论二:任何一个必败态,经过一次操作后必然会回到必胜态。
一次移动中至少有一个二进制位被改变。由于最多只能操作 m m 堆石子,所以对于任意位,1 1 的个数至多改变 m m 。而由于原先每一位 1 1 的数量均为 m + 1 m + 1 的整数倍,所以操作后必然存在一位 1 1 的数量不是 m + 1 m + 1 的整数倍。
结论三:任何一个必胜态,总有一种操作后会回到必败态。
考虑归纳法,假设用某种方法改变了 k k 堆,使比第 i i 位高的所有位的 1 1 的个数都变成 m + 1 m + 1 的整数倍。现在要证明总有一种方法让第 i i 位也变成 m + 1 m + 1 的整数倍。显然,对于已经改变的那 k k 堆,当前位可以自由选择 1 1 或 0 0 。设除去已经更改的 k k 堆,剩下的堆第 i i 位上 1 1 的总和 mod m + 1 = S mod m + 1 = S 。若 S ≤ m − k S ≤ m − k ,此时可以将这些堆上的 1 1 全部拿掉,然后让那 k k 堆的第 i i 位全部置为 0 0 ;否则此时在之前改变的 k k 堆中选择 m + 1 − S m + 1 − S 堆,将它们的第 i i 位置为 1 1 且剩下位置置为 0 0 。
经典模型
巴什博弈
有 n n 个石子,两个人轮流取,每次可以取 1 ∼ m 1 ∼ m 个。
取完者获胜,求先手是否有必胜策略。
结论:当且仅当 n mod ( m + 1 ) = 0 n mod ( m + 1 ) = 0 时先手必败。
威佐夫博弈
P2252 [SHOI2002] 取石子游戏|【模板】威佐夫博弈
有两堆各 n , m n , m 个物品,两个人轮流操作,操作有两种:
从任意一堆中取出至少一个。
同时从两堆中取出同样多的物品,规定每次至少取一个。
取完者获胜,求先手是否有必胜策略。
n , m ≤ 10 9 n , m ≤ 10 9
结论:记两堆石子为 n , m n , m ,其中 n ≤ m n ≤ m ,若 n = ⌊ √ 5 + 1 2 × ( m − n ) ⌋ n = ⌊ 5 + 1 2 × ( m − n ) ⌋ 则先手必败。
注意稍微化一下式子,避免浮点数运算,不然会被卡精度。
view code
#include <bits/stdc++.h>
using namespace std ;
signed main () {
int n, m;
scanf ("%d%d" , &n, &m);
if (n > m)
swap(n, m);
m -= n;
printf ("%d" , !(1ll * (n * 2 - m) * (n * 2 - m) <= 5ll * m * m &&
5ll * m * m < 1ll * (n * 2 - m + 2 ) * (n * 2 - m + 2 )));
return 0 ;
}
斐波那契博弈
有一堆个数为 n n 的石子,双方轮流取石子。要求:
先手不能一次取完所有石子。
之后每次可以取的石子数介于 1 1 到对手上一次取的石子数两倍之间。
取完者获胜,求先手是否有必胜策略。
结论:先手必败当且仅当 n n 是斐波那契数。
证明:先引入齐肯多夫定理:任何整数可以分解成若干个不连续的斐波那契数之和。
记 n = f i + f i − 1 + ⋯ f i − k n = f i + f i − 1 + ⋯ f i − k ,那么先手先取 f i − k f i − k ,由于后手不能取大于等于 2 × f i − k 2 × f i − k 的项,则 n n 中剩下的斐波那契项,先手都可以取到最后一颗。
翻硬币游戏
有 n n 枚硬币,已知其初始状态。每次可以选择一个反面向上的硬币,并将其与其有约束的硬币同时翻转。
不能翻转者输,求先手是否有必胜策略。
结论:局面的 SG 值为局面中每个正(反)面朝上的棋子单一存在时的SG值的异或和。
因此,若翻转一个硬币只会影响比它小的硬币,则其单一存在时后面的硬币可以忽略。
P4077 [SDOI2016] 硬币游戏
有 n n 枚硬币,已知其初始状态。每次可以选择一个反面向上的硬币 x = c 2 a 3 b x = c 2 a 3 b ,其中 2 ∤ c 2 ∤ c 且 3 ∤ c 3 ∤ c ,并选择执行一种操作:
选择 p , q p , q 满足 p q ≤ a p q ≤ a 且 p ≥ 1 p ≥ 1 且 1 ≤ q ≤ M A X Q 1 ≤ q ≤ M A X Q ,然后同时翻转所有编号为 c 2 a − p j 3 b c 2 a − p j 3 b 的硬币,其中 j = 0 , 1 , 2 , ⋯ , q j = 0 , 1 , 2 , ⋯ , q 。
选择 p , q p , q 满足 p q ≤ b p q ≤ b 且 p ≥ 1 p ≥ 1 且 1 ≤ q ≤ M A X Q 1 ≤ q ≤ M A X Q ,然后同时翻转所有编号为 c 2 a 3 b − p j c 2 a 3 b − p j 的硬币,其中 j = 0 , 1 , 2 , ⋯ , q j = 0 , 1 , 2 , ⋯ , q 。
无法操作者输,多组数据询问是否先手必胜。
n ≤ 3 × 10 4 n ≤ 3 × 10 4
首先可以发现不同的 c c 之间互相独立,最后用 SG 定理合并即可。
现在仅需考虑独立的 2 a 3 b 2 a 3 b ,可以发现 2 a 2 a 与 3 b 3 b 局面的并就是 2 a 3 b 2 a 3 b 的局面,因此仅需考虑 2 a 2 a 的情况。
将指数拿下类,那么一次操作相当于翻转 a , a − p , a − 2 p , ⋯ , a − q p a , a − p , a − 2 p , ⋯ , a − q p 的硬币。
根据经典结论,S G ( a ) S G ( a ) 的状态来源有 S G ( a − p ) , S G ( a − p ) ⊕ S G ( a − 2 p ) , ⋯ S G ( a − p ) , S G ( a − p ) ⊕ S G ( a − 2 p ) , ⋯ ,因此:
S G ( a , b ) = mex { S G ( a − p , b ) , S G ( a − p , b ) ⊕ S G ( a − 2 p , b ) , ⋯ , S G ( a , b − p ) , S G ( a , b − p ) ⊕ S G ( a , b − 2 p ) } S G ( a , b ) = mex { S G ( a − p , b ) , S G ( a − p , b ) ⊕ S G ( a − 2 p , b ) , ⋯ , S G ( a , b − p ) , S G ( a , b − p ) ⊕ S G ( a , b − 2 p ) }
view code
#include <bits/stdc++.h>
using namespace std ;
const int N = 3e4 + 7 , Q = 21 , M = 15 ;
int sg[Q][M][M], lg2[N], lg3[N];
inline void prework () {
for (int i = 1 ; i < N; ++i) {
if (~i & 1 )
lg2[i] = lg2[i >> 1 ] + 1 ;
if (!(i % 3 ))
lg3[i] = lg3[i / 3 ] + 1 ;
}
for (int mxq = 1 ; mxq < Q; ++mxq)
for (int a = 1 , j = 0 ; a < N; a <<= 1 , ++j)
for (int b = 1 , k = 0 ; a * b < N; b *= 3 , ++k) {
int s = 0 ;
for (int p = 1 ; p <= j; ++p) {
int t = 0 ;
for (int q = 1 ; p * q <= j && q <= mxq; ++q)
s |= 1 << (t ^= sg[mxq][j - p * q][k]);
}
for (int p = 1 ; p <= k; ++p) {
int t = 0 ;
for (int q = 1 ; p * q <= k && q <= mxq; ++q)
s |= 1 << (t ^= sg[mxq][j][k - p * q]);
}
sg[mxq][j][k] = __builtin_ctz(~s);
}
}
signed main () {
prework();
int T;
scanf ("%d" , &T);
while (T--) {
int n, mxq, ans = 0 ;
scanf ("%d%d" , &n, &mxq);
for (int i = 1 ; i <= n; ++i) {
int x;
scanf ("%d" , &x);
if (!x)
ans ^= sg[mxq][lg2[i]][lg3[i]];
}
puts (ans ? "win" : "lose" );
}
return 0 ;
}
二分图博弈
特点:有向图游戏,满足有向图为二分图。
将每个状态看做一个点,那么这些点和合法转移会构成一个二分图。
将 S S 集合视作轮到先手决策的点,T T 集合则代表轮到后手决策的点。
先跑一遍二分图匹配,对于任意一点 x x ,有两种情况:
不属于最大匹配(非匹配点):走一步后必然会走到一个匹配点(否则相当于找到了一条增广路)。而走到一个匹配点后,对方可以不断沿着匹配边走,最后必然会停留在 S S 集合,也就是先手必败(如果停留在 T T 集合,相当于找到了一条增广路)。
最大匹配的非必须点:不管它怎么走,走到的目标节点一定会在某种情况下属于最大匹配,因此先手必败。
最大匹配的必须点:这个点先手必胜。因为总能走向另一个匹配点。
P4055 [JSOI2009] 游戏
给定一个 n × m n × m 的网格图,有若干格子有障碍。A 先选择一个起点放棋子,然后从 B 开始,两人轮流移动棋子,不能移动到走过的位置,无法移动者输。求 A 是否有必胜策略,若有还需求出所有有必胜策略的起点。
n , m ≤ 100 n , m ≤ 100
将相邻两个非障碍点连边,可以得到一张二分图,找最大匹配的非必须点即可。
view code
#include <bits/stdc++.h>
using namespace std ;
const int dx[] = {0 , 0 , 1 , -1 };
const int dy[] = {1 , -1 , 0 , 0 };
const int N = 1e2 + 7 ;
struct Graph {
vector <int > e[N * N];
inline void insert (int u, int v) {
e[u].emplace_back(v);
}
} G;
int vis[N * N], obj[N * N];
char a[N][N];
bool tag[N * N];
int n, m;
inline int getid (int x, int y) {
return (x - 1 ) * m + y;
}
bool Hungary (int u, const int tag) {
for (int v : G.e[u]) {
if (vis[v] == tag)
continue ;
vis[v] = tag;
if (!obj[v] || Hungary(obj[v], tag))
return obj[v] = u, obj[u] = v, true ;
}
return false ;
}
void dfs (int u) {
tag[u] = true ;
for (int v : G.e[u])
if (obj[v] != u && !tag[obj[v]])
dfs(obj[v]);
}
signed main () {
scanf ("%d%d" , &n, &m);
for (int i = 1 ; i <= n; ++i)
scanf ("%s" , a[i] + 1 );
for (int i = 1 ; i <= n; ++i)
for (int j = 1 ; j <= m; ++j)
if (a[i][j] != '#' )
for (int k = 0 ; k < 4 ; ++k) {
int x = i + dx[k], y = j + dy[k];
if (1 <= x && x <= n && 1 <= y && y <= m && a[x][y] != '#' )
G.insert(getid(i, j), getid(x, y));
}
int Tag = 0 ;
for (int i = 1 ; i <= n; ++i)
for (int j = 1 ; j <= m; ++j)
if (a[i][j] != '#' && ((i + j) & 1 ))
Hungary(getid(i, j), ++Tag);
for (int i = 1 ; i <= n; ++i)
for (int j = 1 ; j <= m; ++j)
if (a[i][j] != '#' && !obj[getid(i, j)])
dfs(getid(i, j));
bool flag = false ;
for (int i = 1 ; i <= n; ++i)
for (int j = 1 ; j <= m; ++j)
if (a[i][j] != '#' && tag[getid(i, j)]) {
flag = true ;
break ;
}
if (!flag)
return puts ("LOSE" ), 0 ;
puts ("WIN" );
for (int i = 1 ; i <= n; ++i)
for (int j = 1 ; j <= m; ++j)
if (a[i][j] != '#' && tag[getid(i, j)])
printf ("%d %d\n" , i, j);
return 0 ;
}
应用
有 n n 堆石子,第 i i 堆有 a i a i 个。
一次操作可以将一堆数量为 i ( i ≥ F ) i ( i ≥ F ) 的石子分为:j − i mod j j − i mod j 堆数量为 ⌊ i j ⌋ ⌊ i j ⌋ 的石子、i mod j i mod j 堆数量为 ⌈ i j ⌉ ⌈ i j ⌉ 的石子。其中 j j 可以自己选定,需要满足 2 ≤ j ≤ i 2 ≤ j ≤ i 。
无法操作者输,求是否存在先手必胜策略。
多组数据,但 F F 不改变。
T , n ≤ 100 T , n ≤ 100 ,F , a i ≤ 10 5 F , a i ≤ 10 5
考虑求出每个数的 SG 值,这样直接判断异或和是否非 0 0 即可。
首先,对于 i < F i < F ,显然先手必败,故 S G ( i ) = 0 S G ( i ) = 0 。否则枚举每个 j j ,计算各个子游戏的 SG 函数的异或和的 mex 即可,直接做是 O ( V 2 + T n ) O ( V 2 + T n ) 的。
考虑整除分块,对于一段 [ l , r ] [ l , r ] ,发现 SG 函数相同的点交替出现,自然想到异或和奇偶性的关系,分类讨论:
2 ∤ ⌊ i j ⌋ 2 ∤ ⌊ i j ⌋ :此时 j − i mod j = j ( 1 + ⌊ i j ⌋ ) − i j − i mod j = j ( 1 + ⌊ i j ⌋ ) − i 奇偶性不变,因此该部分贡献不变。
2 ∣ ⌊ i j ⌋ 2 ∣ ⌊ i j ⌋ :此时 i mod j = i − j ⌊ i j ⌋ i mod j = i − j ⌊ i j ⌋ 奇偶性不变,因此该部分贡献不变。
因此两个部分中,总有一个部分贡献不变,于是只要枚举两个 j j 即可。
时间复杂度 O ( V √ V + T n ) O ( V V + T n ) 。
view code
#include <bits/stdc++.h>
using namespace std ;
const int N = 1e3 + 7 , V = 1e5 + 7 ;
int sg[V];
int F;
int SG (int n) {
if (n < F)
return 0 ;
if (~sg[n])
return sg[n];
bitset <N> vis;
for (int l = 2 , r; l <= n; l = r + 1 ) {
r = n / (n / l);
int res = 0 ;
if ((n % l) & 1 )
res ^= SG(n / l + 1 );
if ((l - n % l) & 1 )
res ^= SG(n / l);
vis.set (res);
if (l < r) {
res = 0 ;
if ((n % (l + 1 )) & 1 )
res ^= SG(n / (l + 1 ) + 1 );
if (((l + 1 ) - n % (l + 1 )) & 1 )
res ^= SG(n / (l + 1 ));
vis.set (res);
}
}
return sg[n] = (~vis)._Find_first();
}
signed main () {
memset (sg, -1 , sizeof (sg));
int T;
scanf ("%d%d" , &T, &F);
while (T--) {
int n, ans = 0 ;
scanf ("%d" , &n);
for (int i = 1 ; i <= n; ++i) {
int x;
scanf ("%d" , &x);
ans ^= SG(x);
}
printf ("%d " , ans ? 1 : 0 );
}
return 0 ;
}
有一个 n × m n × m 的棋盘,棋盘上有一些障碍,还有一个黑棋和两个红棋。
红方先走,黑方后走,双方轮流走棋。红方每次可以选择一个红棋 ( i , j ) ( i , j ) ,走到 ( i − 1 , j ) , ( i + 1 , j ) , ( i , j − 1 ) , ( i , j + 1 ) ( i − 1 , j ) , ( i + 1 , j ) , ( i , j − 1 ) , ( i , j + 1 ) 中的一个,只要这个目的地在棋盘内且没有障碍且没有红方的另一个棋子。
黑方每次可以将自己的棋子 ( i , j ) ( i , j ) 走到 ( i − 1 , j ) , ( i , j − 1 ) , ( i , j + 1 ) ( i − 1 , j ) , ( i , j − 1 ) , ( i , j + 1 ) 这三个格子中的一个,只要这个目的地在棋盘内且没有障碍。
在一方行动之前,如果发生以下情况之一,则立即结束游戏,按照如下的规则判断胜负(列在前面的优先):
黑棋位于第一行:黑方胜。
黑棋和其中一个红棋在同一位置:上一步移动者胜。
当前玩家无法操作:对方胜。
假设双方采用最优策略:
若存在必胜策略,则选择所需步数最大值最少的操作。
若不存在必胜策略,但存在平局策略,则选择任意一种平局策略。
若不存在不败策略,则选择对方获胜所需步数最小值最大的操作。
判断游戏是否会平局,若不会需要求出结束时双方一共移动了多少步。
n , m ≤ 10 n , m ≤ 10
设状态 ( i , j , a , b , x , y , 0 / 1 ) ( i , j , a , b , x , y , 0 / 1 ) 表示黑棋在 ( i , j ) ( i , j ) 、红棋在 ( a , b ) , ( x , y ) ( a , b ) , ( x , y ) 、红/黑先手。先建出有向图,然后考虑移动步数,显然这是一个 DAG 上的最短路问题,直接拓扑排序即可。
时间复杂度 O ( ( n m ) 3 ) O ( ( n m ) 3 ) 。
view code
51. 【UR #4 】元旦三侠的游戏#include <bits/stdc++.h>
using namespace std ;
const int dx[] = {-1 , 0 , 0 , 1 };
const int dy[] = {0 , -1 , 1 , 0 };
const int N = 11 , M = 2e6 + 7 ;
struct Graph {
struct Edge {
int nxt, v;
} e[M << 3 ];
int head[M], indeg[M];
int tot;
inline void clear (int n) {
memset (head, 0 , sizeof (int ) * n);
memset (indeg, 0 , sizeof (int ) * n);
tot = 0 ;
}
inline void insert (int u, int v) {
e[++tot] = (Edge) {head[u], v}, head[u] = tot, ++indeg[v];
}
} G;
int id[N][N][N][N][N][N][2 ], dis[M];
bool win[M], vis[M];
char str[N][N];
int n, m, tot;
inline bool check (const int &i, const int &j, const int &a, const int &b, const int &x, const int &y) {
return 1 <= i && i <= n && 1 <= j && j <= m &&
1 <= a && a <= n && 1 <= b && b <= m &&
1 <= x && x <= n && 1 <= y && y <= m &&
str[i][j] != '#' && str[a][b] != '#' && str[x][y] != '#' && (a != x || b != y);
}
inline void AllocateIndex () {
tot = 0 ;
for (int i = 1 ; i <= n; ++i)
for (int j = 1 ; j <= m; ++j)
for (int a = 1 ; a <= n; ++a)
for (int b = 1 ; b <= m; ++b)
for (int x = 1 ; x <= n; ++x)
for (int y = 1 ; y <= m; ++y)
if (check(i, j, a, b, x, y))
id[i][j][a][b][x][y][0 ] = tot++, id[i][j][a][b][x][y][1 ] = tot++;
}
inline void BuildEdge () {
for (int i = 1 ; i <= n; ++i)
for (int j = 1 ; j <= m; ++j)
for (int a = 1 ; a <= n; ++a)
for (int b = 1 ; b <= m; ++b)
for (int x = 1 ; x <= n; ++x)
for (int y = 1 ; y <= m; ++y)
if (check(i, j, a, b, x, y)) {
int cur = id[i][j][a][b][x][y][0 ];
for (int k = 0 , nx, ny; k < 3 ; ++k)
if (check(nx = i + dx[k], ny = j + dy[k], a, b, x, y))
G.insert(id[nx][ny][a][b][x][y][0 ], cur ^ 1 );
for (int k = 0 , nx, ny; k < 4 ; ++k)
if (check(i, j, nx = a + dx[k], ny = b + dy[k], x, y))
G.insert(id[i][j][nx][ny][x][y][1 ], cur);
for (int k = 0 , nx, ny; k < 4 ; ++k)
if (check(i, j, a, b, nx = x + dx[k], ny = y + dy[k]))
G.insert(id[i][j][a][b][nx][ny][1 ], cur);
if (i == 1 ) {
vis[cur] = vis[cur ^ 1 ] = true ;
win[cur] = false , win[cur ^ 1 ] = true ;
dis[cur] = dis[cur ^ 1 ] = 0 ;
} else if ((i == a && j == b) || (i == x && j == y)) {
vis[cur] = vis[cur ^ 1 ] = true ;
win[cur] = win[cur ^ 1 ] = false ;
dis[cur] = dis[cur ^ 1 ] = 0 ;
} else {
if (!G.indeg[cur])
vis[cur] = true , win[cur] = false , dis[cur] = 0 ;
if (!G.indeg[cur ^ 1 ])
vis[cur ^ 1 ] = true , win[cur ^ 1 ] = false , dis[cur ^ 1 ] = 0 ;
}
}
}
inline void TopoSort (int goal) {
queue <int > q;
for (int i = 0 ; i < tot; ++i)
if (vis[i])
q.emplace(i);
while (!q.empty()) {
int u = q.front();
q.pop();
if (u == goal)
break ;
for (int i = G.head[u]; i; i = G.e[i].nxt) {
int v = G.e[i].v;
if (vis[v])
continue ;
--G.indeg[v];
if (!win[u] || !G.indeg[v])
win[v] = win[u] ^ 1 , dis[v] = dis[u] + 1 , vis[v] = true , q.emplace(v);
}
}
}
signed main () {
int testid, T;
scanf ("%d%d" , &testid, &T);
while (T--) {
scanf ("%d%d" , &n, &m);
pair <int , int > black, red1 = make_pair (0 , 0 ), red2;
for (int i = 1 ; i <= n; ++i) {
scanf ("%s" , str[i] + 1 );
for (int j = 1 ; j <= m; ++j) {
if (str[i][j] == 'X' )
black = make_pair (i, j);
else if (str[i][j] == 'O' ) {
if (red1 == make_pair (0 , 0 ))
red1 = make_pair (i, j);
else
red2 = make_pair (i, j);
}
}
}
AllocateIndex(), G.clear(tot);
memset (dis, 0 , sizeof (int ) * tot);
memset (win, 0 , sizeof (bool ) * tot);
memset (vis, 0 , sizeof (bool ) * tot);
BuildEdge();
int goal = id[black.first][black.second][red1.first][red1.second][red2.first][red2.second][0 ];
TopoSort(goal);
51. 【UR #4 】元旦三侠的游戏
if (vis[goal])
printf ("%s %d\n" , win[goal] ? "Red" : "Black" , dis[goal]);
else
puts ("Tie" );
}
return 0 ;
}
初始时有 a , b a , b ,双方轮流操作,每次可以令 a a 或 b b 增加 1 1 并满足 a b ≤ n a b ≤ n 。
无法操作者失败,给定 n n ,m m 次询问 ( a , b ) ( a , b ) 是否先手必胜。
n ≤ 10 9 n ≤ 10 9 ,m ≤ 10 5 m ≤ 10 5 ,a ≥ 2 a ≥ 2 ,b ≥ 1 b ≥ 1
对于一个 b b ,a ≤ b √ n a ≤ n b ,于是状态只有 ∑ log n b = 1 b √ n ∑ b = 1 log n n b 种。
发现当 a > b + 1 √ n a > n b + 1 时只能增加 a a ,因此有用的状态只有 ∑ log n b = 2 b √ n < √ n log n ∑ b = 2 log n n b < n log n 个,剩下的只要求出 a a 与其的差值的奇偶性即可。
时间复杂度 O ( √ n log n ) O ( n log n ) 。
view code
#include <bits/stdc++.h>
typedef long long ll;
using namespace std ;
map <pair <int , int >, bool > mp;
int n, m;
bool check (int a, int b) {
if (mp.find(make_pair (a, b)) != mp.end())
return mp[make_pair (a, b)];
ll pw = 1 ;
for (int i = 1 ; i <= b; ++i)
pw *= a;
if (pw > n)
return true ;
if (b == 1 && 1ll * a * a > n)
return (n - a) & 1 ;
else
return mp[make_pair (a, b)] = !check(a + 1 , b) || !check(a, b + 1 );
}
signed main () {
scanf ("%d%d" , &n, &m);
while (m--) {
int a, b;
scanf ("%d%d" , &a, &b);
puts (check(a, b) ? "Yes" : "No" );
}
return 0 ;
}
有 n n 堆石子,第 i i 堆有 a i a i 个。每次操作可以选择三个位置 i < j ≤ k i < j ≤ k ,令 a i a i 减一,并令 a j , a k a j , a k 均加一,其中 j , k j , k 可以相等。
双方轮流操作,求是否存在先后必胜策略,若存在需要求出先手必胜时可能的第一次操作种数以及字典序最小的操作。
n ≤ 21 n ≤ 21
先考虑结束状态,显然前 n − 1 n − 1 堆都没有石子时结束。
考虑将每个石子视作一个子游戏,一次操作相当于拿走第 i i 堆的一个石子到 n n ,然后在第 j , k j , k 堆产生一个新的子游戏,那么有 S G ( i ) = mex i < j ≤ k { S G ( j ) ⊕ S G ( k ) } S G ( i ) = mex i < j ≤ k { S G ( j ) ⊕ S G ( k ) } 。
由 SG 定理,一个位置的石子只要保留 mod 2 mod 2 即可(异或抵消)。
对于方案求解,可以直接枚举第一次操作算 SG 值。
时间复杂度 O ( n 3 ) O ( n 3 ) 。
view code
#include <bits/stdc++.h>
using namespace std ;
const int N = 21 ;
int a[N], sg[N];
int n;
signed main () {
int T;
scanf ("%d" , &T);
while (T--) {
scanf ("%d" , &n);
for (int i = 0 ; i < n; ++i)
scanf ("%d" , a + i);
for (int i = n - 1 ; ~i; --i) {
bitset <N * N> vis;
for (int j = i + 1 ; j < n; ++j)
for (int k = j; k < n; ++k)
vis.set (sg[j] ^ sg[k]);
sg[i] = (~vis)._Find_first();
}
int ans = 0 ;
for (int i = 0 ; i < n; ++i)
if (a[i]) {
for (int j = i + 1 ; j < n; ++j)
for (int k = j; k < n; ++k) {
--a[i], ++a[j], ++a[k];
int res = 0 ;
for (int d = 0 ; d < n; ++d)
if (a[d] & 1 )
res ^= sg[d];
if (!res) {
++ans;
if (ans == 1 )
printf ("%d %d %d\n" , i, j, k);
}
++a[i], --a[j], --a[k];
}
}
if (!ans)
puts ("-1 -1 -1" );
printf ("%d\n" , ans);
}
return 0 ;
}
给出一棵树,点有点权。双方轮流操作,每次可以选择点权严格大于上一次点权的点,并删掉其整个子树(包括这个点)。第一次可以任意选取,无法操作者获胜。找出所有先手必胜的操作点,或报告无解。
CF2062E1 The Game (Easy Version) :找到一个先手必胜的操作点,或报告无解。
n ≤ 4 × 10 5 n ≤ 4 × 10 5
先考虑 E1,答案即为点权最大的点 u u ,满足子树外存在一点 v v 满足 v a l v > v a l u v a l v > v a l u 。这样对手剩下能选的点一定不满足这个形式,于是先手就无法操作。
接下来考虑 E2,一个暴力是枚举所有操作点,然后把删去点权不超过它的点和它的子树,再用 E1 的方法判定。
记先手选的点为 u u ,则必胜条件为对于所有 w v > w u w v > w u 均满足两个条件之一:
v v 位于 u u 的子树内。
权值大于 v v 的点 w w 在 u u 或 v v 的子树内。
那么扫到 v v 时,记所有不在 v v 子树内的 w w 的 LCA 为 W W ,那么判定条件为 u u 是 v v 或 W W 的祖先。
考虑按点权倒序枚举点,记点权大于该点的 LCA 为 W W ,则 1 → W 1 → W 的链上的点的交就是 u u 的可行点集,树上差分判断即可,注意判定第一次选完点之后不能让对手无法操作。
接下来考虑求 W W ,只要用一个 set
维护权值大于 u u 的节点的 dfn,根据经典结论每次找到不在 u u 子树内 dfn 最大最小点的 LCA 即可。
时间复杂度 O ( n log n ) O ( n log n ) 。
view code
#include <bits/stdc++.h>
using namespace std ;
const int N = 4e5 + 7 , LOGN = 21 ;
struct Graph {
vector <int > e[N];
inline void clear (int n) {
for (int i = 1 ; i <= n; ++i)
e[i].clear();
}
inline void insert (int u, int v) {
e[u].emplace_back(v);
}
} G;
struct BIT {
int c[N];
int n;
inline void prework (int _n) {
memset (c + 1 , 0 , sizeof (int ) * (n = _n));
}
inline void update (int x, int k) {
for (; x <= n; x += x & -x)
c[x] += k;
}
inline int ask (int x) {
int res = 0 ;
for (; x; x -= x & -x)
res += c[x];
return res;
}
inline int query (int l, int r) {
return ask(r) - ask(l - 1 );
}
} bit1, bit2;
vector <int > vec[N];
int fa[N][LOGN];
int dep[N], in[N], out[N], id[N];
int n, dfstime;
template <class T = int >
inline T read () {
char c = getchar();
bool sign = (c == '-' );
while (c < '0' || c > '9' )
c = getchar(), sign |= (c == '-' );
T x = 0 ;
while ('0' <= c && c <= '9' )
x = (x << 1 ) + (x << 3 ) + (c & 15 ), c = getchar();
return sign ? (~x + 1 ) : x;
}
void dfs (int u, int f) {
fa[u][0 ] = f, dep[u] = dep[f] + 1 , id[in[u] = ++dfstime] = u;
for (int i = 1 ; i < LOGN; ++i)
fa[u][i] = fa[fa[u][i - 1 ]][i - 1 ];
for (int v : G.e[u])
if (v != f)
dfs(v, u);
out[u] = dfstime;
}
inline int LCA (int x, int y) {
if (!x || !y)
return x | y;
if (dep[x] < dep[y])
swap(x, y);
for (int i = 0 , h = dep[x] - dep[y]; h; ++i, h >>= 1 )
if (h & 1 )
x = fa[x][i];
if (x == y)
return x;
for (int i = LOGN - 1 ; ~i; --i)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
return fa[x][0 ];
}
signed main () {
int T = read();
while (T--) {
n = read();
for (int i = 1 ; i <= n; ++i)
vec[i].clear();
for (int i = 1 ; i <= n; ++i)
vec[read()].emplace_back(i);
G.clear(n);
for (int i = 1 ; i < n; ++i) {
int u = read(), v = read();
G.insert(u, v), G.insert(v, u);
}
dfstime = 0 , dfs(1 , 0 );
bit1.prework(n), bit2.prework(n);
vector <int > ans;
set <int > st;
for (int i = n, cnt1 = 0 , cnt2 = 0 ; i; --i) {
for (int it : vec[i])
if (bit1.query(in[it], out[it]) < cnt1 && bit2.query(in[it], out[it]) == cnt2)
ans.emplace_back(it);
for (int it : vec[i]) {
if (bit1.query(in[it], out[it]) == cnt1)
continue ;
int w = 0 ;
if (*st.begin() < in[it])
w = LCA(w, id[*st.begin()]);
else if (st.upper_bound(out[it]) != st.end())
w = LCA(w, id[*st.upper_bound(out[it])]);
if (*st.rbegin() > out[it])
w = LCA(w, id[*st.rbegin()]);
else if (st.lower_bound(in[it]) != st.begin())
w = LCA(w, id[*prev(st.lower_bound(in[it]))]);
bit2.update(in[w], 1 ), bit2.update(in[it], 1 ), bit2.update(in[LCA(w, it)], -1 ), ++cnt2;
}
for (int it : vec[i])
st.emplace(in[it]), bit1.update(in[it], 1 ), ++cnt1;
}
sort(ans.begin(), ans.end());
printf ("%d " , (int )ans.size());
for (int it : ans)
printf ("%d " , it);
puts ("" );
}
return 0 ;
}
七管荧光灯
一个七管荧光灯如图所示:
对于一次游戏,七条边上分别有一些石子(可能没有)。先后手轮流取石子,每一次可以选择某些连通的边(但是选择的边不 得成环),并从其中的每条边上各选择若干个石子移走(可以为 0 0 ,但是一次操作至少要取走一个石子)。无法操作者输。
给出七条边石子数量的上下界 l 1 ∼ 7 , r 1 ∼ 7 l 1 ∼ 7 , r 1 ∼ 7 ,求有多少种先手必胜的方案。
l 1 ∼ 7 , r 1 ∼ 7 ≤ 10 18 l 1 ∼ 7 , r 1 ∼ 7 ≤ 10 18
结论:先手必败当且仅当 1 , 2 , 3 1 , 2 , 3 条边的石子相等,5 , 6 , 7 5 , 6 , 7 条边的石子相等,且 1 , 4 , 7 1 , 4 , 7 条边的石子异或和为 0 0 。
证明类似 Nim 游戏:
若 { 1 , 2 , 3 } , { 4 } , { 5 , 6 , 7 } { 1 , 2 , 3 } , { 4 } , { 5 , 6 , 7 } 三个集合内部石子不相等,则先手可以将它们调整至相等,同时可以使得 1 , 4 , 7 1 , 4 , 7 条边的石子异或和为 0 0 。
对于剩下的操作,显然可以钦定每次集合内选取状态相同(否则后手仍可以在操作的同时调整至相同),又因为每次只能选一个集合,于是转化为 Nim 游戏。
然后直接容斥配合数位 DP 统计必败态即可。
view code
#include <bits/stdc++.h>
typedef long long ll;
using namespace std ;
const ll inf = 0x3f3f3f3f3f3f3f3f ;
const int Mod = 998244353 ;
const int B = 63 ;
ll L[3 ], R[3 ];
int f[B][2 ][2 ][2 ];
ll n[3 ];
inline int add (int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
inline int dec (int x, int y) {
x -= y;
if (x < 0 )
x += Mod;
return x;
}
inline int sgn (int n) {
return n & 1 ? Mod - 1 : 1 ;
}
int dfs (int x, bool limit0, bool limit1, bool limit2) {
if (x == -1 )
return 1 ;
else if (~f[x][limit0][limit1][limit2])
return f[x][limit0][limit1][limit2];
int high0 = (limit0 ? n[0 ] >> x & 1 : 1 ), high1 = (limit1 ? n[1 ] >> x & 1 : 1 ),
high2 = (limit2 ? n[2 ] >> x & 1 : 1 ), res = 0 ;
for (int i = 0 ; i <= high0; ++i)
for (int j = 0 ; j <= high1; ++j)
for (int k = 0 ; k <= high2; ++k)
if (!(i ^ j ^ k))
res = add(res, dfs(x - 1 , limit0 && i == high0, limit1 && j == high1, limit2 && k == high2));
return f[x][limit0][limit1][limit2] = res;
}
signed main () {
freopen("qgygd.in" , "r" , stdin );
freopen("qgygd.out" , "w" , stdout );
R[0 ] = R[1 ] = R[2 ] = inf;
int ans = 1 ;
for (int i = 1 ; i <= 7 ; ++i) {
ll l, r;
scanf ("%lld%lld" , &l, &r);
ans = 1ll * ans * ((r - l + 1 ) % Mod) % Mod;
int x = (i <= 3 ? 0 : (i == 4 ? 1 : 2 ));
L[x] = max(L[x], l), R[x] = min(R[x], r);
}
if (L[0 ] > R[0 ] || L[1 ] > R[1 ] || L[2 ] > R[2 ])
return printf ("%d" , ans), 0 ;
for (int s = 0 ; s < (1 << 3 ); ++s) {
bool flag = true ;
for (int i = 0 ; i < 3 ; ++i)
flag &= ((s >> i & 1 ) || L[i]);
if (!flag)
continue ;
for (int i = 0 ; i < 3 ; ++i)
n[i] = (s >> i & 1 ? R[i] : L[i] - 1 );
memset (f, -1 , sizeof (f));
ans = add(ans, 1ll * sgn(__builtin_popcount(s)) * dfs(B - 1 , true , true , true ) % Mod);
}
printf ("%d" , ans);
return 0 ;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步