洛谷题单指南-图论之树-P2680 [NOIP 2015 提高组] 运输计划

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

题意解读:边带权的n节点树中,给定m条路径,问能否将任意一条边权置0,使得m条路径的最大值最小,求此最大路径的最小值。

解题思路:

最大值最小,第一想到二分,考虑一下能否二分。

设对这个最大路径的最小值进行二分,得到一个mid,分析一下路径的特性:所有路径长度要么<=mid,要么将一条边权置0后长度<=mid,则符合要求,mid可以继续缩小,否则mid要扩大。

1、对于<=mid的路径,显然都是满足

2、对于>mid的路径,要看将某一条边权置0,能否都<=mid,只需要看最长的路径,将所有>mid的路径重叠的边权中最大的减掉,能否<=mid

根据上面分析,有几个关键问题要解决:

1、如何计算一条路径的长度

可以通过dfs计算每个节点到根节点的距离dist[N],这样u->v路径长度就可以转化为dist[u] - dist[lca(u,v)] + dist[v] - dist[lca(u,v)]

2、如何找出多条路径重叠的边

首先,想到的是枚举,枚举所有>mid的路径,再枚举路径中的每一条边,出现的边进行标记+1,最后边的标记数等于路径数的是重叠边,总复杂度为O(n^2)。

如何优化?要将路径上每一条边的标记+1,可以借助树上差分https://www.cnblogs.com/jcwy/p/18754359

对u->v路径上边标记+1,设差分数组为a,操作为a[u] += 1, a[v] += 1, a[lca(u,v)] -= 2

再通过树上前缀和还原边的标记数量,得到数组s,枚举s,找到等于>mid的路径条数的边,记录权值最大的一条,最后用最长路径-最大重叠权值,如果能<=mid,说明符合条件,继续缩小范围二分,否则扩大范围。

注意1:计算路径长度也可以用树链剖分,但是这里加上二分,总的复杂度达到O(n*logn*logn),n=300000会超时。

注意2:在对树上差分还原前缀和时,如果采用如下递归的方式,将会有一个数据点无法通过

void sum(int u, int p)
{
    s[u] = a[u];
    for(auto item : g[u])
    {
        int v = item.v, w = item.w;
        if(v == p) continue;
        sum(v, u);
        s[u] += s[v];
    }
}

因此,可以改成迭代的方式,需要先记录每个节点的dfs序,然后逆着dfs序遍历每一个节点的差分,将其累加到父节点

for(int i = 1; i <= n; i++) s[i] = a[i];
for(int i = n; i >= 1; i--) //逆着dfs序将所有差分累加到父节点
{
    s[fa[dfn[i]][0]] += s[dfn[i]];
}

100分代码:

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

const int N = 300005;

struct Node
{
    int v, w;
};
vector<Node> g[N];

struct Path
{
    int u, v, l, d; //路径起点、终点、lca、长度
};
Path paths[N]; //所有路径信息
int dist[N]; //所有节点到根节点的距离
int depth[N], fa[N][20]; //lca相关
int dfn[N], idx; //dfs序
int a[N], s[N]; //差分数组,前缀和数组
int n, m;

//预处理
void dfs(int u, int p, int d)
{
    dfn[++idx] = u;
    dist[u] = d;
    depth[u] = depth[p] + 1;
    fa[u][0] = p;
    for(int j = 1; j < 20; j++)
        fa[u][j] = fa[fa[u][j - 1]][j - 1];
    for(auto item : g[u])
    {
        int v = item.v, w = item.w;
        if(v == p) continue;
        dfs(v, u, d + w);
    }
}

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

//树上前缀和,注意性能不如直接迭代
void sum(int u, int p)
{
    s[u] = a[u];
    for(auto item : g[u])
    {
        int v = item.v, w = item.w;
        if(v == p) continue;
        sum(v, u);
        s[u] += s[v];
    }
}

bool check(int x)
{
    memset(a, 0, sizeof(a));
    memset(s, 0, sizeof(s));
    int cnt = 0; //记录有多少个路径长度>x
    int maxx = 0; //记录>x的路径中最长的
    for(int i = 1; i <= m; i++)
    {
        if(paths[i].d > x) //长度超过x的需要判断能否找到公共边置0后,长度<=x
        {
            cnt++;
            //树上差分标记边
            a[paths[i].u] += 1;
            a[paths[i].v] += 1;
            a[paths[i].l] -= 2;
            maxx = max(maxx, paths[i].d);
        }
    }
    if(cnt == 0) return true; //没有>x的路径,都满足要求
    //sum(1, 0); //递归还原前缀和,用此方法会超时一个数据点
    //迭代还原前缀和
    for(int i = 1; i <= n; i++) s[i] = a[i];
    for(int i = n; i >= 1; i--) //逆着dfs序将所有差分累加到父节点
    {
        s[fa[dfn[i]][0]] += s[dfn[i]];
    }
    int maxy = 0; //记录公共边中权值最大的
    for(int i = 1; i <= n; i++)
    {
        //判断是公共边
        if(s[i] == cnt) maxy = max(maxy, dist[i] - dist[fa[i][0]]); //i边权值是i到根的距离-i的父节点到根的距离
    }
    if(maxx - maxy <= x) return true;
    return false;
}

int main()
{
    cin.tie(0); cout.tie(0); ios::sync_with_stdio(false);
    cin >> n >> m;
    for(int i = 1; i < n; i++)
    {
        int u, v, w;
        cin >> u >> v >> w;
        g[u].push_back({v, w});
        g[v].push_back({u, w});
    }
    dfs(1, 0, 0);
    //计算所有路径的长度
    for(int i = 1; i <= m; i++)
    {
        int u, v;
        cin >> u >> v;
        int l = lca(u, v);
        paths[i] = {u, v, l, dist[u] + dist[v] - 2 * dist[l]};
    }
    //开始二分
    int l = 0, r = 3e8;
    while(l < r)
    {
        int mid = l + r >> 1;
        if(check(mid)) r = mid;
        else l = mid + 1;
    }
    cout << l;
    return 0;
}

 

posted @ 2025-03-21 11:16  hackerchef  阅读(39)  评论(0)    收藏  举报