1. 程式人生 > 其它 >luogu賽道修建

luogu賽道修建

介紹

  • 本篇文章是我寫過的最詳細易懂的一篇題解,同時也是我用 GitHub 寫的第一篇題解。
  • 這篇題解力求在分析過程方面幫助到更多的人,並且我個人認為比其他題解要容易理解許多。
  • 如果你想要更佳的閱讀體驗,請點選此處

分析階段

要想讓最小值最大,這類題目一般採用二分答案的方法。二分我們的最小賽道長,然後每次在樹上構建長度大於等於二分到的值 \(mid\) 的賽道,看看是否可以構建出不小於 \(m\) 條賽道。

這一步不難想到,此題的難點在於如何去判斷 \(mid\) 是否可以構建出合法條件的賽道,即如何在樹上構建合法賽道才可以最大化賽道條數。

讓我們舉一個例子。

對於上面這棵樹,我們先從它最底層的子樹說起,就比如 \(6,7,8\)

這棵子樹。

如果一條賽道包含有這一棵子樹中的邊,那麼這條賽道可能有如下兩種情況:

  1. 這條賽道的全部部分都由這棵子樹中的邊組成。
  2. 這條賽道的一端有部分邊由這條子樹中的邊組成,剩下的部分由節點 \(6\) 以上(不在這棵子樹中)的邊組成。

如果說得易懂一些,那麼就是,從這棵子樹中的某一個頂點一直向上伸過來,到達子樹根 \(6\),對於第一種情況,他越過頂點 \(6\),去往外探索世界,對於第二種情況,他折回頭繼續去這棵子樹當中的其他分支延伸開去。

當然,還有一種比較特殊的情況,就是剛好它到了 \(6\) 這裡長度大於等於我們二分到的這個值,它就不需要再去探索其他的邊了,我們就把它記做一條賽道。

圖解:

我們考慮:首先,去在這棵子樹裡找兩個分支,使得他們邊權之和大於等於 \(mid\);這一步我們應該儘量“節省”,比如說我們有 \(3\ 4\)\(3\ 5\) 兩種合法的選擇,我們就應該選擇 \(3\ 4\),為後面留下更多的空間。然後,在剩下的無法配對的分支當中,選取邊權最大的一個,呈獻給我們的根節點,這樣,當我們像這樣子去操作上面的 \(2,5,6\) 這個子樹時,\(6\) 這個子節點所能達到的最優分支長度就應該是 邊 2-6 的長加上我們呈獻給 \(6\) 的子分支長度 的和。

策略階段

我們有了大概的思路,應該想想什麼樣策略適合計算機去實現。

  1. 對整棵樹進行遍歷,把輸入的無向圖整合成一棵樹,方便後面實現,同時記錄每個節點的:父親,兒子及到這個兒子的邊之長。
  2. 算出這棵樹的直徑,二分答案的上界就應該是它——因為賽道是一條鏈,所以答案一定不會超過樹的直徑。(樹的直徑就是一棵樹上最長的從一點到一點的路徑長度,常用的求樹的直徑的方法是,從樹上任意一點找到一個樹上距離它最遠的點,然後找到從這個最遠點開始的樹上路徑中最長的長度。)
  3. 每個節點有一個 \(\text{set}\),儲存這個節點為根的子樹的所有分支,當然,我們只需要在 \(\text{set}\) 中放那些需要組合的,也就是說他自己一個人不足賽道長的分支,如果是我們剛才說的第三種情況,那我們直接說我們多了一條賽道就好了(不需要放入 \(\text{set}\))。
  4. 然後在 \(\text{set}\) 中進行配對(配成一對就加了一條賽道),配不成的就取 \(\max\) 然後貢獻給根,我們把每個節點得到的貢獻記為 \(val\)
  5. 最後檢查一下是不是賽道數大於等於 \(m\),如果是,這個 \(mid\) 合法(\(L=mid\)),否則,\(mid\) 不合法(\(R=mid\))。最終的 \(L\) 即是答案。

程式碼階段

有了清晰的思路,程式碼應該比較好寫了,但是還是有一些地方需要注意。

  1. 加快讀
  2. 開 O2
  3. 然後我們就可以 AC 了

程式碼有簡要註釋。

