小奇的仓库「换根DP」

小奇的仓库「换根DP」

题目描述

喵星系有n个星球,星球以及星球间的航线形成一棵树。

从星球a到星球b要花费 (dis(a,b)) (Xor) (M)
秒。

为了给仓库选址,小奇想知道,星球 (i(1<=i<=n)) 到其它所有星球花费的时间之和。

输入格式

第一行包含两个正整数 (n,M)。 接下来 (n-1) 行,每行 (3) 个正整数 (a,b,c),表示 (a,b) 之间的航线长度为 (c)

输出格式

(n) 行,每行一个整数,表示星球 (i) 到其它所有星球花费的时间之和。

样例

样例输入

4 0
1 2 1
1 3 2
1 4 3

样例输出

6
8
10
12

数据范围

对于所有数据,(n <= 100000,M <= 15)


思路分析

刷不动题了来水一篇题解不让自己闲着

  • 暴力很暴力,以每个点为根来跑了一遍 (BFS) 水了 (30pts)
  • 然而在上述过程中其实两个点之间的距离是会被重复计算的,而且每次都是重新出发,以前得到的信息都没有用到,这是暴力需要优化的根本所在
  • 每个节点都要作为根,自然就需要用到换根 (DP) ,换根 (DP) 之所以优秀,是因为既可以对所有节点为根进行一系列统计,又不会每次重新开始,而是从上一个进行转移
  • 首先以 (1) 作为根节点,这个距离直接求出来就行了,得出 (ans[1] = sum_{i=2}^{n}dis[i])
  • 接着就是换根 (DP) 的灵魂操作:
    • 假设我们已经处理出以 (x) 为根节点的信息,接下来我们要处理与 (x) 相连的 (y) 节点作为根节点(均指整棵树)。
    • 我们从 (x) 通过相连的边到达 (y) ,那么变化的根本在于这条边的权值上。
    • 我们在经过这条边时,接近了(y) 子树内的节点,而远离了 (y) 子树外的节点,所以以 (y) 为根节点的距离总和可以直接从 (x) 转移
    • 由此得出公式,即 (ans[y] = ans[x]-siz[y]*e[i].dis + (n-siz[y])*e[i].dis) ((siz) 为子树的节点个数)
  • 然后就是这题的另一个核心——异或
    • 考虑 (M) 会对答案产生什么影响,看一眼数据范围,(M) 最大才 (15),那么每个距离的波动范围最大也就是 (15)
    • 我们将处理出的每一个距离 (mod) (16),这样就可以在异或 (M) 时计算出 (M) 对答案产生的影响
    • 如上,我们开两个数组来记录边, (f[x][i]) 表示 (x) 子树内路径长 (mod) (16) 后为 (i) 的边数,而(g[x][i]) 表示 (x) 作为根节点时整棵树的
    • 接下来就是转移,同 (ans) 数组的转移类似,(f[x][(i+e[i].dis)\%16] = f[y][i]),关键是 (g[y][i]) 的转移,对于子树内,直接从 (f) 数组转移即可,而对于子树外,(g[y][(i+e[i].dis)%16]) 如果直接 (+g[x][i]) 会包括 (y) 子树内的点,但当 (y) 作为根时就不满足了,所以要减去 (f[y][i-(e[i].dis)\%16])

详见代码

Code

#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#define int long long
#define N 100010
using namespace std;
inline int read(){
	int x=0,f=1;
	char ch=getchar();
	while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return x*f;
}
const int mod = 16;
int n,m,dis[N],f[N][25],g[N][25],siz[N],head[N],ans[N];
struct edge{
	int to,next,dis;
}e[N<<1];
int len;
void addedge(int u,int v,int w){
	e[++len].to = v;
	e[len].dis = w;
	e[len].next = head[u];
	head[u] = len;
}
void dfs(int u,int fa){ //预处理出子树节点个数
	siz[u] = 1;
	for(int i = head[u];i;i = e[i].next){
		int v = e[i].to;
		if(v==fa)continue;
		dis[v] = dis[u]+e[i].dis;
		dfs(v,u);
		siz[u] += siz[v];
	}
	return;
}
void get_ans(int u,int fa){ //算出不异或时的结果
	for(int i = head[u];i;i=e[i].next){
		int v = e[i].to;
		if(v==fa)continue;
		ans[v] = ans[u]-(siz[v]*e[i].dis)+(n-siz[v])*e[i].dis;
		get_ans(v,u);
	}
	return;
}
void get_f(int u,int fa){ //计算f
	f[u][0] = 1;
	for(int i = head[u];i;i = e[i].next){
		int v = e[i].to;
		if(v==fa)continue;
		get_f(v,u);
		for(int j = 0;j <= 15;j++){
			f[u][(j+e[i].dis)%mod] += f[v][j];
		}
	}
	return;
}
void get_g(int u,int fa){ //转移g
	for(int i = head[u];i;i = e[i].next){
		int v = e[i].to;
		if(v==fa)continue;
		for(int j = 0;j <= 15;j++){
			g[v][(j+e[i].dis)%mod] += f[v][(j+e[i].dis)%mod]+g[u][j]-f[v][(j-e[i].dis%mod+mod)%mod]; //注意这里直接算肯定会出现负数
		}
		get_g(v,u);
	}
	return;
}

signed main(){
	n = read(),m = read();
	for(int i = 1;i < n;i++){
		int a,b,c;a = read(),b = read(),c = read();
		addedge(a,b,c);addedge(b,a,c);
	}
	dis[0] = 0;
	dfs(1,0);
	for(int i = 2;i <= n;i++)ans[1] += dis[i]; //因为1是最初的根节点,所以我们直接先算出来
	get_ans(1,0);
	get_f(1,0);
	for(int i = 0;i <= 15;i++)g[1][i] = f[1][i];//同理
	get_g(1,0);
	for(int i = 1;i <= n;i++){
		g[i][0]--;
		for(int j = 0;j <= 15;j++){
			int delta = (j-(j^m)); //j^m为变化后的数,而j-(j^m)则为变化的大小
			ans[i] -= delta*g[i][j]; //相应的边减去(也可能为加)变化的贡献
		}
		printf("%lld
",ans[i]);
	}
	return 0;
}

原文地址:https://www.cnblogs.com/hhhhalo/p/13493205.html