noip2018 Day1T3 赛道修建

题意:

\(C\)城将要举办一系列的赛车比赛。在比赛前,需要在城内修建\(m\)条赛道。

\(C\)城一共有n个路口,这些路口编号为\(1,2,…,n\),有\(n-1\)条适合于修建赛道的双向通行的道路,每条道路连接着两个路口。其中,第i条道路连接的两个路口编号为\(a_i\)\(b_i\),该道路的长度为\(l_i\)。借助这\(n-1\)条道路,从任何一个路口出发都能到达其他所有的路口。

一条赛道是一组互不相同的道路\(e_1,e_2,…,e_k\),满足可以从某个路口出发,依次经过道路\(e_1,e_2,…,e_k\)(每条道路经过一次,不允许调头)到达另一个路口。一条赛道的长度等于经过的各道路的长度之和。为保证安全,要求每条道路至多被一条赛道经过。

目前赛道修建的方案尚未确定。你的任务是设计一种赛道修建的方案,使得修建的m条赛道中长度最小的赛道长度最大(即m条赛道中最短赛道的长度尽可能大)。

数据范围

https://cdn.luogu.com.cn/upload/pic/43164.png

分析:

因为要是最小值最大,所以我们先尝试二分答案。

当然在正解之前,我们还是一部分一部分地考虑。

对于\(m=1\)的情况,

我们就是求出这棵树的直径。

    int ans;
    int dis[maxn];//我们的dis数组中dis[i]求的是从i点到它的叶子节点的最长距离
    void dfs(int u,int fa)
    {
        for(int i=head[u];i;i=ed[i].nxt)
        {
            int v=ed[i].to;
            if(v==fa) continue;
            else
            {
                dfs(v,u);
                ans=max(ans,dis[u]+dis[v]+ed[i].w);
                dis[u]=max(dis[u],dis[v]+ed[i].w);
            }
        }
    }

其中\(ans\)就是直径的长度,也即此时的答案。

对于\(b_i=a_i+1\)的情况,

此时的树是一条链,我们直接二分答案就可以了(不过这个二分一直没过)。

对于\(a_i=1\)的情况,

因为所有节点的根都是\(1\),那么这就是一个菊花图,我们把每条边的边权按长度排一下序(从大到小),那么最终的答案就是\(w_1+w_{2*m},w_2+w_{2*m-1},\cdots,w_m+w_{m+1}\)中的最小值。

对于分支不超过\(3\)的情况,

因为它就是正解的弱化版,而且网上好像没看到有专门写这一部分部分分的题解,所以就和正解一起讲了。

先二分答案\(k\),当一条链对答案有贡献的时候它一定满足

\(val_a>=k\)或者\(val_a+val_b>=k\)(其中\(val_b\)是另外一条链的长度)

那我们每次二分答案判断的时候就贪心,把传进来的值加入到一个\(multiset\)中去,对于第一种情况,它不需要被加入到\(multiset\)中去,只需要让答案\(+1\)即可,对于第二种情况,它直接在\(multiset\)中去寻找第一个\(>=k-now_{min}\)的数,然后再把它们两个同时从\(multiset\)中删去,再把此时从的\(multiset\)中所存的最长的且还未被删除的链的长度传给他的父亲即可(因为此时\(multiset\)中存的边都是可以从它父亲可以到达的)。

    multiset<int> s[maxn];
    multiset<int>::iterator it;
    
    int dfs(int u,int fa,int k)
    {
        s[u].clear();
        for(int i=head[u];i;i=ed[i].nxt)
        {
            int v=ed[i].to;
            if(v==fa) continue;
            int val=dfs(v,u,k)+ed[i].w;
            if(val>=k) ++ans;
            else s[u].insert(val);
        }
        int Max=0;
        while(!s[u].empty())
        {
            if(s[u].size()==1) return max(Max,*s[u].begin());
            it=s[u].lower_bound(k-*s[u].begin());
            if(it==s[u].begin()&&s[u].count(*it)==1) it++;//因为此时搜到的最小的满足条件的数
            //就是最小的数且该数在multiset中只有一个也即它本身,所以我们往后再找一个数
            if(it==s[u].end())//此时并没有找到比k-*s[u].begin()更大的值,所以*s[u].begin()这
            {//个值是没办法满足条件的,我们就尝试把它与原先已有的Max取一个max,看它是否能传到上面去
                Max=max(Max,*s[u].begin());//做贡献
                s[u].erase(s[u].find(*s[u].begin()));
            }
            else
            {
                s[u].erase(s[u].find(*it));
                s[u].erase(s[u].find(*s[u].begin()));
            }
        }
        return Max;
    }

