题意
这道题的题意首先要读懂。
简单说,题目是想要我们选取一个树核,使得树核外的其他点到树核的距离的最大值最小。
准备
我们存图采用邻接矩阵和链表兼用法,邻接矩阵用于跑Floyd,链表用于遍历与某个点直接相连的点。
Floyd
我们首先用floyd处理出各个点之间的距离,备用。
找直径长度
接下来我们先找出直径的长度,这个很好解决,直接枚举每两个点之间的距离,最大的一个就是。
找出直径路径
题目说我们的树核一定是在直径上,所以我们应该找出直径再进行下一步。
这个应该比较容易,我们先枚举找出直径的端点,然后dfs一下即可。
但是请注意,我们只需要一个直径即可,不需要每个直径都进行一边找树核
这个我单独放在后面证明。
所以我们找到一个直径后就可以break掉了。
枚举树核
那么当我们把直径存在数组里以后,接下来就是找树核了。
怎么办?枚举好了。
那么应该按什么顺序去枚举呢?
因为我们的直径一定是一条链(这个应该不要证明),而树核一定是连续的,所以我们可以用直径数组的下表来表示,即l和r。
我们此处还要用到一个结论:
一个树核一定不会比它的子树核更差,即一个树核的偏心距一定小于等于其子树核的偏心距
这个我们也放到后面证明。
所以这提示我们,我们枚举树核时,计算过一个树核后一定不能计算它的子树核(即被其包含的树核)。所以我们的移动策略就出来了:
- 如果我们加入树核右边的一个新点时,树核长度小于等于s,那么r++
- 如果不行,那么我们就丢弃左边的点,即l++,直到可以囊括右边的新点,那么r++
- 如果减到只剩一个点时,仍然不能囊括新的点,说明这条边的长度大于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;
}