NOIP2015提高组D2T3:运输计划

题目链接

NOIP2015提高组D2T3:运输计划

题目描述

公元 2044 2044 2044 年,人类进入了宇宙纪元。

L 国有 n n n 个星球,还有 n − 1 n-1 n1 条双向航道,每条航道建立在两个星球之间,这 n − 1 n-1 n1 条航道连通了 L 国的所有星球。

小 P 掌管一家物流公司, 该公司有很多个运输计划,每个运输计划形如:有一艘物流飞船需要从 u i u_i ui 号星球沿最快的宇航路径飞行到 v i v_i vi 号星球去。显然,飞船驶过一条航道是需要时间的,对于航道 j j j,任意飞船驶过它所花费的时间为 t j t_j tj,并且任意两艘飞船之间不会产生任何干扰。

为了鼓励科技创新, L 国国王同意小 P 的物流公司参与 L 国的航道建设,即允许小 P 把某一条航道改造成虫洞,飞船驶过虫洞不消耗时间。

在虫洞的建设完成前小 P 的物流公司就预接了 m m m 个运输计划。在虫洞建设完成后,这 m m m 个运输计划会同时开始,所有飞船一起出发。当这 m m m 个运输计划都完成时,小 P 的物流公司的阶段性工作就完成了。

如果小 P 可以自由选择将哪一条航道改造成虫洞, 试求出小 P 的物流公司完成阶段性工作所需要的最短时间是多少?

输入格式

第一行包括两个正整数 n , m n, m n,m,表示 L 国中星球的数量及小 P 公司预接的运输计划的数量,星球从 1 1 1 n n n 编号。

接下来 n − 1 n-1 n1 行描述航道的建设情况,其中第 i i i 行包含三个整数 a i , b i a_i, b_i ai,bi t i t_i ti,表示第 i i i 条双向航道修建在 a i a_i ai b i b_i bi 两个星球之间,任意飞船驶过它所花费的时间为 t i t_i ti

数据保证

接下来 m m m 行描述运输计划的情况,其中第 j j j 行包含两个正整数 u j u_j uj v j v_j vj,表示第 j j j 个运输计划是从 u j u_j uj 号星球飞往 v j v_j vj号星球。

输出格式

一个整数,表示小 P 的物流公司完成阶段性工作所需要的最短时间。

样例 #1

样例输入 #1

6 3 
1 2 3 
1 6 4 
3 1 7 
4 3 6 
3 5 5 
3 6 
2 5 
4 5

样例输出 #1

11

提示

所有测试数据的范围和特点如下表所示在这里插入图片描述
请注意常数因子带来的程序效率上的影响。

对于 100 % 100\% 100% 的数据,保证: 1 ≤ a i , b i ≤ n 1 \leq a_i,b_i \leq n 1ai,bin 0 ≤ t i ≤ 1000 0 \leq t_i \leq 1000 0ti1000 1 ≤ u i , v i ≤ n 1 \leq u_i,v_i \leq n 1ui,vin

算法思想

根据题目描述,有 n n n 个星球, n − 1 n-1 n1 条双向航道,这 n − 1 n-1 n1 条航道连通了所有星球,即构成一棵树。驶过第 i i i条航道要所花费的时间为 t i t_i ti;又有 m m m 个运输计划,对于第 i i i个运输计划有起点 u i u_i ui和终点 v i v_i vi,会经过起点到终点的每条航道。问若选择将一条航道改造成虫洞(花费时间变为0),最短需要多长时间完成所有运输计划。

二分答案

不妨假设改造一条航道后,最短需要 t t t时间完成所有运输计划,那么对于任意给定的时间 T T T,只要满足 T ≥ t T\ge t Tt,也能够完成所有运输计划。也就是说答案满足两段性,如下图所示:
在这里插入图片描述
满足这个性质,可以使用二分答案求解。二分得到mid后,需要判断是否可以将一条航道边权变成 0 0 0,使得每个运输计划花费的时间t都满足t <= mid。这样就要预处理出每个运输计划所花费的时间。

树上前缀和

一个运输计划从 u u u点出发到 v v v点结束所花费的时间,就是求这条路径的长度,可以使用树上前缀和来实现,如下图所示:
在这里插入图片描述
其基本思想是计算出每个节点到根节点的距离,例如 d i s [ u ] dis[u] dis[u]表示节点 u u u到根节点的距离,那么 d i s u → v = d i s [ u ] + d i s [ v ] − 2 × d i s [ p ] dis_{u\rightarrow v}=dis[u]+dis[v]-2\times dis[p] disuv=dis[u]+dis[v]2×dis[p],其中 p p p u u u v v v的最近公共祖先。如何求解公共祖先,可以参考博主的这篇文章——每周一算法:倍增法求最近公共祖先(LCA)

求出每个运输计划的路径长度之后,对于路径长度<= mid的运输计划无需处理;而对于路径长度> mid的,都需要从中至少选择一条航道将其边权变成 0 0 0。因为题目要求只能改造一条航道,因此所选的航道必须是这些运输计划的公共边

