原题链接:树网的核
题目中说了:树的直径不唯一,但是中点唯一。
如何证明中点唯一(虽然没有必要)
假设一条直径被一个节点分成了左和右两段,我们假设左边小于于右边。
现在要保证直径等于len[left]+len[right]。
①从节点连出长度等于左段的子树,明显,中点还是在右端
②从节点连出长度等于右段的子树,中点位置改变。但是len[left]+len[right]<2*len[right]。直径改变。
不严谨的证明(逃
引理:任何直径上求出的最小偏心距都相等
证明:先确定直径,也是分成左右两段。
①在分割点上添加子树:无法超过左段长度,无论怎么添加,分割点到右段的距离必定小于(子树&&左段)到分割点的距离
②在左右两段添加子树:明显子树长度不能大于子 树的根节点到左/右的长度。所以子树到核的距离必定小于左右端点到核的距离
由于多条直径相当于交换子树和直径(子树->直径,直径->子树)所以可以证明引理。
然后题目就成了计算任意一条直径上的最大偏心距大最小值。
算法一:暴力O(n^3)
计算出一条直径,在直径上枚举p,q,满足dis[p][q]<=s,然后暴力枚举核上的点O(n^2)到其他点的最大值O(n)即可。答案为最大值中的最小值。
算法二:贪心O(n^2)
容易发现,核越长,偏心距的最大值越小,下面给出证明
当前核的长度为k,点u到核的距离为min{d(u,i)}1<=i<=k
若核的长度加1,那么u到核的距离为min{d(u,i)}1<=i<=k+1,明显包含长度为k的情况,而且又可以变得更小。
得证。
那么我们只需要枚举p的位置,然后让q尽可能长(靠近s),然后枚举到其他店的最大值即可,由于省略了枚举q的操作,时间复杂度为O(n^2)。可以通过noip的数据了。
下面是O(n^2)代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=500018;
int n,s,head[N],ver[2*N],edge[2*N],next[2*N],tot=1,p=1,len=0,q=1,fa[N],dis[N];
bool vis[N],f=0;
void add(int x,int y,int val);
void dfs(int x,int k);
int main()
{
int x,y,z;
scanf("%d%d",&n,&s);
for(int i=2;i<=n;i++){
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
}
//第一次dfs,求出到1最远的点为p
memset(dis,0,sizeof(dis));
dfs(1,0);
memset(fa,0,sizeof(fa));
for(int i=1;i<=n;i++)
if(dis[i]>dis[p])p=i;
//第二次dfs,求出到p最远的点为q,p->q为直径。
q=p;
dis[p]=0;
dfs(p,0);
for(int i=1;i<=n;i++)
if(dis[i]>dis[q])q=i;
int ans=(1<<30)-1,j=q;
//ans的下界为直径上中间点到直径两端的距离
for(int i=q;i;i=fa[i]){
//标记直径上的点
vis[i]=1;
while(fa[j]&&dis[i]-dis[fa[j]]<=s)j=fa[j];
ans=min(ans,max(dis[j],dis[q]-dis[i]));
}
for(int i=q;i;i=fa[i])dis[i]=0,dfs(i,0);
for(int i=1;i<=n;i++)ans=max(ans,dis[i]);
printf("%d",ans);
return 0;
}
void add(int x,int y,int val){
ver[++tot]=y;edge[tot]=val;next[tot]=head[x];head[x]=tot;
ver[++tot]=x;edge[tot]=val;next[tot]=head[y];head[y]=tot;
}
void dfs(int x,int k){
for(int i=head[x];i;i=next[i]){
int y=ver[i];
if(vis[y]||y==k)continue;
fa[y]=x;
dis[y]=dis[x]+edge[i];
// printf("1");
dfs(y,x);
}
}
算法三:二分,O(nlogsum),sum为所有边的长度和
枚举最终答案mid,问题变成了是否存在核使最大偏心距不超过mid。
可以假设核的端点是p,q,直径的端点是u,v,考虑两种偏心距。
①分叉点在u,p或者q,v之间,根据直径是最长的路,p到u的距离必定大于p+子树的长度。只要保证u到p,q到v的距离小于mid即可。
②分叉点在p,q之间,dfs判断小于mid即可。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=500018;
int n,s,head[N],ver[2*N],edge[2*N],next[2*N],tot=1,p=1,len=0,q=1,fa[N],dis[N],son[N],dik[N];
bool vis[N],f=0;
void add(int x,int y,int val);
void dfs(int x,int k);
bool check(int mid);
int main()
{
int x,y,z,l=0,r=0,mid;
scanf("%d%d",&n,&s);
for(int i=2;i<=n;i++){
scanf("%d%d%d",&x,&y,&z);
r+=z;
add(x,y,z);
}
//第一次dfs,求出到1最远的点为p
memset(dis,0,sizeof(dis));
dfs(1,0);
memset(fa,0,sizeof(fa));
memset(son,0,sizeof(son));
for(int i=1;i<=n;i++)
if(dis[i]>dis[p])p=i;
//第二次dfs,求出到p最远的点为q,p->q为直径。
q=p;
dis[p]=0;
dfs(p,0);
for(int i=1;i<=n;i++)
if(dis[i]>dis[q])q=i;
for(int i=q;fa[i];i=fa[i]){
son[fa[i]]=i;
}
//标记直径上的点
for(int i=q;i;i=fa[i])vis[i]=1;
f=1;
while(l<=r){
mid=(l+r)/2;
if(check(mid))r=mid-1;
else l=mid+1;
}
printf("%d\n",l);
return 0;
}
void add(int x,int y,int val){
ver[++tot]=y;edge[tot]=val;next[tot]=head[x];head[x]=tot;
ver[++tot]=x;edge[tot]=val;next[tot]=head[y];head[y]=tot;
}
void dfs(int x,int k){
for(int i=head[x];i;i=next[i]){
int y=ver[i];
if(vis[y]||y==k)continue;
fa[y]=x;
if(!f)dis[y]=dis[x]+edge[i];
else dik[y]=dik[x]+edge[i];
// printf("1");
dfs(y,x);
}
}
bool check(int mid){
int u=p,v=q;
while(fa[v]&&dis[q]-dis[fa[v]]<=mid)v=fa[v];
while(son[u]&&dis[son[u]]<=mid&&u!=v)u=son[u];
if(u==v)return true;
if(dis[v]-dis[u]>s)return false;
memset(dik,0,sizeof(dik));
for(int i=v;i!=fa[u];i=fa[i]){
dfs(i,0);
}
for(int i=1;i<=n;i++)
if(dik[i]>mid)return false;
return true;
}
虽然不知道为什么没有那个O(n^2)的算法跑得快,可能那个剪枝优化比较厉害。
算法四、O(n)
观察算法三中答案值的来源可以观察到
d[i]表示点i到其他非直径点的最远距离,dis[x][y]表示x到y的距离。
p,q是核的左右端点,u,v是直径的左右端点
ans=max{d[k],dis[p][u],dis[q][v]}i<=k<=j
可以想到用单调队列优化,时间复杂度为O(n)
但实际上根本不需要单调队列优化
由于在如果1<=k<i的话,那么d[k]的值必然小于dis[p][u]同理j<k<=t也一样(t为直径长度)
那么k的取值范围可以扩充到1<=k<=t,发现max{d[k]}为定值。
接下来只需要枚举dis[p][u]和dis[q][v]即可。
最终答案就是ans中的最小值
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=500018;
int n,s,head[N],ver[2*N],edge[2*N],next[2*N],tot=1,p=1,len=0,q=1,fa[N],dis[N],son[N],dik[N];
bool vis[N],f=0;
void add(int x,int y,int val);
void dfs(int x,int k);
bool check(int mid);
int main()
{
int x,y,z,ans=0x7FFFFFFF;
scanf("%d%d",&n,&s);
for(int i=2;i<=n;i++){
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
}
//第一次dfs,求出到1最远的点为p
memset(dis,0,sizeof(dis));
dfs(1,0);
memset(fa,0,sizeof(fa));
memset(son,0,sizeof(son));
for(int i=1;i<=n;i++)
if(dis[i]>dis[p])p=i;
//第二次dfs,求出到p最远的点为q,p->q为直径。
q=p;
memset(dis,0,sizeof(dis));
dfs(p,0);
for(int i=1;i<=n;i++)
if(dis[i]>dis[q])q=i;
for(int i=q;fa[i];i=fa[i]){
son[fa[i]]=i;
}
//标记直径上的点
for(int i=q;i;i=fa[i])vis[i]=1;
//求出min(max(dis[p][u],dis[q][v]))
int j;
for(int i=q;i;i=fa[i]){
j=i;
while(fa[j]&&dis[i]-dis[fa[j]]<=s)j=fa[j];
ans=min(ans,max(dis[j],dis[q]-dis[i]));
if(!fa[j])break;
}
memset(dis,0,sizeof(dis));
for(int i=q;i;i=fa[i])dfs(i,0);
for(int i=1;i<=n;i++)ans=max(ans,dis[i]);
printf("%d\n",ans);
return 0;
}
void add(int x,int y,int val){
ver[++tot]=y;edge[tot]=val;next[tot]=head[x];head[x]=tot;
ver[++tot]=x;edge[tot]=val;next[tot]=head[y];head[y]=tot;
}
void dfs(int x,int k){
for(int i=head[x];i;i=next[i]){
int y=ver[i];
if(vis[y]||y==k)continue;
fa[y]=x;
dis[y]=dis[x]+edge[i];
dfs(y,x);
}
}