题目描述

Alice 和 Bob 又在玩游戏。

有 $n$ 个节点,$m$ 条边 ($0 \leq m \leq n - 1$),构成若干棵有根树,每棵树的根节点是该连通块内编号最小的点。

Alice 和 Bob 轮流操作 (Alice 先手),每回合选择一个没有被删除的节点 $x$,将 $x$ 及其所有祖先全部删除,不能操作的人输。

需要注意的是,树的形态是在一开始就确定好的,删除节点不会影响剩余节点父亲和儿子的关系。

比如:$1 - 3 - 2$ 这样一条链,$1$ 号点是根节点,删除 $1$ 号点之后,$3$ 号点还是 $2$ 号点的父节点。

假设 Alice 和 Bob 都足够聪明,问 Alice 有没有必胜策略。

输入格式

第一行包含一个正整数 $T$ ($1 \leq T \leq 10$),表示该测试点有 $T$ 组数据。

对于每组数据,第一行包含两个非负整数 $n, m$ ($1 \leq n \leq 10^5; \sum n \leq 2 \times 10^5; 0 \leq m \leq n - 1$),分别表示点数和边数 (节点从 $1$ 开始编号)。

接下来 $m$ 行,每行两个正整数 $a, b$ ($1 \leq a, b \leq n$),表示节点 $a$ 和节点 $b$ 之间有一条边,输入数据中没有重边。

输出格式

对于每组数据,输出一行一个字符串,表示 Alice 先手并且 Alice 和 Bob 都足够聪明的情况下谁获胜。

题解

可以发现,游戏对森林中的每一棵树独立,因此,由 SG 定理,我们只需考虑每一棵树的 SG 值,然后将其异或起来即可。

接下来考虑计算一棵树的 SG 值。

我们采用 SG 函数的定义,它等于所有后继状态的 SG 值的 mex 值。

可以发现,它的每一个后继状态都是一个森林。那如何来求 mex 值呢?

先考虑删除根节点 $r$ 的情况,此时 SG 值为所有子树的 SG 值的异或和 $s_0$,这个不难维护。

若删除的不仅是 $r$,设它在以 $c$ 为根的子树 $C$ 中。则剩下的森林由$r$ 的子节点 ($c$ 除外) 的所有子树,以及在 $C$ 中的若干个子树

发现后半部分是在求 $c$ 的 SG 函数时已经涉及到的子树。

因此,我们得到了:如果在求 $c$ 的时候,最终的 SG 值集合为 $A$ (从而 $\mathrm{SG}(c) = \mathrm{mex} \, A$),则 $r$ 对应的集合应包含 $A$ 中每个元素异或上 "除 $c$ 外的所有子树的 SG 异或和" 的值。

对于每个 $c$,最终的集合都要包含这些元素,再算上 $s_0$,就是所得的最终集合 $R$。其中 $\mathrm{SG}(r) = \mathrm{mex} \, R$。

也就是说,我们需要实现这样一个数据结构:它能维护集合的加元素,以及一个集合异或上一个元素后与另一个集合合并。

这可以用可合并字典树来解决。合并的时候,就像线段树一样写就好了。

具体地,可以在每个点维护一个 val 信息,表示字典树中,以它为根的子树内部有几个互异元素,这样寻找 mex 值 (最小未出现值) 时就可以通过在字典树上二分解决。

其中异或上的元素,等于 "除 $c$ 外的所有子树的 SG 异或和",于是可以用打标记的思想,给整棵字典树打一个标记 $tag$,表示它内部存储的 $x$,实际上表示 $x \oplus tag$。这样合并时只需异或上 $\mathrm{SG}(c)$ 即可,插入和询问的贪心时注意标记的影响。

总时间复杂度 $O \left( \sum n \log n \right)$。

代码

#include <bits/stdc++.h>

const int N = 100010, M = N * 2, Z = N * 24;

int V, E, Es;
int to[M], first[N], next[M];
int cnt, root[N], sg[N], mask[N];
int d[Z][2], val[Z];

inline void addedge(int u, int v) {
	to[++Es] = v; next[Es] = first[u]; first[u] = Es;
	to[++Es] = u; next[Es] = first[v]; first[v] = Es;
}

void clear() {for (; cnt; --cnt) d[cnt][0] = d[cnt][1] = val[cnt] = 0;}

void insert(int t, int x) {
	int b, ch;
	for (b = 23; b >= 0; --b) {
		++val[t]; ch = x >> b & 1;
		t = (d[t][ch] ? d[t][ch] : d[t][ch] = ++cnt);
	}
	++val[t];
}

int merge(int t1, int t2, int offset, int b) {
	if (!(t1 && t2)) return t1 | t2;
	if (!~b) return val[t1] |= val[t2], t1;
	int ch = offset >> b & 1;
	d[t1][0] = merge(d[t1][0], d[t2][ch], offset, b - 1);
	d[t1][1] = merge(d[t1][1], d[t2][ch ^ 1], offset, b - 1);
	val[t1] = d[t1][0][val] + d[t1][1][val];
	return t1;
}

int query_min(int t, int x) {
	int b, ch, cur = 0;
	for (b = 23; b >= 0; --b) {
		if (!t) return cur << (b + 1);
		ch = x >> b & 1;
		val[d[t][ch]] < 1 << b ? (t = d[t][ch], cur = cur << 1) : (t = d[t][ch ^ 1], cur = cur << 1 | 1);
	}
	return cur;
}

void dfs(int x) {
	int i, y; mask[x] = 0;
	insert(root[x] = ++cnt, 0);
	for (i = first[x]; i; i = next[i])
		if (!root[y = to[i]]) {
			dfs(y); mask[x] ^= sg[y];
			root[x] = merge(root[x], root[y], mask[y] ^ sg[y], 23);
		}
	sg[x] = query_min(root[x], mask[x]);
}

void work() {
	int i, u, v, ans = 0; Es = 0;
	memset(first, 0, sizeof first);
	scanf("%d%d", &V, &E);
	for (i = 0; i < E; ++i) scanf("%d%d", &u, &v), addedge(u, v);
	memset(root, 0, sizeof root);
	for (i = 1; i <= V; ++i)
		if (!root[i]) clear(), dfs(i), ans ^= sg[i];
	puts(ans ? "Alice" : "Bob");
}

int main() {
	int T;
	for (scanf("%d", &T); T; --T) work();
	return 0;
}

坑1:注意字典树合并时的深度参数,依据写法的不同而不同,比如,按照上面这种写法,要到 $-1$ 层后才到底层,才可以使用直接合并策略。