树形dp(例题)

树的最长路径带权值

树的直径可能时红色的边

在这里插入图片描述

从上图可以看出,每次要两个变量存放以u为根,最长路径d1,和次长路径d2,那么整个树的最长路径就有可能是d1+d2

我们每次要返回以u为根的贯穿试的最长路径,给他的父节点判断使用如下图
在这里插入图片描述

#include <bits/stdc++.h>
#define ll long long
#define INF 0x7f7f7f7f
using namespace std;

const int N=1e4+10,M=5;

int n,m,ecnt,ans;

int head[N];

struct edge{
	int u,v,w,nxet; 
}E[N<<1];

void add(int u,int v,int w){
	 E[++ecnt].u=u;
	 E[ecnt].v=v;
	 E[ecnt].w=w;
	 E[ecnt].nxet=head[u];
	 head[u]=ecnt; 
} 

int dfs(int u,int fa){
	//dist记录以u为根贯穿试最长路径
	int dist=0,d1=0,d2=0;//d1和d2记录以u为根路径最长和次长值 
	//由于路径可以只包含一个点所以一定不为负数
	for(int i=head[u];i;i=E[i].nxet){
		int v=E[i].v;
		if(v==fa)continue;//不能回头
		int d=dfs(v,u)+E[i].w;//子树v到u的权值 
		dist=max(dist,d);//以u为根的树的最长路径 
		if(d>=d1)d2=d1,d1=d;
		else if(d>d2)d2=d; 
	}
	ans=max(ans,d1+d2);//d1+d2才是树的直径
	return dist;//将以u为根向下纵向穿的最大路线返回 
}


int main() {
   
   cin>>n;
   
   for(int i=1;i<=n-1;i++){
   	 int a,b,c;
   	 cin>>a>>b>>c;
   	 add(a,b,c);
   	 add(b,a,c);
   }
   dfs(1,-1);
   cout<<ans;







	return 0;

}

树的中心

我们要找出一个点到各个点中的最长距离,然后求出整个树所有点中最长距离最短,我们把重心放在最长距离上,树上一个点最长距离无非是向上最长或向下最长距离

向下最长,我们上一道题求树的直径中求出了u点的最长路径,和次长路径那么向下最长一定是u点向下dfs得出的最长路径所以求法和上一道题目相同。

向上最长,这个要分析当前节点紧挨着的父节点,无非两种情况,第一种是其父节点向上最长,第二种是其父节点向下最长那么第二种情况需要讨论,如果父亲节点最长距离d1、次长距离d2,其中d1如果不经过儿子节点,那么儿子节点向上最长一定是max(up[父亲],d1)+w*(父亲儿子之间权值)*,如果d1经过儿子节点,那么d1无法使用,只能使用d2那么儿子向上最长就是max(up[父亲],d2)+w。

在这里插入图片描述
在这里插入图片描述
(别忘记加上儿子和父亲间的权值)

#include <bits/stdc++.h>
#define ll long long
#define INF 0x7f7f7f7f
using namespace std;

const int N=1e5+10,M=5;

int n,m,ecnt,ans;

int head[N];
int d1[N],d2[N],up[N];//记录向下最长直径、次长直径、向上最长直径
int p1[N],p2[N];//记录u最长直径经过的第一个节点,和次长直径经过的第一个节点

struct edge {
	int u,v,w,next;
} E[N<<1];

void add(int u,int v,int w) {
	E[++ecnt].u=u;
	E[ecnt].v=v;
	E[ecnt].w=w;
	E[ecnt].next=head[u];
	head[u]=ecnt;
}

int dfs_down(int u,int fa) {
	d1[u]=-INF,d2[u]=-INF;
	for(int i=head[u]; i; i=E[i].next) {
		int v=E[i].v;
		if(v==fa)continue;//一直向下
		int dist=dfs_down(v,u)+E[i].w;
		if(dist>d1[u]) {
			d2[u]=d1[u],d1[u]=dist;//更新
			p2[u]=p1[u],p1[u]=v;
		} else if(dist>d2[u]) {
			d2[u]=dist;
			p2[u]=v;
		}
	}
	if(d1[u]==-INF)d1[u]=d2[u]=0;//叶子节点
	return d1[u];
}

