树形DP是动态规划在树结构上的应用,是解决许多涉及树形结构问题的重要方法之一。在本文中,我们以一道具体的题目为例,讲解该题目的解法,并以该解法为例,分析树形DP的特点与通用思路。
问题描述
天气越来越冷了,村子里有留守老人缺少照顾,会在这个冬天里挨冻,小华想运用自己的知识帮帮他们。已知村庄里每户人家作为一个节点,这些节点可以组成一个二叉树。我们可以在二叉树的节点上供应暖炉,每个暖炉可以为该节点的父节点、自身及其子节点带来温暖。给定一棵二叉树,求使个村庄都暖和起来至少需要供应多少个暖炉?
输入格式
输入为一个数组,按层遍历顺序描述二叉树的节点情况。值为 1,代表存在一个节点,值为 0,代表不存在该节点。
输出格式
输出最少暖炉供应数量。
输入样例
1, 1, 0, 1, 1
输出样例
1
数据范围
树的节点数的范围是 [1, 1000]。
解法
思路分析
首先,题目要求我们把取暖范围覆盖到所有节点,并以最小暖炉数量为优化目标。由于暖炉的作用范围包括了暖炉所在节点、节点的父节点和该节点的所有子节点,因此这不是一个简单的覆盖问题,而是一个带约束的覆盖问题。直接铺满暖炉显然不是最优解,因为节点的覆盖范围会产生重叠;还有一种很直观的思路是每隔一层安装暖炉,但这样也会产生覆盖范围重叠。于是,我们需要思考每个节点可能会出现的不同情况:
- 如果当前节点未被覆盖,它的父节点或它自己必须安装暖炉。
- 如果当前节点被覆盖,它可能由父节点、子节点或自身的暖炉覆盖。
- 如果当前节点安装了暖炉,子节点和父节点的覆盖状态都受到影响。
因此,我们可以总结出每个节点所有可能的状态:
未覆盖(状态0)被覆盖(状态1)安装暖炉(状态2)
状态设计与转移
由上面的所有可能状态,我们设计出DP中的以下三种状态:
- 被覆盖:该节点及周围相关节点都已被暖炉温暖。
- 安装了暖炉:当前节点安装了一个暖炉。
- 未被覆盖 :当前节点没有被暖炉覆盖。
通过对节点的递归分析,我们可以动态地求解最优解。具体状态转移公式如下:
- 若当前节点未被覆盖,则必须在其父节点或自身安装暖炉。
- 若当前节点被覆盖,则可以选择不安装暖炉。
- 若当前节点安装了暖炉,则其父节点和子节点均被覆盖。
状态定义
设:
dp[u][0]:以节点u为根的子树中,节点u未覆盖时的最小暖炉数量。dp[u][1]:以节点u为根的子树中,节点u被覆盖但未安装暖炉时的最小暖炉数量。dp[u][2]:以节点u为根的子树中,节点u安装暖炉时的最小暖炉数量。
状态转移
- 当
u未被覆盖时:dp[u][0] = dp[left][1] + dp[right][1] - 当
u被覆盖但未安装暖炉时:dp[u][1] = min(dp[left][2] + dp[right][2], ...) - 当
u安装暖炉时:dp[u][2] = 1 + min(dp[left]) + min(dp[right])
代码实现
public class Main {
// 定义二叉树结构
static class TreeNode {
int val;
TreeNode left, right;
TreeNode(int val) {
this.val = val;
this.left = null;
this.right = null;
}
}
public static int solution(int[] nodes) {
// 构建二叉树
TreeNode root = buildTree(nodes);
// 初始化结果数组:返回数组[0]是放置暖炉数量
int[] res = dfs(root);
// 最小的暖炉数量为:放置暖炉状态 或 已覆盖状态
return Math.min(res[1], res[2]);
}
private static int[] dfs(TreeNode node) {
// 如果是空节点,默认状态
if (node == null) {
return new int[] { 0, 0, Integer.MAX_VALUE / 2 };
}
// 后序遍历左右子树
int[] left = dfs(node.left);
int[] right = dfs(node.right);
// 三种状态
int notCovered = left[1] + right[1]; // 子节点需要暖炉
int covered = Math.min(left[2] + Math.min(right[1], right[2]),
right[2] + Math.min(left[1], left[2]));
int withHeater = 1 + Math.min(left[0], Math.min(left[1], left[2]))
+ Math.min(right[0], Math.min(right[1], right[2]));
return new int[] { notCovered, covered, withHeater };
}
private static TreeNode buildTree(int[] nodes) {
if (nodes == null || nodes.length == 0)
return null;
TreeNode root = new TreeNode(nodes[0]);
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
int i = 1;
while (i < nodes.length) {
TreeNode curr = queue.poll();
if (nodes[i] == 1) {
curr.left = new TreeNode(1);
queue.add(curr.left);
}
i++;
if (i < nodes.length && nodes[i] == 1) {
curr.right = new TreeNode(1);
queue.add(curr.right);
}
i++;
}
return root;
}
}
总结
该题目是一道非常经典的树形DP例题。通过我们上面的思考与分析,可以总结出树形DP问题所共同具有的以下2个特点:
- 状态定义与树形结构紧密相关:树形DP通常通过递归定义节点的状态,由叶节点向根节点递推解决问题。
- 自下而上的状态转移:根节点的状态往往在子节点全部求解之后才能求解。
当我们在平时的刷题中遇到具有以上特点的题目时,往往需要思考它是否能够通过树形DP解决。