树形dp(dfs求法详解以及证明)(例题:共同抗疫)

18 篇文章 0 订阅

 


更好的观看体验:http://www.yyycode.cn/index.php/2020/04/07/b-%e9%bd%90%e5%bf%83%e6%8a%97%e7%96%ab/


因为做之前不知道树形dp,所以这道题当时自己的想法只有floyd,骗了48分。这里附上floyd的超时代码

说下自己当时写了这代码但是debug好久,原因时这是个无向图,需要两边建边,只有一边肯定是出问题的

#include <iostream>
#include<algorithm>
#include<cstring>
using namespace std;

const int N = 2010, INF =0x3f3f3f ;

int n, m, k, x, y;
int d[N][N];
int a[N];
void floyd() {
    for(int k = 1; k <= n; k++)
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= n; j++)
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

int main() 
{
	cin.tie(0);std::ios::sync_with_stdio(false);
    cin>>n;
    for(int i=1;i<=n;i++)
    {
    	cin>>a[i];
	}
    m=n-1;
    //初始化 
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
            if(i == j) d[i][j] = 0;
            else d[i][j] = INF;
    while(m--) {
        cin >> x >> y ;
        d[x][y] = min(d[x][y],1);
        d[y][x]=min(d[y][x],1);
        //注意保存最小的边
    }
    
    floyd();
    
	int res=-1;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			if(i!=j&&a[i]>a[j]&&d[i][j]<INF/2)
			res=max(res,a[i]*d[i][j]);	
		}	
	}
	cout<<res<<endl;
    return 0;
}

那正确的思路是什么呢?

我们要在{ax,ay}*dis[x,y]的所有情况中找一个最大的

等价求MAX{(ax,ay)*dis[x,y]} 由于ax,ay不管哪个大哪个小支付的总是max(ax,ay)*dis[x,y],他们的最短路是公共的一样长的。所以我们不妨以ax为研究对象,找max{ax*dis[x,y]}.

ax是我们输入的每个村庄的疫情情况,所有我们找max{dis[x,y]}。而如果我们了解树的直径就知道在树上离x,最远的点只会是树直径的端点中的一个.


我们先对最远的点只会是树的直径的端点中的一个进行证明。

反证法:假设cd是树的直径,假如1>=2,那么1+3>=2+3,而树的直径的定义是树上两点距离最长的点,不会存在有1+3这样严格>2+3(直径)的新的直径,和原命题矛盾。另一种1和3的情况同理。综上, 在树上离x,最远的点只会是树直径的端点中的一个


讲到了这,那么树的直径怎么求呢?本题是树形dp的一个经典问题,即每条边的权重都是1,有一种解法是dfs解法,适用于边权为正的情况

思路:先用dfs求离随意一个点的最远点(c),然后再用最远点(c)求另一个离c最远的最远点(d),cd就是树的直径,cd分别是两端点


我们来对这个求直径的方法进行证明:我们还是用反证法

任取一点a,离a最远点为u,证u一定是某一条直径的一个端点,那么从u出发最远的点连起来就是直径

我们先证明两条路径没有交点的情况(另一个交点为y,1是ux)

由于连通,那么au可以从x走出通过y然后到b或者c;

因为u是到a最远的一个点,所以从a点到c的距离和a到u的距离减去公共部分可以得到: 1>=2+4,则可以推出1+2>=4.

那么从b出发,沿着1-2走的距离>=4,即3+1+2>=3+4,又因为3+4是树的直径,所以我们找到了和bc起码一样长的路径,从而u是直径的一个端点.那么显然离u最远的一个点就是直径的另一个端点


case2:有交点的情况

因为u是距离a最远的一个点,所以au>=ac,从而1>=2,所以b->u>=b->c,又因为bc是直径,从而u是某一条直径的端点。 那么显然离u最远的一个点就是直径的另一个端点.


那么理解了之后代码板子怎么写呢

这是标程

#include <bits/stdc++.h>
#define clr(x) memset(x,0,sizeof (x))
#define For(i,a,b) for (int i=(a);i<=(b);i++)
#define Fod(i,b,a) for (int i=(b);i>=(a);i--)
#define fi first
#define se second
#define pb(x) push_back(x)
#define mp(x,y) make_pair(x,y)
#define outval(x) cerr<<#x" = "<<x<<endl
#define outv(x) cerr<<#x" = "<<x<<"  "
#define outtag(x) cerr<<"--------------"#x"---------------"<<endl
#define outarr(a,L,R) cerr<<#a"["<<L<<".."<<R<<"] = ";\
	For(_x,L,R) cerr<<a[_x]<<" ";cerr<<endl;
