[BZOJ4730][UOJ#266][清华集训2016]Alice和Bob又在玩游戏(SG 函数 + Trie + 线段树合并)

Address

Solution

  • S G [ u ] SG[u] SG[u] 表示当前存在的点只剩下 u u u 的子树时的状态的 S G SG SG
  • 显然整个游戏的 S G SG SG 值为
  • X O R u 是 根 节 点 S G [ u ] XOR_{u是根节点}SG[u] XORuSG[u]
  • 如果上式非零则先手必胜,否则先手必败
  • 考虑通过 DP 求得 S G [ u ] SG[u] SG[u] 。一个显然的转移方程
  • S G [ u ] = m e x { ⨁ w 的 父 亲 在 u 到 v 的 路 径 上 , w 不 在 u 到 v 的 路 径 上 S G [ w ] , v 在 u 的 子 树 内 } SG[u]=mex\{\bigoplus_{w的父亲在u到v的路径上,w不在u到v的路径上} SG[w],v在u的子树内\} SG[u]=mex{wuvwuvSG[w],vu}
  • ⨁ \bigoplus 为异或运算)
  • 这样的 DP 是 O ( n 2 ) O(n^2) O(n2) 的,瓶颈在于枚举 v v v
  • 我们设 a u = ⨁ v 是 u 的 子 节 点 S G [ v ] a_u=\bigoplus_{v是u的子节点} SG[v] au=vuSG[v] b u = ⨁ v 是 u 的 子 节 点 或 v = u S G [ v ] b_u=\bigoplus_{v是u的子节点或v=u}SG[v] bu=vuv=uSG[v]
  • 那么我们就可以把 ⨁ w 的 父 亲 在 u 到 v 的 路 径 上 , w 不 在 u 到 v 的 路 径 上 S G [ w ] \bigoplus_{w的父亲在u到v的路径上,w不在u到v的路径上}SG[w] wuvwuvSG[w] 进行一些转化,根据异或运算的消去律,我们有
  • ⨁ w 的 父 亲 在 u 到 v 的 路 径 上 , w 不 在 u 到 v 的 路 径 上 S G [ w ] = ( ⨁ w 在 u 到 v 的 路 径 上 且 不 为 u b w ) ⨁ a u \bigoplus_{w的父亲在u到v的路径上,w不在u到v的路径上}SG[w]=(\bigoplus_{w在u到v的路径上且不为u}b_w)\bigoplus a_u wuvwuvSG[w]=(wuvubw)au
  • 假设有一棵二进制意义下的 Trie 树,其中储存了所有 u u u 的子树内的 v v v 对应的 ⨁ w 在 u 到 v 的 路 径 上 且 不 为 u b w \bigoplus_{w在u到v的路径上且不为u}b_w wuvubw(如果 v = u v=u v=u 则该式为 0 0 0 ),考虑如何求 S G [ u ] SG[u] SG[u]
  • 易得我们需要求得一个最小的数 r e s res res ,使得 a u a_u au 异或上 Trie 树储存的任意一个数都不能得到res
  • 考虑在 Trie 树上从上往下贪心(从高到低确定 r e s res res 的每一位):设当前要确定第 i i i 位(这里二进制位由低往高,最低位为第 0 0 0 位),现在走到 Trie 树的节点 x x x
  • a u a_u au 的第 i i i 位为 1 1 1 的时候,我们考虑 r e s res res 的第 i i i 位可以为 0 0 0 的条件: x x x 的右子节点(由字符为 1 1 1 的边转移到的子节点)对应的子树不是满二叉树,即子树内的叶子数小于 2 i 2^i 2i (可以在 Trie 的每个节点上储存一个 s i z e size size 表示子树内的叶子个数)
  • 这时候我们就可以把 r e s res res 的第 i i i 位设为 0 0 0 ,否则设为 1 1 1 a u a_u au 的第 i i i 位为 0 0 0 同理
  • x x x 到达了叶子节点之后就得到了 S G [ u ] = r e s SG[u]=res SG[u]=res
  • 但我们显然不能直接把这棵 Trie 建出来,否则这个 DP 不能得到复杂度上的优化
  • 下面我们设 s ( u , v ) = ⨁ w 在 u 到 v 的 路 径 上 b w s(u,v)=\bigoplus_{w在u到v的路径上}b_w s(u,v)=wuvbw (其中 u u u v v v 的祖先),并且 u u u 的所有子节点 v v v 都有一棵 Trie 树,储存 v v v 的子树内所有节点 w w w s ( v , w ) s(v,w) s(v,w)
  • 这时候我们发现:只要把 u u u 的所有子节点的 Trie 树合并起来(即对应的所有值合并),再把 0 0 0 插入 Trie 树,我们就得到了我们按位贪心求 S G [ u ] SG[u] SG[u] 需要用到的 Trie 树
  • u u u 的所有子节点的 Trie 树合并起来,可以使用和线段树合并一样的做法,不断递归合并左子树和右子树,到达叶子节点或空节点时停止递归。注意 s i z e size size 的更新
  • 已经求得了 S G [ u ] SG[u] SG[u] 之后,我们就能求得 b u b_u bu 。这时候我们需要让这棵 Trie 里面储存的是 u u u 的子树内所有节点 v v v s ( u , v ) s(u,v) s(u,v) 值(因为计算祖先的 S G SG SG 时还要用到)
  • 这相当于 Trie 树内储存的所有数都异或上 b u b_u bu
  • 我们可以借鉴线段树打标记的思想,在 Trie 树的根节点打上标记
  • 当 Trie 树中插入新数、合并、按位贪心时需要下放标记
  • 下放标记的具体方法:如果要下放 Trie 树节点 x x x 上的标记 t a g tag tag ,且连接 x x x x x x 的子节点的转移边表示第 i i i 位,则把 x x x 的左子节点的标记和右子节点的标记都异或上 t a g tag tag
  • 如果 t a g tag tag 的第 i i i 位为 1 1 1 则需要交换 x x x 的左右子节点(因为这时异或上 t a g tag tag 相当于第 i i i 位被取反),最后将 x x x 节点上的标记清空
  • 复杂度 O ( T n log ⁡ ⁡ n ) O(Tn\log⁡n) O(Tnlogn)

