树的直径 咕

今天梁老给我们上的课
好像比zgs讲的要清楚一些

树是一个没有环的图
无根树是一个没有环没有根节点的图(所以你可以随便令一个点来做它的根节点)
我想砍树向第一届C·S·P致敬

树的直径

所谓树的直径,就是树中找到两点,使他们的连线最长

搜树的直径:
找到任一点P
求得距离P最远的点Q
再求得离Q最远的点W
于是QW即为树的直径

代码实现

#include<bits/stdc++.h>
using namespace std;
#define in Read()
#define re register
const int NNN =(int)1e5+10;
int n;
int tot,first[NNN],next[NNN<<1],aim[NNN<<1];
int ans,t,d[NNN];

inline int in{
	int i=0,f=1;char ch;
	while((ch>'9'||ch<'0')&&ch!='-')ch=getchar();
	if(ch=='-')ch=getchar(),f=-1;
	while(ch>='0'&&ch<='9')i=(i<<1)+(i<<3)+ch-48,ch=getchar();
	return i*f;
}

inline void Add(int u,int v){
	tot++;
	next[tot]=first[u];
	first[u]=tot;
	aim[tot]=v;
	return ;
}

inline void Dfs(int x,int father){
	if(d[x]>ans){
		ans=d[x];
		t=x;
	}
	for(re int e=first[x];e;e=next[e]){
		if(aim[e]==father)continue;
		d[aim[e]]=d[x]+1;
		Dfs(aim[e],x);
	}
	return ;
}

int main(){
	n=in;
	for(re int i=1;i<n;i++){
		int u=in,v=in;
		Add(u,v);
		Add(v,u);
	}
	
	Dfs(1,0);
	d[t]=0;
	ans=0;
	Dfs(t,0);
	printf("%d\n",ans);
	return 0;
}

如果只求长度:
用树形DP
对于每一个根节点
都找到它第1与第2长的根节点到叶结点的距离
打个擂台就完了
更新的方法:

if(d1[e[son].to]+1>d1[x])d2[x]=d1[x],d1[x]=d1[e[son].to]+1;
else d2[x]=max(d1[e[son].to]+1,d2[x]);

代码实现

#include<bits/stdc++.h>
using namespace std;
#define re register
#define in Read()
const int NNN =(int)1e5+10;
int n,tot;
struct st{
 int to,next;
}e[NNN*2];
int d1[NNN],d2[NNN],first[NNN];
int len;

void Add(int u,int v){
 tot++;
 e[tot].next=first[u];
 first[u]=tot;
 e[tot].to=v;
 return ;
}

int in{
 int i=0,f=1;char ch;
 while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
 if(ch=='-')ch=getchar(),f=-1;
 while(ch>='0'&&ch<='9')i=(i<<1)+(i<<3)+ch-48,ch=getchar();
 return i*f;
}

void Dfs(int x,int father){
 for(re int son=first[x];son;son=e[son].next){
  if(e[son].to==father)continue;
  Dfs(e[son].to,x);
  if(d1[e[son].to]+1>d1[x])d2[x]=d1[x],d1[x]=d1[e[son].to]+1;
  else d2[x]=max(d1[e[son].to]+1,d2[x]);
 }
 len=max(len,d1[x]+d2[x]);
}

int main(){
 n=in;
 for(re int i=1;i<n;i++){
  int u=in,v=in;
  Add(u,v);
  Add(v,u);
 }
 
 Dfs(1,0);
 
 printf("%d\n",len);
 return 0;
} 

思考:求树的中心
树的中心:即求树中的点对最远距离最近
很明显,中心是直径的中点

\quad

\quad

练习

luoguP4408

sol
“可以保证,任两个居住点间有且仅有一条通路” 说明街道与居住点组成树
题意要求:求 ( A B + B C ) m a x , A C > B C (AB+BC)_{max} \quad,满足AC>BC
由于A,B,C是不知道的点
那么如果不满足AC>BC,将A,B调换之后即满足
于是有 ( A B + B C ) m a x = ( A B + m i n { A C , B C } ) m a x (AB+BC)_{max}=(AB+min\{ AC,BC \})_{max}
由感性认识 (证不来) 得到:AB是直径, m i n { A C , B C } min\{ AC,BC \} 是任一点与直径端点相连的最短距离中最长的
要得到直径的端点,只能用两次DFS解决