然后就可以完成了。

\(Code:\)

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #include<cmath>
    #include<set>
    using namespace std;
    
    const int maxn=5e4+10;
    
    multiset<int> s[maxn];
    multiset<int>::iterator it;
    
    int dis[maxn],ans;
    
    int head[maxn],tot;
    struct Edge
    {
        int to,nxt,w;
        Edge(){};
        Edge(int to,int nxt,int w):to(to),nxt(nxt),w(w){};
    }ed[maxn<<1];
    void add(int u,int v,int w)
    {
        ed[++tot]=Edge(v,head[u],w);
        head[u]=tot;
        ed[++tot]=Edge(u,head[v],w);
        head[v]=tot;
    }
    
    int cnt;
    
    int dfs(int u,int fa,int k)
    {
        s[u].clear();
        for(int i=head[u];i;i=ed[i].nxt)
        {
            int v=ed[i].to;
            if(v==fa) continue;
            else
            {
                int val=dfs(v,u,k)+ed[i].w;
                if(val>=k) ++cnt;
                else s[u].insert(val);
            }
        }
        int Max=0;
        while(!s[u].empty())
        {
            if(s[u].size()==1) return max(Max,*s[u].begin());
            it=s[u].lower_bound(k-*s[u].begin());
            if(it==s[u].begin()&&s[u].count(*s[u].begin())==1) it++;
            if(it==s[u].end())
            {
                Max=max(Max,*s[u].begin());
                s[u].erase(s[u].find(*s[u].begin()));
            }
            else
            {
                ++cnt;
                s[u].erase(s[u].find(*s[u].begin()));
                s[u].erase(s[u].find(*it));
            }
        }
        return Max;
    }
    
    template<class T>void read(T &x)
    {
        bool f=0;char ch=getchar();x=0;
        for(;ch<'0'||ch>'9';ch=getchar()) if(ch=='-') f=1;
        for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+ch-'0';
        if(f) x=-x;
    }
    
    void get_the_longest(int u,int fa)
    {
        for(int i=head[u];i;i=ed[i].nxt)
        {
            int v=ed[i].to;
            if(v==fa) continue;
            else
            {
                get_the_longest(v,u);
                ans=max(ans,dis[u]+dis[v]+ed[i].w);
                dis[u]=max(dis[u],dis[v]+ed[i].w);
            }
        }
    }
    
    int main()
    {
        int n,m;
        read(n);read(m);
        for(int i=1;i<n;++i)
        {
            int u,v,w;
            read(u);read(v);read(w);
            add(u,v,w);
        }
        get_the_longest(1,0);
        int l=0,r=ans+1;
        while(l+1<r)
        {
            int mid=(l+r)>>1;
            dfs(1,0,mid);
            if(cnt>=m) l=mid;
            else r=mid;
            cnt=0;
        }
        printf("%d\n",l);
        return 0;
    }

这份代码是开了\(O2\)才过了的,如果不开\(O2\)的话会\(T\)

另外,为了提高答案的精度(主要针对于在实数域上的答案的二分),我们一般写二分的次数,可能有点难以理解,看一下代码就明白了。

    for(int i=1;i<=100;++i)//i表示的就是二分的次数
    {
        double mid=(l+r)/2;
        if(check(mid)) l=mid;
        else r=mid;
    }
    double ans=(l+r)/2;

猜你喜欢

转载自www.cnblogs.com/iwillenter-top1/p/11823552.html