树上差分

每个运输计划都会经过若干条航道。那么可以采用计数的思想,对于不满足要求的运输计划,将其经过的每条航道计数 1 1 1次。假设有 c c c条不满足要求的运输计划,那么它们的公共边一定被计数 c c c次。

朴素的计数做法时间复杂度为 O ( n × m ) O(n\times m) O(n×m),这里可以采用树上差分进行优化。
不妨设 d [ ] d[] d[]为树中每个节点向上连接的边经过次数的差分数组,如果要将从 u u u v v v的路径中每条边的经过次数 + 1 +1 +1,如下图所示:
在这里插入图片描述
可以让d[u] += 1d[v] += 1d[p] -= 2,其中 p p p u 、 v u、v uv的最近公共祖先。

经过若干次操作之后,再自底向上累加每棵子树的和,就可以求出每条边经过的总次数。这就是树上差分。

求出每条边的经过次数之后,就可以判断出公共边。对于每条公共边,将其边权变成 0 0 0,如果所有路径长度都不大于mid,则返回true。这里可以优化为判断最长路径减去该公共边的长度是否满足要求即可。

时间复杂度

  • 二分答案时间复杂为 O ( n × l o g n ) O(n\times logn) O(n×logn)
  • 求每个路径的长度 O ( n × l o g n ) O(n\times logn) O(n×logn)
  • 预处理公共边 O ( n ) O(n) O(n)

代码实现

#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 5, M = N * 2, K = 19;
int n, m, cnt, seq[N], depth[N], f[N][K], dis[N], d[N];
int h[N], e[M], ne[M], w[M], idx;
struct P
{
    int u, v, p, dist;
} q[N];
void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
void dfs(int u, int fa, int de) //预处每个节点的深度、到根节点的距离和倍增数组
{
    seq[cnt ++] = u; //保存深搜序列,方便自底向上求树上前缀和
    depth[u] = de;
    for(int i = h[u]; ~ i; i = ne[i])
    {
        int v = e[i];
        if(v == fa) continue;
        f[v][0] = u; //计算倍增数组
        for(int k = 1; k < K; k ++) f[v][k] = f[f[v][k - 1]][k - 1];
        dis[v] = dis[u] + w[i]; //v节点到根节点的距离
        
        dfs(v, u, de + 1);
    }
}
int lca(int u, int v)
{
    if(depth[u] < depth[v]) swap(u, v); //如果v更深,交换u、v
    for(int i = K - 1; i >= 0; i -- ) //将u跳到和v同层
        if(depth[f[u][i]] >= depth[v]) u = f[u][i];
    if(u == v) return u;
    for(int i = K - 1; i >= 0; i --) //将u和v同时向上跳,直到父节点相同
    {
        if(f[u][i] != f[v][i]) u = f[u][i], v = f[v][i];
    }
    return f[u][0]; //返回共同的父节点
}
bool check(int mid)
{
    memset(d, 0, sizeof d); //初始化差分数组
    int c = 0, max_d = 0; //统计不满足要求的运输计划的个数,以及最大的路径长度
    for(int i = 0; i < m; i ++)
    {
        int u = q[i].u, v = q[i].v, p = q[i].p, dist = q[i].dist;
        if(dist > mid) 
        {
            c ++;
            max_d = max(max_d, dist);
            d[u] += 1, d[v] += 1, d[p] -= 2; //差分处理,将u->v路径上的每条边计数1次
        }
    }
    if(c == 0) return true; //没有不满足要求的运输计划
    for(int i = n - 1; i >= 0; i --) //自底向上统计每条航道的经过次数
    {
        int u = seq[i];
        d[f[u][0]] += d[u]; //求子树的前缀和
    }
    for(int i = 1; i <= n; i ++)
        //如果i是公共边,并且最长的路径减去公共边的长度满足要求
        if(d[i] == c && max_d - (dis[i] - dis[f[i][0]]) <= mid)
            return true;
    return false;
    
}
int main()
{
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    for(int i = 1; i < n; i ++)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c), add(b, a, c);
    }
    dfs(1, -1, 1); //预处每个节点的深度、到根节点的距离和倍增数组
    for(int i = 0; i < m; i ++)
    {
        int u, v;
        scanf("%d%d", &u, &v);
        int p = lca(u, v); // 计算最近公共祖先
        int dist = dis[u] + dis[v] - 2 * dis[p]; //计算u->v的路径长度
        q[i] = {u, v, p, dist}; //保存运输计划
        
    }
    int L = 0, R = 3e8; //二分查找,R = n * max(w)
    while(L < R)
    {
        int mid = (L + R) / 2;
        if(check(mid)) R = mid;
        else L = mid + 1;
    }
    printf("%d\n", L);
    return 0;
    
}
  • 21
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

少儿编程乔老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值