树网的核(NOIP2007提高组)

传送门

题意

这道题的题意首先要读懂。
简单说,题目是想要我们选取一个树核,使得树核外的其他点到树核的距离的最大值最小。

准备

我们存图采用邻接矩阵和链表兼用法,邻接矩阵用于跑Floyd,链表用于遍历与某个点直接相连的点。

Floyd

我们首先用floyd处理出各个点之间的距离,备用。

找直径长度

接下来我们先找出直径的长度,这个很好解决,直接枚举每两个点之间的距离,最大的一个就是。

找出直径路径

题目说我们的树核一定是在直径上,所以我们应该找出直径再进行下一步。
这个应该比较容易,我们先枚举找出直径的端点,然后dfs一下即可。
但是请注意,我们只需要一个直径即可,不需要每个直径都进行一边找树核
这个我单独放在后面证明。
所以我们找到一个直径后就可以break掉了。

枚举树核

那么当我们把直径存在数组里以后,接下来就是找树核了。
怎么办?枚举好了。
那么应该按什么顺序去枚举呢?
因为我们的直径一定是一条链(这个应该不要证明),而树核一定是连续的,所以我们可以用直径数组的下表来表示,即l和r。
我们此处还要用到一个结论:
一个树核一定不会比它的子树核更差,即一个树核的偏心距一定小于等于其子树核的偏心距
这个我们也放到后面证明。
所以这提示我们,我们枚举树核时,计算过一个树核后一定不能计算它的子树核(即被其包含的树核)。所以我们的移动策略就出来了:

  1. 如果我们加入树核右边的一个新点时,树核长度小于等于s,那么r++
  2. 如果不行,那么我们就丢弃左边的点,即l++,直到可以囊括右边的新点,那么r++
  3. 如果减到只剩一个点时,仍然不能囊括新的点,说明这条边的长度大于s,那么我们就直接加入新的点,把旧点全部丢掉,即l++,r++

这样就可以了。我们可以发现按此顺序执行后r一定会增加的,也就是说变换后的树核一定会加入一个新的点,也就绝对和原来的树核不一样,而同时又保证了,直径上每个点都有机会加入树核,所以就成功地枚举完了所有的树核。

寻找偏心距

那么既然已经可以枚举树核了,接下来就是计算每一个树核的偏心距了。
题目说是树核外每一个点到树核距离的最大值,我们可以转换一下,从树核上各个点到其他点的距离的最大值。
但是这样的说法是有问题的,还有一个要求,即树核上的某个点到树核外这个点的路径不能和树核有重叠,那么怎么保证这一点呢?
这就要用到我们的链表了。
我们先枚举树核上的点,对于每个点(设为cur),我们枚举与其相连的点,然后找出非树核内部的点,设为u,然后再枚举所有的点(设为k),满足如下的式子即可说明k与cur的路径不与树核重叠:

mapp[cur][u]+mapp[u][k]==mapp[cur][k];

其中mapp为邻接矩阵。
因为这是树,所以这个式子可以说明k和cur通过u相连。
也正因为这是树,所以这也能说明路径与树核不重叠。
应该很显然,如果需要证明,我放在后面。
这样问题就迎刃而解了。

下面我们来证明一下前面说的几个结论:

只需要一个直径即可,不需要每个直径都进行一边找树核

相信大家应该注意到了题目中的一句话:

直径不一定是唯一的,但可以证明:各直径的中点(不一定恰好是某个结点,可能在某条边的内部)是唯一的,我们称该点为树网的中心。

那么我们现在分情况讨论。
首先,中心在节点。
在这里插入图片描述
如图,数字只是边的标号,不是长度。长度可以由图直接看出。而且,这里只画出了最两端的端点,也就是说我连接的线上还有其他的点未标出,如1、2、3的交点。
这里有六条直径,分别是134、135、136、45、46、56
红点是中心。
那么我们来看其他支链与直径的关系,我们可以很容易地知道,中心上的其他支链,对于每条直径都是等价的,所以不必考虑每一条直径。
再看直径上其他结点的小支链,如2
若2的长度大于1,那么直径就不再是135,违背假设,所以2的长度小于等于1.
对于与2相交的直径,如135,其上的任意一点到2的端点的距离,均小于到4或6的端点,因此并不影响偏心距(因为偏心距是最大值)。
对于与2不相交的直径,如46,那么就更显然了,其上每一点到2端点的距离均小于到1端点的距离,同样不影响。
而对于直径本身的支链,由对称性可知影响是一样的。
综上,所有的直径对于本图是等价的。

