树形DP学习及例题分析

知识1 树的直径

在边权只有1的情况下,树的直径的求法就是先任意找一个点y,求出距离它最远的点u,再找出距离u最远的点v,u与v之间的距离就是树的直径。
但在有边权的情况下,每条边的价值不等同,这时候就要用到树形DP。

题目一 树的最长路径

树形DP学习及例题分析_第1张图片
DP的基本思路就是任取一个点为起点,求出任意一个点的最长的两条子路径,因为一个点的最长子路径可以由它的儿子的最长子路径加上边权遍历得到,所以可以采用dp遍历所有情况。

DP的本质就是通过遍历所有情况来求得最优解,是一种时间换空间的策略。

本题采用dfs的写法,比较方便,容易理解。

#include 
using namespace std;
const int N = 1e4;
struct Edge{
	int fr;
	int to;
	int dis;
	int next;
}edge[N * 2 + 5];
int head[N + 5],next[N + 5];
int cnt,ans;
void add(int fr,int to,int dis){
	cnt ++;
	edge[cnt].next = head[fr];
	edge[cnt].fr = fr;
	edge[cnt].to = to;
	edge[cnt].dis = dis;
	head[fr] = cnt;
	return;
}
int dfs(int n,int father){
	int d1 = 0,d2 = 0;
	for(int i = head[n];i != 0;i = edge[i].next){
		if(edge[i].to == father) continue;
		int tmp = dfs(edge[i].to,n) + edge[i].dis;
		if(tmp >= d1){
			d2 = d1;
			d1 = tmp; 
		}else if(tmp > d2) d2 = tmp;
	}
	if(d1 + d2 > ans) ans = d1 + d2;
	return d1;
}
int main(){
	int n;
	cin >> n;
	for(int i = 1;i < n;i ++){
		int fr,to,dis;
		cin >> fr >> to >> dis;
		add(fr,to,dis);
		add(to,fr,dis);//无向图,要建两次边 
	}
	dfs(1,-1);
	cout << ans;
} 

题目二 树的中心

acwing1073
此题是比较少见的用父亲去更新儿子的题目,首先先跑一遍普通的最长路径,记录每个结点最长的两条的两条路径以及是从哪个儿子记录来的,最长路径记录为,d1[i],d2[i],p1[i],p2[i].再开一个新数组up[i],表示第i个结点往父亲走的最长路径,up[i]由父亲向儿子更新。

void dfs_u(int n,int father){
	for(int i = head[n];i;i = edge[i].next){
		if(edge[i].to == father) continue; //如果这个儿子等于父亲就跳过
		//如果这个儿子是最长边上的点,则最长的up[edge[i].to]为这条边的权值加上次长边的权值或者这条边
		//的权值加上继续往上走的权值。如果不是就是用最长边的权值去更新
		if(p1[n] != edge[i].to) up[edge[i].to] = max(up[n],d1[n]) + edge[i].dis;
		else up[edge[i].to] = max(up[n],d2[n]) + edge[i].dis;
		dfs_u(edge[i].to,n);
	}
	return;
}

最后答案为1~n的所有点的max(up[i],d1[i])的最小值

#include 
using namespace std;
const int N = 1e4;
const int INF = 0x3f3f3f3f;
struct Edge{
	int to;
	int dis;
	int next;
}edge[N * 2 + 5];
int head[N + 5];
int d1[N + 5],d2[N + 5],up[N + 5];
int p1[N + 5],p2[N + 5];
int cnt;
void ad(int fr,int to,int dis){
	cnt ++;
	edge[cnt].to = to;
	edge[cnt].dis = dis;
	edge[cnt].next = head[fr];
	head[fr] = cnt;
	return;
}
int dfs_d(int n,int father){
	for(int i = head[n];i;i = edge[i].next){
		if(edge[i].to == father) continue;
		int tmp = dfs_d(edge[i].to,n) + edge[i].dis;
	//	cout << tmp << endl;
		if(tmp > d1[n]){
			d2[n] = d1[n];
			d1[n] = tmp;
			p2[n] = p1[n];
			p1[n] = edge[i].to;
		}else if(tmp > d2[n]){
			d2[n] = tmp;
			p2[n] = edge[i].to;
		}
	}
	if(d1[n] == -INF) return 0;
	return d1[n];
}
void dfs_u(int n,int father){
	for(int i = head[n];i;i = edge[i].next){
		if(edge[i].to == father) continue;
		if(p1[n] != edge[i].to) up[edge[i].to] = max(up[n],d1[n]) + edge[i].dis;
		else up[edge[i].to] = max(up[n],d2[n]) + edge[i].dis;
		dfs_u(edge[i].to,n);
	}
	return;
}
int main(){
	int n;
	scanf("%d",&n);
	for(int i = 1;i < n;i ++){
		int fr,to,dis;
		scanf("%d%d%d",&fr,&to,&dis);
		ad(fr,to,dis);
		ad(to,fr,dis);
	}
	for(int i = 1;i <= n;i ++)
		d1[i] = d2[i] = -INF;
	dfs_d(1,-1);
//	for(int i = 1;i <= n;i ++)
//		cout << d1[i] << " " << d2[i] << endl;
	dfs_u(1,-1);
	int minn = INF;
	for(int i = 1;i <= n;i ++){
		if(max(d1[i],up[i]) < minn) minn = max(d1[i],up[i]);
	}
	cout << minn;
	return 0;
}  

