树是指头节点没有父亲,其他节点只有一个父亲的有向无环图,直观理解为发散状。在树上,从头节点出发到任何节点的路径是唯一的,不管二叉树还是多叉树都如此。
树型dp在树上做动态规划,依赖关系比一般动态规划简单。因为绝大部分多数都是父依赖子。
树型dp套路
1)分析父树得到答案需要子树的哪些信息
2)把子树信息的全集定义成递归返回值
3)通过递归让子树返回全集信息
4)整合子树的全集信息得到父树的全集信息并返回
dfn序
用深度优先遍历的方式遍历整棵树,给每个节点依次标记序号,编号从小到大的顺序就是dfn序。dfn序 + 每颗子树的大小,可以起到定位子树节点的作用。如果某个节点的dfn序号是x,以这个节点为头的子树大小为y,那么可知,dfn序号从x ~ x+y-1所代表的节点,都属于这个节点的子树。利用这个性质, 节点间的关系判断,跨子树的讨论就会变得方便。
下面通过题目加深理解。
题目一
测试链接:1373. 二叉搜索子树的最大键值和 - 力扣(LeetCode)
分析:这道题父节点需要从子节点得到子节点的最大键值和、以子节点为头节点的子树中的最小值和最大值以及子树是否是一个二叉搜索树以及子树的累加和。在进行可能性展开的时候,如果不需要用到当前节点,则只需从左右子节点得到最大值;如果需要用到当前节点,则以当前节点为头节点的树需要是一棵二叉收索树,所以需要刚刚的信息进行判断及得出答案,所有答案求最大值即可。下面代码用一个vector存储信息,代码如下。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> f(TreeNode* t){
vector<int> ans;
if(t == nullptr){
ans.push_back(0);
ans.push_back(-((1 << 31) + 1));
ans.push_back((1 << 31));
ans.push_back(1);
ans.push_back(0);
}else{
vector<int> left = f(t->left);
vector<int> right = f(t->right);
ans.push_back(left[0] > right[0] ? left[0] : right[0]);
ans.push_back(left[1] < right[1] ? (left[1] < t->val ? left[1] : t->val) : (right[1] < t->val ? right[1] : t->val));
ans.push_back(left[2] > right[2] ? (left[2] > t->val ? left[2] : t->val) : (right[2] > t->val ? right[2] : t->val));
if(left[3] && right[3] && t->val > left[2] && t->val < right[1]){
ans[0] = ans[0] > left[4] + right[4] + t->val ?
ans[0] : left[4] + right[4] + t->val;
ans.push_back(1);
}else{
ans.push_back(0);
}
ans.push_back(left[4] + right[4] + t->val);
}
return ans;
}
int maxSumBST(TreeNode* root) {
vector<int> ans = f(root);
return ans[0];
}
};
其中,对于空指针,最大值要设置为int最小值,最小值要设置为int最大值,方便子树是否是搜索二叉树的判断。一般情况下,可以直接定义一个类去存储信息,代码如下。
class Solution {
public:
class Info{
public:
int maxBSTvalue;
int minValue;
int maxValue;
bool isBST;
int sum;
Info(int a, int b, int c, bool d, int e){
maxBSTvalue = a;
minValue = b;
maxValue = c;
isBST = d;
sum = e;
}
};
Info f(TreeNode* t){
if(t == nullptr){
Info ans(0, -((1 << 31) + 1), (1 << 31), true, 0);
return ans;
}else{
Info left = f(t->left);
Info right = f(t->right);
Info ans(left.maxBSTvalue > right.maxBSTvalue ? left.maxBSTvalue : right.maxBSTvalue,
left.minValue < right.minValue ? left.minValue < t->val ? left.minValue : t->val : right.minValue < t->val ? right.minValue : t->val,
left.maxValue > right.maxValue ? left.maxValue > t->val ? left.maxValue : t->val : right.maxValue > t->val ? right.maxValue : t->val,
true,
left.sum + right.sum + t->val);
if(left.isBST && right.isBST && t->val > left.maxValue && t->val < right.minValue){
ans.maxBSTvalue = ans.maxBSTvalue > ans.sum ? ans.maxBSTvalue : ans.sum;
}else{
ans.isBST = false;
}
return ans;
}
}
int maxSumBST(TreeNode* root) {
Info ans = f(root);
return ans.maxBSTvalue;
}
};
题目二
测试链接:543. 二叉树的直径 - 力扣(LeetCode)
分析:这道题父节点需要从子节点得到以子节点为头节点的子树的直径以及从子节点往下的最大深度。可能性的展开也是需不需要当前节点,不需要则得到左右子节点的最大直径;需要则利用左右子节点的最大深度,加上当前节点。所有结果取最大值即可得到答案。代码如下。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
class Info{
public:
int D;
int deep;
Info(int a, int b){
D = a;
deep = b;
}
};
Info f(TreeNode* t){
if(t == nullptr){
Info ans(0, -1);
return ans;
}else{
Info left = f(t->left);
Info right = f(t->right);
Info ans(left.D > right.D ? left.D : right.D,
(left.deep > right.deep ? left.deep : right.deep)+1);
ans.D = ans.D > left.deep + right.deep + 2 ? ans.D : left.deep + right.deep + 2;
return ans;
}
}
int diameterOfBinaryTree(TreeNode* root) {
Info ans = f(root);
return ans.D;
}
};
题目三
测试链接:979. 在二叉树中分配硬币 - 力扣(LeetCode)
分析:这道题父节点需要从子节点得到以子节点为头节点的子树的节点个数以及硬币个数以及子树分配完成需要的最少移动次数。如果子树的节点数和硬币数不相等的话,那么必然会通过当前节点进行硬币的转移,所以两棵子树会给出和拿入相应的硬币数,所以以当前节点为头节点的子树的移动次数就是子树的移动次数加上两颗子树给出和拿入的次数。代码如下。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
class Info{
public:
int nodeNums;
int coinNums;
int moveTimes;
Info(int a, int b, int c){
nodeNums = a;
coinNums = b;
moveTimes = c;
}
};
Info f(TreeNode* t){
if(t == nullptr){
Info ans(0, 0, 0);
return ans;
}else{
Info left = f(t->left);
Info right = f(t->right);
Info ans(left.nodeNums + right.nodeNums + 1,
left.coinNums + right.coinNums + t->val,
left.moveTimes + right.moveTimes + abs(left.coinNums - left.nodeNums) + abs(right.coinNums - right.nodeNums));
return ans;
}
}
int distributeCoins(TreeNode* root) {
Info ans = f(root);
return ans.moveTimes;
}
};
题目四
测试链接:337. 打家劫舍 III - 力扣(LeetCode)
分析:这道题父节点需要从子节点得到打劫子节点可以盗取的最高金额和不打劫子节点盗取的最高金额。对可能性的展开即打劫父节点,那么两个子节点就只能取不打劫的部分;如果不打劫父节点,则两个子节点取最大部分。代码如下。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
class Info{
public:
int steal;
int not_steal;
Info(int a, int b){
steal = a;
not_steal = b;
}
};
Info f(TreeNode* t){
if(t == nullptr){
Info ans(0, 0);
return ans;
}else{
Info left = f(t->left);
Info right = f(t->right);
Info ans(left.not_steal + right.not_steal + t->val,
(left.steal > left.not_steal ? left.steal : left.not_steal) + (right.steal > right.not_steal ? right.steal : right.not_steal));
return ans;
}
}
int rob(TreeNode* root) {
Info ans = f(root);
return ans.steal > ans.not_steal ? ans.steal : ans.not_steal;
}
};
题目五
测试链接:968. 监控二叉树 - 力扣(LeetCode)
分析:这道题可以将节点的状态分为三个,分别是没有摄像头无监控状态、没有摄像头有监控状态、有摄像头有监控状态,分别将其用012表示。则可以得到,如果子节点有0状态,则父节点必须是2状态;如果子节点有2状态,则父节点是可以是1状态,也可以是2状态,但是要求最小摄像头数量,所以父节点是1状态;其余为0状态。但这个推导都是基于当前节点有父节点的情况,所以如果头节点返回了0状态,则在最后结果需要再次加上1个摄像头。代码如下。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int f(TreeNode* t){
if(t == nullptr){
return 1;
}else{
int left = f(t->left);
int right = f(t->right);
if(left == 0 || right == 0){
++ans;
return 2;
}else if(left == 2 || right == 2){
return 1;
}else{
return 0;
}
}
}
int ans = 0;
int minCameraCover(TreeNode* root) {
if(f(root) == 0){
++ans;
}
return ans;
}
};
题目六
测试链接:437. 路径总和 III - 力扣(LeetCode)
分析:这道题是子节点从父节点得到信息。从头节点来到当前节点后,如果以当前节点作为截止,计算节点值之和满足条件的路径数目,则需要知道,从头节点出发累加和为头节点到当前节点的累加和加上当前节点的值减去目标值的数目。这个数目就是以当前节点作为截止满足条件的路径数目。代码如下。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int ans = 0;
void f(TreeNode* t, long long prefix, map<long long, int> m, int targetSum){
if(t == nullptr){
return;
}
long long k = prefix + t->val - targetSum;
map<long long, int>::iterator pos = m.find(k);
if(pos != m.end()){
ans += ((*pos).second);
}
k = prefix + t->val;
pos = m.find(k);
if(pos != m.end()){
(*pos).second++;
}else{
m.insert(pair<long long, int>(k, 1));
}
f(t->left, prefix+t->val, m, targetSum);
f(t->right, prefix+t->val, m, targetSum);
}
int pathSum(TreeNode* root, int targetSum) {
map<long long, int> m;
m.insert(pair<long long, int>(0, 1));
f(root, 0, m, targetSum);
return ans;
}
};
其中,刚开始出发时,累加和为0,出现1次。
题目七
测试链接:2477. 到达首都的最少油耗 - 力扣(LeetCode)
分析:这道题用邻接表建图,以0作为头节点进行遍历,父节点需要从子节点知道以子节点作为头节点的子树中所有人到达子节点的花费以及子节点现在的人数。然后从两个子节点到当前节点需要的花费加上两个子节点的花费即是以当前节点作为头节点的子树所有人来到当前节点的花费,人数依此类推计算。代码如下。
class Solution {
public:
class Info{
public:
long long cost;
int person;
Info(long long a, int b){
cost = a;
person = b;
}
};
Info f(int during, int father, int seats, vector<vector<int>>& graph){
long long cost = 0;
int person = 0;
Info temp(0, 0);
for(int i = 0;i < graph[during].size();++i){
if(graph[during][i] != father){
temp = f(graph[during][i], during, seats, graph);
person += temp.person;
cost += (((temp.person + seats - 1) / seats) + temp.cost);
}
}
Info ans(cost, person+1);
return ans;
}
long long minimumFuelCost(vector<vector<int>>& roads, int seats) {
vector<vector<int>> graph;
vector<int> temp;
int length = roads.size();
graph.assign(length+1, temp);
for(int i = 0;i < length;++i){
graph[roads[i][0]].push_back(roads[i][1]);
graph[roads[i][1]].push_back(roads[i][0]);
}
Info ans = f(0, -1, seats, graph);
return ans.cost;
}
};
题目八
测试链接:2246. 相邻字符不同的最长路径 - 力扣(LeetCode)
分析:这道题因为有n个节点,n-1条边,所以任意两个节点之间只有一条边,故从任意节点作为头节点进行遍历答案一样。父节点需要从子节点得到以子节点做头节点的子树的最长路径以及从子节点往下相邻节点没有分配到相同字符的最大深度。可能性的展开即考虑不考虑当前节点,如果不考虑则最长路径取子节点最长路径的最大值;如果考虑当前节点,则以子节点往下的最大深度的最大两个值加上头节点,这里需要注意用于计算的子节点的字符不能与当前节点字符相同。所有结果取最大值即是答案。代码如下。
class Solution {
public:
class Info {
public:
int maxLen;
int maxHeadLen;
Info(int a, int b) : maxLen(a), maxHeadLen(b) {}
};
Info f(int during, vector<vector<int>>& graph, string& s) {
if (graph[during].empty()) {
return Info(1, 1);
}
int maxLen = 0;
int maxHeadLen1 = 0, maxHeadLen2 = 0;
for (int v : graph[during]) {
Info childInfo = f(v, graph, s);
maxLen = max(maxLen, childInfo.maxLen);
if (s[during] != s[v]) {
if (childInfo.maxHeadLen > maxHeadLen1) {
maxHeadLen2 = maxHeadLen1;
maxHeadLen1 = childInfo.maxHeadLen;
} else if (childInfo.maxHeadLen > maxHeadLen2) {
maxHeadLen2 = childInfo.maxHeadLen;
}
}
}
maxLen = max(maxLen, maxHeadLen1 + maxHeadLen2 + 1);
return Info(maxLen, maxHeadLen1 + 1);
}
int longestPath(vector<int>& parent, string s) {
int n = parent.size();
vector<vector<int>> graph(n);
for (int i = 1; i < n; ++i) {
graph[parent[i]].push_back(i);
}
return f(0, graph, s).maxLen;
}
};
题目九
测试链接:2458. 移除子树后的二叉树高度 - 力扣(LeetCode)
分析:这题需要用到dfn序,这样便于知道移除一个节点,以这个节点做根节点的子树的范围。具体判断方法。在文章开头有介绍。故此题思路就是遍历移除每一个子树得到深度。代码如下。
class Solution {
public:
int cnt = 0;
int dfn[100001];
int size[100001];
int deep[100001];
int left[100001];
int right[100001];
int get_data(TreeNode* t){
if(t == nullptr){
return 0;
}else{
++cnt;
dfn[t->val] = cnt;
int left = get_data(t->left);
int right = get_data(t->right);
size[dfn[t->val]] = left + right + 1;
return size[dfn[t->val]];
}
}
void f(TreeNode* t, int deep_len){
if(t == nullptr){
return;
}else{
deep[dfn[t->val]] = deep_len;
f(t->left, deep_len+1);
f(t->right, deep_len+1);
}
}
vector<int> treeQueries(TreeNode* root, vector<int>& queries) {
vector<int> ans;
int length = queries.size();
get_data(root);
f(root, 0);
int max_deep = deep[1];
left[1] = 0;
for(int i = 2;i <= cnt;++i){
max_deep = max(max_deep, deep[i-1]);
left[i] = max_deep;
}
max_deep = deep[cnt];
right[cnt] = 0;
for(int i = cnt-1;i >= 0;--i){
max_deep = max(max_deep, deep[i+1]);
right[i] = max_deep;
}
int dfn_num, size_num;
for(int i = 0;i < length;++i){
dfn_num = dfn[queries[i]];
size_num = size[dfn_num];
ans.push_back(max(left[dfn_num], right[dfn_num+size_num-1]));
}
return ans;
}
};
其中,deep数组存储每个dfn序节点的深度;left数组存储dfn序i之前的最大深度;right数组存储dfn序i之后的最大深度。
题目十
测试链接:2322. 从树中删除边的最小分数 - 力扣(LeetCode)
分析:这道题用到dfn序去判断删除边之后的子树范围。大体思路就是遍历每一种删除情况,对每一种删除情况求异或值得到结果,取所有结果的最小值。在删除时有两种可能性,一种是删除的两个部分分是分开的;另一种是删除的两个部分其中一个部分是另一个部分的子树。需要分情况进行求解。代码如下。
class Solution {
public:
int dfn[1001];
int size[1001];
int dfn_nums[1001];
int cnt = 0;
int get_data(int during, vector<vector<int>>& graph, int father){
++cnt;
dfn[during] = cnt;
int num = 1;
for(int i = 0;i < graph[during].size();++i){
if(graph[during][i] != father){
num += (get_data(graph[during][i], graph, during));
}
}
size[dfn[during]] = num;
return num;
}
int f(int dfn1, int dfn2, int whole){
int size1 = size[dfn1];
int size2 = size[dfn2];
int val1 = 0;
for(int i = dfn1;i <= dfn1+size1-1;++i){
val1 ^= dfn_nums[i];
}
int val2 = whole ^ val1;
int val3 = 0;
if(dfn2 > dfn1 && dfn2 <= dfn1+size1-1){
for(int i = dfn2;i <= dfn2+size2-1;++i){
val3 ^= dfn_nums[i];
}
val1 = val1 ^ val3;
return max(val1, max(val2, val3)) - min(val1, min(val2, val3));
}else{
for(int i = dfn2;i <= dfn2+size2-1;++i){
val3 ^= dfn_nums[i];
}
val2 = val2 ^ val3;
return max(val1, max(val2, val3)) - min(val1, min(val2, val3));
}
}
int minimumScore(vector<int>& nums, vector<vector<int>>& edges) {
vector<vector<int>> graph;
vector<int> temp;
int length = nums.size();
int whole = 0;
int ans = -((1 << 31) + 1);
graph.assign(length, temp);
for(int i = 0;i < length-1;++i){
whole ^= nums[i];
graph[edges[i][0]].push_back(edges[i][1]);
graph[edges[i][1]].push_back(edges[i][0]);
}
whole ^= nums[length-1];
get_data(0, graph, -1);
for(int i = 0;i < length;++i){
dfn_nums[dfn[i]] = nums[i];
}
for(int i = 0;i < length-1;++i){
for(int j = i+1;j < length-1;++j){
int dfn1 = max(dfn[edges[i][0]], dfn[edges[i][1]]);
int dfn2 = max(dfn[edges[j][0]], dfn[edges[j][1]]);
ans = min(ans, f(min(dfn1, dfn2), max(dfn1, dfn2), whole));
}
}
return ans;
}
};
其中,采用邻接表进行建图。
题目十一
测试链接:[CTSC1997] 选课 - 洛谷
分析:这道题乍一看和树没有关系,但是如果将每一门没有直接先修课的课都连到一个头节点,题目转化为从这个头节点开始选课,选课数为m+1门,能得到的最大分,头节点的学分为0。所以可能性的展开为对最后一棵子树不选课以及选1、2、3到选课数减1门,因为必须有一门选当前节点才能保证连续。代码如下。
#include <iostream>
using namespace std;
int N, M;
int tree[301][301];
int num[301] = {0};
int score[301];
int dp[301][301][302];
int f(int i, int j, int k){
if(k == 0){
return 0;
}
if(dp[i][j][k] != -1){
return dp[i][j][k];
}
if(j == 0 || k == 1){
dp[i][j][k] = score[i];
return score[i];
}
int ans = f(i, j-1, k);
int v = tree[i][j-1];
for(int a = 1;a < k;++a){
ans = max(ans, f(i, j-1, k-a) + f(v, num[v], a));
}
dp[i][j][k] = ans;
return ans;
}
int main(void){
scanf("%d%d", &N, &M);
int pre, suf;
for(int i = 0;i < N;++i){
scanf("%d%d", &pre, &score[i+1]);
tree[pre][num[pre]++] = i+1;
}
score[0] = 0;
for(int i = 0;i <= N;++i){
for(int j = 0;j <= num[i];++j){
for(int k = 0;k <= M+1;++k){
dp[i][j][k] = -1;
}
}
}
printf("%d", f(0, num[0], M+1));
return 0;
}
其中,以邻接表建图;f函数返回在节点i为头结点的j颗子树上选k门课情况下的最大分。