树的直径总结(两种方法)点对最大值/树上子链/Two/Cow Marathon

方法一:bfs/dfs(适用于边权为正数的树)
1.任取一点作为起点,找到距离最远的一个点u。
2.在找到距离u最远的一个点v。
u,v之间的路径就是一条直径
(建议使用bfs,dfs存在爆栈的可能)

模板题:https://vjudge.net/contest/378522#problem/H
一定要记录一下这道题。
刚开始用的long long一直runtime errer
改成int就好了 结果是因为 ans没有初始化为x
至今没搞懂原因
代码:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<queue>
#define ll long long
using namespace std;
const int N=1e6+10;
int h[N],ver[N*2],ne[N*2],tot;ll edge[N*2];
int st[N];
void add(int a,int b,ll c)
{
	ver[tot]=b;
	edge[tot]=c;
	ne[tot]=h[a];
	h[a]=tot++;
}
int bfs(int x,ll &d)
{
	memset(st,0,sizeof st);
	queue< pair<int,ll> > q;
	q.push(make_pair(x,0ll));
	st[x]=1;
	int ans=x;//一定要初始化
	ll maxx=0;
	while(q.size())
	{
		pair<int,ll> t=q.front();q.pop();
		if(maxx<t.second)
		{
			maxx=t.second;
			ans=t.first;
		}
		int x=t.first;
		for(int i=h[x];i!=-1;i=ne[i])
		{
			int y=ver[i];
			if(st[y]==0)
			{
				q.push(make_pair(y,(t.second+edge[i])));
				st[y]=1;
			}
		}
	}
	d=maxx;
	return ans;	
}
int main()
{
	int a,b;ll c;
	tot=0;
	memset(h,-1,sizeof h);
	while(scanf("%d%d%lld",&a,&b,&c)!=EOF)
	{
		add(a,b,c);
		add(b,a,c);
	}
	ll d;	
	int u=bfs(1,d);
	bfs(u,d);
	cout<<d<<endl;
	return 0;
}

应用:https://vjudge.net/contest/378522#problem/J
题意: 有一颗n个结点的带权的无向树, 在s结点放两个机器人, 这两个机器人会把树的每条边都走一遍, 可是最后机器人不要求回到出发点. 问你两个机器人走的路总长之和的最小值是多少?

分析:
首先本题仅仅要求出树的直径, 然后用树的总长sum2-树的直径就是所求结果. 以下一步步来说明为什么是这种.
1.如果仅仅有1个机器人遍历树,且要求回到原点, 它最少须要走多少路?
答: 它须要走树总长sum的两倍, 即每条树边它都要走两次才行. 这个结论画个图就明确了, 对于每条边, 机器人要走过该边, 之后还要从该边回去(不回来就不能回到出发点了). 所以自然是sum
2.
2.如果1问中的机器人遍历树,可是不要求它回到原点, 那么它最少须要走多少路?
答: 最少须要走sum-[从出发点能走到最远的点的距离]. 在行走的过程中每一个分叉, 它走过去,又走回来就可以. 能够反证得出.
3.如果有两个机器人从s出发,遍历整个树且终于回到出发点. 它们行走的最短距离是?
答: 树总长的两倍. 每一个机器人都必须回到原点, 那么必定每条边至少要被走两次.
4.如果有两个机器人从s出发,遍历整个树且它们不须要回到出发点. 它们行走的最短距离是?
答: 树总长的两倍-树的直径. 机器人出去不回来,则所走路径中有一条简单路径是能够仅仅走一遍的,派出了两个点去遍历,也就是说有两条简单路径是能够只走一遍的,我们要使这两条简单路径的总和尽可能的长,就转换为了树的最长路径问题了.两个机器人从哪点出发都是没有不论什么差别的. 由于假设它们出发点不在树的直径上, 那么它们一定能够一起移动到树直径上的某个点上,然后分别朝树直径的两个方向走.

代码:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<queue>
#define ll long long
using namespace std;
const int N=1e6+10;
int h[N],ver[N*2],ne[N*2],tot;ll edge[N*2];
int st[N];
void add(int a,int b,ll c)
{
	ver[tot]=b;
	edge[tot]=c;
	ne[tot]=h[a];
	h[a]=tot++;
}
int bfs(int x,ll &d)
{
	memset(st,0,sizeof st);
	queue< pair<int,ll> > q;
	q.push(make_pair(x,0ll));
	st[x]=1;
	int ans=x;
	ll maxx=0;
	while(q.size())
	{
		pair<int,ll> t=q.front();q.pop();
		if(maxx<t.second)
		{
			maxx=t.second;
			ans=t.first;
		}
		int x=t.first;
		for(int i=h[x];i!=-1;i=ne[i])
		{
			int y=ver[i];
			if(st[y]==0)
			{
				q.push(make_pair(y,(t.second+edge[i])));
				st[y]=1;
			}
		}
	}
	d=maxx;
	return ans;	
}
int main()
{
	int a,b;ll c;
	int n,s;
	while(scanf("%d%d",&n,&s)!=EOF)
	{
		tot=0;
		ll ans=0;
		memset(h,-1,sizeof h);
		for(int i=1;i<n;i++)
		{
		
			scanf("%d%d%lld",&a,&b,&c);
			add(a,b,c);
			add(b,a,c);
			ans+=(c*2);
		}
		ll d;	
		int u=bfs(1,d);
		bfs(u,d);
		cout<<(ans-d)<<endl;
	}
	return 0;
}

