E. Not a Nim Problem
E. Not a Nim Problem
Two players, Alice and Bob, are playing a game. They have n piles of stones, with the i-th pile initially containing ai stones.
On their turn, a player can choose any pile of stones and take any positive number of stones from it, with one condition:
- let the current number of stones in the pile be x. It is not allowed to take from the pile a number of stones y such that the greatest common divisor of x and y is not equal to 1.
The player who cannot make a move loses. Both players play optimally (that is, if a player has a strategy that allows them to win, no matter how the opponent responds, they will win). Alice goes first.
Determine who will win.
Input
The first line contains a single integer t (1≤t≤104) — the number of test cases.
Each test case consists of two lines:
- the first line contains a single integer n (1≤n≤3⋅105);
- the second line contains n integers a1,a2,…,an (1≤ai≤107).
Additional constraint on the input: the sum of n across all test cases does not exceed 3⋅105.
Output
For each test case, output Alice if Alice wins, or Bob if Bob wins.
Example
Input
3
3
3 2 9
4
3 3 6 1
5
1 2 3 4 5
Output
Bob
Alice
Bob
解题思路
借这题学一下 SG 函数。SG 函数是解决像取石子这类博弈论问题的一大利器。
以取石子游戏为例,我们把每一堆石子可能发生的变化用 DAG 来表示,其中节点代表一个局面,边代表可以从一个局面转移到另一个局面。比如以本题的规则对 3 堆石子 [1,3,4] 进行游戏得到的 DAG 如下:
每个节点(局面)u 的 SG 值就是其后继节点 v1,…vm 的 SG 值的 mex 值,即 SG(u)=mex{SG(v1),…,SG(vm)},其中定义集合 S 的 mex 值为不属于 S 中的最小非负整数。上图中每个节点的 SG 值如下所示:
可以发现每一个 DAG 都只有一个起点 S,因此如果 SG(S)≠0 则先手必胜,否则 SG(S)=0 则先手必败。这是因为任意 SG 大于 0 的节点都可以走到 SG 为 0 的节点,而任意 SG 为 0 的节点必然走到 SG 大于 0 的节点(否则当前节点的 SG 值就大于 0 了),又因为所有的终点(即出度为 0 的点)的 SG 值为 0,对应着先手必败,因此结论得证。
而如果有 n 个 DAG,每个 DAG 的起点分别为 S1,…,Sn,那么整个游戏先手必胜的条件是 SG(S1)⊕⋯⊕SG(Sn)≠0,否则 SG(S1)⊕⋯⊕SG(Sn)=0 则先手必败。可以将其看作是 Nim 游戏,证明的方法也与 Nim 游戏十分相似。
回到本题,令 A=max,显然我们需要求出 1 \sim A 的 SG 值,然后根据 \mathrm{SG}(a_1) \oplus \cdots \oplus \mathrm{SG}(a_n) 判断先手必胜还是必败,时间复杂度为 O(A^2 \log{A})。由于 A 最大取 10^7,因此这种做法显会然超时。
我们先打表看看 SG 值是否满足某些规律,以下是求出 1 \sim 100 的 SG 值的程序和结果:
打表代码
#include <bits/stdc++.h>
using namespace std;
const int N = 105;
int f[N];
int dfs(int u) {
if (f[u] != -1) return f[u];
set<int> st;
for (int i = 1; i <= u; i++) {
if (__gcd(i, u) == 1) st.insert(dfs(u - i));
}
for (int i = 0; true; i++) {
if (!st.count(i)) return f[u] = i;
}
}
int main() {
memset(f, -1, sizeof(f));
for (int i = 1; i <= 100; i++) {
cout << "SG(" << i << ") = " << dfs(i) << '\n';
}
return 0;
}
打表结果
SG(1) = 1
SG(2) = 0
SG(3) = 2
SG(4) = 0
SG(5) = 3
SG(6) = 0
SG(7) = 4
SG(8) = 0
SG(9) = 2
SG(10) = 0
SG(11) = 5
SG(12) = 0
SG(13) = 6
SG(14) = 0
SG(15) = 2
SG(16) = 0
SG(17) = 7
SG(18) = 0
SG(19) = 8
SG(20) = 0
SG(21) = 2
SG(22) = 0
SG(23) = 9
SG(24) = 0
SG(25) = 3
SG(26) = 0
SG(27) = 2
SG(28) = 0
SG(29) = 10
SG(30) = 0
SG(31) = 11
SG(32) = 0
SG(33) = 2
SG(34) = 0
SG(35) = 3
SG(36) = 0
SG(37) = 12
SG(38) = 0
SG(39) = 2
SG(40) = 0
SG(41) = 13
SG(42) = 0
SG(43) = 14
SG(44) = 0
SG(45) = 2
SG(46) = 0
SG(47) = 15
SG(48) = 0
SG(49) = 4
SG(50) = 0
SG(51) = 2
SG(52) = 0
SG(53) = 16
SG(54) = 0
SG(55) = 3
SG(56) = 0
SG(57) = 2
SG(58) = 0
SG(59) = 17
SG(60) = 0
SG(61) = 18
SG(62) = 0
SG(63) = 2
SG(64) = 0
SG(65) = 3
SG(66) = 0
SG(67) = 19
SG(68) = 0
SG(69) = 2
SG(70) = 0
SG(71) = 20
SG(72) = 0
SG(73) = 21
SG(74) = 0
SG(75) = 2
SG(76) = 0
SG(77) = 4
SG(78) = 0
SG(79) = 22
SG(80) = 0
SG(81) = 2
SG(82) = 0
SG(83) = 23
SG(84) = 0
SG(85) = 3
SG(86) = 0
SG(87) = 2
SG(88) = 0
SG(89) = 24
SG(90) = 0
SG(91) = 4
SG(92) = 0
SG(93) = 2
SG(94) = 0
SG(95) = 3
SG(96) = 0
SG(97) = 25
SG(98) = 0
SG(99) = 2
SG(100) = 0
可以发现以下规律:
- 如果 x 是偶数,那么 \mathrm{SG}(x) = 0。
- 如果 x 是从 3 开始的质数,那么 \mathrm{SG}(x) = p(x),其中 p(x) 指 x 在所有质数中的第几个。
- 如果 x 是奇合数,那么 \mathrm{SG}(x) = \mathrm{SG}(d(x)),其中 d(x) 指 x 的最小质因子。
- 特别的,\mathrm{SG}(1) = 1,\mathrm{SG}(2) = 0。
因此我们就可以用欧拉筛以 O(A) 的复杂度求出 1 \sim A 的 SG 值。
AC 代码如下,时间复杂度为 O(A):
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e7 + 5;
int primes[N], cnt;
bool vis[N];
int f[N];
void get_prime(int n) {
for (int i = 2; i <= n; i++) {
if (!vis[i]) {
primes[cnt++] = i;
f[i] = cnt;
}
for (int j = 0; primes[j] * i <= n; j++) {
int t = primes[j] * i;
vis[t] = true;
if (t & 1) f[t] = f[primes[j]];
if (i % primes[j] == 0) break;
}
}
f[1] = 1, f[2] = 0;
}
void solve() {
int n;
cin >> n;
int ret = 0;
while (n--) {
int x;
cin >> x;
ret ^= f[x];
}
cout << (ret ? "Alice" : "Bob") << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
get_prime(N - 1);
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
参考资料
第四章 数学知识(四):https://www.acwing.com/file_system/file/content/whole/index/content/4810/
公平组合游戏 - OI Wiki:https://oi-wiki.org/math/game-theory/impartial-game/
Educational Codeforces Round 169 Editorial:https://codeforces.com/blog/entry/132790
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/18364766
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步