#include <stdio.h>
int main()
{
puts("转载请注明出处[vmurder]谢谢");
puts("网址:blog.csdn.net/vmurder/article/details/44418207");
}
首先比较显然会想到分数规划模型。
不太好想,先放过。
我们树分治处理经过每个点的路径。
然后对于在 [L,R] 区间内的每个长度记录最长距离。
然后每棵子树跟之前记录的数组处理一下,算出当前这棵子树中点为一端点,经过根节点(重心)的最优答案,然后再更新记录最长距离的数组。
貌似很科学,但是这个更新答案的过程,实际上是 O(n2) 的,并不能接受。
所以就有了下述神奇的处理方法:
我们进行分数规划,二分答案,这样新的边权赋值为len-ans(mid),然后check答案是否>0。。
而check的过程中对于每个当前子树中的最优深度函数值,我们只需要取之前记录的最优的一项更新就好了。我们可以把当前子树中的函数值按深度从小到大来更新答案,这个过程中使用单调队列维护一个深度上升权值(新赋值的权值)上升的序列,然后每次取队尾更新答案即可。
这个过程是线性的,而对于全部子树 i 深度最长距离的数组的更新也只是取个max,它也是线性的。
这样就可以在 O(nlog2n) 的时间内出解了。
然后有一个细节问题,就是二分写在在对每个重心处理时还是写在树分治的外面的问题。
网上很多都说写在里面快,而我的同学 PoPoQQQ大爷 实测是写在外面快。
所以当然是写在外面快!!!
(并不是因为“实测”,而是因为这是大爷说的!!! 2333)
下面给出一定的分析。
首先写在里面的话是对于每个重心更新答案之后,之后的重心在处理时下边界会增大,这样的话实际上时间受影响于每次在当前重心更新的幅度。
而写在外面的话,则是严格完整时间复杂度?并不是的。我们在某个重心进行处理时发现符合要求的答案时,可以直接跳出。
写在里面是一点一点调整答案,但是调整的幅度欠佳,因为还有一个上界未被改变(这个我不确定是否有下调上界的剪枝,没有细想,不要D我)。
而写在外面则是大步跳,每次得到mid以后期望很快得到一个重心可以求出符合答案的解,也就是很多重心不需要被处理,剪枝的幅度比较大。
这个代码是刚开始不知道单调队列分数规划shenmegui,然后一点点扒的别人代码,发现看不懂都是因为人家代码太渣太丑了,其实思想水得要命,而且实现上也非常简单。。
总之这个代码也是一款又渣又丑的产品,如果要扒代码请一定要跳过这个代码。
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define N 101000
#define INF 1e13
#define eps 1e-4
#define V e[i].v
using namespace std;
struct Eli
{
int v,len,next;
}e[N<<1];
int head[N],cnt;
inline void add(int u,int v,int len)
{
e[++cnt].v=v;
e[cnt].len=len;
e[cnt].next=head[u];
head[u]=cnt;
}
int can[N],size[N],deep[N],q[N];
int L,R,tl,tr;
int n,length,dep;
int root,DEP;
double a[N],b[N],sum[N];
double ans,mid;
inline void getroot(int x,int p)
{
int d=0;
size[x]=1;
for(int i=head[x];i;i=e[i].next)
if(can[V]&&V!=p)
{
getroot(V,x);
size[x]+=size[V];
d=max(d,size[V]);
}
d=max(n-size[x],d);
if(d<length)length=d,root=x;
}
inline void dfs(int x,int p)
{
size[x]=1;
deep[x]=deep[p]+1;
for(int i=head[x];i;i=e[i].next){
if(can[V]&&V!=p){
dfs(V,x);
size[x]+=size[V];
}
}
if(deep[x]>DEP)DEP=deep[x];
}
inline void dfs1(int x,int p)
{
if(deep[x]>R)return; // 此深度不能经过重心
a[deep[x]]=max(a[deep[x]],sum[x]);
for(int i=head[x];i;i=e[i].next)
{
if(can[V]&&V!=p)
{
sum[V]=sum[x]+e[i].len-mid;
dfs1(V,x);
}
}
if(deep[x]>dep)dep=deep[x];
}
bool check()
{
double mx,tmp,maxx;
int pos,i,j;
b[0]=0;
for(i=1;i<=DEP;i++)b[i]=-INF;
tmp=-INF;maxx=-INF;
int maxp=0;
for(i=head[root];i;i=e[i].next)if(can[V])
{
tl=tr=0;
a[0]=0;
for(j=1;j<=DEP;j++)a[j]=-INF;
// 枚举深度 a[i]记录此子树深度为i的最大权
dep=0;
sum[V]=e[i].len-mid;
// 目前只要处理出深度i时的最长,所以mid不用*路径长度
dfs1(V,root);
// 处理a数组,同时记录当前子树中最深深度。
if(maxp)q[++tr]=maxp;
for(j=1;j<=dep;j++)
{
if(j>R)break;
if(tl<tr&&q[tl+1]>R-j)tl++;
if(L>=j&&L-j<=DEP)
{
while(tl<tr&&b[q[tr]]<b[L-j])tr--;
q[++tr]=L-j;
tmp=max(tmp,b[q[tl+1]]+a[j]);
}
else if(j>L)tmp=max(tmp,b[q[tl+1]]+a[j]);
}
for(j=1;j<=dep;j++) // b_i是当前整棵树的深度i最大权
{
b[j]=max(b[j],a[j]);
if(j>=L&&j<=R&&maxx<b[j])
maxx=b[j],maxp=j;
}
}
return tmp>0;
}
void work(int x)
{
length=n,getroot(x,0); // 找树的重心
if(n<=L)return;
DEP=0;
dfs(root,0); // 处理每个点以重心为根的deep和深度,同时记录最大深度
double l=ans,r=1e10;
while(l<r-eps) // 分数规划
{
mid=(l+r)/2;
if(check())l=mid;
else r=mid;
}
ans=l;
can[root]=0; // 递归分治
for(int i=head[root];i;i=e[i].next)
if(can[V])n=size[V],work(V);
}
int main()
{
scanf("%d%d%d",&n,&L,&R);
int x,y,z;
for(int i=1;i<n;i++)
{
scanf("%d%d%d",&x,&y,&z);
add(x,y,z),add(y,x,z);
}
memset(can,-1,sizeof can);
deep[0]=-1;
work(1);
printf("%.3lf\n",ans);
return 0;
}