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