1. 程式人生 > >NOIP2015 D2T3 運輸計劃

NOIP2015 D2T3 運輸計劃

getch add pan AD num namespace memset HA tdi

拿到題目的第一眼


首先這是一棵n個節點的樹(別說你看不出來)


然後對於樹上的m條鏈
我們可以選取樹上的唯一一條邊使它的邊權變為0


求處理後最長鏈的長度


20分


m=1
好啦,好像可做
一眼望去全是水


只需求出一條鏈上的所有邊並計算邊權和及最大邊權(暴力往上跳並記錄即可)
邊權和減去最大邊權即為答案
那麽我們就可以O(n)過掉這道題了(不嫌麻煩的話也可以O(log n)搞樹上路徑)


60分?


從未如此接近滿分
全是鏈,這意味著什麽(並不意味著什麽)


想了想,發現好像60分並不好搞
考慮一下暴力吧


超級暴力:暴力枚舉刪每一條邊,統計刪完這條邊之後最長鏈的長度,取最小值就是答案,復雜度O(n^2 m),25分


剛才的小優化


考慮優化暴力


枚舉刪哪條邊O(n)顯然已經達到理論下限
如果非要搞它的話只能排除那些不被經過的邊,效率高不了多少


接下來是統計每條鏈的長度
全是鏈哎,求線性區間和,前綴和優化,消去一個O(n)


那個O(m)好像沒有什麽有效的優化


這樣,復雜度降至O(nm),40分


然後其他數據,搞樹鏈剖分動態修改、查詢可以多拿一些分,復雜度O(nm log n),60分


怎麽辦


QAQ,60分都拿不到了嗎


可不可以不實際改邊權呢?


經過不會就猜二分
經過深入思考,我們發現:


最短時間為t,前提是對於length>t的所有鏈,總能找到至少一條長為k公共邊,使得最長鏈的長度max length-k<=t
如果知道答案,好像不僅不用枚舉最長鏈,還可以把枚舉刪邊變為貪心刪掉被全部滿足條件的鏈經過的最長邊,穩賺一個O(n)和一個O(m)


考慮二分答案


如果能夠在時間t1內完成任務,那麽對於t2>t1,總能在時間t2內完成任務
所以答案符合單調性
可以二分答案


Check函數怎麽寫呢,看一看能不能找到找到至少一條長為k公共邊,使得最長鏈的長度max length-k<=t
設length>t的邊的個數為number
我們必須知道一條邊是否曾被number個鏈同時經過,唯一的方法好像就是差分了,check函數可以寫成O(n + m)的,總復雜度O((n + m)log n),60分


100分


二分答案的做法放到樹上呢


考慮線性數據上二分的完整做法
預處理每一條鏈的length,二分答案,放到check函數裏搞


沒問題
LCA求出每條鏈的length,還是二分,check函數換成樹上差分


最後發現正解只要一句話:
求鏈長+二分


上代碼:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn=3e5+10;
struct edge{
    int next,to,dis;
}e[2*maxn],q[2*maxn];
struct length{
    int len,lca,u,v;
}len[maxn];
int head[maxn],cnt,headq[maxn],dis[maxn],maxlen,n,m,a[maxn],ans,s[maxn],num,ret,f[maxn];    //一堆變量
bool vis[maxn];
inline int readn()        //隨處可見的快讀
{
    int x=0;
    char ch=getchar();
    while(ch>9||ch<0)
        ch=getchar();
    while(ch>=0&&ch<=9)
    {
        x=(x<<3)+(x<<1)+(ch^0);
        ch=getchar();
    }
    return x;
}
inline void add_edge(int x,int y,int d)
{
    e[++cnt].next=head[x];
    e[cnt].to=y;
    e[cnt].dis=d;
    head[x]=cnt;
}
inline void add_que(int x,int y)
{
    q[++cnt].next=headq[x];
    q[cnt].to=y;
    headq[x]=cnt;
}
int find(int x)
{
    return f[x]==x?f[x]:f[x]=find(f[x]);    //一行並查集
}
void dfs(int u,int pre)
{
    for(int i=head[u];i;i=e[i].next)
    {
        int v=e[i].to;
        if(v==pre)
            continue;
        dfs(v,u);
        s[u]+=s[v];        //統計經過次數
    }
    if(s[u]==num&&a[u]>ret)
        ret=a[u];        //貪心地選取最長公共邊
}
inline bool check(int x)
{
    memset(s,0,sizeof(s));
    num=ret=0;
    for(int i=1;i<=m;i++)        //樹上差分
        if(len[i].len>x)
        {
            s[len[i].u]++;
            s[len[i].v]++;
            s[len[i].lca]-=2;
            num++;        //記錄len>x的鏈的個數
        }
    dfs(1,0);        //跑差分結果
    if(maxlen-ret>x)    //如果不能滿足,返回NO
        return 0;
    return 1;    //能滿足
}
void tarjan(int u,int pre)        //tarjan求鏈長
{
    for(int i=head[u];i;i=e[i].next)
    {
        int v=e[i].to;
        if(v==pre)
            continue;
        dis[v]=dis[u]+e[i].dis;
        tarjan(v,u);
        a[v]=e[i].dis;
        int f1=find(v);
        int f2=find(u);
        if(f1!=f2)
            f[f1]=find(f2);
        vis[v]=1;
    }
    for(int i=headq[u];i;i=q[i].next)
        if(vis[q[i].to])
        {
            int p=(i+1)>>1;
            len[p].lca=find(q[i].to);
            len[p].len=dis[u]+dis[q[i].to]-2*dis[len[p].lca];
            maxlen=max(maxlen,len[p].len);
        }
}
int main()
{
    n=readn(),m=readn();
    for(int i=1;i<n;i++)
    {
        int ai=readn(),bi=readn(),ti=readn();
        add_edge(ai,bi,ti);        //鄰接表存圖
        add_edge(bi,ai,ti);
    }
    for(int i=1;i<=n;i++)
        f[i]=i;
    cnt=0;
    for(int i=1;i<=m;i++)
    {
        int x=readn(),y=readn();        //輸入
        len[i].u=x;
        len[i].v=y;
        add_que(x,y);
        add_que(y,x);
    }
    tarjan(1,0);
    int l=0,r=maxlen,mid;
    while(l<=r)        //二分答案
    {
        mid=(l+r)>>1;
        if(check(mid))
        {
            r=mid-1;
            ans=mid;    //記錄答案
        }
        else
            l=mid+1;    //不能滿足則二分更大答案,以使得條件可以得到滿足
    }
    printf("%d\n",ans);
    return 0;
}

NOIP2015 D2T3 運輸計劃