小奇的仓库「换根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\n",ans[i]);
	}
	return 0;
}

posted @ 2020-08-13 06:17  HH_Halo  阅读(160)  评论(0编辑  收藏  举报