五一假期发博文任务达成。
LeetCode中比较像的一些题,树中的最X路径,聚类一下,纵向拓展,便于总结,举一反三。
题目是刷不完的,但技能却可以在学习中一直增长。
树中的路径
- 1. [LeetCode 543 二叉树的直径](https://leetcode-cn.com/problems/diameter-of-binary-tree/)
- 2. [LeetCode 124. 二叉树中的最大路径和](https://leetcode-cn.com/problems/binary-tree-maximum-path-sum/)
- 3. [LeetCode1425 树的直径](https://leetcode-cn.com/problems/tree-diameter/)
- 4. [LeetCode 2246 相邻字符不同的最长路径](https://leetcode-cn.com/problems/longest-path-with-different-adjacent-characters/)
- 5. [LeetCode 2242 节点序列的最大得分](https://leetcode-cn.com/problems/maximum-score-of-a-node-sequence/)
1. LeetCode 543 二叉树的直径
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
好的, 直奔主题,可以求出以节点为根的子树的深度。
求树的深度的过程,实际会递归求出了每个节点左右子树的深度。
那么,二叉树的直径就等于 max(节点左子树深度和右子树深度的和)。利用递归的过程,更新直径这个全局变量的值。
class Solution {
int maxLen = 0;
public int diameterOfBinaryTree(TreeNode root) {
getDepth(root);
return maxLen;
}
private int getDepth(TreeNode t)
{
if(t == null) return 0;
int leftDepth = getDepth(t.left);
int rightDepth = getDepth(t.right);
maxLen = Math.max(maxLen, leftDepth+rightDepth);
return Math.max(leftDepth, rightDepth) + 1;
}
}
2. LeetCode 124. 二叉树中的最大路径和
路径被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中至多出现一次 。该路径至少包含一个节点,且不一定经过根节点。 路径和是路径中各节点值的总和。 给你一个二叉树的根节点 root ,返回其最大路径和。
求经过根节点的最大路径和的过程,实际上会递归求出经过每个二叉树单元的最大路径和。
经过一个二叉树单元的最大路径和 = 左子树根节点能贡献的最大值+右子树根节点能贡献的最大值+二叉树单元的值。左右子树实际能贡献的最大值可能小于0,因此取最大值和0的较大值。
某个二叉树单元能贡献的最大值 = 二叉树单元值 + max(左节点能贡献最大值, 右节点能贡献的最大值)。
思路和求二叉树的直径是不是很像?就连代码都是神似的!
问题 | 子问题 | 辅助问题 |
---|---|---|
二叉树直径 | 二叉树单元为转折点的路径 1+ depth(left)+depth(right) | 二叉树单元的深度 1+ max(depth(left), depth(right)) |
二叉树最大路径和 | 二叉树单元为转折点的最大和路径 node.val + gain(left)+gain(right) | 二叉树单元的最大贡献值 node.val + max(gain(left), gain(right)) |
class Solution {
int maxSum = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
maxGain(root);
return maxSum;
}
public int maxGain(TreeNode node) {
if (node == null) {
return 0;
}
// 递归计算左右子节点的最大贡献值
// 只有在最大贡献值大于 0 时,才会选取对应子节点
int leftGain = Math.max(maxGain(node.left), 0);
int rightGain = Math.max(maxGain(node.right), 0);
// 节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
int priceNewpath = node.val + leftGain + rightGain;
// 更新答案
maxSum = Math.max(maxSum, priceNewpath);
// 返回节点的最大贡献值
return node.val + Math.max(leftGain, rightGain);
}
}
3. LeetCode1425 树的直径
【二叉树的直径】的变式,只不过从树的遍历变为了图的遍历。
我们可以将每个树节点单元作为转折点,求出深度最大的两个子树作为类似于二叉树的左子树和右子树。那么,以这个树节点单元作为转折点的最大路径长度就是这两个深度的和。
由于edges一定形成一棵无向树,那么是不存在回路的,在dfs的时候,下层dfs的节点只要不访问上一层的节点即可,只需要记录一个前驱节点preNode,就可避免重复访问。
class Solution {
int max;
List<List<Integer>> graph;
public int treeDiameter(int[][] edges) {
max = 0;
buildGraph(edges);
dfs(0, -1);
return max;
}
private int dfs(int cur, int pre)
{
int maxDepth = 0, secDepth = 0;
List<Integer> neis = graph.get(cur);
for(int nei : neis)
{
if(nei != pre)
{
int tmp = dfs(nei, cur);
if(tmp > maxDepth)
{
secDepth = maxDepth;
maxDepth = tmp;
}
else if(tmp > secDepth)
{
secDepth = tmp;
}
}
max = Math.max(max, maxDepth + secDepth);
}
return maxDepth + 1;
}
private void buildGraph(int[][] edges) {
graph = new ArrayList<>();
for(int i=0; i<=edges.length; i++)
{
graph.add(new ArrayList<>());
}
for(int[] edge : edges)
{
graph.get(edge[0]).add(edge[1]);
graph.get(edge[1]).add(edge[0]);
}
}
}
当然,这题还有其他很好思路,比如:
1.从任意一个点出发找到最远的点P,从P点出发找到最远的点Q ,PQ之间的距离即为树的直径。
2.从所有度为1的端点作为起点,dfs求最大深度。 (这种近似暴力解法,判题严格时会超时)
LeetCode 1522 N 叉树的直径 跟这道题几乎一样。
有时间还可以试试这道综合题锻炼下自己:湫湫系列故事——设计风景线
里面涉及到 无向图判断是否有环,以及树的最大直径,是一道比较好的集成题。
4. LeetCode 2246 相邻字符不同的最长路径
这题又是上一题的变式,题目更是直接说“ 树(即一个连通、无向、无环图)”。只不过多了一个限制,相邻节点的字符(值)不同。值得注意的是,我们还是要从任意节点出发遍历到每一个节点,只不过当相邻的节点的字符不同时,我们才去更新最大深度。
class Solution {
int max;
List<List<Integer>> graph;
public int longestPath(int[] parent, String s) {
max = 0;
buildGraph(parent);
dfs(0, -1, s);
return max + 1;
}
private int dfs(int cur, int pre, String s)
{
int maxDepth = 0, secDepth = 0;
List<Integer> neis = graph.get(cur);
for(int nei : neis)
{
if(nei != pre)
{
int tmp = dfs(nei, cur, s);
if(s.charAt(nei) != s.charAt(cur))
{
if(tmp > maxDepth)
{
secDepth = maxDepth;
maxDepth = tmp;
}
else if(tmp > secDepth)
{
secDepth = tmp;
}
}
}
max = Math.max(max, maxDepth + secDepth);
}
return maxDepth + 1;
}
private void buildGraph(int[] parent) {
graph = new ArrayList<>();
for(int i=0; i<parent.length; i++)
{
graph.add(new ArrayList<>());
}
for(int i=0; i<parent.length; i++)
{
if(parent[i] == -1) continue;
graph.get(i).add(parent[i]);
graph.get(parent[i]).add(i);
}
}
}
5. LeetCode 2242 节点序列的最大得分
这道题也很有意思,求的是无向图中长度为4的最大序列和。如果用DFS暴力破解,那么肯定会超时,题目的边和点的数量都很大。长度为4的最大序列,需要三条边,对于每条边,我们只需要分别以它的两个端点出发,再拼接两条边。这个思路跟前面是类似的,之前几题我们的遍历单元都是树节点,这道题我们要换个思维,遍历单元是图中的边。那么,对于图中的每个点,我们只保存与它相邻的值较大的最多三个相邻节点(这里如何证明呢?),这样我们的遍历次数就降下来了。
class Solution {
public int maximumScore(int[] scores, int[][] edges) {
PriorityQueue<Integer>[] queue = new PriorityQueue[scores.length];
for (int i = 0; i < scores.length; i++) {
queue[i] = new PriorityQueue<>((o, p) -> scores[o] - scores[p]);
}
for (int[] edge : edges) {
queue[edge[0]].offer(edge[1]);
queue[edge[1]].offer(edge[0]);
if (queue[edge[0]].size() > 3) {
queue[edge[0]].poll();
}
if (queue[edge[1]].size() > 3) {
queue[edge[1]].poll();
}
}
int max = -1;
for (int[] edge : edges) {
for (int i : queue[edge[0]]) {
for (int j : queue[edge[1]]) {
if (i != edge[1] && j != edge[0] && j != i) {
max = Math.max(max, scores[edge[0]] + scores[edge[1]] + scores[i] + scores[j]);
}
}
}
}
return max;
}
}