题目描述
给定一颗有 n n n 个节点的树,带边权,要进行 Q Q Q 次操作,操作有两种:
1 i w
:将第 i i i 条边的边权改为 w w w。
2 u v
:询问 u , v u,v u,v 两点的距离。
输入格式
第一行,一个正整数 n n n。
接下来 n − 1 n-1 n−1 行,每行三个数 u , v , w u,v,w u,v,w,表示一条树边。
接下来一个正整数 Q Q Q。
接下来 Q Q Q 行,每行三个数,描述一个询问,格式如上。
输出格式
对于每个 2 2 2 操作,输出一行一个数,表示该询问的答案。
说明/提示
1 ≤ n , Q ≤ 2 × 1 0 5 , 1 ≤ w i ≤ 1 0 9 1\le n,Q\le 2\times10^5,1\le w_i\le 10^9 1≤n,Q≤2×105,1≤wi≤109
样例 #1
样例输入 #1
5
1 2 3
1 3 6
1 4 9
4 5 10
4
2 2 3
2 1 5
1 3 1
2 1 5
样例输出 #1
9
19
11
样例 #2
样例输入 #2
7
1 2 1000000000
2 3 1000000000
3 4 1000000000
4 5 1000000000
5 6 1000000000
6 7 1000000000
3
2 1 6
1 1 294967296
2 1 6
样例输出 #2
5000000000
4294967296
样例 #3
样例输入 #3
1
1
2 1 1
样例输出 #3
0
样例 #4
样例输入 #4
8
1 2 105
1 3 103
2 4 105
2 5 100
5 6 101
3 7 106
3 8 100
18
2 2 8
2 3 6
1 4 108
2 3 4
2 3 5
2 5 5
2 3 1
2 4 3
1 1 107
2 3 1
2 7 6
2 3 8
2 1 5
2 7 6
2 4 7
2 1 7
2 5 3
2 8 6
样例输出 #4
308
409
313
316
0
103
313
103
525
100
215
525
421
209
318
519
提示
数据范围
- $ 1\leq\ N\leq\ 2\times10^5 $
- $ 1\leq\ u\ _\ i,v\ _\ i\leq\ N\ (1\leq\ i\leq\ N-1) $
- $ 1\leq\ w\ _\ i\leq\ 10^9\ (1\leq\ i\leq\ N-1) $
- $ 1\leq\ Q\leq\ 2\times10^5 $
题解
本题用到了 LCA、DFS序、树状数组和差分。
前置知识参考文章:LCA讲解,DFS序讲解,树状数组讲解,差分算法
这道题有两个需要解决的问题。
我们先看操作2:询问两点的距离。
要求两点的距离,可以将 1 1 1 作为根节点,初始化为有向树,初始化出每个节点到根节点的距离 d i s t dist dist,然后计算出两个节点的最近公共祖先节点 l c a lca lca,两点之间的距离等于 d i s t [ u ] + d i s t [ v ] − 2 ∗ d i s t [ l c a ] dist[u] + dist[v] - 2*dist[lca] dist[u]+dist[v]−2∗dist[lca],因为这道题的权重在边上,所以初始化的时候,对于一条边有两个端点,那么这个边的权重会添加到 深度更大的节点上,深度小的节点不需要添加该权重。
操作 1 1 1:将某条边的边权改成 w w w,试想,如果操作 2 2 2 按上面的做法,那操作 1 1 1 执行一次,操作 2 2 2 就得重新初始化一次,因为改动一条边会对整个 d i s t dist dist 产生影响。因此此时我们想到树状数组和差分。
我们可以对树进行一次搜索,得到一个 D F S DFS DFS 序,每个节点都有两个时间戳,一个是到达这个节点的时间戳 i n [ u ] in[u] in[u],另一个是离开这个节点的时间戳 o u t [ u ] out[u] out[u],以第一次到达该结点的时间戳作为节点编号进行树状数组操作。
详细可以参考代码注释。
code
cpp
#include "bits/stdc++.h"
using namespace std;
#define int long long
const int N = 2e5+7;
struct Edge{
int u, v, w, ne;
}es[N<<1];
int n, c[N], q, h[N], t, op, u, v, w, x, y, z, idx = 0, f[N][20], dep[N], in[N], out[N];
// dep[x] 表示 节点 x 的深度,根节点深度为 1
// f[i][j] : 节点 i 往上 2^j 的 父节点
// in[u]: 表示到达节点 u 的时间戳
// out[u]: 表示离开节点 u 的时间戳
void add(int u, int v, int w) {
es[++idx] = Edge{u, v, w, h[u]};
h[u] = idx;
return ;
}
// dfs: 初始化dep数组,进入时间戳数组in,和离开节点时间戳数组 out
void dfs(int u, int fa) {
in[u] = ++t;
dep[u] = dep[fa] + 1;
f[u][0] = fa;
for(int e = h[u]; e; e = es[e].ne) {
int v = es[e].v;
if(v != fa) dfs(v, u);
}
out[u] = t;
}
int lca(int u, int v) {
if(u == v) return u;
if(dep[u] < dep[v]) swap(u, v);
for(int i=19; i>=0; i--) {
if(dep[f[u][i]] >= dep[v]) u = f[u][i];
}
if(u == v) return u;
for(int i=19; i>=0; i--) {
if(f[u][i] != f[v][i]) u = f[u][i], v = f[v][i];
}
return f[u][0];
}
int lowbit(int x) { return x&-x; }
// 树状数组 add 操作,给 原数组 x 位置加上 w
void add(int x, int w) {
for(int i=x; i<=n; i+=lowbit(i)) { c[i] += w;}
return;
}
// 求[1...x] 的前缀和
int sum(int x) {
int res = 0;
while(x) {
res += c[x];
x -= lowbit(x);
}
return res;
}
void update(int e, int w) {
int u = es[e].u, v = es[e].v;
if(dep[u] < dep[v]) swap(u, v); // u指向下面的节点
add(in[u], -es[e].w + w); // 因为是修改操作,比如原来是 w1, 修改为 w2,则相当于加上 -w1 + w2;
add(out[u]+1, es[e].w - w); // 差分,给out[u]+1 往后减去,这样只影响 区间
es[e].w = w; // 修改边权值
return;
}
int query(int u, int v) {
int fa = lca(u, v); // 求最近公共祖先
return sum(in[u]) + sum(in[v]) - 2*sum(in[fa]); // 两节点距离公式
}
signed main() {
cin >> n;
for(int i=1; i<n; i++) {
cin >> x >> y >> z;
add(x, y, z);
add(y, x, z);
}
dfs(1, 0); // 初始化 DFS 序,深度数组
for(int j=1; j<20; j++) { // 初始化 LCA 的 f 数组
for(int i=1; i<=n; i++) {
f[i][j] = f[f[i][j-1]][j-1];
}
}
for(int i=1; i<=idx; i+=2) { // 遍历所有边,因为是双向边,一条边只需要遍历一次,所以 i+=2
int u = es[i].u, v = es[i].v;
if(dep[u] < dep[v]) swap(u, v); // u 指向更深的节点,因为边的权重相当于影响了下面的节点(深度大的节点),对上面节点没影响
// 这里多想想,当前边的权重影响的是哪些节点?其实影响的是从 DFS 序中编号为 in[u], out[u] 的节点,所以基于差分思想
// 只需要修改 in[u] 位置和 out[u]+1 位置,树状数组针对的是 每个节点的时间戳编号数组。
add(in[u], es[i].w);
add(out[u] + 1, -es[i].w); // 差分
}
cin >> q;
while(q--) {
cin >> x >> y >> z;
if(x == 1) update(y*2, z); // 因为是双向边,所以要找输入的第 y条边,等价于数组中的 y*2 条边
else cout << query(y, z) << endl;
}
return 0;
}