那么对于中心在边上的也是类似的证明思路,自己证明试试看吧!

一个树核一定不会比它的子树核更差,即一个树核的偏心距一定小于等于其子树核的偏心距

我们直接上图:
在这里插入图片描述
这里我们取123作为树核,那么其子树核之一为23。
对于未变动的2和3,其连接的4,5,7到其的距离均未变。
而对与1这一边,我们加上一个1后,6到树核的距离变短了。
也就是说树核长度增加后,其他点到树核的距离,要么没变,要么减小了,可见偏心距也是要么没变,要么减小。
所以一个树核一定不会比子树核差。

如果一个树核内部的一点,通过一个与之直接相连的外部点,与另一外部点相连,那么该路径与树核仅有这一个端点为交点。

挺拗口的,反正你也知道我要证明前面哪句话。
这是较为明显的。
若该路径与树核还有另一交点v,即cur和v之间有两条不同路径,一条是我们选取的路径,另一条是通过树核的路径(一定是不同的,只要注意两条路径与cur的连接点即可),那么我们就形成了一个环,然而这是一个树,违背题意。

所以,最后就形成了我们的代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int maxn=305; 
vector<int> G[maxn];//链表
int n,s;
int mapp[maxn][maxn];//邻接矩阵 
int dm[maxn];//保存直径路径 
int vis[maxn];//是否存在于当前选定的和核 
int dia;//直径长度 
int ans;//最终答案 
int num;//直径所含节点数 
int calc(int l,int r){//计算当前核的偏心距 
	int curans=0;
	for(int i=l;i<=r;i++){
		int cur=dm[i];//提出核中的点 
		for(int j=0;j<G[cur].size();j++){//枚举当前点连接的点 
			int u=G[cur][j]; //取另一个点 
			if(vis[u]){//如果是核里的点,跳过 
				continue;
			}
			for(int k=1;k<=n;k++){//寻找最大值 
				if(vis[k]==0&&((mapp[cur][u]+mapp[u][k])==mapp[cur][k])){
					curans=max(mapp[cur][k],curans);
				}
			}
		}
	}
	return curans;
}
void dfs(int cur,int tar){//找直径 
	dm[++num]=cur;
	if(cur==tar){	
		return;
	}
	for(int i=0;i<G[cur].size();i++){
		int u=G[cur][i];
		if(mapp[cur][tar]==(mapp[cur][u]+mapp[u][tar])){
			dfs(u,tar);
		}
	}
}
void work(int u,int v){
	memset(vis,0,sizeof(vis));
	num=0;
	dfs(u,v);
	int l=1,r=1;
	vis[dm[l]]=1;
	ans=min(ans,calc(l,r));
	while(r<num){//枚举核心 
		if(mapp[dm[l]][dm[r]]+mapp[dm[r]][dm[r+1]]<=s){
			r++;
			vis[dm[r]]=1;
		}else{
			while(l!=r){
				vis[dm[l]]=0;
				l++;
				if(mapp[dm[l]][dm[r]]+mapp[dm[r]][dm[r+1]]<=s){
					r++;
					vis[dm[r]]=1;
					break;
				}
			}
			if(l==r){
				vis[dm[l]]=0;
				l++;
				r++;
				vis[dm[l]]=1;
			}
		}
		ans=min(ans,calc(l,r));
	}
}
int main(){
	memset(mapp,0x3f,sizeof(mapp));
	scanf("%d%d",&n,&s);
	for(int i=0;i<n-1;i++){
		int u,v,w;
		scanf("%d%d%d",&u,&v,&w);
		mapp[u][v]=mapp[v][u]=w;
		G[u].push_back(v);
		G[v].push_back(u);
	}
	for(int k=1;k<=n;k++){//Floyd 
		mapp[k][k]=0;
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				if(mapp[i][k]<0x3f3f3f3f&&mapp[k][j]<0x3f3f3f3f){
					mapp[i][j]=min(mapp[i][j],mapp[i][k]+mapp[k][j]);
				}
			}
		}
	}
	ans=0x3f3f3f3f;
	dia=0;
	for(int i=1;i<=n;i++){//寻找直径的值 
		for(int j=i+1;j<=n;j++){
			if(mapp[i][j]<0x3f3f3f3f){
				dia=max(dia,mapp[i][j]);
			}
		}
	}
	for(int i=1;i<=n;i++){//找到直径端点 
		for(int j=i+1;j<=n;j++){
			if(mapp[i][j]==dia){
				work(i,j);
				break;
			}
		}
	}
	printf("%d",ans);
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值