【NOIP 2015】Day2 T3 运输计划

Author Avatar
洛水·锦依卫 1月 25, 2019

【NOIP 2015】Day2 T3 运输计划

题面

题目背景

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

题目描述

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

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

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

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

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

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

输入输出格式

输入格式:

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

接下来 $n-1$ 行描述航道的建设情况,其中第 $i$ 行包含三个整数 $a_i, b_i$ 和 $t_i$,表示第 $i$ 条双向航道修建在 $a_i$ 与 $b_i$两个星球之间,任意飞船驶过它所花费的时间为 $t_i$。数据保证 $1 \leq a_i,b_i \leq n$ 且 $0 \leq t_i \leq 1000$。

接下来 $m$ 行描述运输计划的情况,其中第 $j$ 行包含两个正整数 $u_j$ 和 $v_j$,表示第 $j$ 个运输计划是从 $u_j$ 号星球飞往 $v_j$号星球。数据保证 $1 \leq u_i,v_i \leq n$

输出格式:

一个整数,表示小 $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

说明

所有测试数据的范围和特点如下表所示

请注意常数因子带来的程序效率上的影响。

考点

二分答案,树上乱搞

思路

首先看到题目所求,是求最大值最小化。一下子就发现答案是单调的,那么二分答案就确定了。那么二分之后如何$check$呢?

  • 可以发现,当我们二分到一个值$mid$时,对于所有小于等于它的路径都无需处理。
  • 对于所有大于$mid$的路径,我们所修改的边必定是这些路径所共同覆盖的,否则对于某条未覆盖此边的路径它的值不会改变,仍旧大于等于$mid$
  • 在此基础上,修改所有路径共同覆盖的边中权值最大的一定最优。

那么一步步来。

  • 首先是二分,这个肯定没有问题。
  • 那么对于所有大于 mid 的路径,路径从大到小排序确定前缀大于$mid$的最靠后位置即可。路径长度可通过记录树上前缀和做到—不难想到,两个端点的树上前缀和之和减去两倍 $lca$ 的树上前缀和。
  • 如何寻找被共同覆盖的边?
    • 首先引入概念—树上前缀和与子树和。树上前缀和即是记录当前节点 $i$ 到根结点的距离,子树和则是以 $i$ 为根结点的子树内所有节点的权值和。
    • 考虑利用子树和进行树上差分。我们将路径的左右端点的用于差分的数组 $+1$ ,然后如果此时我们统计整棵树所有结点的数组的子树和,我们会神奇地发现,从左端点以及右端点到 $lca$ 的路径上所有结点的子树和都为 $1$ ,从 $lca$ 到根结点的路径上所有结点的子树和都为 $2$ !那么不难发现,如果我们在 $lca$ 的上再 $-1$ ,在 $lca$ 的父结点上也 $-1$,再计算一次子树和,就会发现此时这条路径上所有结点的数组子树和都为 $1$ !也就是说,正好整条路径上的结点都 $+1$ 了!
    • 只需要稍加修改,一个结点的子树和表示的是它到父结点的边的子树和,接着对于 $lca$ 我们改为在 $lca$ 处 $-2$ 并不对父结点处理即可!
    • 所以差分真是个妙东西(小声bb)。
  • 当对于所有的路径做完差分之后再统一做子树和后,对于每个子树和为大于 $mid$ 路径总数的点,它到父结点的边一定被这些路径同时经过,对于这些路径,我们取 $max$ 即可。
  • 至于最长的路径,直接拿最长的路径减去 $max$ 并判断是否小于等于 $mid$ 即可。

然后这道题就轻松 A 掉了!

代码

