树上问题

树上问题涉及许多经典的算法技巧, 这里我们一个个枚举 "倍增 距离 重心 直径 差分"

倍增  

代表算法: LCA (最近公共祖先) 

LCA 介绍:

给你一颗树S, 对于任意给定两个节点from, to, 找到一个离根最远的节点x, 使得x是from, to的祖先, 同理寻找到的 x 也是from, to的最近公共祖先 那么我们用公式描述就是LCA(from, to) = x

看图: 

 如图对于节点3 4 我们明显发现离它最近的祖先是2及 LCA(3, 2) = 2 但是我们可以发现如果节点1 7那他的公共祖先是什么, 可以发现这两个点在同一颗子树上, 可以得到LCA(1, 7) = 7 我们清楚了LCA的直观描述, 那我们尝试使用代码解决这个问题, 这里将采用倍增的写法, 利用朴素进阶过渡.

朴素:

写法1:

对于两个节点from, to, 我们每次找到深度最大的点, 让它往上跳, 这两个点最后一定会相遇, 相遇的位置x 就是我们需要求出的最近公共祖先x  

写法2:(通常解放)

对于两个节点from, to, 我们深度最大的点向上跳, 使得两个点的深度相同, 后面一起往上跳, 最后相遇的位置x 就是我们需要求出的最近公共祖先x

结论:

不难得出对于朴素算法, 我们需要dfs预处理整棵树, 对于单次查询的复杂度是(n),如果对于多次查询O(q * n) 可能会导致时间超时

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
constexpr int N = 5e5 + 5;
int dep[N], dp[N];
vector <int> adj[N];
 
void dfs (int from, int fa) {
    dep[from] = dep[fa] + 1;
    dp[from] = fa;
    for (int to : adj[from]) {
        if (to == fa) continue;
        dfs (to, from);
    }
}
 
int lca (int x, int y) {
    if (dep[x] < dep[y]) swap(x, y);
    while (dep[y] < dep[x]) {
        x = dp[x];
    }
    if (x == y) return y;
    while (x != y) {
        x = dp[x]; y = dp[y];
    }
    if (!x) return 1;// 默认1是根节点
    return x;
}

倍增优化:

保证dep[from] > dp[to], 那么dep[from] - dp[to]的长度我们可以通过倍增来优化, 避免了暴力 调整后的深度相同后,找到LCA, 当两个节点的深度相同后,我们可以开始同时上跳这两个节点,直到它们的父节点相同,最终找到它们的LCA

解释: 任意整数都可以拆分为若干以2为底的幂项和 

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
constexpr int N = 5e4 + 5, M = 22;
int n, m, self, cnt = 0, ans = 0, dep[N], dp[N][M], head[N], diff[N], dist[N];
 
struct Edge {
    int to, w, nxt;
} edges[N << 1];
  
void init () {
    cnt = 0;
    for (int i = 1; i <= n; i++) {
        head[i] = 0, dep[i] = 0, dist[i] = 0; // diff[i] = 0;
        for (int j = 0; j < M; j++) {
            dp[i][j] = 0;           
        }
    }
}
 
void add_edge (int from, int to, int w = 1) {
    edges[++cnt] = {to, w, head[from]};
    head[from] = cnt;
}
 
void dfs (int from, int fa) {
    dp[from][0] = fa, dep[from] = dep[fa] + 1;
    for (int i = 1; (1 << i) <= dep[from]; i++) {
        dp[from][i] = dp[dp[from][i - 1]][i - 1];
    }
    for (int i = head[from]; i; i = edges[i].nxt) {
        if (edges[i].to == fa) continue;
        dist[edges[i].to] = dist[from] + edges[i].w;
        dfs (edges[i].to, from);
    }
}
 
int lca (int from, int to) {
    if (dep[from] < dep[to]) swap(from, to);
    for (int i = 20; i >= 0; i--) {
        if (dep[from] - (1 << i) >= dep[to]) from = dp[from][i]; 
    }
    if (from == to) return to;
    for (int i = 20; i >= 0; i--) {
        if (dp[from][i] ^ dp[to][i]) {
            from = dp[from][i], to = dp[to][i];
        }
    }
    return dp[from][0];
}
 
void dfs2 (int from, int fa) { // 树上差分
    for (int i = head[from]; i; i = edges[i].nxt) {
        if (edges[i].to == fa) continue;
        dfs2 (edges[i].to, from);
        diff[from] += diff[edges[i].to];
    }
    ans = max(ans, diff[from]);
}

  

  

G - Tree Destruction

题意:

一颗n个顶点的树, 可以选择两个顶点a, b一次, 删除从a到b的路径上所有的顶点,

包括本身, 如果a = b则删除这一个顶点, 找到获得连通块最大的个数.

思路:

 

dp[from] 表示从from开始往下选择一条路径删除, 最后能得到的最多连通块

Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <bits/stdc++.h>
  
using namespace std;
typedef int64_t i64;
const int N = 2e5 + 5;
vector<int> adj[N];
int dp[N], ans = 0, n;
 
void dfs (int from, int fa) {
    dp[from] = adj[from].size();
    ans = max(ans, dp[from]);
    for (int to : adj[from]) {
        if (to == fa) continue;
        dfs(to, from);
        ans = max(ans, dp[from] + dp[to] - 2);
        dp[from] = max(dp[from], dp[to] + int(adj[from].size()) - 2);
    }
}
 
void Solve() {
    ans = 1;
    cin >> n;
    for (int i = 1; i < n; i++) {
        int from, to;
        cin >> from >> to;
        adj[from].push_back(to);
        adj[to].push_back(from);
    }
    dfs(1, 0);
    cout << ans << '\n';
    for (int i = 1; i <= n; i++) {
        adj[i].clear();
        dp[i] = 0;
    }
}
 
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);
    int t = 1;
    cin >> t;
    while (t--)
        Solve();
    return 0;
}

  

参考博客:

https://oi-wiki.org/graph/

 

posted @   Iter-moon  阅读(28)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示