代码实现

#include<bits/stdc++.h>
using namespace std;
#define in Read()
#define re register
#define int long long
const int NNN =(int)2e5+10;
int n,m;
int tot,first[NNN],next[NNN<<1],aim[NNN<<1],wei[NNN<<1];
int d[NNN][3],st,A,B,ans,len;//求到了端点,就把它存进A或B中 

inline int in{
	int i=0,f=1;char ch;
	while((ch>'9'||ch<'0')&&ch!='-')ch=getchar();
	if(ch=='-')ch=getchar(),f=-1;
	while(ch>='0'&&ch<='9')i=(i<<1)+(i<<3)+ch-48,ch=getchar();
	return i*f;
}

inline void Add(int u,int v,int w){
	tot++;
	next[tot]=first[u];
	first[u]=tot;
	aim[tot]=v;
	wei[tot]=w;
	return ;
}

inline void Dfs(int x,int father,int p){
	if(d[x][p]>ans){
		ans=d[x][p];
		st=x;
	}
	for(re int e=first[x];e;e=next[e]){
		if(aim[e]==father)continue;
		d[aim[e]][p]=d[x][p]+wei[e];
		Dfs(aim[e],x,p);
	}
	return ;
}

signed main(){
	n=in,m=in;
	for(re int i=1;i<=m;i++){
		int u=in,v=in,w=in;
		Add(u,v,w);
		Add(v,u,w);
	}
	
	//两次DFS求端点和直径长
	Dfs(1,0,0);
	A=st;
	d[A][1]=0;
	ans=0;
	Dfs(A,0,1);
	B=st;
	
	//刚刚DFS的时候已经求出了每个点到A的距离,只需要再求出B,然后比较就得到min{AC,BC}
	Dfs(B,0,2);
	for(re int i=1;i<=n;i++){
		int Antimony=min(d[i][1],d[i][2]);
		len=max(len,Antimony);
//		printf("%lld %lld\n",Antimony,len);
	}
//	printf("%lld %lld\n",A,B);
//	for(re int i=1;i<=n;i++)
//		printf("%lld %lld %lld\n",i,d[i][1],d[i][2]);
	printf("%lld\n",ans+len);
	return 0;
}

这题太坑了
必须开long long—— T [ 1 , 1 0 9 ] T\in [1,10^9]
稍微一不注意就炸int了

luoguP3629

“在一个地区中有 n 个村庄,编号为 1, 2, …, n. 有 n – 1 条道路连接着这些村庄,每条道路刚好连接两个村庄,从任何一个村庄,都可以通过这些道路到达其 他任一个村庄。”——无根树,随便取顶点
这道题劲道太大了,如果一来就看加几条边的话,多半做不起
那·由易至难

请看这个图(树被我砍了,而且“树”这个词在另一个人的字典里面消失了)
在这里插入图片描述
如果只加一条边
那么会形成一个环在这里插入图片描述
原来每条边要经过2遍,一共2n遍
加上环,环上的每条边都只用经过1遍,一共只需经过m遍(树上有m条边)
于是现在只需要走2n-m条边

如果再加一条边
会出现两种情况——
case1
与刚刚加成的环有公共边
在这里插入图片描述
一共形成了3个环
要走只能走一个,另外的跟不加环一样要走2遍
case2
没有形成公共边
于是又形成了一个环
又减少了m次
这种情况很简单,再找一个环出来就行了

究其因,要减少次数,就要形成没有公共边(可能有公共点)的环
tarjan派上用场(然而某巨巨又在给本蒟蒻讲基环树-听不懂听不懂听不懂我能想到tarjan我已经尽力了!!!)
好吧实际上不需要tarjan那么复杂(而且tarjan还是错的,因为有重边的情况可能是可以优化的)

稍微数学推导——
设树共有n个点,直径上长为d
不加边:2n-2
加一边: 2 n 2 d + 1 2n-2-d+1 其中d-1为直径上少走的边数,+1是经过加的一条边
\qquad\quad 所以只需要减去直径长+1就可以了
加两边:画图可知,连了边之后公共边的部分跟不连边要花费一样的时间
\quad\qquad 为什么呢?
2 n 2 d x = 2 n 2 x = d \quad\qquad 2n-2-d-x=2n-2 \Rightarrow x=-d
\quad\qquad 完美
\quad\qquad 直径上的边权全部改为负的
\quad\qquad 一直这样更新ans
\quad\qquad 直到ans’(更新之后的ans)>ans(原ans)(如果可以加无穷多条边)

