树状dp

树状dp

在树结构上做dp,常见的题型是:给出一棵树,让你实现最小代价或找到最大收益。
树这种结构本身具有“子结构”具有递归性,所以非常适合进行dp。
基于树的解题步骤一般是:先把树转为有根树(如果是几个互不连通的树,就加一个虚拟根,它连接所有孤立的树),然后在树上做DFS,递归到最底层的叶子节点,再一层层返回信息更新至根结点。显然,树上的DP所操作的就是这一层层返回的信息。不同的题目需要灵活设计不同的DP状态和转移方程。

树形dp的基本操作

先看一个简单的入门题。通过这一题,了解树的存储,以及如何在树上设计DP和进行状态转移。请读者特别注意DP设计时的两种处理方法:二叉树、多叉树

二叉苹果树 洛谷P2015 https://www.luogu.com.cn/problem/P2015
题目描述:有一棵苹果树,如果树枝有分叉,一定是分2叉。这棵树共有n个结点,编号为1~n,树根编号是1。用一根树枝两端连接的结点的编号来描述一根树枝的位置,下面是一棵有4个树枝的树:
2 5
\ /
3 4
\ /
1
   这棵树的枝条太多了,需要剪枝。但是一些树枝上长有苹果,最好别剪。给定需要保留的树枝数量,求出最多能留住多少苹果。
输入格式:第1行2个数,n和q(1 ≤ Q ≤N, 1 < N ≤ 100)。n表示树的结点数,q表示要保留的树枝数量。接下来n - 1行描述树枝的信息。每行3个整数,前两个是它连接的结点的编号。第3个数是这根树枝上苹果的数量。每根树枝上的苹果不超过30000个。
输出格式:一个数,最多能留住的苹果的数量。
输入样例:
5 2
1 3 1
1 4 10
2 3 20
3 5 20
输出样例:
21

首先要进行图的储存一定会用到数据结构,一般有三种结构,邻接矩阵(简单易写但效率低);邻接表(比较难写但效率高);链式向前星(相对中庸)。邻接矩阵适于储存稠密的图,邻接表适合稀疏的图,树本身就是一种稀疏的图所以一般不用邻接矩阵储存。后面用到的都是邻接表储存。

对于二叉树,三叉数等树枝个数确定的树可以用树的节点储存。

在这里插入图片描述
这一题的求解从根到叶,非常常规的dp思路。

定义dp,dp [u][j]表示以u为根的树(可以为子树)上保留j个树枝最多可保留的果实。对于整个题来说dp[1][q]就是答案。
对于程序有两种思路:二叉树,多叉数。
状态转移方程:
(1)二叉树
本题是一棵二叉树,根据二叉树的特征,考虑u的左右子树,如果左儿子lson留k条边,右儿子rson就留j - k条边,用k在[0, j]内遍历不同的分割。

       dp[u][j] = max(dp[u][j], dp[u.lson,k] + dp[u.rson, j-k]); //左右儿子合起来

这里将节点与其子树的连线也视为了子树的一部分
(2)多叉树

 dp[u][j] = max(dp[u][j], dp[u][j-k-1] + dp[v][k] + w);

其中v是u的一个子结点。dp[u][j]的计算分为2部分:
  1)dp[v][k]:在v上留k个边;
  2)dp[u][j-k-1]:除了v上的k个边,以及边[u,v],那么以u为根的这棵树上还有j-k-1个边,它们在u的其他子结点上。

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

const int MAX = 10;

int n, q;//n个树枝,保留p个

struct node//用于构建邻接表的结构体
{
	int v, w;
	node(int v = 0, int w = 0) : v(v), w(w) {}//结构体的初始化函数
};

vector<node>edge[MAX];//用vector建立结构体列表的数组

int dp[MAX][MAX], sum[MAX];//sum用于记录每个节点的树枝数

void dfs(int u, int father)
{
	for (int i = 0; i < edge[u].size(); i++)
	{
		int v = edge[u][i].v;
		int w = edge[u][i].w;
		if (father == v)continue;//因为题中是无向边所以避免回头
		dfs(v, u);

		sum[u] += sum[v] + 1;//计算树枝数

		for (int j = min(q, sum[u]); j >= 0; j--)
		{
			for (int k = 0; k <= min(sum[v], j - 1); k++)
			{
				dp[u][j] = max(dp[u][j], dp[u][j - k - 1] + dp[v][k] + w);
			}
		}

	}
}

int main()
{
	cin >> n >> q;

	int u, v, w;
	for (int i = 1; i < n; i++)
	{
		cin >> u >> v >> w;
		edge[u].push_back(node(v, w));//输入无向边
		edge[v].push_back(node(u, w));
	}

	dfs(1, 0);
	
	cout << dp[1][q] << endl;
	
	system("pause");
	return 0;
}

二叉树和多叉树的讨论。本题是二叉树,但是上面的代码是按多叉树处理的。代码中用v遍历了u的所有子树,并未限定是二叉树。状态方程计算dp[u][j]时包含两部分dp[u][j-k-1]和dp[v][k],其中dp[v][k]是u的一个子树v,dp[u][j-k-1]是u的其他所有子树。
算法的关键是j的for循环必须是递减循环,因为在计算当前数字的多树枝的状态数时调用到了上一树枝的少树枝的状态数,此时必须保证此值没有被修改过。例如计算第三个树枝的dp[u][5]时用到了dp[u][4],dp[u][3]等这些代表计算第二个树枝的值,要保证他们的正确性j要递减循环。
这种新值覆盖原值的操作叫“自我滚动”是滚定数组的一种。
复杂度:dfs()递归到每个结点,每个结点有2个for循环,总复杂度小于 O(n3)

背包与树形dp

分组背包问题:背包有一定容量,物品分几组拥有不同的体积和价值,同组物品互相排斥(即一组物品中只能选择一个)要求找到价值最大的方案。

有一些树形DP问题,可以抽象为背包问题,被称为“树形依赖的背包问题”。可以建模为“分组背包”。
(1)分组。根结点u的每个子树是一个分组。
(2)背包的容量。把u为根的整棵树上的树枝数,看成背包容量。
(3)物品。把每个树枝看成一个物品,体积为1,树枝上的苹果数量看成物品的价值。
(4)背包目标。求能放到背包的物品的总价值最大,就是求留下树枝的苹果数最多。

分组背包的代码

for(int i = 1; i <= n; i++)                 //遍历每个组
    for(int j = C; j>=0; j--)               //背包总容量C
        for(int k = 1; k <= m; k++)         //用k遍历第i组的所有物品
            if(j >= c[i][k])                //第k个物品能装进容量j的背包
               dp[j] = max(dp[j], dp[j-c[i][k]] + w[i][k]);    //第i组第k个

多叉数的代码

for(int i = 0; i < edge[u].size(); i++) {    //把u的每个子树看成一个组
    ......
    for(int j = sum[u]; j >= 0; j--)         //把u的枝条总数看成背包容量
        for(int k = 0; k <= j - 1; k++)      //用k遍历每个子树的每个枝条
            dp[u][j] = max(dp[u][j], dp[u][j-k-1] + dp[v][k] + w);

需要注意的是,代码(1)和代码(2)的j循环都是从大到小,具体原因已经在对应的章节中详细解释。

树状dp的应用

树的最大独立集、重心、最长点对是常见的问题

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值