一、树型dp
1.1 树型dp套路
- 树形dp套路第一步: 以某个节点X为头节点的子树中,分析答案的可能性:在X不参与的情况下,所有可能性就是左子树的最大高度或右子树的最大高度;在X参与的情况下,所有可能性就是左子树的最大高度+右子树的最大高度+1。
- 树形dp套路第二步: 根据第一步的可能性分析,列出所有需要的信息:例如左右子树的高度。
- 树形dp套路第三步: 合并第二步的信息,对左树和右树提出同样的要求,并写出信息结构。
- 树形dp套路第四步: 设计递归函数,递归函数是处理以X为头节点的情况下的答案。 包括设计递归的base case,默认直接得到左树和右树的所有信息,以及把可能性做整合,返回第三步的信息结构这四个步骤。
1.2 二叉树节点间的最大距离问题
1.2.1 题目介绍
从二叉树的节点 A 出发,可以向上或者向下走,但沿途的节点只能经过一次,当到达节点 B 时,路径上的节点数叫作 A 到 B 的距离。
现在给出一棵二叉树,求整棵树上每对节点之间的最大距离。
题目链接:二叉树节点间的最大距离问题__牛客网
1.2.2 解题思路
-
树形DP套路第一步:分析答案来自哪些可能性:
1、以
root
为头节点的子树,最大距离可能是左子树上的最大距离;2、以
root
为头节点的子树,最大距离可能是右子树上的最大距离;3、以
root
为头节点的子树,最大距离可能是左子树上高度 + 右子树高度 + 1(root
节点本身); -
第二步:根据第一步的分析列出所有需要的信息,左子树和右子树需要知道自己这棵子树上的最大距离
maxDistance
以及高度height
这两个信息; -
第三步:根据第二步的信息汇总,用一个新的结构体
ReturnType
来存储上述两个信息; -
第四步:设计递归函数,整合信息。
1.2.3 代码实现
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<stack>
using namespace std;
struct TreeNode
{
int val;
TreeNode* left, * right;
TreeNode(int _val) :val(_val), left(NULL), right(NULL) {}
};
struct ReturnType
{
int maxDistance;
int height;
//初始化赋值
ReturnType(int _maxDistance, int _height) : maxDistance(_maxDistance), height(_height) {}
};
//建树
void createTree(TreeNode* root)
{
int rootVal, leftVal, rightVal;
cin >> rootVal >> leftVal >> rightVal;
if (leftVal != 0)
{
root->left = new TreeNode(leftVal);
createTree(root->left);
}
if (rightVal != 0)
{
root->right = new TreeNode(rightVal);
createTree(root->right);
}
}
ReturnType* process(TreeNode* root)
{
if (root == NULL)
return new ReturnType(0, 0);
//索要左右子树的高度信息和最长距离信息
ReturnType* leftData = process(root->left);
ReturnType* rightData = process(root->right);
int p1 = leftData->maxDistance;
int p2 = rightData->maxDistance;
int p3 = leftData->height + rightData->height + 1;
int _maxDistance = max(p3, max(p1, p2));
int _height = max(leftData->height, rightData->height) + 1;
return new ReturnType(_maxDistance, _height);
}
int main()
{
int n, rootVal;
cin >> n >> rootVal;
TreeNode* root = new TreeNode(rootVal);
createTree(root);
cout << process(root)->maxDistance << endl;
}
1.3 派对的最大快乐值问题
1.3.1 题目介绍
题目链接:派对的最大快乐值__牛客网
整个公司的人员结构可以看作是一棵标准的多叉树。树的头节点是公司唯一的老板,除老板外,每个员工都有唯一的直接上级,叶节点是没有任何下属的基层员工,除基层员工外,每个员工都有一个或多个直接下级,另外每个员工都有一个快乐值。
这个公司现在要办 party,你可以决定哪些员工来,哪些员工不来。但是要遵循如下的原则:
1.如果某个员工来了,那么这个员工的所有直接下级都不能来。
2.派对的整体快乐值是所有到场员工快乐值的累加。
3.你的目标是让派对的整体快乐值尽量大。
给定一棵多叉树,请输出派对的最大快乐值。
1.3.2 解题思路
很明显员工之间的关系是一个树状结构,所以采用树型dp来解决。
第一步:针对每个节点X与它的子节点a、b,考虑可能性。当X参加时,总快乐值 = X的快乐值 + a不来时其子树的最大快乐值之和 + b不来时其子树的最大快乐值之和;当X不参与时,总快乐值 = a参与时a和其子树的快乐值之和 (或 a不参与时其子树的快乐值之和) + b参与时b和其子树的快乐值之和 (或 b不参与时其子树的快乐值之和)。
第二步:列出所有需要的信息。X需要向 a 和 b 分别索要它(指a和b)参加时的整棵树的最大快乐值和它不参加时整棵树的最大快乐值。
第三步:根据第二步的信息汇总,用一个新的结构体 ReturnType
来存储上述两个信息。
第四步:设计递归函数,整合信息。
1.3.3 代码实现
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<vector>
using namespace std;
struct TreeNode
{
int happy;//该员工可以带来的快乐值
vector<TreeNode*> next;//该员工的直接下级
TreeNode(int x) :happy(x) {}//初始化
};
struct ReturnType
{
int nohappy;//不来的最大快乐值
int yeshappy;//来的最大快乐值
ReturnType(int n, int y) :nohappy(n), yeshappy(y) {}
};
ReturnType process(TreeNode* root)
{
if (root->next.empty())
return ReturnType(0, root->happy);//基层员工,没有子树
int n = 0, y = root->happy;
for (auto r : root->next)//遍历所有直接下层
{
auto k = process(r);
y += k.nohappy;//当r来的时候,下层员工只能不来
n += max(k.yeshappy, k.nohappy); //当r不来的时候,下层员工有来不来两种选择
}
return ReturnType(n, y);
}
int main()
{
int n, root;
cin >> n >> root;
vector<TreeNode*>happy(n);
int t, v, u;
for (int i = 0; i < n; i++)
{
cin >> t;
happy[i] = new TreeNode(t);//快乐值
}
for (int i = 1; i < n; i++)
{
cin >> u >> v;
happy[u - 1]->next.push_back(happy[v - 1]);
}
auto x = process(happy[root - 1]);
cout << max(x.nohappy, x.yeshappy) << endl;
}
二、二叉树的Morris遍历
2.1 Morris遍历套路
Morris遍历是一种遍历二叉树的方式,时间复杂度O(N),额外空间复杂度O(1) 。通过利用原树中大量空闲指针的方式,达到节省空间的目的。实质是建立一种机制,对于没有左子树的节点只到达一次,对于有左子树的节点会到达两次。
Morris遍历细节:
假设来到当前节点cur,开始时cur来到头节点位置。
1)如果cur没有左孩子,cur向右移动(cur = cur.right)
2)如果cur有左孩子,找到左子树上最右的节点mostRight:
- a.如果mostRight的右指针指向空,让其指向cur, 然后cur向左移动(cur = cur.left)
- b.如果mostRight的右指针指向cur,让其指向null, 然后cur向右移动(cur = cur.right)
3)cur为空时遍历停止
2.2 遍历过程
按照最一般的遍历方法,实际上是根据系统压栈的方式告诉你是第几次来到这个节点。
void process(Node head)
{
if (head == NULL)
return;
//printf写在这里:前序遍历
process(head->left);
//printf写在这里:中序遍历
process(head->right);
//printf写在这里:后序遍历
}
Morris遍历的原理是模拟压栈的过程,但是区别在于只能在遍历完左子树后回到头节点,遍历完右子树后就直接结束了。所以对于没有左子树的节点只到达一次,对于有左子树的节点会到达两次。标准递归方式则会到达三次。
2.3 代码实现
Morris遍历模板:
struct TreeNode
{
int val;
TreeNode* left, * right;
TreeNode(int _val) :val(_val), left(NULL), right(NULL) {}
};
void morris(TreeNode* head)
{
if (head == NULL)
return;
TreeNode* cur = head;
TreeNode* mostRight = NULL;
while (cur != NULL)
{
mostRight = cur->left;
if (mostRight != NULL)//如果cur有左孩子
{
//找到左子树上最右的节点mostRight
while (mostRight->right != NULL && mostRight->right != cur)
mostRight = mostRight->right;
//如果mostRight的右指针指向空
if (mostRight->right == NULL)
{
mostRight->right = cur;
cur = cur->left;
continue;
}
else//如果mostRight的右指针指向cur
{
mostRight->right = NULL;
}
}
//无左孩子和mostRight右指针指向cur两种情况会走到这
cur = cur->right;
}
}
先序遍历:只到达一次的节点直接打印;到达两次的节点在第一次打印。
struct TreeNode
{
int val;
TreeNode* left, * right;
TreeNode(int _val) :val(_val), left(NULL), right(NULL) {}
};
void morrisPre(TreeNode* head)
{
if (head == NULL)
return;
TreeNode* cur = head;
TreeNode* mostRight = NULL;
while (cur != NULL)
{
mostRight = cur->left;
if (mostRight != NULL)//如果cur有左孩子
{
//找到左子树上最右的节点mostRight
while (mostRight->right != NULL && mostRight->right != cur)
mostRight = mostRight->right;
//如果mostRight的右指针指向空
if (mostRight->right == NULL)//第一次来到cur,直接打印
{
mostRight->right = cur;
printf("%d ", cur->val);
cur = cur->left;
continue;
}
else//如果mostRight的右指针指向cur
{
mostRight->right = NULL;
}
}
else//只到达这个点一次,直接打印
printf("%d ", cur->val);
//无左孩子和mostRight右指针指向cur两种情况会走到这
cur = cur->right;
}
}
中序遍历:只到达一次的节点直接打印;到达两次的节点在第二次打印。
struct TreeNode
{
int val;
TreeNode* left, * right;
TreeNode(int _val) :val(_val), left(NULL), right(NULL) {}
};
void morrisIn(TreeNode* head)
{
if (head == NULL)
return;
TreeNode* cur = head;
TreeNode* mostRight = NULL;
while (cur != NULL)
{
mostRight = cur->left;
if (mostRight != NULL)//如果cur有左孩子
{
//找到左子树上最右的节点mostRight
while (mostRight->right != NULL && mostRight->right != cur)
mostRight = mostRight->right;
//如果mostRight的右指针指向空
if (mostRight->right == NULL)
{
mostRight->right = cur;
cur = cur->left;
continue;
}
else//如果mostRight的右指针指向cur
{
mostRight->right = NULL;
}
}
//第二次到达时打印
printf("%d ", cur->val);
//无左孩子和mostRight右指针指向cur两种情况会走到这
cur = cur->right;
}
}
后序遍历的顺序是左右中,可以通过回退到上一层把下一个左节点的右边节点一次全遍历过去再将结果翻转打印,因为回退到上一层节点意味着你这一层已经走过了一遍了。
上例中Morris遍历顺序应该是:8,5,1,5,4,2,4,3,8,7,6,7
后序遍历顺序为:1,2,3,4,5,6,7,8.
struct TreeNode
{
int val;
TreeNode* left, * right;
TreeNode(int _val) :val(_val), left(NULL), right(NULL) {}
};
//逆序
TreeNode* reverseEdge(TreeNode* from)
{
TreeNode* pre = NULL;
TreeNode* next = NULL;
while (from != NULL)
{
next = from->right;
from->right = pre;
pre = from;
from = next;
}
return pre;
}
//逆序打印右边界
void printEdge(TreeNode* X)
{
TreeNode* tail = reverseEdge(X);
TreeNode* cur = tail;
while (cur != NULL)
{
printf("%d ", cur->val);
cur = cur->right;
}
reverseEdge(tail);
}
void morrisIn(TreeNode* head)
{
if (head == NULL)
return;
TreeNode* cur = head;
TreeNode* mostRight = NULL;
while (cur != NULL)
{
mostRight = cur->left;
if (mostRight != NULL)//如果cur有左孩子
{
//找到左子树上最右的节点mostRight
while (mostRight->right != NULL && mostRight->right != cur)
mostRight = mostRight->right;
//如果mostRight的右指针指向空
if (mostRight->right == NULL)
{
mostRight->right = cur;
cur = cur->left;
continue;
}
else//如果mostRight的右指针指向cur
{
mostRight->right = NULL;
//逆序打印左树的右边界
printEdge(cur->left);
}
}
//无左孩子和mostRight右指针指向cur两种情况会走到这
cur = cur->right;
}
printEdge(head);
}
2.4 实际应用
使用Morris遍历判断一个树是不是搜索二叉树。
只需要对二叉树进行中序遍历,如果遍历结果能保持递增序列,说明是搜索二叉树。
struct TreeNode
{
int val;
TreeNode* left, * right;
TreeNode(int _val) :val(_val), left(NULL), right(NULL) {}
};
bool isBST(TreeNode* head)
{
if (head == NULL)//空树是搜索二叉树
return true;
TreeNode* cur = head;
TreeNode* mostRight = NULL;
int preValue = -99999;
while (cur != NULL)
{
mostRight = cur->left;
if (mostRight != NULL)//如果cur有左孩子
{
//找到左子树上最右的节点mostRight
while (mostRight->right != NULL && mostRight->right != cur)
mostRight = mostRight->right;
//如果mostRight的右指针指向空
if (mostRight->right == NULL)
{
mostRight->right = cur;
cur = cur->left;
continue;
}
else//如果mostRight的右指针指向cur
{
mostRight->right = NULL;
}
}
if (cur->val <= preValue)//当前值小于上一个值,不是递增的
return false;
preValue = cur->val;
//无左孩子和mostRight右指针指向cur两种情况会走到这
cur = cur->right;
}
return true;
}
三、总结
3.1 什么时候用树型dp,什么时候用Morris遍历?
当需要对左右子树的搜索结果做强整合的时候,使用树型dp;否则使用Morris遍历。