题意:
\(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;