• Luogu 2680 NOIP 2015 运输计划(树链剖分,LCA,树状数组,树的重心,二分,差分)


    Luogu 2680 NOIP 2015 运输计划(树链剖分,LCA,树状数组,树的重心,二分,差分)

    Description

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

    小 P 掌管一家物流公司,该公司有很多个运输计划,每个运输计划形如:有一艘物

    流飞船需要从 ui 号星球沿最快的宇航路径飞行到 vi 号星球去。显然,飞船驶过一条航道 是需要时间的,对于航道 j,任意飞船驶过它所花费的时间为 tj,并且任意两艘飞船之 间不会产生任何干扰。

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

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

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

    Input

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

    接下来 n-1 行描述航道的建设情况,其中第 i 行包含三个整数 ai, bi 和 ti,表示第

    i 条双向航道修建在 ai 与 bi 两个星球之间,任意飞船驶过它所花费的时间为 ti。

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

    Output

    输出 共1行,包含1个整数,表示小P的物流公司完成阶段性工作所需要的最短时间。

    Sample Input

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

    Sample Output

    11

    Http

    Luogu:https://www.luogu.org/problem/show?pid=2680

    Source

    树链剖分,最近公共祖先LCA,树的重心,二分,差分,树状数组

    题目大意

    给出一棵边权树,现在给定若干对点,要求选择一条树边使其权值为0,使得所有的点对的距离的最大值最小。

    解决思路

    基本的思路是先通过求最近公共祖先LCA来求得这若干对点对的距离。按照距离从大到小排序后,二分最大的距离(mid),将那些距离大于(mid)的连接点对的路径打上标记,设有k个这样的路径。找出那些这k条路径都覆盖的边,判断如果把这条边距离设为0是否可以使得所有的路径长度都小于(mid),如果可以,则说明该(mid)是可行的,否则不行。
    但单纯的照上面这样做是不行的,我们考虑优化。
    首先是求这若干组点对的距离。我们知道在树上求两点之间的距离与LCA有关,而求LCA有两种算法,一种是倍增法,另一种是树链剖分。这里为了方面后面的差分,同时也是为了减小常数,选用树链剖分的方法。我们将树按照轻重剖分成轻链和重链,将树上的问题转换成若干区间的问题。
    接下来考虑如何维护区间。由题可知,本题需要维护的是路径长度,也就是若干边权之和。但对于边权的操作远不如点权方便,所以我们把边权转化成为点权,具体方法是将点(u)到其父亲节点(f)的边的权转化成为点(u)的点权。这里需要注意的是,将边权转化成为点权之后,两个点(u,v)之间的路径长度就不是(u)(lca(u,v))的点权和加(v)(lca(u,v))的点权和了,因为(lca(u,v))的点权是到其父亲的边权,不在(u)(v)的路径上,所以在向上跳转的时候要跳到(lca)的深度+1的点。同样需要改变的也包括区间查询和差分。
    至于维护这个和,因为只有查询和插入,所以这里考虑使用常数更小的树状数组来实现。
    现在考虑将选出的长度大于(mid)的路径标记。可以将路径上的每一条边标记+1,然后可以知道,如果有k条这样的路径,那么标记为k的边就是这k条路径都覆盖的。但是暴力标记是不行的。我们发现每次都是将k条路径都标记完后再操作,所以我们可以考虑差分。而又因为我们用树链剖分将树划分成链,所以可以参考求解LCA的在链上跳转的方式,在重链的top位置++,而在当前位置+1的地方--,注意,这些位置都是在点的新编号的位置(也就是在点在树状数组中的编号),因为这样就变成区间差分了。但是最后一条链的起始位置(即lca的位置)要+1,具体原因是我们把边权转化成了点权,而lca的点权代表的边权不在路径上。
    最后来判断是否可行。因为最长路径一定会被选,所以我们在从(1~n)累加差分数组,当累加器(ret==k)时,判断最长路径减去当前点(i)对应的点权是否小于当前二分的(mid),如果存在一条这样的边,则可行,否则不行。
    另外,我们知道在树上操作的效率一定程度上取决于对根节点的选取,所以我们可以在最开始求出树的重心,并以重心为根,这样可以提高效率。

    代码

    #include<iostream>
    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<algorithm>
    using namespace std;
    
    #define ll long long
    #define lowbit(x) ((x)&(-x))
    
    const int maxN=300101;
    const int maxM=300101;
    const int inf=2147483647;
    
    int n;
    
    class Edge//边
    {
    public:
        int u,v,w;
    };
    
    class TreeArr//树状数组
    {
    public:
        int A[maxN];
        TreeArr()
            {
                memset(A,0,sizeof(A));
                return;
            }
        void Add(int pos,int x)//插入
            {
                while (pos<=n)
                {
                    A[pos]=A[pos]+x;
                    pos=pos+lowbit(pos);
                }
                return;
            }
        int Sum(int x)//求和1~x
            {
                int ret=0;
                while (x>0)
                {
                    ret=ret+A[x];
                    x=x-lowbit(x);
                }
                return ret;
            }
        int query(int l,int r)//求区间和
            {
                return Sum(r)-Sum(l-1);
            }
    };
    
    class Question//对于每一条路径
    {
    public:
        int u,v,w;
    };
    
    bool operator < (Question A,Question B)//按照路径长度降序排序
    {
        return A.w>B.w;
    }
    
    int qus;
    int root,rootsize;//重心,以及对应的最大子树-最小子树
    int longestpath=0;//最长路径
    //Graph 原来树中的data
    int cnt=0;
    int Head[maxN];
    int Next[maxN*2];
    Edge E[maxN*2];
    int Node_W[maxN];//边权转化后的点权
    //Tree_Chain 树链剖分维护的data
    int Depth[maxN];//深度
    int Father[maxN];//父节点编号
    int Top[maxN];//链顶
    int Size[maxN];//大小
    int HeavySon[maxN];//重儿子
    int Id[maxN];//Id[i]表示树状数组中i对应的原节点编号。若Id[i]==j则dfn[j]==i
    int dfn[maxN];//原节点i对应的在树链剖分中分配的新编号
    //TreeArr
    TreeArr TA;//树状数组
    //Q
    Question Q[maxM];//点对和路径
    //Insert
    int Num[maxN];//差分数组
    
    int read();
    void Add_Edge(int u,int v,int w);
    void dfs(int u,int father);//求重心
    void dfs1(int u,int father);//树链剖分第一遍dfs
    void dfs2(int u,int nowTop);//树链剖分第二遍dfs
    int Calc_Path(int u,int v);//计算u到v的路径长度
    bool check(int nowmid);//检查当前最大距离是否合法
    void Insert(int u,int v);//将u到v的路径差分标记
    
    int main()
    {
        memset(Head,-1,sizeof(Head));
        rootsize=inf;
        n=read();
        qus=read();
        for (int i=1;i<n;i++)//读入边
        {
            int u,v,w;
            u=read();
            v=read();
            w=read();
            Add_Edge(u,v,w);
            Add_Edge(v,u,w);
        }
        //处理出重心
        dfs(1,1);
    
        //树链剖分
        Depth[1]=1;
        dfs1(root,root);
        cnt=0;
        dfs2(root,root);
        for (int i=1;i<=n;i++)//将边权转化成点权并存入树状数组
            TA.Add(dfn[i],Node_W[i]);
    
        //求解路径
        for (int i=1;i<=qus;i++)
        {
            Q[i].u=read();
            Q[i].v=read();
            Q[i].w=Calc_Path(Q[i].u,Q[i].v);
            longestpath=max(Q[i].w,longestpath);//选出最长路径
            //cout<<Q[i].w<<endl;
        }
    
        sort(&Q[1],&Q[qus+1]);//将路径排序
    
        int l=1,r=longestpath;//二分
        int Ans;
        do
        {
            int mid=(l+r)/2;
            if (check(mid))//检查
            {
                Ans=mid;
                r=mid-1;
            }
            else
                l=mid+1;
        }
        while (l<=r);
        printf("%d
    ",Ans);
        return 0;
    }
    
    int read()
    {
        int x=0;
        char ch=getchar();
        while ((ch>'9')||(ch<'0'))
            ch=getchar();
        while ((ch<='9')&&(ch>='0'))
        {
            x=x*10+ch-48;
            ch=getchar();
        }
        return x;
    }
    
    void Add_Edge(int u,int v,int w)
    {
        cnt++;
        Next[cnt]=Head[u];
        Head[u]=cnt;
        E[cnt].u=u;
        E[cnt].v=v;
        E[cnt].w=w;
        return;
    }
    
    void dfs(int u,int father)//求重心
    {
        Size[u]=1;
        int mx=0,mn=inf;//分别记录最大子树和最小子树
        for (int i=Head[u];i!=-1;i=Next[i])
        {
            int v=E[i].v;
            if (v==father)
                continue;
            dfs(v,u);
            Size[u]+=Size[v];
            if (Size[v]>mx)
                mx=Size[v];
            else
                if (Size[v]<mn)
                    mn=Size[v];
        }
        if (n-Size[u]>mx)
            mx=n-Size[u];
        else
            if (n-Size[u]<mn)
                mn=n-Size[u];
        if ((mx!=0)&&(mn!=inf)&&(mx-mn<rootsize))
        {
            root=u;
            rootsize=mx-mn;
        }
        return;
    }
    
    void dfs1(int u,int father)//树链剖分第一次dfs
    {
        Size[u]=1;
        HeavySon[u]=0;
        for (int i=Head[u];i!=-1;i=Next[i])
        {
            int v=E[i].v;
            if (v==father)
                continue;
            Father[v]=u;
            Depth[v]=Depth[u]+1;
            Node_W[v]=E[i].w;//将边权转换成点权
            dfs1(v,u);
            Size[u]=Size[u]+Size[v];
            if ((HeavySon[u]==0)||(Size[HeavySon[u]]<Size[v]))
                HeavySon[u]=v;
        }
        return;
    }
    
    void dfs2(int u,int nowTop)//树链剖分第二次dfs
    {
        cnt++;
        dfn[u]=cnt;
        Id[cnt]=u;
        Top[u]=nowTop;
        if (HeavySon[u]==0)
            return;
        dfs2(HeavySon[u],nowTop);
        for (int i=Head[u];i!=-1;i=Next[i])
            if ((E[i].v!=Father[u])&&(E[i].v!=HeavySon[u]))
                dfs2(E[i].v,E[i].v);
        return;
    }
    
    int Calc_Path(int u,int v)//计算u到v的路径长
    {
        int path=0;
        while (Top[u]!=Top[v])
        {
            if (Depth[Top[u]]<Depth[Top[v]])
                swap(u,v);
            path+=TA.query(dfn[Top[u]],dfn[u]);
            u=Father[Top[u]];
        }
        if (Depth[u]>Depth[v])
            swap(u,v);
        return path+TA.query(dfn[u]+1,dfn[v]);
    }
    
    bool check(int nowmid)//检查合法性
    {
        int pos=0;
        while (Q[pos+1].w>nowmid)//将路径长度大于mid的路径加入差分
        {
            pos++;
            Insert(Q[pos].u,Q[pos].v);
        }
        int now=0;
        for (int i=1;i<=n;i++)
        {
            now=now+Num[i];
            Num[i]=0;
            if ((now==pos)&&(longestpath-Node_W[Id[i]]<=nowmid))//当当前第i个点被所有pos条不满足的路径都覆盖时,若最长路径减去其点权比nowmid小,说明能够满足
            {
                for (int j=i+1;j<=n;j++)
                    Num[j]=0;
                return 1;
            }
        }
        return 0;
    }
    
    void Insert(int u,int v)//差分
    {
        while (Top[u]!=Top[v])
        {
            if (Depth[Top[u]]<Depth[Top[v]])
                swap(u,v);
            Num[dfn[Top[u]]]++;
            Num[dfn[u]+1]--;//注意这里+1
            u=Father[Top[u]];
        }
        if (Depth[u]>Depth[v])
            swap(u,v);
        Num[dfn[u]+1]++;//常规的差分这里是不要+1的,但这里需要,因为dfn[u]所对应的边不在u到v的路径上
        Num[dfn[v]+1]--;
        return;
    }
    
  • 相关阅读:
    2018左其盛差评榜(截至10月31日)
    4星|《环球科学》2018年10月号:习惯悬崖上生活的游隼现在开心地在摩天大楼上捕食鸽子
    4星|《财经》2018年第25期:中国的五大主要城市群具备“大、快、活”三个独特优势
    2018左其盛好书榜(截至10月31日)
    4星|《门口的野蛮人1》:1988年惊动美国的一次杠杆收购事件
    在 ASP.NET Core 具体使用文档
    .Net Core 部署到 CentOS7 64 位系统中的步骤
    [Linux/Ubuntu] vi/vim 使用方法讲解
    CentOS7使用httpd apache 和firewalld打开关闭防火墙与端口
    Linux软件管理器(如何使用软件管理器来管理软件)2---安装及管理Linux应用程序
  • 原文地址:https://www.cnblogs.com/SYCstudio/p/7629268.html
Copyright © 2020-2023  润新知