洛谷题单指南-图论之树-P3128 [USACO15DEC] Max Flow P

原题链接:https://www.luogu.com.cn/problem/P3128

题意解读:一棵树,每次选取两个节点,在这两个节点之间路径运送牛奶,每运送一次,经过的所有节点都会增加1,最终统计记录最大的节点。

解题思路:

要计算树上两个节点之间的路径,可以采用LCA算法,确定路径之后如何将路径中所有点都加1呢?

1、暴力法

直接枚举,加1,复杂度O(n),一共要操作k次,总体复杂度O(nlogn + kn)

2、树上差分

借助于树上差分操作,可以O(1)复杂度对要路径上的点进行+1操作,最后还原前缀和数组就是每个节点运送牛奶的次数,总体复杂度O(nlogn+k+n)

树上差分详解

树上差分是一种用于处理树上路径问题的高效算法技巧,其核心思想与序列上的差分类似。在序列上,差分是通过记录相邻元素的差值来简化区间修改和单点查询操作;而在树上,树上差分则是利用树的结构特点,通过对树上某些节点的标记来实现对树上路径的修改和查询,最终通过一次深度优先搜索(DFS)将标记信息传递到整棵树,从而高效地统计路径上的信息。
树上差分主要分为两种类型:边差分和点差分,下面分别进行介绍。

边差分

边差分用于处理树上路径边的修改问题。对于树上的一条路径u-v ,我们需要对这条路径上的所有边进行某种修改(例如增加某个值),可以通过在路径两端点u和v以及它们的最近公共祖先lca(u,v)处进行标记,然后通过一次 DFS 计算出每条边的实际修改值。

点差分

点差分用于处理树上路径点的修改问题。对于树上的一条路径u-v,我们需要对这条路径上的所有点进行某种修改(例如增加某个值),同样可以在路径两端点u和v以及它们的最近公共祖先lca(u,v)处进行标记,不同的是点差分还需要对lca(u,v)的父节点进行额外标记,最后通过一次 DFS 计算出每个点的实际修改值

示例代码

#include <iostream>
#include <vector>
using namespace std;

const int MAXN = 100005;
vector<int> adj[MAXN];  // 邻接表存储树的结构
int depth[MAXN];        // 存储每个节点的深度
int parent[MAXN][20];   // 用于求 LCA,parent[u][i] 表示节点 u 的 2^i 级祖先
int diff_edge[MAXN];    // 边差分标记数组
int diff_point[MAXN];   // 点差分标记数组

// 深度优先搜索,计算每个节点的深度和父节点
void dfs(int u, int p, int d) {
    depth[u] = d;
    parent[u][0] = p;
    for (int i = 1; i < 20; i++) {
        parent[u][i] = parent[parent[u][i - 1]][i - 1];
    }
    for (int v : adj[u]) {
        if (v != p) {
            dfs(v, u, d + 1);
        }
    }
}

// 求两个节点的最近公共祖先
int lca(int u, int v) {
    if (depth[u] < depth[v]) swap(u, v);
    for (int i = 19; i >= 0; i--) {
        if (depth[u] - (1 << i) >= depth[v]) {
            u = parent[u][i];
        }
    }
    if (u == v) return u;
    for (int i = 19; i >= 0; i--) {
        if (parent[u][i] != parent[v][i]) {
            u = parent[u][i];
            v = parent[v][i];
        }
    }
    return parent[u][0];
}

// 边差分操作,对路径 u - v 上的边进行修改
void edge_diff(int u, int v, int val) {
    int l = lca(u, v);
    diff_edge[u] += val;
    diff_edge[v] += val;
    diff_edge[l] -= 2 * val;
}

// 点差分操作,对路径 u - v 上的点进行修改
void point_diff(int u, int v, int val) {
    int l = lca(u, v);
    diff_point[u] += val;
    diff_point[v] += val;
    diff_point[l] -= val;
    if (parent[l][0] != -1) {
        diff_point[parent[l][0]] -= val;
    }
}

// 深度优先搜索,计算边差分的结果
void dfs_edge(int u, int p) {
    for (int v : adj[u]) {
        if (v != p) {
            dfs_edge(v, u);
            diff_edge[u] += diff_edge[v];
        }
    }
}