// luogu-judger-enable-o2 开O2是因为有个点极度卡人,目前我已知的人里只有用树剖求LCA的过掉了......
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;
int n,m,ans;
int head[300001],f[300001][20];//head数组用于链式前向星,f数组为倍增数组
int change[300001],jl[300001],num[300001];//change数组为差分数组,jl(就是距离的首字母^_^)数组为树上前缀和,num数组为子树和
struct tree
{
    int fa,deep;
}k[300001];//记录树上结点信息
struct node
{
    int u,v,lenth,lca;
}s[300001];//路径信息
struct egde
{
    int next,to,w;
}g[600001];//边信息
int cmp(node x,node y)
{
    return x.lenth>y.lenth;
}
void read(int &x)
{
    x=0;
    char number=getchar();
    while(!isdigit(number))
        number=getchar();
    while(isdigit(number))
    {
        x=x*10+number-'0';
        number=getchar();
    }
}//快读
void build(int x)
{
    book[x]=1;
    for(int p=head[x];p;p=g[p].next)
        if(g[p].to!=k[x].fa)
        {
            k[g[p].to].deep=k[x].deep+1;
            k[g[p].to].fa=x;
            jl[g[p].to]=jl[x]+g[p].w;
            build(g[p].to);
        }
}//建树,造出深度,父亲结点,树上前缀和的信息
int dfs(int x)
{
    book[x]=1;
    num[x]+=change[x];
    int p=head[x];
    while(p)
    {
        if(g[p].to!=k[x].fa)
            num[x]+=dfs(g[p].to);
        p=g[p].next;
    }
    return num[x];
}//统计子树和
bool pd(int mid)
{
    int top=1;
    for(int i=1;i<=n;i++)
    {
        num[i]=0;
        change[i]=0;
        book[i]=0;
    }
    while(s[top].lenth>mid&&top<=m)
        top++;//加入需要处理的路径
    top--;
    for(int i=1;i<=top;i++)
    {
        change[s[i].u]++;
        change[s[i].v]++;
        change[s[i].lca]-=2;
    }//差分数组加减
    dfs(1);
    int maxx=0;
    for(int  i=1;i<=n;i++)
        if(num[i]==top)
            maxx=max(maxx,jl[i]-jl[k[i].fa]);//取最大值
    if(s[1].lenth-maxx<=mid)
        return true;
    return false;
}//判断mid是否可行
int LCA(int a,int b)
{
    if(k[a].deep>k[b].deep)
    {
        for(int j=19;j>=0;j--)
            if(k[f[a][j]].deep>=k[b].deep)
                a=f[a][j];
    }
    else
        for(int j=19;j>=0;j--)
            if(k[f[b][j]].deep>=k[a].deep)
                b=f[b][j];
    for(int j=19;j>=0;j--)
        if(f[a][j]!=f[b][j])
        {
            a=f[a][j];
            b=f[b][j];
        }
    if(a==b)
        return a;
    return k[a].fa;
}//倍增求lca
int main()
{
    read(n);
    read(m);
    int u,v,w;
    for(int i=1;i<n;i++)
    {
        read(u);
        read(v);
        read(w);
        g[i].to=v;
        g[i].next=head[u];
        head[u]=i;
        g[i+n].to=u;
        g[i+n].next=head[v];
        head[v]=i+n;
        g[i].w=g[i+n].w=w;
    }//存边
    k[1].deep=1;
    build(1);
    for(int i=1;i<=n;i++)
        f[i][0]=k[i].fa;
    for(int j=1;j<=19;j++)
        for(int i=1;i<=n;i++)
            f[i][j]=f[f[i][j-1]][j-1];//倍增数组处理
    for(int i=1;i<=m;i++)
    {
        read(s[i].u);
        read(s[i].v);
        s[i].lca=LCA(s[i].u,s[i].v);
        s[i].lenth=jl[s[i].u]+jl[s[i].v]-jl[s[i].lca]*2;
    }//处理路径相关信息
    sort(s+1,s+m+1,cmp);
    int l=1,r=s[1].lenth;
    ans=r;
    while(l<=r)
    {
        int mid=(l+r)/2;
        if(pd(mid))
        {
            ans=min(ans,mid);
            r=mid-1;
        }
        else
            l=mid+1;
    }//二分答案
    cout<<ans;
}