P3128 [USACO15DEC] Max Flow P 树上差分LCA

解题思路

本题是一道典型的树上差分问题,结合了LCA(最近公共祖先)算法。题目要求我们统计树中每个节点被多少条路径覆盖,并找出被覆盖次数最多的节点。

关键步骤

  1. LCA预处理:使用倍增法预处理每个节点的各层祖先信息,以便快速查询任意两点的LCA

  2. 树上差分

    • 对于每条路径s→t,在s和t节点处+1

    • 在它们的LCA处-1

    • 在LCA的父节点处-1

  3. 统计计算:通过后序遍历统计每个节点被覆盖的次数,并维护最大值

#include<bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10;

// f[i][j]表示节点i的2^j级祖先
int f[N][26];  
// dep[i]表示节点i的深度
int dep[N];    
// d[i]表示节点i的差分值
int d[N];      
// 存储最终答案
int ans;       
// 邻接表存储树结构
vector<int> g[N]; 
int n,m;

// DFS预处理LCA信息
void dfs(int x,int fa)
{
    f[x][0] = fa; // 直接父节点
    dep[x] = dep[fa] + 1; // 计算深度
    
    // 预处理2^i级祖先
    for(int i = 1; (1 << i) <= dep[x]; i++)
    {
        int y = f[x][i - 1];
        f[x][i] = f[y][i - 1]; // 2^i级祖先是2^(i-1)级祖先的2^(i-1)级祖先
    }
    
    // 递归处理子节点
    for(int i = 0; i < g[x].size(); i++)
    {
        int y = g[x][i];
        if(y != fa) dfs(y,x);
    }
}

// 后序遍历统计差分结果
void dfs2(int x,int fa)
{
    for(int i = 0; i < g[x].size(); i++)
    {
        int y = g[x][i];
        if(y == fa) continue;
        dfs2(y,x);
        d[x] += d[y]; // 累加子节点的差分值
    }
    ans = max(ans,d[x]); // 更新最大值
}

// LCA查询函数
int lca(int x,int y)
{
    // 确保x是较深的节点
    if(dep[x] < dep[y]) swap(x,y);
    
    // 将x跳到与y同一深度
    for(int i = 20; i >= 0; i--)
        if(dep[x] - (1 << i) >= dep[y])
            x = f[x][i];
    
    // 如果此时已经相同,直接返回
    if(x == y) return x;
    
    // 同时向上跳跃查找
    for(int i = 20; i >= 0; i--){
        if(f[x][i] != f[y][i]){ // 祖先不同才跳跃
            x = f[x][i],y = f[y][i];
        }
    }
    return f[x][0]; // 返回最终的LCA
}

int main()
{
    cin >> n >> m;
    
    // 构建树结构
    for(int i = 1; i < n; i++)
    {
        int x,y; cin >> x >> y;
        g[x].push_back(y);
        g[y].push_back(x);
    }
    
    // 预处理LCA信息
    dfs(1,0);
    
    // 处理每条路径
    for(int i = 1; i <= m; i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        int rt = lca(x,y); // 计算LCA
        d[x]++, d[y]++;    // 起点和终点+1
        d[rt]--;           // LCA处-1
        if(f[rt][0]) d[f[rt][0]]--; // LCA的父节点-1(如果存在)
    }
    
    // 统计最终结果
    dfs2(1,0);
    cout << ans;
    return 0;
}

算法分析

  1. 时间复杂度

    • LCA预处理:O(NlogN)

    • 每条路径处理:O(logN)(LCA查询)

    • 统计计算:O(N)

    • 总复杂度:O(NlogN + KlogN + N) ≈ O((N+K)logN)

  2. 空间复杂度

    • O(NlogN)(存储倍增表)

  3. 优势

    • 高效处理大规模树结构(N可达5×10^4)

    • 可以处理大量路径查询(K可达10^5)

    • 差分技巧将路径更新转化为O(1)操作

 

树上差分算法公式推导

1. 基本概念

树上差分是一种处理树结构路径修改的高效算法,能在O(1)时间完成路径标记,最后通过一次DFS遍历计算出每个节点的最终值。

2. 核心问题

给定一棵树和若干路径(u,v),需要对路径上所有节点进行加减操作,最后求每个节点的值。

3. 差分数组思想

