luogu賽道修建
阿新 • • 發佈:2021-06-30
介紹
- 本篇文章是我寫過的最詳細易懂的一篇題解,同時也是我用 GitHub 寫的第一篇題解。
- 這篇題解力求在分析過程方面幫助到更多的人,並且我個人認為比其他題解要容易理解許多。
- 如果你想要更佳的閱讀體驗,請點選此處。
分析階段
要想讓最小值最大,這類題目一般採用二分答案的方法。二分我們的最小賽道長,然後每次在樹上構建長度大於等於二分到的值 \(mid\) 的賽道,看看是否可以構建出不小於 \(m\) 條賽道。
這一步不難想到,此題的難點在於如何去判斷 \(mid\) 是否可以構建出合法條件的賽道,即如何在樹上構建合法賽道才可以最大化賽道條數。
讓我們舉一個例子。
對於上面這棵樹,我們先從它最底層的子樹說起,就比如 \(6,7,8\)
如果一條賽道包含有這一棵子樹中的邊,那麼這條賽道可能有如下兩種情況:
- 這條賽道的全部部分都由這棵子樹中的邊組成。
- 這條賽道的一端有部分邊由這條子樹中的邊組成,剩下的部分由節點 \(6\) 以上(不在這棵子樹中)的邊組成。
如果說得易懂一些,那麼就是,從這棵子樹中的某一個頂點一直向上伸過來,到達子樹根 \(6\),對於第一種情況,他越過頂點 \(6\),去往外探索世界,對於第二種情況,他折回頭繼續去這棵子樹當中的其他分支延伸開去。
當然,還有一種比較特殊的情況,就是剛好它到了 \(6\) 這裡長度大於等於我們二分到的這個值,它就不需要再去探索其他的邊了,我們就把它記做一條賽道。
圖解:
我們考慮:首先,去在這棵子樹裡找兩個分支,使得他們邊權之和大於等於 \(mid\);這一步我們應該儘量“節省”,比如說我們有 \(3\ 4\) 和 \(3\ 5\) 兩種合法的選擇,我們就應該選擇 \(3\ 4\),為後面留下更多的空間。然後,在剩下的無法配對的分支當中,選取邊權最大的一個,呈獻給我們的根節點,這樣,當我們像這樣子去操作上面的 \(2,5,6\) 這個子樹時,\(6\) 這個子節點所能達到的最優分支長度就應該是 邊 2-6
的長加上我們呈獻給 \(6\) 的子分支長度 的和。
策略階段
我們有了大概的思路,應該想想什麼樣策略適合計算機去實現。
- 對整棵樹進行遍歷,把輸入的無向圖整合成一棵樹,方便後面實現,同時記錄每個節點的:父親,兒子及到這個兒子的邊之長。
- 算出這棵樹的直徑,二分答案的上界就應該是它——因為賽道是一條鏈,所以答案一定不會超過樹的直徑。(樹的直徑就是一棵樹上最長的從一點到一點的路徑長度,常用的求樹的直徑的方法是,從樹上任意一點找到一個樹上距離它最遠的點,然後找到從這個最遠點開始的樹上路徑中最長的長度。)
- 每個節點有一個 \(\text{set}\),儲存這個節點為根的子樹的所有分支,當然,我們只需要在 \(\text{set}\) 中放那些需要組合的,也就是說他自己一個人不足賽道長的分支,如果是我們剛才說的第三種情況,那我們直接說我們多了一條賽道就好了(不需要放入 \(\text{set}\))。
- 然後在 \(\text{set}\) 中進行配對(配成一對就加了一條賽道),配不成的就取 \(\max\) 然後貢獻給根,我們把每個節點得到的貢獻記為 \(val\)。
- 最後檢查一下是不是賽道數大於等於 \(m\),如果是,這個 \(mid\) 合法(\(L=mid\)),否則,\(mid\) 不合法(\(R=mid\))。最終的 \(L\) 即是答案。
程式碼階段
有了清晰的思路,程式碼應該比較好寫了,但是還是有一些地方需要注意。
- 加快讀
- 開 O2
- 然後我們就可以 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;
}
希望你能收穫更多!