题解:洛谷 P3128 Max Flow

【题目来源】

洛谷:P3128 [USACO15DEC] Max Flow P - 洛谷

【题目描述】

FJ 给他的牛棚的 \(N\) 个隔间之间安装了 \(N−1\) 根管道,隔间编号从 \(1\)\(N\)。所有隔间都被管道连通了。

FJ 有 \(K\) 条运输牛奶的路线,第 \(i\) 条路线从隔间 \(s_i\) 运输到隔间 \(t_i\)。一条运输路线会给它的两个端点处的隔间以及中间途径的所有隔间带来一个单位的运输压力,你需要计算压力最大的隔间的压力是多少。

【输入】

第一行输入两个整数 \(N\)\(K\)

接下来 \(N−1\) 行每行输入两个整数 \(x\)\(y\),其中 \(x\neq y\)。表示一根在牛棚 \(x\)\(y\) 之间的管道。

接下来 \(K\) 行每行两个整数 \(s\)\(t\),描述一条从 \(s\)\(t\) 的运输牛奶的路线。

【输出】

一个整数,表示压力最大的隔间的压力是多少。

【输入样例】

5 10
3 4
1 5
4 2
5 4
5 4
5 4
3 5
4 3
4 3
1 3
3 5
5 4
1 5
3 4

【输出样例】

9

【核心思想】

  1. 问题分析:给定一棵 \(N\) 个节点的树和 \(K\) 条路径 \((s_i, t_i)\),每条路径会给路径上的所有节点(包括端点和中间节点)增加 \(1\) 单位压力。需要求所有操作完成后压力最大的节点的压力值。若对每个路径暴力遍历,时间复杂度为 \(O(K \cdot N)\)。关键观察是:树上路径的批量更新可以通过树上差分优化为 \(O(1)\) 的单点标记,最后通过一次 DFS 累加得到所有节点的压力。

  2. 算法选择

    • 树上差分 + LCA:对每条路径 \((s, t)\),在端点 \(s\)\(t\)\(+1\),在 LCA\((s, t)\) 及其父节点处 \(-1\),最后通过 DFS 后序遍历将子树差分标记累加到父节点
    • 倍增法求 LCA:预处理节点深度和 \(2^i\) 级祖先,实现 \(O(\log N)\) 的 LCA 查询
  3. 关键步骤

    • 建树与预处理
      • 读入 \(N\) 个节点和 \(N-1\) 条边,建立无向图
      • 以节点 \(1\) 为根进行 DFS,计算每个节点的深度 \(dep[u]\) 和倍增祖先 \(fa[u][i]\)
    • 处理 \(K\) 条路径
      • 对每条路径 \((x, y)\),计算 \(l = \text{LCA}(x, y)\)
      • 树上差分标记:
        • power[x]++power[y]++(两端点加 \(1\)
        • power[l]--(LCA 减 \(1\),避免向上传播时重复计算)
        • power[fa[l][0]]--(LCA 的父节点减 \(1\),切断向根的传播)
    • DFS 累加统计
      • 后序遍历树,对每个节点 \(u\)power[u] += power[v](累加所有子节点的差分值)
      • 更新 ans = max(ans, power[u])
    • 输出答案\(ans\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N \log N + K \log N)\),预处理 DFS \(O(N \log N)\),每次 LCA 查询 \(O(\log N)\),共 \(K\) 次,最后累加 DFS \(O(N)\)
    • 空间复杂度:\(O(N \log N)\),存储倍增数组 \(fa[N][22]\) 和树的邻接表
  5. 树上差分的核心思想

    • 路径转化为点差分:对于路径 \(s \to t\),差分标记为 \(s\)\(t\)\(+1\),LCA 和 LCA 的父节点各 \(-1\)。这一组操作的效果是:在 DFS 累加时,从 \(s\) 向上到根的路径加 \(1\),从 \(t\) 向上到根的路径加 \(1\),从 LCA 向上到根的路径被减去 \(2\),最终恰好使得 \(s \to t\) 路径上的所有节点(含端点)加 \(1\),而 LCA 以上的节点不受影响
    • LCA 的关键作用:LCA 是路径上深度最小的节点,也是两条向上路径的交汇点。在 LCA 处减 \(1\) 是为了抵消从 \(s\)\(t\) 两条路径向上传播时在 LCA 处的重复累加
    • 倍增法优化 LCA:通过预处理每个节点的 \(2^i\) 级祖先,将 LCA 查询从 \(O(N)\) 优化至 \(O(\log N)\),是树上问题的经典加速技巧
    • 后序遍历累加:差分标记是"自底向上"传播的,因此需要后序遍历(先处理子节点再处理父节点)来确保子节点的差分值正确累加到父节点
    • 适用于树上路径的批量更新、路径覆盖统计、树形 DP 的预处理等场景

