小奇的仓库「换根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;
}