树形dp总结

树形dp是一类在树上作状态转移的问题。一般情况下,如果题目求的是最大值/最小值/数量等可以用dp解决的问题且明确给出一棵树,都可以考虑尝试树形dp解法。

核心思想:

        1. 利用子节点信息更新当前节点。一般先进行dfs,此时所有子节点信息都已更新完毕,之后利用子节点状态dp[son]更新状态dp[now]。

        2. 利用当前节点信息更新子节点。一般先用dp[now]更新dp[son],在更新前son的父节点信息都已更新完毕,之后dfs下去遍历整棵树。

可以发现,两种思想都是从根开始向叶子节点遍历,只是更新状态的时刻不同罢了。第一种思想是先dfs递归到底部,之后在向上回溯的过程中更新当前节点,而第二种思想是先更新子节点,然后dfs向下遍历,当递归到底部时这一支就已经更新完毕,这时的dfs更像是一种向叶子遍历的工具。

相关例题(题目顺序由易到难):

        1. Anniversary party POJ2342

        题目大意,给出一棵关系树,每个人都不想和自己的上级一起聚会,且每人有一个快乐值,求最大快乐值。这题是个树形dp模板题,首先考虑状态设计,令dp[i][0]表示在以i为根的子树中且i不去的情况下最大快乐值,dp[i][1]表示在以i为根的子树中且i去的情况下最大快乐值。设计好状态后,转移方程也就呼之欲出了,考虑选当前节点,其子节点必然都不可选,即dp[now][1]=sum(dp[son][0]),若不选当前节点,其子节点可选可不选,我们必然取最优的情况,即dp[now][0]=sum(max(dp[son][1],dp[son][0]))。最终答案即为max(dp[root][0],dp[root][1])。

        代码如下:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
//不可以贪心地去想,每层选取状态10101或01010,因为有可能10010获得的快乐值更大 
int n, cnt, head[6005], hpy[6005], in[6005];//in数组用来找到根节点 
int dp[6005][2];//dp[i][1]表示i来时,他和他的下属们能获得的最大快乐值,dp[i][0]表示i不来时... 
struct edges
{
	int to, next;
}edge[200005];

void init()
{
	cnt = 0;
	for(int i = 1; i <= n; i++)
		head[i] = in[i] = 0;
	memset(dp, 0, sizeof dp);
}

void add(int u, int v)
{
	edge[++cnt].to = v;
	edge[cnt].next = head[u];
	head[u] = cnt;
}

void dfs(int now)//有向树,不可能往回遍历 
{
	dp[now][0] = 0, dp[now][1] = hpy[now];
	for(int i = head[now]; i; i = edge[i].next)
	{
		dfs(edge[i].to);
		dp[now][1] += dp[edge[i].to][0]; 
		dp[now][0] += max(dp[edge[i].to][0], dp[edge[i].to][1]);//此时下属们来不来都可以,谁的贡献大谁来 
	}
}

signed main()
{
	while(cin >> n)
	{
		if(n == 0)
			break;
		init();
		for(int i = 1; i <= n; i++)
			scanf("%d", &hpy[i]);
		int a, b;
		for(int i = 1; i <= n-1; i++)
		{
			scanf("%d%d", &a, &b);
			add(b, a);
			in[a]++;
		}
		int s;
		for(int i = 1; i <= n; i++)
			if(in[i] == 0)
			{
				s = i;
				break;
			}
		dfs(s);
		cout << max(dp[s][0], dp[s][1]) << endl;
	}
	return 0;
}

        2. Strategic game POJ1463

        题目大意,给出一个城市地图,各城市之间构成一棵树,现在需要派士兵守卫每一条边,一个士兵可以守卫其周围的所有边,求最少派兵数。与上题类似,也是树形dp模板题,状态设计类似,考虑如果当前点不派兵守卫,则子节点一定要派兵守卫,而当前点派兵守卫后,其子节点是否派兵其实无所谓,根据情况最优选择即可,故状态转移方程为dp[now][1]=sum(max(dp[son][1],dp[son][0])),dp[now][0]=sum(dp[son][1])。答案即为max(dp[root][0],dp[root][1])。

        代码如下:

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

int n, dp[1505][2];//dp[i][0]表示以i为根(实际根不变)i处不放士兵的子树最少士兵数,dp[i][1]表示i处放士兵... 
vector<int> a[1505];

void dfs(int now, int fa)
{
	dp[now][1] = 1;
	dp[now][0] = 0;
	for(int i = 0; i < a[now].size(); i++)
	{
		if(a[now][i] == fa)
			continue;
		dfs(a[now][i], now);
		dp[now][1] += min(dp[a[now][i]][1], dp[a[now][i]][0]);
		dp[now][0] += dp[a[now][i]][1];
	}
} 

signed main()
{
	while(cin >> n)
	{
		int u, t, v;
		for(int i = 0; i <= n-1; i++)
			a[i].clear();
		for(int i = 0; i <= n-1; i++)
		{
			scanf("%d:(%d)", &u, &t);
			for(int j = 1; j <= t; j++)
			{
				scanf("%d", &v);
				a[u].push_back(v);
				a[v].push_back(u);
			}
		}
		memset(dp, 0x3f, sizeof dp);
		dfs(0, -1);
		cout << min(dp[0][0], dp[0][1]) << endl;
	}
	return 0;
}

        另外需要注意的一点是,本题给出的是一棵树,树中不含奇圈,即树是二分图。根据konig定理:二分图中的最大匹配数等于这个图中的最小点覆盖数。而最小点覆盖数即以最少的点覆盖所有相邻的边,正是题目所求。因此本题还可转化为求二分图中最大匹配数,用匈牙利算法解决即可。

        代码有空再补......

        3. Average distance HDU2376

        题目大意,给定一棵带权树,对每两对点之间的距离求和,并除以点的对数。本题有两种解法,第一种就是朴素树形dp,第二种是考虑每条边的贡献。首先是第一种解法,状态设计为令dp[i][0]表示以i为根的子树中i到各点的距离和,dp[i][1]表示除去以i为根的子树的部分中i到各点的距离和。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值