【算法标签】

普及+ #树上差分

【代码详解】

#include <bits/stdc++.h>
using namespace std;

const int N = 50005, M = 2 * N;  // N: 最大节点数, M: 最大边数
int n, k, ans;                     // n: 节点数, k: 查询次数, ans: 最终结果
int power[N];                      // 存储每个节点的权值
int h[N], to[M], ne[M], tot;       // 链式前向星存储树结构
int dep[N], fa[N][22];             // dep: 节点深度, fa: 倍增数组

// 添加边到链式前向星
void add(int x, int y)
{
    to[++tot] = y;                 // 存储终点
    ne[tot] = h[x];                // 存储下一条边
    h[x] = tot;                    // 更新头指针
}

// 深度优先搜索预处理倍增信息
void dfs(int u, int f)
{
    dep[u] = dep[f] + 1;          // 计算节点深度
    fa[u][0] = f;                 // 直接父节点

    // 预处理倍增数组
    for (int i = 1; i <= 20; i++)
        fa[u][i] = fa[fa[u][i - 1]][i - 1];  // 计算2^i级祖先

    // 遍历所有子节点
    for (int i = h[u]; i; i = ne[i])
    {
        int v = to[i];
        if (v == f) continue;      // 跳过父节点
        dfs(v, u);                 // 递归处理子节点
    }
}

// 倍增法求最近公共祖先
int lca(int u, int v)
{
    // 保证u是较深的节点
    if (dep[u] < dep[v]) swap(u, v);

    // 将u提升到与v相同深度
    for (int i = 20; i >= 0; i--)
        if (dep[fa[u][i]] >= dep[v])
            u = fa[u][i];

    if (u == v) return v;          // 如果已经是同一个节点

    // 同时向上跳跃寻找LCA
    for (int i = 20; i >= 0; i--)
        if (fa[u][i] != fa[v][i])
            u = fa[u][i], v = fa[v][i];

    return fa[u][0];               // 返回LCA
}

// 深度优先搜索统计答案
void dfs2(int u, int f)
{
    // 遍历所有子节点
    for (int i = h[u]; i; i = ne[i])
    {
        int v = to[i];
        if (v == f) continue;      // 跳过父节点
        dfs2(v, u);                // 递归处理子节点
        power[u] += power[v];      // 累加子节点的权值
    }
    ans = max(ans, power[u]);      // 更新最大值
}

int main()
{
    // 输入树结构和查询次数
    cin >> n >> k;

    // 构建树结构
    for (int i = 1; i < n; i++)
    {
        int x, y;
        cin >> x >> y;
        add(x, y);                 // 无向图添加双向边
        add(y, x);
    }

    // 预处理倍增信息
    dfs(1, 0);

    // 处理每个查询
    for (int i = 1; i <= k; i++)
    {
        int x, y;
        cin >> x >> y;
        int l = lca(x, y);         // 求LCA

        // 树上差分操作
        ++power[x];                // 起点+1
        ++power[y];                // 终点+1
        --power[l];                // LCA-1
        --power[fa[l][0]];         // LCA的父节点-1
    }

    // 统计最终答案
    dfs2(1, 0);

    // 输出结果
    cout << ans << endl;

    return 0;
}

【运行结果】

5 10
3 4
1 5
4 2
5 4
5 4
5 4
3 5
4 3
4 3
1 3
3 5
5 4
1 5
3 4
9
posted @ 2026-07-02 22:44  团爸讲算法  阅读(11)  评论(0)    收藏  举报