树形动态规划
[ code ]

居然已经快一个月没有写新博客了,这段时间在steam和switch二者的陪伴下我渐渐地变得乐不思蜀了起来……咳咳,最近的研究课题没有什么明显的进展,也没有学到特别多的新知识,但是博客还是坚持更新比较好。

本篇博客的内容是树形动态规划,顾名思义,就是在基于树的数据结构做动态规划,一般是在一棵树上选出某些结点,作为问题的最优解。动态规划相关的一些定义如下(来自百度百科):

动态规划

问题可以分解成若干相互联系的阶段,在每一个阶段都要做出决策,全部过程的决策是一个决策序列。要使整个活动的总体效果达到最优的问题,称为多阶段决策问题。动态规划就是解决多阶段决策最优化问题的一种思想方法

阶段

将所给问题的过程,按时间或空间特征分解成若干相互联系的阶段,以便按次序去求每阶段的解。

状态

各阶段开始时的客观条件叫做状态。

决策

当各段的状态取定以后,就可以做出不同的决定,从而确定下一阶段的状态,这种决定称为决策。

策略

由开始到终点的全过程中,由每段决策组成的决策序列称为全过程策略,简称策略。

状态转移方程

前一阶段的终点就是后一阶段的起点,前一阶段的决策选择导出了后一阶段的状态,这种关系描述了由k阶段到k+1阶段状态的演变规律,称为状态转移方程。(我认为这是动态规划问题里最难的部分,一般只要求得状态转移方程,问题就解决了大半,但是有时候状态转移方程的规律很难找。)

一个简单的线性动态规划实例

这里以一个简单的爬楼梯问题为例:假设我们要总共要爬$n$层楼梯,每次可以爬1或2层,爬到每一层需要不同的体力值,存储在数组$cost$里。我们需要求出爬到第$n$层所消耗的最小体力值。

为了解决这个问题,我们可以用一个长度为n的数组$dp$来记录爬到每一层所消耗的最小体力,其中$dp[i]$表示爬到第$i$层所消耗的最小体力。很容易可以知道$dp[0]=cost[0]$,$dp[1] = cost[1]$。而针对更高的楼层,我们只能从第$i-1$层或第$i-2$层爬上去,由此写出如下的状态转移方程:

\[dp[i] = \min(dp[i-1],dp[i-2])+cost[i]\]

想要求得爬到第n层所需的最小体力,只需要根据以上的状态转移方程,遍历 $0$ ~ $n-1$层,依次求出对应的dp值,最后返回$dp[n-1]$即可。

树形动态规划

由于树形动态规划是基于树形结构的,它的遍历方式比上述线性动态规划的例子要复杂。树形dp的遍历有两种方向,一种是从根节点到叶子节点,另一种是从叶子节点到根节点(用得多一些),往往需要用到递归以及记忆化搜索。

以下是一道leetcode上的树形动态规划例题 二叉树染色

有一个根节点为$root$的二叉树模型,初始所有结点为白色,每个结点有一个对应的价值$val$,可以用蓝色颜料给结点染色。为了美观,希望最后二叉树上每个蓝色相连部分的结点个数不能超过$k$个(取值范围为1~10),求所有染成蓝色的结点价值总和的最大值。

一个简单的例子如下,此时$k=2$,因此最多只能有2个蓝色结点相连。则最大价值总和为5+3+4=12。

enter description here

我们可以用一个长度为$k+1$的数组$dp$来记录状态,其中$dp[i]$代表以当前节点为根节点,且最大相连结点个数为$i$时,所获得的最大价值总和。状态转移的过程分析如下:

当$i=0$时,表示当前节点是白色,此时$dp[i]$的值等于该节点的左右子树中,蓝色结点最大价值的总和,即:

\[dp[0] = max(dp_l)+max(dp_r)\]

当$i \geq 1$时,当前节点是蓝色,且有$i-1$个蓝色结点与其相连,蓝色节点的分布有$i$种情况:左边连着$j (0 \le j \le i-1)$个蓝色结点,右边连着$(i-j-1)$个。此时的状态转移方程为:

\[dp[i] = \max(dp_l[j]+dp_r[i-j-1])+val\]

初始状态下,$dp[i]=0$,我们选择从叶子节点->根节点的遍历方向来更新每一层结点对应的dp值,最终返回根节点对应的所有染色状态的最大值$max(dp)$。

我们用深度优先搜索(DFS)来解决这个问题,代码如下:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    int maxValue(TreeNode* root, int k) {
       vector<int> dp = dfs(root, k);
        return *max_element(dp.begin(), dp.end()); //返回dp中的最大值
    }
	
    vector<int> dfs(TreeNode* root, int k) {
        vector<int> dp(k + 1, 0); //初始化
        if (!root) 
			return dp; //如果root为空结点,那么直接返回
			
        //分别求出左子树和右子树对应的dp数组
		auto dpl = dfs(root->left, k); 
        auto dpr = dfs(root->right, k);
		//当i=0时
        dp[0] = *max_element(dpl.begin(), dpl.end()) + *max_element(dpr.begin(), dpr.end());
		//当i >= 1时
        for (int i = 1; i <= k; ++i) {
            for (int j = 0; j < i; ++j) {
			   //更新dp[i]
                dp[i] = max(dp[i], root->val + dpl[j] + dpr[i - j - 1]);
            }
        }
        return dp;
    }
};

其实无论是树形dp还是线性dp,解决问题最重要的步骤就是求出状态转移方程。

Reference: