树形动态规划(dp)

一、基本概念

	树形动态规划,顾名思义,就是在“树”的数据结构上做动态规划,
通过有限次地遍历树,记录相关信息,以求解问题。通常,动态规划
都是线性的或者是建立在图上的,线性的动态规划的顺序有两种方向
即向前和向后,相应的状态转移方程有两种,即顺推与逆推,而树形
动态规划是建立在树上的,树中的父子关系天然就是个递归(子问题)
结构,所以也相应的有两个方向。
	1)叶→根,即根的子结点传递有用的信息给根,之后由根得出
最优解的过程。这种方式DP的题目应用比较多。
	2)根→叶,即需要取所有点作为一次根结点进行求值,此时父
结点得到了整棵树的信息,只需要去除这个儿子的DP值的影响,然后
再转移给这个儿子,这样就能达到根→叶的顺序。
	动态规划的顺序:一般按照后序遍历的顺序,即处理完儿子再处
理当前结点,才符合树的子结构的性质。
	实现方式:树形DP是通过记忆化搜索实现的,因此采用的是递归
方式。
	时间复杂度:树形动态规划的时间复杂度基本上是O(n);若有附
加维m,则是O(n×m)

二、经典问题

1.树的重心
	对于一棵n个结点的无根树,找到一个点,使得把树变成以该点
为根的有根树时,最大子树的结点数最小。换句话说,删除这个点后
最大连通块的结点数最小,那么这个点就是树的重心。
	解法:任选一个结点为根,把无根树变成有根树,然后设f[i]