题目三 数字转换

树形DP学习及例题分析_第2张图片
和树的最长路径很像,而且边权是1,在两个可以变换的点之间建边就行。普通求约数是n根号n
不过可以用一个复杂度更加低的方式来建边,直接让约数对数本身作贡献,复杂度是n ln n

for(int i = 1;i <= n;i ++)
	for(int j = 2;j <= n / i;j ++)
		sum[i * j] += i;

建完之后求最长路径就可以

#include 
using namespace std;
const int N = 5	* 1e4;
struct Edge{
	int to;
	int next;
}edge[2 * N + 5];
int cnt,ans;
int head[N + 5];
void ad(int fr,int to){
	cnt ++;
	edge[cnt].to = to;
	edge[cnt].next = head[fr];
	head[fr] = cnt;
	return;
}
int dfs(int n,int father){
	int d1 = 0,d2 = 0;
	for(int i = head[n];i;i = edge[i].next){
		if(edge[i].to == father) continue;
		int tmp = dfs(edge[i].to,n) + 1;
		if(tmp >= d1){
			d2 = d1;
			d1 = tmp;
		}else if(tmp > d2) d2 = tmp;
	}
	ans = max(ans,d1 + d2);
	return d1;
}
int main(){
	int n;
	scanf("%d",&n);
	for(int i = 1;i <= n;i ++){
		int sum = 0;
		for(int j = 1;j <= int(sqrt(i));j ++){
			if(i % j == 0){
				sum += j;
				int tmp = i / j;
				if(tmp < i && tmp != j) sum += tmp;
			}
		}
		if(sum < i){
			ad(sum,i);
			ad(i,sum);
		}
	}
	dfs(1,-1);
	cout << ans;
	return 0;
}

题目四 二叉苹果树

树形DP学习及例题分析_第3张图片
类似分组背包,枚举背包空间,再分别枚举从左右子树取的个数就行。

#include 
using namespace std;
const int N = 1e2;
struct Edge{
	int to;
	int dis;
	int next;
}edge[N * 2 + 5];
int cnt;
int head[N + 5];
int f[N + 5][N + 5];
int n,m;
void ad(int fr,int to,int dis){
	cnt ++;
	edge[cnt].to = to;
	edge[cnt].dis = dis;
	edge[cnt].next = head[fr];
	head[fr] = cnt;
	return;
}
void dfs(int u,int father){
	for(int i = head[u];i;i = edge[i].next){
		if(edge[i].to == father) continue;
		dfs(edge[i].to,u);
		for(int j = m;j >= 1;j --) //枚举空间,注意从大到小
			for(int k = 0;k < j;k ++) //枚举从每棵子树取的个数
				f[u][j] = max(f[u][j],f[u][j - k - 1] + f[edge[i].to][k] + edge[i].dis);
	}
	return;
}
int main(){	
	scanf("%d%d",&n,&m);
	for(int i = 1;i < n;i ++){
		int fr,to,dis;
		scanf("%d%d%d",&fr,&to,&dis);
		ad(fr,to,dis);
		ad(to,fr,dis);
	}
	dfs(1,-1);
	cout << f[1][m];
	return 0;
}

题目五 战略游戏

树形DP学习及例题分析_第4张图片
概括一下就是每一条边上至少有一个点。
f[i][0/1]表示第i个点,i是否放士兵的最小方案数
f[i][0] += f[j][1]
f[i][1] += min(f[j][0],f[j][1])
如果不放,那么儿子必须放
如果放了,儿子可放可不放,选最小值