Code

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

inline int read()
{
	int res = 0; bool bo = 0; char c;
	while (((c = getchar()) < '0' || c > '9') && c != '-');
	if (c == '-') bo = 1; else res = c - 48;
	while ((c = getchar()) >= '0' && c <= '9')
		res = (res << 3) + (res << 1) + (c - 48);
	return bo ? ~res + 1 : res;
}

template <class T>
inline void Swap(T &a, T &b) {T t = a; a = b; b = t;}

const int N = 1e5 + 5, M = N << 1, L = 7e6 + 5;

int n, m, ecnt, nxt[M], adj[N], go[M], ToT, SG[N], rt[N];
bool vis[N];

struct Trie
{
	int lc, rc, tag, sze;
	
	void init()
	{
		lc = rc = tag = sze = 0;
	}
} T[L];

void add_edge(int u, int v)
{
	nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v;
	nxt[++ecnt] = adj[v]; adj[v] = ecnt; go[ecnt] = u;
}

void downdate(int x, int i)
{
	if ((T[x].tag >> i) & 1) Swap(T[x].lc, T[x].rc);
	if (T[x].lc) T[T[x].lc].tag ^= T[x].tag;
	if (T[x].rc) T[T[x].rc].tag ^= T[x].tag;
}

void ins(int i, int &x, int num)
{
	if (!x) T[x = ++ToT].init();
	if (i == -1) return (void) (T[x].sze = 1);
	downdate(x, i);
	if ((num >> i) & 1) ins(i - 1, T[x].rc, num);
	else ins(i - 1, T[x].lc, num);
	T[x].sze = T[T[x].lc].sze + T[T[x].rc].sze;
}

int min_mex_xor(int x, int num)
{
	int res = 0;
	for (int i = 30; i >= 0; i--)
	{
		downdate(x, i);
		if ((num >> i) & 1)
		{
			if (T[T[x].rc].sze < (1 << i)) x = T[x].rc;
			else x = T[x].lc, res |= 1 << i;
		}
		else
		{
			if (T[T[x].lc].sze < (1 << i)) x = T[x].lc;
			else x = T[x].rc, res |= 1 << i;
		}
	}
	return res;
}

int merge(int i, int x, int y)
{
	if (!x || !y) return x + y;
	if (i == -1) return x;
	downdate(x, i); downdate(y, i);
	T[x].lc = merge(i - 1, T[x].lc, T[y].lc);
	T[x].rc = merge(i - 1, T[x].rc, T[y].rc);
	return T[x].sze = T[T[x].lc].sze + T[T[x].rc].sze, x;
}

void dfs(int u, int fu)
{
	vis[u] = 1;
	rt[u] = 0;
	ins(30, rt[u], 0);
	int x = 0;
	for (int e = adj[u], v; e; e = nxt[e])
	{
		if ((v = go[e]) == fu) continue;
		dfs(v, u);
		merge(30, rt[u], rt[v]);
		x ^= SG[v];
	}
	SG[u] = min_mex_xor(rt[u], x);
	T[rt[u]].tag ^= x ^ SG[u];
}

void work()
{
	int x, y, res = 0;
	n = read(); m = read();
	ecnt = 0;
	for (int i = 1; i <= n; i++) adj[i] = 0, vis[i] = 0;
	while (m--) x = read(), y = read(), add_edge(x, y);
	ToT = 0;
	for (int i = 1; i <= n; i++) if (!vis[i])
		res ^= (dfs(i, 0), SG[i]);
	puts(res ? "Alice" : "Bob");
}

int main()
{
	int T = read();
	while (T--) work();
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值