上接:【题解】—— LeetCode一周小结10
11.将标题首字母大写
题目链接:2129. 将标题首字母大写
给你一个字符串 title ,它由单个空格连接一个或多个单词组成,每个单词都只包含英文字母。请你按以下规则将每个单词的首字母 大写 :
如果单词的长度为 1 或者 2 ,所有字母变成小写。
否则,将单词首字母大写,剩余字母变成小写。
请你返回 大写后 的 title 。
示例 1:
输入:title = “capiTalIze tHe titLe”
输出:“Capitalize The Title”
解释:
由于所有单词的长度都至少为 3 ,将每个单词首字母大写,剩余字母变为小写。
示例 2:
输入:title = “First leTTeR of EACH Word”
输出:“First Letter of Each Word”
解释:
单词 “of” 长度为 2 ,所以它保持完全小写。
其他单词长度都至少为 3 ,所以其他单词首字母大写,剩余字母小写。
示例 3:
输入:title = “i lOve leetcode”
输出:“i Love Leetcode”
解释:
单词 “i” 长度为 1 ,所以它保留小写。
其他单词长度都至少为 3 ,所以其他单词首字母大写,剩余字母小写。
提示:
1 <= title.length <= 100
title 由单个空格隔开的单词组成,且不含有任何前导或后缀空格。
每个单词由大写和小写英文字母组成,且都是 非空 的。
题解:
目的将给定标题字符串中的单词首字母大写,并将其余字母转换为小写。
方法:通过遍历标题字符串中的每个单词,对每个单词进行处理:如果单词长度大于 2,则将单词的首字母大写,并将其余字母转换为小写;然后将处理后的单词添加到结果字符串中。
public class Solution { /** * 将给定标题字符串中的单词首字母大写,并将其余字母转换为小写。 * @param title 给定的标题字符串 * @return 首字母大写的标题字符串 */ public String capitalizeTitle(String title) { StringBuilder ans = new StringBuilder(); // 用于构建结果的字符串 for (String s : title.split(" ")) { // 按空格分割标题字符串,并遍历每个单词 if (!ans.isEmpty()) { // 如果结果字符串非空,则在单词间添加空格 ans.append(' '); } if (s.length() > 2) { // 如果单词长度大于 2 ans.append(s.substring(0, 1).toUpperCase()); // 将单词首字母大写 s = s.substring(1); // 去掉首字母 } ans.append(s.toLowerCase()); // 将单词中剩余字母转换为小写,并添加到结果字符串中 } return ans.toString(); // 返回首字母大写的标题字符串 } }
12.在受污染的二叉树中查找元素
题目链接:1261. 在受污染的二叉树中查找元素
给出一个满足下述规则的二叉树:
- root.val == 0
- 如果 treeNode.val == x 且 treeNode.left != null,那么 treeNode.left.val == 2 * x + 1
- 如果 treeNode.val == x 且 treeNode.right != null,那么 treeNode.right.val == 2 * x + 2
现在这个二叉树受到「污染」,所有的 treeNode.val 都变成了 -1。
请你先还原二叉树,然后实现 FindElements 类:
- FindElements(TreeNode* root) 用受污染的二叉树初始化对象,你需要先把它还原。
- bool find(int target) 判断目标值 target 是否存在于还原后的二叉树中并返回结果。
示例 1:
输入:
[“FindElements”,“find”,“find”]
[[[-1,null,-1]],[1],[2]]
输出:
[null,false,true]
解释:
FindElements findElements = new FindElements([-1,null,-1]);
findElements.find(1); // return False
findElements.find(2); // return True
示例 2:
输入:
[“FindElements”,“find”,“find”,“find”]
[[[-1,-1,-1,-1,-1]],[1],[3],[5]]
输出:
[null,true,true,false]
解释:
FindElements findElements = new FindElements([-1,-1,-1,-1,-1]);
findElements.find(1); // return True
findElements.find(3); // return True
findElements.find(5); // return False
示例 3:
输入:
[“FindElements”,“find”,“find”,“find”,“find”]
[[[-1,null,-1,-1,null,-1]],[2],[3],[4],[5]]
输出:
[null,true,false,false,true]
解释:
FindElements findElements = new FindElements([-1,null,-1,-1,null,-1]);
findElements.find(2); // return True
findElements.find(3); // return False
findElements.find(4); // return False
findElements.find(5); // return True
提示:
TreeNode.val == -1
二叉树的高度不超过 20
节点的总数在 [1, 10^4] 之间
调用 find() 的总次数在 [1, 10^4] 之间
0 <= target <= 10^6
题解:
方法:哈希表
代码定义了一个名为 FindElements
的类,用于在恢复的二叉树中搜索特定的元素。采用了哈希表方法来实现高效的搜索。类中包含一个私有成员变量 s
,用于存储所有恢复的元素。
该类提供了以下方法:
FindElements(TreeNode root)
:构造方法,根据给定的根节点初始化一个FindElements
对象,并恢复二叉树中的所有元素。find(int target)
:检查恢复的二叉树中是否存在指定的目标值。dfs(TreeNode node, int val)
:递归遍历二叉树,并恢复所有元素的方法。
import java.util.HashSet; import java.util.Set; /** * FindElements 类用于在恢复的二叉树中搜索特定的元素。 * 它采用了哈希表方法来实现高效的搜索。 */ class FindElements { private final Set<Integer> s = new HashSet<>(); // 使用 HashSet 存储所有恢复的元素 /** * 初始化 FindElements 对象,并根据给定的根节点恢复二叉树中的所有元素。 * @param root 二叉树的根节点。 */ public FindElements(TreeNode root) { dfs(root, 0); // 从根节点开始深度优先遍历恢复元素 } /** * 检查恢复的二叉树中是否存在指定的目标值。 * @param target 目标值。 * @return 如果目标值存在于恢复的二叉树中,则返回 true,否则返回 false。 */ public boolean find(int target) { return s.contains(target); // 检查 HashSet 中是否包含目标值 } /** * 深度优先遍历二叉树,并恢复所有元素。 * @param node 当前正在处理的节点。 * @param val 当前节点的值。 */ private void dfs(TreeNode node, int val) { if (node == null) { // 如果当前节点为空,直接返回 return; } s.add(val); // 将当前节点的值添加到 HashSet 中 dfs(node.left, val * 2 + 1); // 递归遍历左子树 dfs(node.right, val * 2 + 2); // 递归遍历右子树 } }
方法:二进制路径
- 构造方法 (
FindElements(TreeNode root)
): 在构造方法中,将恢复的二叉树的根节点传入,以便后续搜索。 - 搜索方法 (
find(int target)
):在搜索方法中,通过对目标值进行二进制路径搜索来确定目标元素是否存在于二叉树中。具体步骤如下:
- 对目标值进行处理,以便与二进制路径匹配。
- 从根节点开始,根据目标值的二进制表示,选择左子节点或右子节点。
- 沿着二进制路径向下移动,直到遇到空节点或者搜索完目标值的所有比特位。
- 如果遇到空节点,则说明目标元素不存在于二叉树中,返回
false
;否则,返回true
,表示目标元素存在于二叉树中。
通过这种方法,可以在恢复的二叉树中快速定位特定的元素,提高了搜索的效率。
/** * FindElements 类用于在恢复的二叉树中搜索特定的元素。 * 它采用了二进制路径方法来实现高效的搜索。 */ class FindElements { private TreeNode root; // 二叉树的根节点 /** * 初始化 FindElements 对象,并设置根节点。 * @param root 二叉树的根节点。 */ public FindElements(TreeNode root) { this.root = root; // 设置根节点 } /** * 搜索是否存在目标值。 * @param target 目标值。 * @return 如果目标值存在于恢复的二叉树中,则返回 true,否则返回 false。 */ public boolean find(int target) { target++; // 对目标值进行处理 TreeNode cur = root; // 从根节点开始搜索 // 从目标值的次高位开始向下搜索二进制路径 for (int i = 30 - Integer.numberOfLeadingZeros(target); i >= 0; i--) { int bit = (target >> i) & 1; // 获取目标值在当前位的比特值 cur = bit == 0 ? cur.left : cur.right; // 根据比特值选择左子节点或右子节点 if (cur == null) { // 如果当前节点为空,表示目标值不在二叉树中 return false; } } return true; // 没有遇到空节点,目标值存在于二叉树中 } }
13.最大二进制奇数
题目链接:2864. 最大二进制奇数
给你一个 二进制 字符串 s ,其中至少包含一个 ‘1’ 。
你必须按某种方式 重新排列 字符串中的位,使得到的二进制数字是可以由该组合生成的 最大二进制奇数 。
以字符串形式,表示并返回可以由给定组合生成的最大二进制奇数。
注意 返回的结果字符串 可以 含前导零。
示例 1:
输入:s = “010”
输出:“001”
解释:因为字符串 s 中仅有一个 ‘1’ ,其必须出现在最后一位上。所以答案是 “001” 。
示例 2:
输入:s = “0101”
输出:“1001”
解释:其中一个 ‘1’ 必须出现在最后一位上。而由剩下的数字可以生产的最大数字是 “100” 。所以答案是 “1001” 。
提示:
1 <= s.length <= 100
s 仅由 ‘0’ 和 ‘1’ 组成
s 中至少包含一个 ‘1’
题解:
方法:贪心
由于奇数的二进制末尾一定是 1,我们可以把一个 1 放在末尾,其余的 1 全部放在开头,这样构造出的奇数尽量大。
public class Solution { /** * 将给定的二进制字符串转换为最大奇数的二进制字符串。 * @param s 给定的二进制字符串 * @return 最大奇数的二进制字符串 */ public String maximumOddBinaryNumber(String s) { // 统计字符串中 '1' 的个数 int cnt1 = (int) s.chars().filter(c -> c == '1').count(); // 构造最大奇数的二进制字符串 // '1' 的个数减一后重复 '1',然后加上剩余长度的 '0',最后再加一个 '1' return "1".repeat(cnt1 - 1) + "0".repeat(s.length() - cnt1) + "1"; } }
14.合并后数组中的最大元素
题目链接:2789. 合并后数组中的最大元素
给你一个下标从 0 开始、由正整数组成的数组 nums 。
你可以在数组上执行下述操作 任意 次:
- 选中一个同时满足 0 <= i < nums.length - 1 和 nums[i] <= nums[i + 1] 的整数 i 。将元素 nums[i + 1] 替换为 nums[i] + nums[i + 1] ,并从数组中删除元素 nums[i] 。
返回你可以从最终数组中获得的 最大 元素的值。
示例 1:
输入:nums = [2,3,7,9,3]
输出:21
解释:我们可以在数组上执行下述操作:
- 选中 i = 0 ,得到数组 nums = [5,7,9,3] 。
- 选中 i = 1 ,得到数组 nums = [5,16,3] 。
- 选中 i = 0 ,得到数组 nums = [21,3] 。
最终数组中的最大元素是 21 。可以证明我们无法获得更大的元素。
示例 2:
输入:nums = [5,3,3]
输出:11
解释:我们可以在数组上执行下述操作:
- 选中 i = 1 ,得到数组 nums = [5,6] 。
- 选中 i = 0 ,得到数组 nums = [11] 。
最终数组中只有一个元素,即 11 。
提示:
1 <= nums.length <= 105
1 <= nums[i] <= 106
题解:
方法:枚举 遍历
- 获取数组的长度。
- 将数组的最后一个元素作为初始的子数组和。
- 从倒数第二个元素开始向前遍历数组:
- 如果当前元素与之前的子数组和相加后的结果比当前元素大,则更新子数组和为相加后的结果;
- 否则更新子数组和为当前元素,表示从当前元素开始重新计算子数组和。
- 返回最大的子数组和。
class Solution { /** * 寻找数组中最大的子数组和。 * @param nums 给定的整数数组 * @return 最大的子数组和 */ public long maxArrayValue(int[] nums) { int n = nums.length; // 获取数组长度 long sum = nums[n - 1]; // 将最后一个元素作为初始的子数组和 // 从倒数第二个元素开始向前遍历数组 for (int i = n - 2; i >= 0; i--) { // 如果当前元素与之前的子数组和相加后的结果比当前元素大,则更新子数组和为相加后的结果, // 否则更新子数组和为当前元素,表示从当前元素开始重新计算子数组和 sum = nums[i] <= sum ? sum + nums[i] : nums[i]; } return sum; // 返回最大的子数组和 } }
15.卖木头块
题目链接:2312. 卖木头块
给你两个整数 m 和 n ,分别表示一块矩形木块的高和宽。同时给你一个二维整数数组 prices ,其中 prices[i] = [hi, wi, pricei] 表示你可以以 pricei 元的价格卖一块高为 hi 宽为 wi 的矩形木块。
每一次操作中,你必须按下述方式之一执行切割操作,以得到两块更小的矩形木块:
- 沿垂直方向按高度 完全 切割木块,或
- 沿水平方向按宽度 完全 切割木块
在将一块木块切成若干小木块后,你可以根据 prices 卖木块。你可以卖多块同样尺寸的木块。你不需要将所有小木块都卖出去。你 不能 旋转切好后木块的高和宽。
请你返回切割一块大小为 m x n 的木块后,能得到的 最多 钱数。
注意你可以切割木块任意次。
示例 1:
输入:m = 3, n = 5, prices = [[1,4,2],[2,2,7],[2,1,3]]
输出:19
解释:上图展示了一个可行的方案。包括:
- 2 块 2 x 2 的小木块,售出 2 * 7 = 14 元。
- 1 块 2 x 1 的小木块,售出 1 * 3 = 3 元。
- 1 块 1 x 4 的小木块,售出 1 * 2 = 2 元。
总共售出 14 + 3 + 2 = 19 元。 19 元是最多能得到的钱数。
示例 2:
输入:m = 4, n = 6, prices = [[3,2,10],[1,4,2],[4,1,3]]
输出:32
解释:上图展示了一个可行的方案。包括:
- 3 块 3 x 2 的小木块,售出 3 * 10 = 30 元。
- 1 块 1 x 4 的小木块,售出 1 * 2 = 2 元。
总共售出 30 + 2 = 32 元。 32 元是最多能得到的钱数。 注意我们不能旋转 1 x 4 的木块来得到 4 x 1 的木块。
提示:
1 <= m, n <= 200
1 <= prices.length <= 2 * 104
prices[i].length == 3
1 <= hi <= m
1 <= wi <= n
1 <= pricei <= 106
所有 (hi, wi) 互不相同 。
题解:
方法:动态规划(枚举切割位置+循环优化)
思路:
- 创建一个二维数组
pr
,用于存储每个切割方案的价格,其中pr[i][j]
表示在长度为i
、宽度为j
的木材上的切割方案价格。 - 将给定的切割方案价格赋值到
pr
数组的对应位置。 - 创建一个二维数组
f
,用于存储每个位置的最大收益,其中f[i][j]
表示在长度为i
、宽度为j
的木材上的最大收益。 - 遍历木材的长度和宽度:
- 对于每个位置
(i, j)
,初始情况下,其收益就是切割方案的价格pr[i][j]
。 - 针对每个位置,分别考虑垂直切割和水平切割的情况,找到使收益最大的切割方案。
- 返回最大收益,即
f[m][n]
。
class Solution { /** * 计算卖出木材的最大收益。 * @param m 木材的长度 * @param n 木材的宽度 * @param prices 二维数组,表示每个切割方案的价格 * @return 最大收益 */ public long sellingWood(int m, int n, int[][] prices) { int[][] pr = new int[m + 1][n + 1]; // 创建一个二维数组存储每个切割方案的价格 for (int[] p : prices) { pr[p[0]][p[1]] = p[2]; // 将给定的切割方案价格赋值到对应的位置 } long[][] f = new long[m + 1][n + 1]; // 创建一个二维数组存储每个位置的最大收益 for (int i = 1; i <= m; i++) { // 遍历木材的长度 for (int j = 1; j <= n; j++) { // 遍历木材的宽度 f[i][j] = pr[i][j]; // 初始情况下,收益就是切割方案的价格 // 垂直切割:将木材垂直切割成两部分,找到使收益最大的切割方案 for (int k = 1; k < j; k++) { f[i][j] = Math.max(f[i][j], f[i][k] + f[i][j - k]); } // 水平切割:将木材水平切割成两部分,找到使收益最大的切割方案 for (int k = 1; k < i; k++) { f[i][j] = Math.max(f[i][j], f[k][j] + f[i - k][j]); } } } return f[m][n]; // 返回最大收益 } }
优化思路:
- 垂直切割时,我们只需考虑长度为
i
的木材在宽度上的一半以下的切割方案,因为长度为i
的木材在宽度上一半以上的切割方案与之对称,不需要重复计算。 - 水平切割时,同样只需考虑宽度为
j
的木材在长度上的一半以下的切割方案,同样利用对称性避免重复计算。
class Solution { /** * 计算卖出木材的最大收益(优化版)。 * @param m 木材的长度 * @param n 木材的宽度 * @param prices 二维数组,表示每个切割方案的价格 * @return 最大收益 */ public long sellingWood(int m, int n, int[][] prices) { long[][] f = new long[m + 1][n + 1]; // 创建一个二维数组存储每个位置的最大收益 for (int[] p : prices) { f[p[0]][p[1]] = p[2]; // 将给定的切割方案价格赋值到对应的位置 } for (int i = 1; i <= m; i++) { // 遍历木材的长度 for (int j = 1; j <= n; j++) { // 遍历木材的宽度 // 垂直切割:将木材垂直切割成两部分,找到使收益最大的切割方案 for (int k = 1; k <= j / 2; k++) { f[i][j] = Math.max(f[i][j], f[i][k] + f[i][j - k]); } // 水平切割:将木材水平切割成两部分,找到使收益最大的切割方案 for (int k = 1; k <= i / 2; k++) { f[i][j] = Math.max(f[i][j], f[k][j] + f[i - k][j]); } } } return f[m][n]; // 返回最大收益 } }
16.矩阵中移动的最大次数
题目链接:2684. 矩阵中移动的最大次数
给你一个下标从 0 开始、大小为 m x n 的矩阵 grid ,矩阵由若干 正 整数组成。
- 你可以从矩阵第一列中的 任一 单元格出发,按以下方式遍历 grid :
从单元格 (row, col) 可以移动到 (row - 1, col + 1)、(row, col + 1) 和 (row + 1, col + 1) 三个单元格中任一满足值 严格 大于当前单元格的单元格。
返回你在矩阵中能够 移动 的 最大 次数。
示例 1:
输入:grid = [[2,4,3,5],[5,4,9,3],[3,4,2,11],[10,9,13,15]]
输出:3
解释:可以从单元格 (0, 0) 开始并且按下面的路径移动:
- (0, 0) -> (0, 1).
- (0, 1) -> (1, 2).
- (1, 2) -> (2, 3).
可以证明这是能够移动的最大次数。
示例 2:
输入:grid = [[3,2,4],[2,1,9],[1,1,7]]
输出:0
解释:从第一列的任一单元格开始都无法移动。
提示:
m == grid.length
n == grid[i].length
2 <= m, n <= 1000
4 <= m * n <= 105
1 <= grid[i][j] <= 106
题解:
方法1:深度优先搜索(DFS)
使用深度优先搜索(DFS)算法,从第一列的任一单元格出发,递归地探索可到达的最大步数。
在每个单元格处,分别尝试向右上、右、右下三个方向移动一步,如果下一步的高度高于当前单元格的高度,则可以继续移动。
递归搜索过程中,通过更新 ans
变量记录最大移动步数,当 ans
达到最大值时,停止搜索,提前返回结果。
class Solution { private int ans; // 存储最大移动步数 /** * 计算在给定网格中,从第一列的任一单元格出发,能够移动的最大步数。 * @param grid 给定的二维网格 * @return 最大移动步数 */ public int maxMoves(int[][] grid) { for (int i = 0; i < grid.length; i++) { dfs(i, 0, grid); // 从第一列的任一单元格出发,进行深度优先搜索 } return ans; // 返回最大移动步数 } /** * 深度优先搜索函数,用于递归地搜索可以到达的最大步数。 * @param i 当前单元格的行索引 * @param j 当前单元格的列索引 * @param grid 给定的二维网格 */ private void dfs(int i, int j, int[][] grid) { ans = Math.max(ans, j); // 更新最大移动步数 if (ans == grid[0].length - 1) { // 如果已达到最大值,则无需继续搜索 return; } // 向右上/右/右下走一步,探索下一步可行的移动方向 for (int k = Math.max(i - 1, 0); k < Math.min(i + 2, grid.length); k++) { if (grid[k][j + 1] > grid[i][j]) { // 仅当下一步高度高于当前单元格时,才可以移动 dfs(k, j + 1, grid); // 递归搜索下一步 } } grid[i][j] = 0; // 标记当前单元格已经访问过 } }
方法2:广度优先搜索(BFS)算法
使用广度优先搜索(BFS)算法,在每一列的基础上,逐列进行搜索。
维护一个 vis
数组,用于记录每一行最右边已经访问到的列索引,初始化为 -1。
初始时,将所有行的索引添加到 q
列表中,表示从第一列的任一单元格出发可以移动的行索引。
对于每一列,遍历当前可移动的行索引列表 q
,在每一行的上下范围内检查是否满足条件可以移动,如果满足则将该行索引添加到下一步可移动的列表中。
如果下一步没有可移动的行索引,说明无法再往右移动了,返回当前列索引。
如果遍历完所有列都有可移动的行索引,则返回最后一列的索引。
import java.util.*; class Solution { /** * 计算在给定网格中,从第一列的任一单元格出发,能够移动的最大步数。 * @param grid 给定的二维网格 * @return 最大移动步数 */ public int maxMoves(int[][] grid) { int m = grid.length; // 网格的行数 int n = grid[0].length; // 网格的列数 int[] vis = new int[m]; // 用于记录每一行最右边已访问到的列索引 Arrays.fill(vis, -1); // 初始化为 -1,表示未访问 List<Integer> q = new ArrayList<>(m); // 存储当前可移动的行索引 for (int i = 0; i < m; i++) { q.add(i); // 初始化为所有行 } for (int j = 0; j < n - 1; j++) { // 从第一列开始逐列进行搜索 List<Integer> nxt = new ArrayList<>(); // 存储下一步可移动的行索引 for (int i : q) { // 遍历当前可移动的行索引 for (int k = Math.max(i - 1, 0); k < Math.min(i + 2, m); k++) { // 在当前行的上下范围内进行搜索 if (vis[k] != j && grid[k][j + 1] > grid[i][j]) { // 检查是否满足条件可以移动 vis[k] = j; // 更新第 k 行最右边访问到的列索引 nxt.add(k); // 将可移动的行索引添加到下一步可移动的列表中 } } } if (nxt.isEmpty()) { // 如果下一步没有可移动的行索引,说明无法再往右移动了,返回当前列索引 return j; } q = nxt; // 更新当前可移动的行索引列表 } return n - 1; // 如果遍历完所有列都有可移动的行索引,则返回最后一列的索引 } }
优化思路:
- 遍历网格的每一列,从第一列开始,判断是否能够继续向右移动。
- 使用负值作为入队标记,在每一列中标记可以继续向右移动的单元格。
- 在遍历每一列时,对当前列中的每一个单元格进行考虑,如果当前单元格的值小于等于 0,说明它可以移动到其周围的某些单元格,即相邻单元格的值要大于当前单元格的绝对值。
- 如果当前列无法继续向右移动,则返回当前已经遍历到的列索引;如果遍历完所有列仍能继续向右移动,则返回最后一列的索引。
class Solution { /** * 计算在给定网格中,从第一列的任一单元格出发,能够移动的最大步数。 * @param grid 给定的二维网格 * @return 最大移动步数 */ public int maxMoves(int[][] grid) { int m = grid.length; // 网格行数 int n = grid[0].length; // 网格列数 // 使用负值作为入队标记,将第一列网格元素的值全部取相反数 for (int[] row : grid) { row[0] *= -1; // 入队标记 } // 从第一列开始进行遍历,判断是否可以继续向右移动 for (int j = 0; j < n - 1; j++) { boolean ok = false; // 记录是否可以继续向右移动的标志 for (int i = 0; i < m; i++) { if (grid[i][j] > 0) { // 如果当前单元格值大于 0,说明不在队列中,无需考虑 continue; } // 遍历当前单元格周围的相邻单元格 for (int k = Math.max(i - 1, 0); k < Math.min(i + 2, m); k++) { // 如果相邻单元格的值大于当前单元格的绝对值,说明可以移动到该相邻单元格 if (grid[k][j + 1] > -grid[i][j]) { grid[k][j + 1] *= -1; // 入队标记,将相邻单元格的值取相反数 ok = true; // 设置标志为 true,表示可以继续向右移动 } } } // 如果无法继续向右移动,则返回当前已经遍历到的列索引 if (!ok) { return j; } } // 如果遍历完所有列仍能继续向右移动,则返回最后一列的索引 return n - 1; } }
17.最小高度树
题目链接:310. 最小高度树
树是一个无向图,其中任何两个顶点只通过一条路径连接。 换句话说,一个任何没有简单环路的连通图都是一棵树。
给你一棵包含 n 个节点的树,标记为 0 到 n - 1 。给定数字 n 和一个有 n - 1 条无向边的 edges 列表(每一个边都是一对标签),其中 edges[i] = [ai, bi] 表示树中节点 ai 和 bi 之间存在一条无向边。
可选择树中任何一个节点作为根。当选择节点 x 作为根节点时,设结果树的高度为 h 。在所有可能的树中,具有最小高度的树(即,min(h))被称为 最小高度树 。
请你找到所有的 最小高度树 并按 任意顺序 返回它们的根节点标签列表。
树的 高度 是指根节点和叶子节点之间最长向下路径上边的数量。
示例 1:
输入:n = 4, edges = [[1,0],[1,2],[1,3]]
输出:[1]
解释:如图所示,当根是标签为 1 的节点时,树的高度是 1 ,这是唯一的最小高度树。
示例 2:
输入:n = 6, edges = [[3,0],[3,1],[3,2],[3,4],[5,4]]
输出:[3,4]
提示:
1 <= n <= 2 * 104
edges.length == n - 1
0 <= ai, bi < n
ai != bi
所有 (ai, bi) 互不相同
给定的输入 保证 是一棵树,并且 不会有重复的边
题解:
方法:**树形 DP **
初始化一些数组和索引,包括邻接表所需的数组和节点相关的辅助数组。
添加边关系到邻接表中。
使用DFS进行两次遍历,分别求得每个节点到其它节点的最大距离和次大距离。
遍历所有节点,找出使得高度最小的根节点。如果有多个根节点高度相同,都加入到结果集合中。
class Solution { int N = 20010, M = N * 2, idx = 0; int[] he = new int[N], e = new int[M], ne = new int[M]; int[] f1 = new int[N], f2 = new int[N], g = new int[N], p = new int[N]; // 添加边关系到邻接表 void add(int a, int b) { e[idx] = b; ne[idx] = he[a]; he[a] = idx++; } // 找出最小高度的树的根节点 public List<Integer> findMinHeightTrees(int n, int[][] edges) { Arrays.fill(he, -1); for (int[] e : edges) { int a = e[0], b = e[1]; add(a, b); add(b, a); } dfs1(0, -1); dfs2(0, -1); List<Integer> ans = new ArrayList<>(); int min = n; for (int i = 0; i < n; i++) { int cur = Math.max(f1[i], g[i]); if (cur < min) { min = cur; ans.clear(); ans.add(i); } else if (cur == min) { ans.add(i); } } return ans; } // DFS计算每个节点到其它节点的最大距离 int dfs1(int u, int fa) { for (int i = he[u]; i != -1; i = ne[i]) { int j = e[i]; if (j == fa) continue; int sub = dfs1(j, u) + 1; if (sub > f1[u]) { f2[u] = f1[u]; f1[u] = sub; p[u] = j; } else if (sub > f2[u]) { f2[u] = sub; } } return f1[u]; } // DFS根据dfs1的结果计算每个节点的次大距离 void dfs2(int u, int fa) { for (int i = he[u]; i != -1; i = ne[i]) { int j = e[i]; if (j == fa) continue; if (p[u] != j) g[j] = Math.max(g[j], f1[u] + 1); else g[j] = Math.max(g[j], f2[u] + 1); g[j] = Math.max(g[j], g[u] + 1); dfs2(j, u); } } }
下接:【题解】—— LeetCode一周小结12