#include 
using namespace std;
const int N = 1500;
const int INF = 0x3f3f3f3f;
struct Edge{
	int to;
	int next;
}edge[N + 5];
int head[N + 5];
int f[N + 5][2];
bool st[N + 5];
int cnt;
void ad(int fr,int to){
	cnt ++;
	edge[cnt].to = to;
	edge[cnt].next = head[fr];
	head[fr] = cnt;
	return;
}
void dfs(int u){
	f[u][1] = 1;
	for(int i = head[u];i;i = edge[i].next){
		dfs(edge[i].to);
		f[u][0] += f[edge[i].to][1]; 
		f[u][1] += min(f[edge[i].to][0],f[edge[i].to][1]);
	}
	return;
}
int main(){
	int n;
	while(scanf("%d",&n) == 1){
		memset(head,0,sizeof head);
		memset(st,0,sizeof st);
		memset(f,0,sizeof f);
		cnt = 0;
		for(int i = 1;i <= n;i ++){
			int fa,cnt;
			scanf("%d:(%d)",&fa,&cnt);
			for(int j = 1;j <= cnt;j ++){
				int tmp;
				scanf("%d",&tmp);
				ad(fa,tmp);
				st[tmp] = true;
			}
		}
		int root = 0;
		while(st[root]) root ++;
		dfs(root);
		printf("%d\n",min(f[root][0],f[root][1]));
	}
	return 0;
}

题目六 皇宫看守

树形DP学习及例题分析_第5张图片
此题和上一题不同的地方在于,这道题必须要每个点都被看见,而上一道题是每条边都要被看见,相比边来说,点要复杂很多,但是一个点就三种情况,被父亲看见,被儿子看见,被自己看见。
定义f[i][0/1/2]分别为被父亲看见,被儿子看见,被自己看见
f[i][0] += min(f[j][1],f[j][2]) 如果被父亲看见,那么儿子有没有都无所谓,但是儿子不能被这个点看见,所以不能继承儿子被父亲看见的值
f[i][2] += min(f[j][0],f[j][1],f[j][2]) 被自己看见,没有限制,直接继承儿子的最小值
被儿子看见比较复杂,必须规定被哪个儿子看见,然后取其他儿子的任意,万一所有儿子都没看见则不成立,被儿子看见详见代码

#include 
using namespace std;
const int N = 1500;
const int INF = 0x3f3f3f3f;
struct Edge{
	int to;
	int next;
}edge[N + 5];
int cost[N + 5],head[N + 5];
int f[N + 5][3];//0表示被父节点看到,1表示被子节点看到,2表示自己放上一个 
bool st[N + 5];
int cnt;
void ad(int fr,int to){
	cnt ++;
	edge[cnt].to = to;
	edge[cnt].next = head[fr];
	head[fr] = cnt;
	return;
}
void dfs(int u){
	f[u][2] = cost[u];
//	cout << f[u][2] << " ";
	f[u][1] = INF;
	for(int i = head[u];i;i = edge[i].next){
		dfs(edge[i].to);
		//f[u][1] = min(f[u][1],sum);
		f[u][0] += min(f[edge[i].to][2],f[edge[i].to][1]);//被父亲看见
		f[u][2] += min(min(f[edge[i].to][0],f[edge[i].to][1]),f[edge[i].to][2]);//被自己看见
	}
	for(int i = head[u];i;i = edge[i].next){//被儿子看见
		int sum = 0;
		for(int j = head[u];j;j = edge[j].next){
			if(j == i) sum = sum + f[edge[j].to][2];
			else sum = sum + min(f[edge[j].to][2],f[edge[j].to][1]);
//			if(u == 1) cout << edge[j].to << endl;
//			if(u == 1) cout << f[2][2] << " " << f[2][1] << " ";
		}
		f[u][1] = min(f[u][1],sum);
	}
	return;
}
int main(){
	int n;
	scanf("%d",&n);
	for(int i = 1;i <= n;i ++){
		int x;
		scanf("%d",&x);
		scanf("%d",&cost[x]);
		int tmp;
		scanf("%d",&tmp);
		for(int j = 1;j <= tmp;j ++){
			int tmp1;
			scanf("%d",&tmp1);
			ad(x,tmp1);
			st[tmp1] = true; 
		}
	}
//	cout << cnt << endl;
	int root = 1;
	while(st[root]) root ++;
	dfs(root);
//	cout << f[3][4] << endl;
	printf("%d",min(f[root][1],f[root][2]));
	return 0;
}

总结

树形DP的难点,第一,要准确找到一个数组可以完美地描述一个点的所有状态,第二,要周全的考虑状态的继承,不能缺漏。
算是第一篇写的总盘复习,这两天一直在赶工,算法学习之路道阻且长。

你可能感兴趣的:(大一算法学习,学习,图论,深度优先)