// 深度优先搜索,计算点差分的结果
void dfs_point(int u, int p) {
    for (int v : adj[u]) {
        if (v != p) {
            dfs_point(v, u);
            diff_point[u] += diff_point[v];
        }
    }
}

int main() {
    int n;  // 节点数量
    cin >> n;
    for (int i = 0; i < n - 1; i++) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
        adj[v].push_back(u);
    }
    // 初始化根节点的父节点为 -1
    dfs(1, -1, 0);

    int q;  // 查询数量
    cin >> q;
    while (q--) {
        int u, v, val;
        cin >> u >> v >> val;
        // 边差分操作
        edge_diff(u, v, val);
        // 点差分操作
        point_diff(u, v, val);
    }

    // 计算边差分的结果
    dfs_edge(1, -1);
    // 计算点差分的结果
    dfs_point(1, -1);

    // 输出边差分的结果
    cout << "边差分结果:" << endl;
    for (int i = 1; i <= n; i++) {
        cout << "节点 " << i << " 关联边的修改值: " << diff_edge[i] << endl;
    }

    // 输出点差分的结果
    cout << "点差分结果:" << endl;
    for (int i = 1; i <= n; i++) {
        cout << "节点 " << i << " 的修改值: " << diff_point[i] << endl;
    }

    return 0;
}

前缀和数组的本质

计算点差分数组的前缀和后得到的前缀和数组,表示每个节点在所有路径修改操作后的实际修改量。在 DFS 过程中,子节点的差分值会累加到其父节点上,最终每个节点的前缀和值就是该节点在所有子树路径修改操作影响下的最终变化值。

例如,在一个需要统计每个节点被经过次数的问题中,我们可以通过点差分对经过的路径上的节点进行标记,然后计算前缀和,这样每个节点的前缀和值就是该节点被经过的总次数。或者在一个节点权值修改的问题中,通过点差分和前缀和计算,我们可以得到每个节点修改后的实际权值。

综上所述,树上差分的差分数组计算出的前缀和数组本质上是对树上路径操作在节点或边上的综合影响结果,它能帮助我们高效地处理树上路径的修改和查询问题。

对于此题,用到点差分即可,下面给出代码。

100分代码:

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

const int N = 50005;
vector<int> g[N];
int depth[N], fa[N][20];
int b[N]; //树上差分数组,本质是对节点值的修改
int s[N]; //树上前缀和数组,本质对子树所有节点值修改的累加
int n, k, ans;

void dfs(int u, int parent)
{
    depth[u] = depth[parent] + 1;
    fa[u][0] = parent;
    for(int j = 1; j <= 18; j++)
        fa[u][j] = fa[fa[u][j - 1]][j - 1];
    for(auto v : g[u])
    {
        if(v == parent) continue;
        dfs(v, u);
    }
}

int lca(int u, int v)
{
    if(depth[u] < depth[v]) swap(u, v);
    for(int j = 18; j >= 0; j--)
    {
        if(depth[fa[u][j]] >= depth[v])
            u = fa[u][j];
    }
    if(u == v) return u;
    for(int j = 18; j >= 0; j--)
    {
        if(fa[u][j] != fa[v][j])
        {
            u = fa[u][j];
            v = fa[v][j];
        }
    }
    return fa[u][0];
}

//树上前缀和,节点u的前缀和是节点u的值加上所有子树的前缀和
void sum(int u, int parent)
{
    s[u] = b[u]; 
    for(auto v : g[u])
    {
        if(v == parent) continue;
        sum(v, u);
        s[u] += s[v];
    }
    ans = max(ans, s[u]);
}

int main()
{
    cin >> n >> k;
    int u, v;
    for(int i = 1; i < n; i++)
    {
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    dfs(1, 0);
    while(k--)
    {
        cin >> u >> v;
        int p = lca(u, v);
        b[u]++;
        b[v]++;
        b[p]--;
        b[fa[p][0]]--;
    }
    sum(1, 0);
    cout << ans;

    return 0;
}

 

posted @   五月江城  阅读(13)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
历史上的今天:
2024-03-06 洛谷题单指南-搜索-P1605 迷宫
2024-03-06 洛谷题单指南-搜索-P1433 吃奶酪
2024-03-06 洛谷题单指南-搜索-P2036 [COCI 2008/2009 #2] PERKET
点击右上角即可分享
微信分享提示