diff --git "a/Solutions/0124. \344\272\214\345\217\211\346\240\221\344\270\255\347\232\204\346\234\200\345\244\247\350\267\257\345\276\204\345\222\214.md" "b/Solutions/0124. \344\272\214\345\217\211\346\240\221\344\270\255\347\232\204\346\234\200\345\244\247\350\267\257\345\276\204\345\222\214.md" index 7a24a763..c0a8db4a 100644 --- "a/Solutions/0124. \344\272\214\345\217\211\346\240\221\344\270\255\347\232\204\346\234\200\345\244\247\350\267\257\345\276\204\345\222\214.md" +++ "b/Solutions/0124. \344\272\214\345\217\211\346\240\221\344\270\255\347\232\204\346\234\200\345\244\247\350\267\257\345\276\204\345\222\214.md" @@ -5,13 +5,13 @@ ## 题目大意 -**描述**:给定一个二叉树的根节点 `root`。 +**描述**:给定一个二叉树的根节点 $root$。 **要求**:返回其最大路径和。 **说明**: -- **路径**:从树中的任意节点出发,沿父节点——子节点连接,到达任意节点的序列。同一个节点在一条路径序列中至多出现一次。该路径至少包含一个节点,且不一定经过根节点。 +- **路径**:被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中至多出现一次。该路径至少包含一个节点,且不一定经过根节点。 - **路径和**:路径中各节点值的总和。 - 树中节点数目范围是 $[1, 3 * 10^4]$。 - $-1000 \le Node.val \le 1000$。 @@ -40,44 +40,64 @@ ## 解题思路 -### 思路 1:深度优先搜索 +### 思路 1:树形 DP + 深度优先搜索 -使用深度优先搜索递归遍历二叉树。递归遍历的同时,维护一个最大路径和变量 `self.max_sum`。定义函数 `dfs(self, root)` 计算二叉树中以该节点为根节点,并且经过该节点的最大贡献值。 +根据最大路径和中对应路径是否穿过根节点,我们可以将二叉树分为两种: -计算的结果可能的情况有 $2$ 种: +1. 最大路径和中对应路径穿过根节点。 +2. 最大路径和中对应路径不穿过根节点。 + +如果最大路径和中对应路径穿过根节点,则:$该二叉树的最大路径和 = 左子树中最大贡献值 + 右子树中最大贡献值 + 当前节点值$。 + +而如果最大路径和中对应路径不穿过根节点,则:$该二叉树的最大路径和 = 所有子树中最大路径和$。 + +即:$该二叉树的最大路径和 = max(左子树中最大贡献值 + 右子树中最大贡献值 + 当前节点值, \quad 所有子树中最大路径和)$。 + +对此我们可以使用深度优先搜索递归遍历二叉树,并在递归遍历的同时,维护一个最大路径和变量 $ans$。 -1. 经过空节点的最大贡献值等于 `0`。 -2. 经过非空节点的最大贡献值等于「当前节点值」+「左右子节点的最大贡献值中较大的一个」。 +然后定义函数 ` def dfs(self, node):` 计算二叉树中以该节点为根节点,并且经过该节点的最大贡献值。 -在递归时,我们先计算左右子节点的最大贡献值,再更新维护当前最大路径和变量。最终 `self.max_sum` 即为答案。 +计算的结果可能的情况有 $2$ 种: + +1. 经过空节点的最大贡献值等于 $0$。 +2. 经过非空节点的最大贡献值等于 $当前节点值 + 左右子节点提供的最大贡献值中较大的一个$。如果该贡献值为负数,可以考虑舍弃,即最大贡献值为 $0$。 -具体步骤如下: +在递归时,我们先计算左右子节点的最大贡献值,再更新维护当前最大路径和变量。最终 $ans$ 即为答案。具体步骤如下: -1. 如果根节点 `root` 为空,则返回 `0`。 -2. 递归计算左子树的最大贡献值为 `left_max`。 -3. 递归计算右子树的最大贡献值为 `right_max`。 -4. 更新维护最大路径和变量,即 `self.max_sum = max(self.max_sum, root.val + left_max + right_max)`。 -5. 返回以当前节点为根节点,并且经过该节点的最大贡献值。即返回「当前节点值」+「左右子节点的最大贡献值中较大的一个」。 -6. 最终 `self.max_sum` 即为答案。 +1. 如果根节点 $root$ 为空,则返回 $0$。 +2. 递归计算左子树的最大贡献值为 $left\underline{}max$。 +3. 递归计算右子树的最大贡献值为 $right\underline{}max$。 +4. 更新维护最大路径和变量,即 $self.ans = max \lbrace self.ans, \quad left\underline{}max + right\underline{}max + node.val \rbrace$。 +5. 返回以当前节点为根节点,并且经过该节点的最大贡献值。即返回 $当前节点值 + 左右子节点提供的最大贡献值中较大的一个$。 +6. 最终 $self.ans$ 即为答案。 ### 思路 1:代码 ```Python +# Definition for a binary tree node. +# class TreeNode: +# def __init__(self, val=0, left=None, right=None): +# self.val = val +# self.left = left +# self.right = right class Solution: def __init__(self): - self.max_sum = float('-inf') - - def dfs(self, root): - if not root: + self.ans = float('-inf') + + def dfs(self, node): + if not node: return 0 - left_max = max(self.dfs(root.left), 0) - right_max = max(self.dfs(root.right), 0) - self.max_sum = max(self.max_sum, root.val + left_max + right_max) - return root.val + max(left_max, right_max) + left_max = max(self.dfs(node.left), 0) # 左子树提供的最大贡献值 + right_max = max(self.dfs(node.right), 0) # 右子树提供的最大贡献值 + + cur_max = left_max + right_max + node.val # 包含当前节点和左右子树的最大路径和 + self.ans = max(self.ans, cur_max) # 更新所有路径中的最大路径和 + + return max(left_max, right_max) + node.val # 返回包含当前节点的子树的最大贡献值 - def maxPathSum(self, root: TreeNode) -> int: + def maxPathSum(self, root: Optional[TreeNode]) -> int: self.dfs(root) - return self.max_sum + return self.ans ``` ### 思路 1:复杂度分析 diff --git "a/Solutions/0543. \344\272\214\345\217\211\346\240\221\347\232\204\347\233\264\345\276\204.md" "b/Solutions/0543. \344\272\214\345\217\211\346\240\221\347\232\204\347\233\264\345\276\204.md" index f81d6deb..cbb4aa41 100644 --- "a/Solutions/0543. \344\272\214\345\217\211\346\240\221\347\232\204\347\233\264\345\276\204.md" +++ "b/Solutions/0543. \344\272\214\345\217\211\346\240\221\347\232\204\347\233\264\345\276\204.md" @@ -5,7 +5,7 @@ ## 题目大意 -**描述**:给一个二叉树的根节点 `root`。 +**描述**:给一个二叉树的根节点 $root$。 **要求**:计算该二叉树的直径长度。 @@ -13,6 +13,7 @@ - **二叉树的直径长度**:二叉树中任意两个节点路径长度中的最大值。 - 两节点之间的路径长度是以它们之间边的数目表示。 +- 这条路径可能穿过也可能不穿过根节点。 **示例**: @@ -31,36 +32,58 @@ ## 解题思路 -### 思路 1:深度优先搜索 +### 思路 1:树形 DP + 深度优先搜索 -这道题的重点是理解直径长度的定义。这里的直径并不是简单的「左子树高度」+「右子树高度」。 +这道题重点是理解直径长度的定义。「二叉树的直径长度」的定义为:二叉树中任意两个节点路径长度中的最大值。并且这条路径可能穿过也可能不穿过根节点。 -而是 `当前节点的直径 = max{左子树高度+右子树高度,所有子树中最大直径}`。 +对于根为 $root$ 的二叉树来说,其直径长度并不简单等于「左子树高度」加上「右子树高度」。 -也就是说当前节点的直径可能来自于 「左子树高度」+「右子树高度」,也可能来自于「子树中的最大直径」。 +根据路径是否穿过根节点,我们可以将二叉树分为两种: -这就需要在递归求解子树高度的时候维护一个 `maxDiameter` 变量。每次递归都要去判断 当前「左子树高度」+「右子树的高度」是否大于 `self.maxDiameter`,如果大于,则更新最大值。 +1. 直径长度所对应的路径穿过根节点。 +2. 直径长度所对应的路径不穿过根节点。 + +我们来看下图中的两个例子。 + +![](https://qcdn.itcharge.cn/images/20230427111005.png) + +如图所示,左侧这棵二叉树就是一棵常见的平衡二叉树,其直径长度所对应的路径是穿过根节点的($D\rightarrow B \rightarrow A \rightarrow C$)。这种情况下:$二叉树的直径 = 左子树高度 + 右子树高度$。 + +而右侧这棵特殊的二叉树,其直径长度所对应的路径是没有穿过根节点的($F \rightarrow D \rightarrow B \rightarrow E \rightarrow G$)。这种情况下:$二叉树的直径 = 所有子树中最大直径长度$。 + +也就是说根为 $root$ 的二叉树的直径长度可能来自于 $左子树高度 + 右子树高度$,也可能来自于 $子树中的最大直径$,即 $二叉树的直径 = max(左子树高度 + 右子树高度, \quad 所有子树中最大直径长度)$。 + +那么现在问题就变成为如何求「子树的高度」和「子树中的最大直径」。 + +1. 子树的高度:我们可以利用深度优先搜索方法,递归遍历左右子树,并分别返回左右子树的高度。 +2. 子树中的最大直径:我们可以在递归求解子树高度的时候维护一个 $ans$ 变量,用于记录所有 $左子树高度 + 右子树高度$ 中的最大值。 + +最终 $ans$ 就是我们所求的该二叉树的最大直径,将其返回即可。 ### 思路 1:代码 ```Python +# Definition for a binary tree node. +# class TreeNode: +# def __init__(self, val=0, left=None, right=None): +# self.val = val +# self.left = left +# self.right = right class Solution: def __init__(self): - # 保存当前最大直径 - self.maxDiameter = 0 + self.ans = 0 - def diameterOfBinaryTree(self, root: TreeNode) -> int: - self.height(root) - return self.maxDiameter - - def height(self, root): - if root == None: + def dfs(self, node): + if not node: return 0 - leftHeight = self.height(root.left) - rightHeight = self.height(root.right) - self.maxDiameter = max(self.maxDiameter, leftHeight + rightHeight) - - return max(leftHeight, rightHeight) + 1 + left_height = self.dfs(node.left) # 左子树高度 + right_height = self.dfs(node.right) # 右子树高度 + self.ans = max(self.ans, left_height + right_height) # 维护所有路径中的最大直径 + return max(left_height, right_height) + 1 # 返回该节点的高度 = 左右子树最大高度 + 1 + + def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int: + self.dfs(root) + return self.ans ``` ### 思路 1:复杂度分析 diff --git "a/Solutions/2246. \347\233\270\351\202\273\345\255\227\347\254\246\344\270\215\345\220\214\347\232\204\346\234\200\351\225\277\350\267\257\345\276\204.md" "b/Solutions/2246. \347\233\270\351\202\273\345\255\227\347\254\246\344\270\215\345\220\214\347\232\204\346\234\200\351\225\277\350\267\257\345\276\204.md" new file mode 100644 index 00000000..5e555a73 --- /dev/null +++ "b/Solutions/2246. \347\233\270\351\202\273\345\255\227\347\254\246\344\270\215\345\220\214\347\232\204\346\234\200\351\225\277\350\267\257\345\276\204.md" @@ -0,0 +1,101 @@ +# [2246. 相邻字符不同的最长路径](https://leetcode.cn/problems/longest-path-with-different-adjacent-characters/) + +- 标签:树、深度优先搜索、图、拓扑排序、数组、字符串 +- 难度:困难 + +## 题目大意 + +**描述**:给定一个长度为 $n$ 的数组 $parent$ 来表示一棵树(即一个连通、无向、无环图)。该树的节点编号为 $0 \sim n - 1$,共 $n$ 个节点,其中根节点的编号为 $0$。其中 $parent[i]$ 表示节点 $i$ 的父节点,由于节点 $0$ 是根节点,所以 $parent[0] == -1$。再给定一个长度为 $n$ 的字符串,其中 $s[i]$ 表示分配给节点 $i$ 的字符。 + +**要求**:找出路径上任意一对相邻节点都没有分配到相同字符的最长路径,并返回该路径的长度。 + +**说明**: + +- $n == parent.length == s.length$。 +- $1 \le n \le 10^5$。 +- 对所有 $i \ge 1$ ,$0 \le parent[i] \le n - 1$ 均成立。 +- $parent[0] == -1$。 +- $parent$ 表示一棵有效的树。 +- $s$ 仅由小写英文字母组成。 + +**示例**: + +- 示例 1: + +![](https://assets.leetcode.com/uploads/2022/03/25/testingdrawio.png) + +```Python +输入:parent = [-1,0,0,1,1,2], s = "abacbe" +输出:3 +解释:任意一对相邻节点字符都不同的最长路径是:0 -> 1 -> 3 。该路径的长度是 3 ,所以返回 3。 +可以证明不存在满足上述条件且比 3 更长的路径。 +``` + +- 示例 2: + +![](https://assets.leetcode.com/uploads/2022/03/25/graph2drawio.png) + +```Python +输入:parent = [-1,0,0,0], s = "aabc" +输出:3 +解释:任意一对相邻节点字符都不同的最长路径是:2 -> 0 -> 3 。该路径的长度为 3 ,所以返回 3。 +``` + +## 解题思路 + +### 思路 1:树形 DP + 深度优先搜索 + +因为题目给定的是表示父子节点的 $parent$ 数组,为了方便递归遍历相邻节点,我们可以根据 $partent$ 数组,建立一个由父节点指向子节点的有向图 $graph$。 + +如果不考虑相邻节点是否为相同字符这一条件,那么这道题就是在求树的直径(树的最长路径长度)中的节点个数。 + +对于根节点为 $u$ 的树来说: + +1. 如果其最长路径经过根节点 $u$,则 $最长路径长度 = 某子树中的最长路径长度 + 另一子树中的最长路径长度 + 1$。 +2. 如果其最长路径不经过根节点 $u$,则 $最长路径长度 = 某个子树中的最长路径长度$。 + +即:$最长路径长度 = max(某子树中的最长路径长度 + 另一子树中的最长路径长度 + 1, \quad 某个子树中的最长路径长度)$。 + +对此,我们可以使用深度优先搜索递归遍历 $u$ 的所有相邻节点 $v$,并在递归遍历的同时,维护一个全局最大路径和变量 $ans$,以及当前节点 $u$ 的最大路径长度变量 $u\underline{}len$。 + +1. 先计算出从相邻节点 $v$ 出发的最长路径长度 $v\underline{}len$。 +2. 更新维护全局最长路径长度为 $self.ans = max(self.ans, \quad u\underline{}len + v\underline{}len + 1)$。 +3. 更新维护当前节点 $u$ 的最长路径长度为 $u\underline{}len = max(u\underline{}len, \quad v\underline{}len + 1)$。 + +因为题目限定了「相邻节点字符不同」,所以在更新全局最长路径长度和当前节点 $u$ 的最长路径长度时,我们需要判断一下节点 $u$ 与相邻节点 $v$ 的字符是否相同,只有在字符不同的条件下,才能够更新维护。 + +最后,因为题目要求的是树的直径(树的最长路径长度)中的节点个数,而:$路径的节点 = 路径长度 + 1$,所以最后我们返回 $self.ans + 1$ 作为答案。 + +### 思路 1:代码 + +```Python +class Solution: + def __init__(self): + self.ans = 0 + + def dfs(self, graph, s, u): + u_len = 0 # u 节点的最大路径长度 + for v in graph[u]: # 遍历 u 节点的相邻节点 + v_len = self.dfs(graph, s, v) # 相邻节点的最大路径长度 + if s[u] != s[v]: # 相邻节点字符不同 + self.ans = max(self.ans, u_len + v_len + 1) # 维护最大路径长度 + u_len = max(u_len, v_len + 1) # 更新 u 节点的最大路径长度 + return u_len # 返回 u 节点的最大路径长度 + + def longestPath(self, parent: List[int], s: str) -> int: + size = len(parent) + + # 根据 parent 数组,建立有向图 + graph = [[] for _ in range(size)] + for i in range(1, size): + graph[parent[i]].append(i) + + self.dfs(graph, s, 0) + + return self.ans + 1 +``` + +### 思路 1:复杂度分析 + +- **时间复杂度**:$O(n)$,其中 $n$ 是树的节点数目。 +- **空间复杂度**:$O(n)$