方法二:树形dp(对边权没有要求,且dp更易拓展)
1.dp[i]表示以i点为最高点的链的长度(最高点这种状态表示的方法应该可以解决很多树上的链问题)。
有两种情况:
a.该点是链的一端(选择以该点子节点为端点的最长链 )
b.该点是链的中间点(选择以该点子节点为端点的最长链和次长链 )
2.d1[i]:以该点为端点的最长链 d2[i]:以该点为端点的次长链

例一https://ac.nowcoder.com/acm/contest/4462/B
这道题应该是一般树的直径稍微拓展了一点点,把边权变成了点权。
由于方法一本身就需要证明,所以扩展起来正确性无法保障
方法二dp更加容易拓展,正确性也更有保证(如果dp学得不错的话)
代码:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
#define ll long long
const int N=2e5+10;
int h[N],ver[N*2],ne[N*2],tot;
ll a[N],dp[N],d1[N],d2[N];
ll ans=-1e18;
void add(int a,int b)
{
	ver[tot]=b;
	ne[tot]=h[a];
	h[a]=tot++;
}
void dfs(int x,int fa)
{
	d1[x]=0;d2[x]=0;//当以改点的子节点为最高点的最大链为负数时,可以不要链(这是初始化为0的原因)。 
	for(int i=h[x];i!=-1;i=ne[i])
	{
		int y=ver[i];
		if(y==fa) continue;
		dfs(y,x);
		if(d1[y]>d1[x])//求最大值的次大值的常用方法
		{
			d2[x]=d1[x];
			d1[x]=d1[y];
		}
		else if(d1[y]>d2[x])
		{
			d2[x]=d1[y];
		}
		 
	}
	
	dp[x]=max(d1[x],d1[x]+d2[x])+a[x];//如果初始化时就加上a[x],d1[x]+d2[x]会加上两个a[x]
	d1[x]+=a[x];d2[x]+=a[x];
	ans=max(dp[x],ans);
}
int main()
{
	int n;
	memset(h,-1,sizeof h);
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%lld",&a[i]); 
	for(int i=1;i<n;i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		add(x,y);
		add(y,x);
	}
	dfs(1,-1);
	printf("%lld\n",ans);
}

例二:https://ac.nowcoder.com/acm/contest/5758/A
只要稍微改变一个数组所代表的含义就可以
思路(画个图会好理解很多):
dp[i]:以i为最高点的最大点对价值(最少包含两个点)
d1[i]:以i为最高点的链(包含且只包含较低的那个端点的权值)的最大值(至少一个点)。
d2[i]:以i为最高点的链(包含且只包含较低的那个端点的权值)的次大值(至少一个点)。
d1[i]和d2[i]含义的确定是为了更方便求dp[i]

代码:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
#define ll long long
const int N=2e6+10;
int h[N],ver[N*2],ne[N*2],tot;ll edge[N*2];
ll a[N],dp[N],d1[N],d2[N];
ll ans=-1e18;
void add(int a,int b,ll c)
{
	ver[tot]=b;
	edge[tot]=c;
	ne[tot]=h[a];
	h[a]=tot++;
}
void dfs(int x,int fa)
{
	dp[x]=-1e18,d1[x]=-1e18;d2[x]=-1e18;//因为最少要有两个点,所以要这样初始化(这样也可以保证在处理叶节点是dp[x]是负无穷,不用特殊处理) 
	for(int i=h[x];i!=-1;i=ne[i])
	{
		int y=ver[i];
		if(y==fa) continue;
		dfs(y,x);
		if(d1[y]+edge[i]>d1[x])
		{
			d2[x]=d1[x];
			d1[x]=d1[y]+edge[i];
		}
		else if(d1[y]+edge[i]>d2[x])
		{
			d2[x]=d1[y]+edge[i];
		}
		
	    
	}
	dp[x]=max(d1[x]+a[x],d1[x]+d2[x]);
    d1[x]=max(a[x],d1[x]);d2[x]=max(a[x],d2[x]);//可以只包含x点的权值
    if(d2[x]>d1[x]) swap(d1[x],d2[x]);//改变d1[x],d2[x]后相对大小可能改变
	ans=max(dp[x],ans);
}
int main()
{
	int n;
	int t;
	scanf("%d",&t);
	while(t--)
	{
		tot=0;
		ans=-1e18;
		memset(h,-1,sizeof h);
		scanf("%d",&n);	
		for(int i=2;i<=n;i++)
		{
			int x;ll y;
			scanf("%d%lld",&x,&y);
			add(x,i,y);
			add(i,x,y);
		}
		for(int i=1;i<=n;i++) scanf("%lld",&a[i]); 
		dfs(1,-1);
		printf("%lld\n",ans);
	}

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值