[bzoj]1999: [Noip2007]Core树网的核

原题链接:树网的核

题目中说了:树的直径不唯一,但是中点唯一。

如何证明中点唯一(虽然没有必要)

假设一条直径被一个节点分成了左和右两段,我们假设左边小于于右边。

现在要保证直径等于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);
	}
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值