示以i为根的子树的结点个数。不难发现f[i] ∑f[j]+1(j∈s[i]
程序实现很简单:只需要一次DFS,在无根树转有根树的同时计算即
可。其实在删除结点i后,最大的连通块有多少个结点呢?结点i的子
树中最大的有max{f[j]}个结点,i的“上方子树”中有n-f[i]个结点,
在动态规划的过程中就可以根据定义顺便找出树的重心了。
2.树的最长路径(最远点对)
	给定一棵n个结点的边带权的树,找到一条最长路径。换句话说,
要找到两个点,使得它们的距离最远,它们之间的路径就是树最长路径。
解法:一棵有根树的最长链,可能出现如图5-2-1所示的两种情况。

在这里插入图片描述

	要解决这个问题,我们只需要求出以每个结点为根的子树中的最
长链,取其中的最大值即为该树的最长链,
	对于每个结点我们都要记录两个值,d1[i]表示以i为根的子树中
i到叶子结点的距离最大值,d2[i]表示以i为根的子树中,除距离最大
值所在子树,i到叶子结点的距离最大值(也就是次大值);令j是i的儿
子。则:
  (1)若d1[j]+dis[i][j]>d1[i]
 	 则d2[i]=d1[i];d1[i]=d1[j]+ds[i][j];
  (2)否则,若d1[j]+dis[i][j]>d2[i],
 	 则d2[i]=d1[j]+dis[i][j];
  最后扫描所有的结点,找最大的d1[i]+d2[i]的值。
3.树的中心问题
	给出一棵边带权的树,求树中的点,使得此点到树中的其他结点
的最远距离最近。
	分析:从任意一点i出发的最长路径的可能形态有两种。
	(1)从i点出发向上,即终点不在以i为根的子树中的最长路径长度
为u[i]
	(2)从i点出发向下,即终点在以i为根的子树中的最长路径长度
为d1[i]
	这里的关键是如何计算u[i]。i点向上的路径必经过(i,prt[i])
而i点的父结点prt[i]又引出了两条路径:一条是prt[i]向上的最长路
径,其长度为u[prt[i]];另一条是prt[i]向下的路径,该路径不能途
经i点,否则会产生重复计算。
	设d1[i]表示以i为根的子树中,i到叶子结点的距离最大值;
	d2[i]表示以i为根的子树中,i到叶子结点的距离次大值;
	分别用c1[i]和c2[i]记录d1[i],d2[i]是从哪个子树更新来的。
	u[i]表示除了以i为根的子树中的叶子结点外,其他的叶子结点到
i的最大值。
	(1)首先,一遍树形DP算出d1,d2,c1,c2。令j是i的儿子,则:
		<1>若d1[j]+dist[i][j]>d1[i],
			d2[i]=d1[i],d1[i]=d1[j]+dist[i][j]
		<2>否则,若d1[j]+dist[i][j]>d2[i],则
			d2[i]=d1[j]+dist[i][j]
	(2)设prt[i]=x,
		若c1[x]!=i即d1[x]不从i更新而来的,那么
			u[i]=max{d1[x],u[x]}+dist[x][i];
		若c1{x]=i即d1[x]从i更新而来的,那么
			u[i]=max{d2[x],u[x]}+dist[x][i];
	(3)最后在n个结点中找出最大值,即:
			t[i]=max(u[i],d1[i])(1<=i<=n).
	(4)树的中心:ans=min{t[i]}(1≤i=<n)
4.普通的树形DP
	给定一棵树,现在要从中选出最少的结点,使得所有边至少有一
个端点在选中的集合中。
	分析:按照要求构建一棵树。对于这类最值问题,向来是用动态
规划求解的。
	点的取舍可以看作一种决策,那么状态就是在某个点取的时候或
者不取的时候,以它为根的子树的最小代价、分别可以用f[j][1]
f[j][0]表示。
	当这个点不取的时候,它的所有儿子都要取,所以
		f[i][0]=∑f[i][1](j∈son[i])
当这个点要取的时候,它的所有儿子取不取无所谓,不过当然应该取
最优的一种情况。所以
	f[i][1]=(∑min{f[j][1],f[j][0]}(j∈son[i])+1
	普通的树形DP中,常常会采用叶→根的转移形式,根据父结点的
状态确定子结点的状态,若子结点有多个,则需要一一枚举,将子
结点(子树)的DP值合并。树形DP还有一个重要拓展是与各类树形数
据结构结合。例如,Trie上的DP,AC自动机上的DP、后缀自动机上
的DP等。

三、例题

加分二叉树

题目描述
原题来自:NOIP 2003
设一个n个节点的二叉树tree的中序遍历为(1,2,3,…,n),其中数字1,2,3,…,n为节点编号。每个节点都有一个分数(均为正整数),记第i个节点的分数为di,tree及它的每个子树都有一个加分,任一棵子树subtree(也包含tree本身)的加分计算方法如下:
subtree的左子树的加分× subtree的右子树的加分+subtree的根的分数。
若某个子树为空,规定其加分为1,叶子的加分就是叶节点本身的分数。不考虑它的空子树。
试求一棵符合中序遍历为(1,2,3,…,n)且加分最高的二叉树tree。要求输出;
(1)tree的最高加分
(2)tree的前序遍历

输入格式
第一行一个整数 n 表示节点个数;

第二行 n 个空格隔开的整数,表示各节点的分数。

输出格式
第一行一个整数,为最高加分 b;

第二行 n 个用空格隔开的整数,为该树的前序遍历。

样例
Input
5
5 7 1 2 10
Output
145
3 1 2 4 5
数据范围与提示
对于 100% 的数据,n<30,b<100,结果不超过 4*10^9 。

我们发现,中序遍历区间内任何一点都可以当做根,根左边的部分一定是左子树,根右边的一定是右子树。我们可以区间DP枚举根,然后开个数组记录根。先序遍历时就按照根的顺序递归二分输出根就好了。

AC代码

#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 30;
int n;
int w[N];
int f[N][N], g[N][N];
void dfs(int l,int r)
{
    if(l>r) 
		return;
    int k = g[l][r];
    printf("%d ",k);
    dfs(l, k - 1);
    dfs(k + 1, r);
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
		scanf("%d",&w[i]);
    for(int len=1;len<=n;len++)
        for(int l=1;l+len-1<=n;l++){
            int r=l+len-1;
            if(len==1) 
				f[l][r]=w[l],g[l][r]=l;
            else{
                for(int k=l;k<=r;k++){
                    int left=(k==l?1:f[l][k-1]);
                    int right=(k==r?1:f[k+1][r]);
                    int score=left*right+w[k];
                    if(score>f[l][r]){
                        f[l][r]=score;
                        g[l][r]=k;
                    }
                }
            }
        }
	printf("%d\n",f[1][n]); 
    dfs(1, n);
    return 0;
}

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值