Luogu2680 [NOIP2015 提高组] 运输计划

15 篇文章 0 订阅
5 篇文章 0 订阅

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

运输计划

题目背景

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

题目描述

公元 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
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

题解

👴就是被这题的树剖标签引过来的,劈里啪啦敲了一堆链修改、链求和、链求最值,结果™这题的树剖就是拿来求 L C A \mathcal{LCA} LCA的?

大概想了想朴素的树剖+贪心和一堆乱七八糟的东西可能顶天 80 80 80分,果然退役咸鱼还是要向各路神仙低头。

首先,我们要变为虫洞的边肯定是在用时最长的那一条链上的,否则最长时间始终是用时最长的那一条链。同时因为删掉边后所有途经该边的路径用时的变化量是相等的,在所有途经该边的路径里不会有比原最长路径更长的,可能跟原最长路径竞争的路径只有未经过被删去边的所有路径里最长的那条。所以最终答案是: m a x ( 原 最 长 路 径 − 原 最 长 路 径 上 某 条 边 , 未 经 过 被 删 去 的 那 条 边 的 最 长 路 径 ) max(原最长路径-原最长路径上某条边,未经过被删去的那条边的最长路径) max()但是这样一来,删去哪条边,又如何找到未经过那条边的路径并不好处理,我们可以尝试反向思考:如何验证一个最长时间是否可行?

这样思路就有了,假设对于给定的最长时间 t t t,有 k k k条路径的用时 > t >t >t,那么我们要删掉的边一定是这 k k k条路径的公共边,且这条边的用时可以尽可能长。如果用时最长的路径 − - 找出来的这条边 ≤ t \le t t那么 t t t就是可行的。

更进一步,我们发现 t t t是否可行是满足单调的:如果 t t t可行, ≥ t \ge t t的任意时间都可行;如果 t t t不可行, ≤ t \le t t的任意时间都不可行。那么我们就可以二分答案了。

具体实现时,可以用用 d f s dfs dfs将该无根树变为有根树,把边权下放为点权,同时预处理每个点到根的距离方便计算题目给出的每个路径的长度。树剖完成后,可以预处理每条路径端点的 L C A \mathcal{LCA} LCA,求出路径用时,将所有路径按用时降序排序之后就可以二分答案。

check函数里,统计公共边不需要用树剖区间加,只需要树上差分就可以轻松满足统计公共边的需求。具体做法是:当边权下放为点权后,对于要覆盖的链,将两个端点权值分别 + 1 +1 +1,两端点 L C A \mathcal{LCA} LCA权值 − 2 -2 2。最后 d f s dfs dfs下去每个点子树的权值和就是这个点到其父亲的边被覆盖的次数。

代码

👴兴高采烈的复制粘贴了刚打的树剖板子,结果就这?(不过能一遍过👴还是很欣慰嗷)

第一次过了之后发现下放边权的时候不需要真的开数组记录, d f s dfs dfs多带一个参数就行了,可以少开一个数组。

#include<bits/stdc++.h>
#define ls v<<1
#define rs v<<1|1
using namespace std;
const int M=3e5+5;
struct Task{int u,v,tim,ace;}task[M];
bool cmp(Task a,Task b){return a.tim>b.tim;}
struct Edge{int to,val;};
int n,m,root,tot,k,les,dad[M],eld[M],dep[M],dis[M],siz[M],top[M],id[M],cov[M];
vector<Edge>edge[M];
int dfs1(int v,int f,int dp,int ds)
{
    dad[v]=f,dep[v]=dp,siz[v]=1,dis[v]=ds;
    for(int i=edge[v].size()-1;i>=0;--i)
    {
        if(edge[v][i].to==f)continue;
        siz[v]+=dfs1(edge[v][i].to,v,dp+1,ds+edge[v][i].val);
        if(siz[edge[v][i].to]>siz[eld[v]])eld[v]=edge[v][i].to;
    }
    return siz[v];
}
void dfs2(int v,int topf)
{
    id[v]=++tot,top[v]=topf;
    if(!eld[v])return;
    dfs2(eld[v],topf);
    for(int i=edge[v].size()-1;i>=0;--i)
    if(edge[v][i].to!=dad[v]&&edge[v][i].to!=eld[v])dfs2(edge[v][i].to,edge[v][i].to);
}
int lca(int x,int y)
{
    for(;top[x]!=top[y];x=dad[top[x]])
    if(dep[top[x]]<dep[top[y]])swap(x,y);
    return dep[x]<dep[y]?x:y;
}
int dfs(int v,int val)
{
    int r=cov[v];
    for(int i=edge[v].size()-1;i>=0;--i)
    if(edge[v][i].to!=dad[v])r+=dfs(edge[v][i].to,edge[v][i].val);
    if(r==k)les=max(les,val);
    return r;
}
bool check(int t)
{
    int pre=k;
    for(k=1;k<=m;++k)if(task[k+1].tim<=t)break;
    for(int i=pre+1;i<=k;++i)++cov[task[i].u],++cov[task[i].v],cov[task[i].ace]-=2;
    for(int i=pre;i>k;--i)--cov[task[i].u],--cov[task[i].v],cov[task[i].ace]+=2;
    les=0,dfs(1,0);
    return task[1].tim-les<=t;
}
void in()
{
    scanf("%d%d",&n,&m);
    for(int i=n-1,a,b,c;i;--i)
    scanf("%d%d%d",&a,&b,&c),edge[a].push_back((Edge){b,c}),edge[b].push_back((Edge){a,c});
}
void ac()
{
    dfs1(1,0,1,0);
    dfs2(1,1);
    int max1=0,max2=0,delta;
    for(int i=1,a,b;i<=m;++i)
    {
        scanf("%d%d",&task[i].u,&task[i].v);
        task[i].tim=dis[task[i].u]+dis[task[i].v]-2*dis[task[i].ace=lca(task[i].u,task[i].v)];
    }
    sort(task+1,task+1+m,cmp);
    int le=0,ri=task[1].tim;
    for(int mid;le<ri;check(mid=le+ri>>1)?ri=mid:le=mid+1);
    printf("%d\n",ri);
}
int main()
{
    in(),ac();
    system("pause");
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ShadyPi

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

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

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

打赏作者

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

抵扣说明:

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

余额充值