#pragma GCC optimize(2) //O2優化
#include <bits/stdc++.h>
using namespace std;
const int N=5e4+10;
int n,m,cnt,val[N],u[N],v[N],w[N],book[N],fa[N],Max,V;
//n,m如題所述,cnt用來記每次二分到的值對應合法賽道數
//u,v,w是題目輸入的兩個頂點、一條邊長
//book是在還沒有把樹造出來的情況下用來記錄dfs時哪些點走過沒有
struct race {
    int node,edge; //頂點編號、邊長
};
vector<int> _g[N];
vector<race> W[N];
vector<race> G[N];
multiset<int> s[N];
multiset<int>::iterator it;
void dfs(int step,int k){ 
    int x=0;
    s[step].clear();
    for(int i=0;i<G[step].size();i++){
        dfs(G[step][i].node,k);
        if(val[G[step][i].node]+G[step][i].edge>=k)
            cnt++;
        else 
            s[step].insert(val[G[step][i].node]+G[step][i].edge);
    }
    while(!s[step].empty()){
        if(s[step].size()==1){ //只剩一個頂點沒有處理了,取個max呈獻給根
            val[step]=max(x,*s[step].begin());
            return;
        }
        it=s[step].lower_bound(k-*s[step].begin()); //第一個和s.begin()相加能大於等於k的
        if(it==s[step].begin() && s[step].count(*it)==1) it++; //如果是自己那沒辦法只能找後面一個
        if(it==s[step].end()){ //沒有合適的也就是說配不了對
            x=max(x,*s[step].begin()); //按照我們之前說的找一個個兒大的
            s[step].erase(s[step].find(*s[step].begin())); //處理過的就要刪掉
        }
        else {
            cnt++; //配成一對兒
            //同樣,配成對的兩個不能再用了,刪掉
            s[step].erase(s[step].find(*it));
			s[step].erase(s[step].find(*s[step].begin()));
        }
    }
    val[step]=x; //呈現給子樹根
    return;
}
bool check(int k){
    cnt=0;
    dfs(1,k);
    return cnt>=m;
}
void init(int step){ //把父節點什麼的整合出來
    book[step]=1;
    for(int i=0;i<_g[step].size();i++)
        if(!book[_g[step][i]]){
            fa[_g[step][i]]=step;
            init(_g[step][i]);
        }
    return;
}
int read(){ //快速讀入
    int x=0;
    char ch=getchar();
    while(ch<'0' || ch>'9') ch=getchar();
    while(ch>='0' && ch<='9') x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
    return x;
}
void getdis(int step,int sum){ //找從一個點出發的最遠點
    book[step]=1;
    if(sum>Max) V=step,Max=sum; //V是最遠點,Max是最長路徑長度
    for(int i=0;i<W[step].size();i++)
        if(!book[W[step][i].node])
            getdis(W[step][i].node,sum+W[step][i].edge);
}
int tree_D(){ //返回值就是樹的直徑
    Max=0;
    memset(book,0,sizeof(book));
    getdis(1,0);
    Max=0;
    memset(book,0,sizeof(book));
    getdis(V,0);
    return Max;
}
int main()
{
    n=read(),m=read();
    race t;
    for(int i=1;i<=n-1;i++){
        u[i]=read(),v[i]=read(),w[i]=read();
        _g[u[i]].push_back(v[i]);
        _g[v[i]].push_back(u[i]);
        t.node=v[i],t.edge=w[i];
        W[u[i]].push_back(t);
        t.node=u[i],t.edge=w[i];
        W[v[i]].push_back(t);
    }
    init(1);
    //以上是一些基礎樹上操作,不贅述
    for(int i=1;i<=n-1;i++){ //把無向圖整合成一棵樹
    //G[x]是x的所有兒子和分別到他們的距離
        if(fa[u[i]]==v[i]){
            t.node=u[i],t.edge=w[i];
            G[v[i]].push_back(t);
        } 
        else {
            t.node=v[i],t.edge=w[i];
            G[u[i]].push_back(t);
        }
    }
    int L=1,R=tree_D()+1,mid;
    while(L<R-1){
        mid=(L+R)/2;
        if(check(mid)) L=mid;
        else R=mid;
    }
    printf("%d\n",L); //輸出答案
    return 0;
}

希望你能收穫更多!