题解:[ABC294G] Distance Queries on a Tree

题目描述

给定一颗有 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; 
}
相关推荐
chenziang116 分钟前
leetcode hot 100 二叉搜索
数据结构·算法·leetcode
single5942 小时前
【c++笔试强训】(第四十五篇)
java·开发语言·数据结构·c++·算法
呆头鹅AI工作室2 小时前
基于特征工程(pca分析)、小波去噪以及数据增强,同时采用基于注意力机制的BiLSTM、随机森林、ARIMA模型进行序列数据预测
人工智能·深度学习·神经网络·算法·随机森林·回归
一勺汤3 小时前
YOLO11改进-注意力-引入自调制特征聚合模块SMFA
人工智能·深度学习·算法·yolo·目标检测·计算机视觉·目标跟踪
每天写点bug3 小时前
【golang】map遍历注意事项
开发语言·算法·golang
程序员JerrySUN4 小时前
BitBake 执行流程深度解析:从理论到实践
linux·开发语言·嵌入式硬件·算法·架构
王老师青少年编程4 小时前
gesp(二级)(16)洛谷:B4037:[GESP202409 二级] 小杨的 N 字矩阵
数据结构·c++·算法·gesp·csp·信奥赛
robin_suli5 小时前
动态规划子序列问题系列一>等差序列划分II
算法·动态规划
cxylay5 小时前
自适应滤波算法分类及详细介绍
算法·分类·自适应滤波算法·自适应滤波·主动噪声控制·anc
茶猫_5 小时前
力扣面试题 - 40 迷路的机器人 C语言解法
c语言·数据结构·算法·leetcode·机器人·深度优先