题解 P5021 【赛道修建】
XG_Zepto
2018-11-13 15:21:35
首发于 [个人博客](https://zepto.page/post/noip-2018-track)。
### 思路
我就顺着我考场上的思路,把所有部分分都讲一讲吧。
首先对于 $m=1$ 的数据,我们显然只要找到直径长度就行;
对于一条链的情况,二分答案,对于每次二分出来的 $mid$,从根开始贪心选取一段边,累计长度大于等于 $mid$ 的时候就计入答案,重新开始寻找下一段,得出对于 $mid$ 最大的答案,判断它是否大于 $m$;
对于菊花图,由于路径最多经过两条边,我们实质上是要在 $n-1$ 条边中选一共 $m$ 次,每次选出一条独立的边,或者一对边,使得最终每条边的长度 / 每对边的长度和尽量大。对于每次二分出来的 $mid$,长度大于 $mid$ 的边独立地选出,而小于 $mid$ 的边尝试一长一短两两配对,用双指针在排过序的长度数据中扫描一遍,得出对于 $mid$ 最大的答案,判断它是否大于 $m$;
对于 “分支不超过 3” 的图,也就是除根节点之外,每个点只有两个儿子,同样我们二分答案。首先定义几个概念,我们称从一条赛道两端点 $LCA$ 开始的,到一个端点结束的链为对应赛道的半链,令 $f_i$ 为以 $i$ 为 $LCA$ 的最优半链长度。暂时不考虑根节点,如果 $i$ 只有一个儿子 $v$,若 $f_v + l_i \geq mid$,直接将 $f_v + l_i$ 计入答案,否则,$f_i = f_v + l_i$;如果 $i$ 有两个儿子,如果它们可以直接单独计入答案则直接计入,否则考虑是否可以将这两个儿子牵引的两条半链合并,合并后长度大于 $mid$ 则直接计入答案,否则选取最长的一段转移到 $f_i$,对于可能有三个儿子的根节点,遵循的原则同样是尽量独立选取两个儿子,否则尝试合并两条半链计入答案;
如果我们结合菊花图和 “分支不超过 3” 的图这两种情况,我们就可以搞定 100% 的数据。继承上面半链和 $f_i$ 的定义,我们要做的是在使最多的儿子对答案做出贡献的同时,尽量大地将 $f_{son}$ 的值转移给 $f_i$。因为 $f$ 的一个值最多对答案产生 1 的贡献,所以使儿子做出更少贡献,转移更大的 $f_i$ 不可能使答案变优,我们的贪心第一关键字就可以设为使儿子做出最大的贡献。在菊花图的算法中,我们只满足了第一个条件,但不需要考虑 $f_i$ 进一步的转移,而我认为这一点正是 A 掉本题的重点。也就是说,我们要在儿子的 $f$ 值中,选出一个最优的转移至 $f_i$,使得剩下的 $f$ 值的合法匹配最多。同样可以二分转移至 $f_i$ 的儿子,剩下的和菊花图的做法相同。
总结一下正解:首先二分答案,贪心计算对于 $mid$ 的最大可能答案,判断它和 $m$ 的大小。我们称从一条赛道两端点 $LCA$ 开始的,到一个端点结束的链为对应赛道的半链,令 $f_i$ 为以 $i$ 为 $LCA$ 的最优半链长度。要求对于 $i$ 所有儿子的 $f$,在保证两两匹配数(两个半链合成一条赛道)最多的同时,向 $f_i$ 转移尽可能大的值。于是将所有 $f$ 排序贪心找到最大匹配数,然后,二分找到转移给 $f_i$ 的值。
复杂度不太会证,应该接近 $\Theta(n\log n \log \dfrac{\sum{l_i}}{m})$ 吧。
### 代码
```cpp
#include <bits/stdc++.h>
#define mid ((l+r)>>1)
#define ll long long
#define maxn 50010
using namespace std;
struct Edge{
int to,next,val;
Edge(int a=0,int b=0,int c=0){
to=a,next=b,val=c;
}
}l[maxn<<1];
int head[maxn],cnt,n,m;
vector<int> son[maxn];
ll f[maxn];
int subans[maxn];
void Add(int a,int b,int c){
l[++cnt]=Edge(b,head[a],c);
head[a]=cnt;
}
int check(int u,int pos,int tot,ll x){
int res=0,l=0;
for (register int r=tot-1;r;--r){
r-=r==pos;
while (l<r&&son[u][l]+son[u][r]<x) ++l;
l+=l==pos;
if (l>=r) break;
++res;++l;
}
return res;
}
void Dfs(int u,int fa,ll x){
f[u]=subans[u]=0;
son[u].clear();
for (register int i=head[u];i;i=l[i].next){
int v=l[i].to;
if (v==fa) continue;
Dfs(v,u,x);
f[v]+=l[i].val;
if (f[v]>=x) subans[u]++;
// 如果已经大于 x 则直接计入答案
else son[u].push_back(f[v]);
}
int tot=son[u].size();
sort(son[u].begin(),son[u].end());
int l=0,r=tot,sub=0,res;
for (register int r=tot-1;r;--r){
while (l<r&&son[u][l]+son[u][r]<x) ++l;
if (l>=r) break;
++sub;++l;
}
subans[u]+=sub;
// 贪心找出最大的匹配数(两个半链合成一条赛道)
if (sub*2==tot) return;
l=0,r=tot-1;
while(l<=r){
int tem=check(u,mid,tot,x);
if (tem==sub) res=mid,l=mid+1;
else r=mid-1;
}
// 二分找到满足最大匹配的,可以转移给 f_u 的最大值
f[u]=son[u][res];
}
bool check(ll x){
int tem=0;
Dfs(1,0,x);
for (register int i=1;i<=n;++i)
tem+=subans[i];
return tem>=m;
}
int main(){
ll l=0,r=0,ans,c;
scanf("%d%d",&n,&m);
for (register int i=1,a,b;i<n;++i)
scanf("%d%d%lld",&a,&b,&c),
Add(a,b,c),Add(b,a,c),r+=c;
r/=(ll)m;
// 在这里二分答案
while(l<=r){
if (check(mid)) ans=mid,l=mid+1;
else r=mid-1;
}
printf("%lld\n",ans);
return 0;
}
```