【题解】CF1498F Christmas Game(换根 dp)

发布时间 2023-03-30 10:13:42作者: linyihdfj

题目分析:

感觉这个题目难度适中,而且换根 \(dp\) 的过程相当好写并且很 educational,所以就当作换根 \(dp\) 的典例,来讲讲换根 \(dp\) 到底是个啥吧。
换根 \(dp\) 其实就是用来解决:树上询问以每个点为根的相关信息,以指定某个点为根的时候信息很好求解,在换根的时候只会影响极少点的信息,而且可以支持快速维护在某个点上删除某棵子树后的信息。
虽然说了这一大堆但是其实关键就是是否询问以每个点为根的相关信息,如果是就可以考虑一下换根 \(dp\)
对于换根 \(dp\) 的一般过程就是先指定某一个点为根,维护出以这个点为根的所有节点的相关信息,然后通过一遍 \(dfs\) 实现换根,这样就可以实现最终得到的每个点的信息都是它作为根的信息。而在换根即根 \(u \to v\) 的过程中,其实就是在 \(u\) 中删去 \(v\) 这棵子树后再合并到 \(v\) 上的过程。

来具体看看这个题。
首先这个题可能主要是套了一层博弈,所以就显得有点难,但是其实还可以的,那就先分析一下这个博弈吧。
可以发现对于 \(dep \% k\) 不同的点互不影响,所以就可以按 \(dep \% k\) 分类,每一类单独考虑。发现分类之后其实就是一个树上阶梯博弈,若根的 \(dep = 0\) 则一个经典的结论就是树上阶梯博弈相当于 \(dep\) 为奇数的点的 Nim 游戏,因为考虑对于 \(dep\) 为偶数的点,若我们当前移动了它到了奇数位置,对手可以立即将他移动到偶数位置,而对于 \(0\) 这个偶数位置意味着删除,所以在偶数位的数都可以理解为删除。而对于 \(dep \% k\) 不同的可以理解为子游戏,一个定理就是整个游戏的 SG 值等于所有子游戏的 SG 值的异或值。
当然上面说的并不是很严谨,因为我们按 \(dep \% k\) 分类了,所以其实是 \(\lfloor \frac{dep}{k} \rfloor\) 为奇数的点,也就是在这一类中的 'dep' 为奇数。
所以就可以显然想到一个 \(dp\) 状态,即 \(dp[i][j][0/1]\) 表示以 \(i\) 为根的子树,\(dep_u \% k = j\)\(\lfloor \frac{dep_u}{k} \rfloor \% 2 = 0/1\)\(a_u\) 的异或和,注意为了方便换根,我们这里的 \(dep_u\) 是指的以 \(i\) 为根的情况下 \(u\)\(dep\),因为如果是全局的 \(dep\),显然换根就不是很方便了。
这个状态转移也是简单的,就直接枚举一下子树的状态判断会贡献到根的哪个状态就好了。
因为这是异或操作,所以对于换根中的删除子树直接异或就可以删除了,所以换根也是简单的。

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
struct edge{
	int nxt,to;
	edge(){}
	edge(int _nxt,int _to){
		nxt = _nxt,to = _to;
	}
}e[2 * N];
int cnt,head[N],n,k,a[N],f[N][30][3],tmp[30][3];
void add_edge(int from,int to){
	e[++cnt] = edge(head[from],to);
	head[from] = cnt;
}
void dfs(int now,int fath){
	f[now][0][0] = a[now];
	for(int i=head[now]; i; i = e[i].nxt){
		int to = e[i].to;
		if(to == fath)	continue;
		dfs(to,now);
		for(int j=0; j<k; j++){
			for(int p=0; p<2; p++){
				int tmp1 = j,tmp2 = p;
				tmp1++;
				if(tmp1 >= k)	tmp1 -= k,tmp2 = (tmp2 + 1) % 2;
				f[now][tmp1][tmp2] ^= f[to][j][p];
			}
		}
	}
}
void dp(int now,int fath){
	if(now != 1){
		for(int j=0; j<k; j++){
			for(int p=0; p<2; p++){
				tmp[j][p] = f[fath][j][p];
			}
		}
		//找到原父亲对应的子树的信息,即去掉 now 这棵子树的贡献 
		for(int j=0; j<k; j++){
			for(int p=0; p<2; p++){
				int tmp1 = j,tmp2 = p;
				tmp1++;
				if(tmp1 >= k)	tmp1 -= k,tmp2 = (tmp2 + 1) % 2;
				tmp[tmp1][tmp2] ^= f[now][j][p];
			}
		}
		//将父亲所在的子树与当前节点合并
		for(int j=0; j<k; j++){
			for(int p=0; p<2; p++){
				int tmp1 = j,tmp2 = p;
				tmp1++;
				if(tmp1 >= k)	tmp1 -= k,tmp2 = (tmp2 + 1) % 2;
				f[now][tmp1][tmp2] ^= tmp[j][p];
			}
		} 
	}
	for(int i=head[now]; i;i = e[i].nxt){
		int to = e[i].to;
		if(to == fath)	continue;
		dp(to,now);
	}
}
int main(){
//	freopen("in.txt","r",stdin);
//	freopen("out.txt","w",stdout);
	scanf("%d%d",&n,&k);
	for(int i=1; i<n; i++){
		int from,to;scanf("%d%d",&from,&to);
		add_edge(from,to);add_edge(to,from);
	}
	for(int i=1; i<=n; i++)	scanf("%d",&a[i]);
	dfs(1,0);
	dp(1,0);
	for(int i=1; i<=n; i++){
		int ans = 0;
		for(int j=0; j<k; j++)	ans = ans^f[i][j][1];
		if(ans == 0)	printf("0 ");
		else	printf("1 ");
	}
	return 0;
}