借鉴一维差分思想:

  • 原数组:A[]

  • 差分数组:D[i] = A[i] - A[i-1]

  • 区间[l,r]加x:D[l]+x, D[r+1]-x

4. 树上差分公式推导

对于树结构,我们使用点差分边差分两种形式:

4.1 点差分
路径u→v上所有节点加x:

D[u] += x
D[v] += x
D[lca(u,v)] -= x
D[parent[lca(u,v)]] -= x  # 如果是点权,需要减去双倍

4.2 边差分
路径u→v上所有边加x(假设边权保存在子节点):

D[u] += x
D[v] += x
D[lca(u,v)] -= 2x

5. 数学推导

设:

  • f(u)表示从根到u的路径和

  • 路径u→v的和 = f(u) + f(v) - 2*f(lca)

要使得路径上所有点加x:

  • 相当于对u→root和v→root路径加x

  • 但lca→root被加了两次,需要减去

  • 最终影响:
    Δf = x*(depth[u]+depth[v]-2*depth[lca]+1)

6. 实现步骤

  1. 预处理LCA(倍增/Tarjan)

  2. 应用差分公式

  3. 后序遍历累加差分值

7. 示例代码(C++)

void dfs(int u, int p) {
    for(int v : tree[u]) {
        if(v != p) {
            dfs(v, u);
            diff[u] += diff[v];
        }
    }
    value[u] += diff[u];
}

// 点差分操作
void point_update(int u, int v, int x) {
    int l = lca(u, v);
    diff[u] += x;
    diff[v] += x;
    diff[l] -= x;
    if(parent[l] != -1) diff[parent[l]] -= x;
}

8. 复杂度分析

  • 预处理LCA:O(nlogn)

  • 每次操作:O(1)

  • 最终计算:O(n)

9. 应用场景

  • 网络流量统计

  • 子树求和

  • 路径染色问题

 

树上点差分的正确性推导

1. 基本逻辑误区澄清

在点差分操作中,对 LCA 和其父节点都减去 x 的操作看起来确实有些反直觉,但这是为了精确控制影响范围。让我们通过具体例子来理解。

2. 关键概念图解

考虑以下树结构:

        A
       / \
      B   C
     / \ 
    D   E

假设我们要对路径 D→C 上的所有节点加 x(即节点 D、B、A、C)

3. 错误做法分析

如果仅按照线性差分的直觉:

D[D] += x
D[C] += x
D[A] -= x  # 只减去LCA

这样计算的结果:

  • D的值:x (正确)

  • B的值:x (错误,不应被影响)

  • A的值:x (正确)

  • C的值:x (正确)

  • E的值:0 (正确)

问题出在节点B被错误地加了x!

4. 正确的差分公式

正确的点差分操作:

D[D] += x
D[C] += x
D[A] -= x       # LCA
D[parent[A]] -= x  # 通常设为虚拟根节点的父节点(若存在)

5. 数学证明

设 f(u) 是从根到u的路径和,则:

  • 对D和C加x相当于:

    • 把D→root和C→root两条路径都+x

  • 这两条路径的交集是A→root

  • 因此需要:

    1. 在LCA(A)处减去x(消除一次重复)

    2. 在A的父节点处再减x(完全消除多余影响)

6. 实际影响范围验证

        A(-x)
       / \
(+x)B   C(+x)
     / 
(+x)D

最终影响:

  • D: +x (D→root)

  • B: +x -x = 0 (正确)

  • A: +x +x -x = +x (正确)

  • C: +x (正确)

7. 为什么需要双重减法?

因为:

  1. 第一次减法(LCA处)消除两条路径在LCA以下的重叠

  2. 第二次减法(LCA的父节点)消除两条路径在LCA以上的重叠

8. 边界情况处理

当LCA就是根节点时:

D[u] += x
D[v] += x
D[root] -= x
D[null] -= x  # 可忽略

9. 标准模板代码

void point_update(int u, int v, int x) {
    int l = lca(u, v);
    diff[u] += x;
    diff[v] += x;
    diff[l] -= x;
    if(parent[l] != -1) diff[parent[l]] -= x;
}

10. 记忆口诀

"加两端,减LCA,再减LCA的爸"

这种设计确保了修改严格限定在u-v路径上的所有节点,不会影响其他无关节点。这是树上差分区别于线性差分的核心特征。

 
 
 
posted @ 2025-04-29 17:02  CRt0729  阅读(51)  评论(0)    收藏  举报