代码实现(必须用DFS,要求直径上每个点)(A70,A码咕掉了)

#include<bits/stdc++.h>
using namespace std;
#define in Read()
#define re register
const int NNN =(int)1e5+10;
int n,k;
int tot,first[NNN],next[NNN<<1],wei[NNN<<1],aim[NNN<<1],near[NNN<<1];
int st,d[NNN],ans,Antimony,tmp;
bool I_have_found_D;
int fr[NNN];//记录直径上的点 

inline int in{
	int i=0,f=1;char ch;
	while((ch>'9'||ch<'0')&&ch!='-')ch=getchar();
	if(ch=='-')ch=getchar(),f=-1;
	while(ch>='0'&&ch<='9')i=(i<<1)+(i<<3)+ch-48,ch=getchar();
	return i*f;
}

inline void Add(int u,int v){
	tot++;
	next[tot]=first[u];
	first[u]=tot;
	aim[tot]=v;
	wei[tot]=1;
	return ;
}

inline void Dfs(int x,int father){
	if(d[x]>=ans){
		ans=d[x];
		st=x;
	}
	for(re int e=first[x];e;e=next[e]){
		if(aim[e]==father)continue;
		d[aim[e]]=d[x]+wei[e];
		Dfs(aim[e],x);
	}
	return ;
}

inline void changewei(int now,int father,int target){
	if(I_have_found_D)return ;
	for(re int e=first[now];e;e=next[e]){
		if(aim[e]==father)continue;
		if(I_have_found_D)return ;
		if(aim[e]==target){
			I_have_found_D=true;
			fr[now]=aim[e];
			return ;
		}
		fr[now]=aim[e];
		changewei(aim[e],now,target);
		if(I_have_found_D==true)return ;
	}
	return ;
}

int main(){
	n=in,k=in;
	for(re int i=1;i<n;i++){
		int u=in,v=in;
		Add(u,v);
		Add(v,u);
		near[tot]=tot-1;
		near[tot-1]=tot;
	}
	
	//k==1
	st=1;
	d[1]=0;
	ans=0;
	Dfs(1,0);
	tmp=st;
	d[st]=0;
	ans=0;
	Dfs(st,0);
	Antimony=ans;
	if(k==1){
		printf("%d\n",((n-1)<<1)-Antimony+1);
		return 0;
	}
	
	//k==2
//	printf("%d %d\n",st,tmp);
//	printf("\n");
	changewei(st,0,tmp);
//	for(re int i=st;i!=tmp;i=fr[i])
//		printf("%d %d\n",i,fr[i]);
//	
//	for(re int i=1;i<=n;i++){
//		printf("%d ",i);
//		for(re int e=first[i];e;e=next[e]){
//			if(wei[e]<0)printf("-");
//			printf("%d ",aim[e]);
//		}
//		printf("\n");
//	}
//	
	for(re int i=st;i!=tmp;i=fr[i])
		for(re int e=first[i];e;e=next[e])
			if(aim[e]==fr[i])
				wei[e]=-1,wei[near[e]]=-1;
//	
//	for(re int i=1;i<=n;i++){
//		printf("%d ",i);
//		for(re int e=first[i];e;e=next[e]){
//			if(wei[e]<0)printf("-");
//			printf("%d ",aim[e]);
//		}
//		printf("\n");
//	}
	
	st=1;
	d[st]=0;
	ans=0;
	Dfs(1,0);
	tmp=st;
	d[st]=0;
	ans=0;
	Dfs(st,0);
	
//	printf("%d\n",ans);
	printf("%d\n",(n<<1)-Antimony-ans);
	return 0;
}

第二天早上才发现 k [ 1 , 2 ] k\in [1,2] k Z k\in \mathcal{Z}
关键在于处理改边权(无向图)
两种方法:
1 利用数学知识,发现建邻接表的时候两条反向的边是相邻的
2 开数组再存一个元素

经验:用邻接表的思想求直径上的点(推广到任意两点之间)

消防

不管什么我只知道这条道路可能不在树的直径上面

发布了26 篇原创文章 · 获赞 3 · 访问量 905

猜你喜欢

转载自blog.csdn.net/Antimonysbguy/article/details/103314623