#define outtime() cerr<<"User Time = "<<(double)clock()/CLOCKS_PER_SEC<<endl
using namespace std;
typedef long long LL;
typedef unsigned long long ULL;
typedef unsigned uint;
typedef long double LD;
typedef vector <int> vi;
typedef pair <int,int> pii;
LL read(){
	LL x=0,f=0;
	char ch=getchar();
	while (!isdigit(ch))
		f=ch=='-',ch=getchar();
	while (isdigit(ch))
		x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
	return f?-x:x;
}
const int N=50005;
int n;
int a[N],dx[N],dy[N];
vector <int> e[N];
int dfs(int x,int pre,int *d){
	int res=x;
	for (auto y : e[x])
		if (y!=pre){
			d[y]=d[x]+1;
			int now=dfs(y,x,d);
			if (d[now]>d[res])
				res=now;
		}
	return res;
}
int main(){
	n=read();
	For(i,1,n)
		a[i]=read();
	For(i,1,n-1){
		int x=read(),y=read();
		e[x].pb(y),e[y].pb(x);
	}
	int x=dfs(1,0,dy);//x为直径的第一个端点,路径存在dy里 
	
	memset(dx,0,sizeof dx); 
	int y=dfs(x,0,dx);///y为直径的第二个端点 ,路径存在dx里 
	memset(dy,0,sizeof dy); 
	dfs(y,0,dy);//求出每个点到直径两端点的距离值 

	LL ans=0;
	For(i,1,n)
		ans=max(ans,(LL)a[i]*max(dx[i],dy[i]));
	cout<<ans<<endl;
	return 0;
}

这是最后我理解了写出的代码,虽然不那么简洁,但是我觉得方便理解

  • 如果程序找找自己的vis两次是否清空
  • 关于dfs有个地方要说:因为是树的结构所以每一个搜到的节点都是终点,无需回溯
#include<iostream>
#include<vector>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long LL;
int const maxn=50010;
int n;
LL a[maxn];
LL dx[maxn],dy[maxn];
LL vis[maxn];
dx[] 存ai到直径第二个端点的距离
//dy[] 存ai到直径第一个端点的距离
vector<int>e[maxn];
//指针指向数组的第一个元素地址,这么写方便调用函数,不用再主函数再写一个for 
void dfs(int x,LL *d)
{
	vis[x]=1;///dfs容易忘的标记 
	for(int i=0;i<e[x].size();i++)
	{
		int to=e[x][i];
		if(!vis[to])
		{
			vis[to]=1;	
			d[to]=d[x]+1;//注意,本代码计算的是无权树的直径,所以边权为1
            //如果是有权树,则这里的1要改为边权
			dfs(to,d);
		
		}	
	}
//	vis[x]=0;///不用回溯,树 
}

int main(void)
{
	cin.tie(0);std::ios::sync_with_stdio(false); 
	int n;cin>>n;
	for(int i=1;i<=n;i++)
		{
			cin>>a[i];
		}
	for(int i=1;i<=n-1;i++)
		{
			int x;int y;
			cin>>x>>y;
			e[x].push_back(y);//无向图存储,若是有权树还要用结构体
			e[y].push_back(x);					
		}
		//先找到直径的一个端点 
	dfs(1,dx);
	int res=0;int q=0;//q表示直径的第一个端点 
	for(int i=1;i<=n;i++)
	{
		if(res<dx[i]) res=dx[i],q=i;
		dx[i]=vis[i]=0; //还原 
	}
	dfs(q,dx);//通过q找直径的另外一个端点
	int ans=0;int p=0;
	for(int i=1;i<=n;i++)
	{
		if(ans<dx[i]) ans=dx[i],p=i;
		vis[i]=0;///要清空 
	} 
	dfs(p,dy);//dy存的是所有节点到p点的距离 
	LL weight=0;
	for(int i=1;i<=n;i++)
	{
		weight=max(weight,(LL)a[i]*max(dx[i],dy[i])) ;
	}
	cout<<weight<<endl;
return 0;
} 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值