void dfs_up(int u,int fa) {
	//根节点不用特殊处理,根节点的up为0
	for(int i=head[u]; i; i=E[i].next) {
		int v=E[i].v;
		if(v==fa)continue;
		if(p1[u]==v) { //u最长直径,经过v
			up[v]=max(up[u],d2[u])+E[i].w;//更新v向上的最长路径
		} else {
			up[v]=max(up[u],d1[u])+E[i].w;
		}
		dfs_up(v,u);//继续向下处理
	}
}



int main() {

	cin>>n;

	for(int i=1; i<=n-1; i++) {
		int a,b,c;
		cin>>a>>b>>c;
		add(a,b,c);
		add(b,a,c);
	}
	dfs_down(1,-1);
	dfs_up(1,-1);
	ans=INF;
   
	for(int i=1; i<=n; i++)ans=min(ans,max(d1[i],up[i]));
	cout<<ans<<endl;


	return 0;
}

数字转换

每一个数字和对应的约数和,可以链接一条边,这样依次类推就构成了一棵树,它要求转换步数最多,那么也就是求这颗树的直径(当然了可能存在多颗树,所以要用一个数组vis记录那些是根节点)

在求约数和时采用了比较新颖的做法

#include <bits/stdc++.h>
#define ll long long
#define INF 0x7f7f7f7f
using namespace std;

const int N=5e4+10;

int n,m,ecnt,ans;

int head[N];
int sum[N],vis[N];//sum记录i的约数和,vis记录i是否为树根
struct edge {
	int u,v,w,next;
} E[N<<1];

void add(int u,int v,int w) {
	E[++ecnt].u=u;
	E[ecnt].v=v;
	E[ecnt].w=w;
	E[ecnt].next=head[u];
	head[u]=ecnt;
}

//求树的直径 
int dfs(int u) {
	int d1=0,d2=0;
	for(int i=head[u]; i; i=E[i].next) {
		int v=E[i].v;
		int dist=dfs(v)+E[i].w;
		if(dist>=d1) {
			d2=d1,d1=dist;
		} else if(dist>d2) {
			d2=dist;
		}
	}
	ans=max(ans,d1+d2);
	return d1;
}




int main() {

	cin>>n;

	for(int i=1; i<=n; i++) {
		for(int j=2; j<=n/i; j++) { //i*j<=n 除过去防止溢出
			sum[i*j]+=i;//i*j的约数一定有i
		}
	}
	for(int i=2; i<=n; i++) {
		if(sum[i]<i) {
			add(sum[i],i,1);//约数和小于本身添加边
			vis[i]=1;//i不为树根
		}
	}

	for(int i=1; i<=n; i++) {
		if(!vis[i])//没被标记就为树根 
			dfs(i); //1不存在,所以从2开始
	}
	cout<<ans<<endl;









	return 0;

}

树上分组背包
增强版本

#include <bits/stdc++.h>
#define ll long long
#define INF 0x7f7f7f7f
using namespace std;

const int N=100+10;

int n,m,ecnt,ans;

int head[N];
int f[N][N]; 
struct edge {
	int u,v,w,next;
} E[N<<1];

void add(int u,int v,int w) {
	E[++ecnt].u=u;
	E[ecnt].v=v;
	E[ecnt].w=w;
	E[ecnt].next=head[u];
	head[u]=ecnt;
}

void dfs(int u,int fa){
	
	//分组背包(按支的个数分组) 
	for(int i=head[u];i;i=E[i].next){
		int v=E[i].v;
		if(v==fa)continue;
		dfs(v,u);
		for(int j=m;j>=0;j--){//从后向前遍历,因为需要f[u][j-k-1]的值,这个值不能更新
			for(int k=0;k<j;k++)
			  f[u][j]=max(f[u][j],f[u][j-k-1]+f[v][k]+E[i].w); 
		} 
	} 
} 




int main() {

	cin>>n>>m;
	
	for(int i=1;i<=n-1;i++){
		int u,v,w;
		cin>>u>>v>>w; 
		add(u,v,w);
		add(v,u,w); 
	} 
	dfs(1,-1);
	cout<<f[1][m]<<endl; 
	
	return 0;

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值