上接:【题解】—— LeetCode一周小结8
26.二叉搜索树的范围和
题目链接:938. 二叉搜索树的范围和
给定二叉搜索树的根结点 root,返回值位于范围 [low, high] 之间的所有结点的值的和。
示例 1:
输入:root = [10,5,15,3,7,null,18], low = 7, high = 15
输出:32
示例 2:
输入:root = [10,5,15,3,7,13,18,1,null,6], low = 6, high = 10
输出:23
提示:
树中节点数目在范围 [1, 2 * 104] 内
1 <= Node.val <= 105
1 <= low <= high <= 105
所有 Node.val 互不相同
题解:
方法:递归
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { int low, high; int ans; public int rangeSumBST(TreeNode root, int _low, int _high) { low = _low; high = _high; dfs(root); return ans; } void dfs(TreeNode root) { if (root == null) return; dfs(root.left); if (low <= root.val && root.val <= high) ans += root.val; dfs(root.right); } }
方法:迭代
使用栈来模拟递归过程
class Solution { public int rangeSumBST(TreeNode root, int low, int high) { int ans = 0; Deque<TreeNode> d = new ArrayDeque<>(); while (root != null || !d.isEmpty()) { while (root != null) { d.addLast(root); root = root.left; } root = d.pollLast(); if (low <= root.val && root.val <= high) { ans += root.val; } root = root.right; } return ans; } }
27.统计树中的合法路径数目
题目链接:2867. 统计树中的合法路径数目
给你一棵 n 个节点的无向树,节点编号为 1 到 n 。给你一个整数 n 和一个长度为 n - 1 的二维整数数组 edges ,其中 edges[i] = [ui, vi] 表示节点 ui 和 vi 在树中有一条边。
请你返回树中的 合法路径数目 。
如果在节点 a 到节点 b 之间 恰好有一个 节点的编号是质数,那么我们称路径 (a, b) 是 合法的 。
注意:
路径 (a, b) 指的是一条从节点 a 开始到节点 b 结束的一个节点序列,序列中的节点 互不相同 ,且相邻节点之间在树上有一条边。
路径 (a, b) 和路径 (b, a) 视为 同一条 路径,且只计入答案 一次 。
示例 1:
输入:n = 5, edges = [[1,2],[1,3],[2,4],[2,5]]
输出:4
解释:恰好有一个质数编号的节点路径有:
- (1, 2) 因为路径 1 到 2 只包含一个质数 2 。
- (1, 3) 因为路径 1 到 3 只包含一个质数 3 。
- (1, 4) 因为路径 1 到 4 只包含一个质数 2 。
- (2, 4) 因为路径 2 到 4 只包含一个质数 2 。
只有 4 条合法路径。
示例 2:
输入:n = 6, edges = [[1,2],[1,3],[2,4],[3,5],[3,6]]
输出:6
解释:恰好有一个质数编号的节点路径有:
- (1, 2) 因为路径 1 到 2 只包含一个质数 2 。
- (1, 3) 因为路径 1 到 3 只包含一个质数 3 。
- (1, 4) 因为路径 1 到 4 只包含一个质数 2 。
- (1, 6) 因为路径 1 到 6 只包含一个质数 3 。
- (2, 4) 因为路径 2 到 4 只包含一个质数 2 。
- (3, 6) 因为路径 3 到 6 只包含一个质数 3 。
只有 6 条合法路径。
提示:
1 <= n <= 105
edges.length == n - 1
edges[i].length == 2
1 <= ui, vi <= n
输入保证 edges 形成一棵合法的树。
题解:
方法:O(n) 线性做法
计算从每个质数出发,经过不同路径到达非质数的路径总数。首先使用深度优先搜索遍历树的每个连通块,并将连通块中的节点数量保存在数组 size 中。然后对于每个质数节点,遍历其相连的非质数节点,根据每个非质数节点所在的连通块大小计算路径总数,并累加到答案中。
class Solution { private final static int MX = (int) 1e5; private final static boolean[] np = new boolean[MX + 1]; // 质数=false 非质数=true // 预处理出所有质数 static { np[1] = true; for (int i = 2; i * i <= MX; i++) { if (!np[i]) { for (int j = i * i; j <= MX; j += i) { np[j] = true; } } } } /** * 计算从每个质数出发,经过不同路径到达非质数的路径总数。 * @param n 树的节点数 * @param edges 树的边集合 * @return 路径总数 */ public long countPaths(int n, int[][] edges) { // 构建树的邻接表 List<Integer>[] g = new ArrayList[n + 1]; Arrays.setAll(g, e -> new ArrayList<>()); for (var e : edges) { int x = e[0], y = e[1]; g[x].add(y); g[y].add(x); } long ans = 0; // 结果路径总数 int[] size = new int[n + 1]; // 保存每个连通块的节点数量 var nodes = new ArrayList<Integer>(); // 保存当前连通块的节点集合 // 遍历每个质数 for (int x = 1; x <= n; x++) { if (np[x]) { // 跳过非质数 continue; } int sum = 0; // 记录所有非质数的节点数量 // 遍历与当前质数相连的节点 for (int y : g[x]) { if (!np[y]) { // 只考虑非质数节点 if (size[y] == 0) { // 若当前连通块尚未计算过 nodes.clear(); // 清空节点集合 dfs(y, -1, g, nodes); // 从当前非质数节点出发,深度优先遍历连通块,并将节点添加到集合中 // 更新当前连通块中每个节点的连通块大小 for (int z : nodes) { size[z] = nodes.size(); } } // 计算从当前质数节点出发,经过不同路径到达非质数节点的路径总数 ans += (long) size[y] * sum; sum += size[y]; // 更新所有非质数的节点数量 } } ans += sum; // 将从当前质数节点出发的路径总数加入结果中 } return ans; // 返回路径总数 } /** * 深度优先搜索函数,用于遍历树的连通块并保存节点。 * @param x 当前处理的节点 * @param fa 当前节点的父节点 * @param g 树的邻接表表示 * @param nodes 保存当前连通块的节点集合 */ private void dfs(int x, int fa, List<Integer>[] g, List<Integer> nodes) { nodes.add(x); // 将当前节点加入节点集合 // 遍历当前节点的邻居节点 for (int y : g[x]) { if (y != fa && np[y]) { // 跳过父节点和质数节点 dfs(y, x, g, nodes); // 递归遍历当前节点的邻居节点 } } } }
28.使二叉树所有路径值相等的最小代价
给你一个整数 n 表示一棵 满二叉树 里面节点的数目,节点编号从 1 到 n 。根节点编号为 1 ,树中每个非叶子节点 i 都有两个孩子,分别是左孩子 2 * i 和右孩子 2 * i + 1 。
树中每个节点都有一个值,用下标从 0 开始、长度为 n 的整数数组 cost 表示,其中 cost[i] 是第 i + 1 个节点的值。每次操作,你可以将树中 任意 节点的值 增加 1 。你可以执行操作 任意 次。
你的目标是让根到每一个 叶子结点 的路径值相等。请你返回 最少 需要执行增加操作多少次。
注意:
满二叉树 指的是一棵树,它满足树中除了叶子节点外每个节点都恰好有 2 个子节点,且所有叶子节点距离根节点距离相同。
路径值 指的是路径上所有节点的值之和。
示例 1:
输入:n = 7, cost = [1,5,2,2,3,3,1]
输出:6
解释:我们执行以下的增加操作:
- 将节点 4 的值增加一次。
- 将节点 3 的值增加三次。
- 将节点 7 的值增加两次。
从根到叶子的每一条路径值都为 9 。
总共增加次数为 1 + 3 + 2 = 6 。
这是最小的答案。
示例 2:
输入:n = 3, cost = [5,3,3]
输出:0
解释:两条路径已经有相等的路径值,所以不需要执行任何增加操作。
提示:
3 <= n <= 105
n + 1 是 2 的幂
cost.length == n
1 <= cost[i] <= 104
题解:
方法:贪心
将二叉树中的每个非叶节点的两个子节点变为相同值所需的最小增量之和。从最后一个非叶节点开始,逐层向上计算。对于每个非叶节点,计算将两个子节点变为相同值所需的增量,并累加到结果中。然后将当前节点更新为其子节点中值较大的那个,以满足父节点的要求。
class Solution { /** * 计算将二叉树中的每个非叶节点的两个子节点变为相同值所需的最小增量之和。 * @param n 二叉树的节点数 * @param cost 二叉树中每个节点的值 * @return 最小增量之和 */ public int minIncrements(int n, int[] cost) { int ans = 0; // 初始化最小增量之和为0 for (int i = n / 2; i > 0; i--) { // 从最后一个非叶节点开始向上计算 // 计算将两个子节点变为相同值所需的增量,累加到结果中 ans += Math.abs(cost[i * 2 - 1] - cost[i * 2]); // 将当前节点更新为其子节点中值较大的那个,以满足父节点的要求 cost[i - 1] += Math.max(cost[i * 2 - 1], cost[i * 2]); } return ans; // 返回最小增量之和 } }
29.统计可能的树根数目
题目链接:2581. 统计可能的树根数目
Alice 有一棵 n 个节点的树,节点编号为 0 到 n - 1 。树用一个长度为 n - 1 的二维整数数组 edges 表示,其中 edges[i] = [ai, bi] ,表示树中节点 ai 和 bi 之间有一条边。
Alice 想要 Bob 找到这棵树的根。她允许 Bob 对这棵树进行若干次 猜测 。每一次猜测,Bob 做如下事情:
选择两个 不相等 的整数 u 和 v ,且树中必须存在边 [u, v] 。
Bob 猜测树中 u 是 v 的 父节点 。
Bob 的猜测用二维整数数组 guesses 表示,其中 guesses[j] = [uj, vj] 表示 Bob 猜 uj 是 vj 的父节点。
Alice 非常懒,她不想逐个回答 Bob 的猜测,只告诉 Bob 这些猜测里面 至少 有 k 个猜测的结果为 true 。
给你二维整数数组 edges ,Bob 的所有猜测和整数 k ,请你返回可能成为树根的 节点数目 。如果没有这样的树,则返回 0。
示例 1:
输入:edges = [[0,1],[1,2],[1,3],[4,2]], guesses =
[[1,3],[0,1],[1,0],[2,4]], k = 3
输出:3
解释:
根为节点 0 ,正确的猜测为 [1,3], [0,1], [2,4]
根为节点 1 ,正确的猜测为 [1,3], [1,0], [2,4]
根为节点 2 ,正确的猜测为 [1,3], [1,0], [2,4]
根为节点 3 ,正确的猜测为 [1,0], [2,4]
根为节点 4 ,正确的猜测为 [1,3], [1,0]
节点 0 ,1 或 2 为根时,可以得到 3 个正确的猜测。
示例 2:
输入:edges = [[0,1],[1,2],[2,3],[3,4]], guesses =
[[1,0],[3,4],[2,1],[3,2]], k = 1
输出:5
解释:
根为节点 0 ,正确的猜测为 [3,4]
根为节点 1 ,正确的猜测为 [1,0], [3,4]
根为节点 2 ,正确的猜测为 [1,0], [2,1], [3,4]
根为节点 3 ,正确的猜测为 [1,0], [2,1], [3,2], [3,4]
根为节点 4 ,正确的猜测为 [1,0], [2,1], [3,2]
任何节点为根,都至少有 1 个正确的猜测。
提示:
edges.length == n - 1
2 <= n <= 105
1 <= guesses.length <= 105
0 <= ai, bi, uj, vj <= n - 1
ai != bi
uj != vj
edges 表示一棵有效的树。
guesses[j] 是树中的一条边。
guesses 是唯一的。
0 <= k <= guesses.length
题解:
方法:换根 DP
知识点:【图解】一张图秒懂换根 DP!
首先通过深度优先搜索以根节点0为根的情况,计算每个节点为根时猜对的次数。然后对于每个节点,通过深度优先搜索重新确定根节点,并计算满足猜对次数的根节点个数。
class Solution { private List<Integer>[] g; // 邻接表表示的图 private Set<Long> s = new HashSet<>(); // 哈希表存储猜测的结果 private int k, ans, cnt0; // k: 猜对的最小次数;ans: 可能的根节点个数;cnt0: 以根节点为0时的猜对次数 /** * 计算以哪些节点为根时,满足猜对的最小次数的根节点个数。 * @param edges 二维数组表示边的关系 * @param guesses 二维数组表示猜测的结果 * @param k 猜对的最小次数 * @return 满足猜对的最小次数的根节点个数 */ public int rootCount(int[][] edges, int[][] guesses, int k) { this.k = k; // 初始化猜对的最小次数 g = new ArrayList[edges.length + 1]; // 初始化邻接表 Arrays.setAll(g, i -> new ArrayList<>()); // 初始化每个顶点的邻接表 for (int[] e : edges) { // 建图 int x = e[0]; int y = e[1]; g[x].add(y); g[y].add(x); } for (int[] e : guesses) { // 将猜测的结果转换成哈希表存储 s.add((long) e[0] << 32 | e[1]); // 将两个 4 字节 int 压缩成一个 8 字节 long } dfs(0, -1); // 以0为根进行dfs reroot(0, -1, cnt0); // 以每个节点为根进行dfs return ans; // 返回结果 } /** * 以根节点为0进行深度优先搜索,计算每个节点为根时猜对的次数。 * @param x 当前处理的节点 * @param fa 当前节点的父节点 */ private void dfs(int x, int fa) { for (int y : g[x]) { // 遍历当前节点的相邻节点 if (y != fa) { // 如果相邻节点不是父节点 if (s.contains((long) x << 32 | y)) { // 如果当前边是猜对的 cnt0++; // 增加以0为根时猜对的次数 } dfs(y, x); // 递归处理相邻节点 } } } /** * 以每个节点为根进行深度优先搜索,计算满足猜对次数的根节点个数。 * @param x 当前处理的节点 * @param fa 当前节点的父节点 * @param cnt 以当前节点为根时猜对的次数 */ private void reroot(int x, int fa, int cnt) { if (cnt >= k) { // 如果以当前节点为根时猜对的次数大于等于要求的最小次数k ans++; // 增加满足条件的根节点个数 } for (int y : g[x]) { // 遍历当前节点的相邻节点 if (y != fa) { // 如果相邻节点不是父节点 int c = cnt; // 记录以当前节点为根时猜对的次数 if (s.contains((long) x << 32 | y)) c--; // 如果原来是对的,现在是错的 if (s.contains((long) y << 32 | x)) c++; // 如果原来是错的,现在是对的 reroot(y, x, c); // 递归处理相邻节点 } } } }
2024.3
1.检查数组是否存在有效划分
题目链接:2369. 检查数组是否存在有效划分
给你一个下标从 0 开始的整数数组 nums ,你必须将数组划分为一个或多个 连续 子数组。
如果获得的这些子数组中每个都能满足下述条件 之一 ,则可以称其为数组的一种 有效 划分:
子数组 恰 由 2 个相等元素组成,例如,子数组 [2,2] 。
子数组 恰 由 3 个相等元素组成,例如,子数组 [4,4,4] 。
子数组 恰 由 3 个连续递增元素组成,并且相邻元素之间的差值为 1 。例如,子数组 [3,4,5] ,但是子数组 [1,3,5] 不符合要求。
如果数组 至少 存在一种有效划分,返回 true ,否则,返回 false 。
示例 1:
输入:nums = [4,4,4,5,6]
输出:true
解释:数组可以划分成子数组 [4,4] 和 [4,5,6] 。
这是一种有效划分,所以返回 true 。
示例 2:
输入:nums = [1,1,1,2]
输出:false
解释:该数组不存在有效划分。
提示:
2 <= nums.length <= 105
1 <= nums[i] <= 106
题解:
方法:动态规划
使用f[i]表示前i个元素是否可以构成连续子序列。初始化f[0]=true,表示前0个元素肯定可以构成连续子序列。然后,遍历数组中的元素,根据当前元素与前一个元素的关系以及前两个元素的关系,更新f[i]的值。最终返回f[n],表示前n个元素是否可以构成连续子序列。
class Solution { /** * 判断数组是否可以分成若干连续子序列,每个子序列长度至少为3,且相邻元素之间的差值为1。 * @param nums 给定的整数数组 * @return 若数组可以分成若干连续子序列,返回true;否则,返回false */ public boolean validPartition(int[] nums) { int n = nums.length; // 数组的长度 boolean[] f = new boolean[n + 1]; // 动态规划数组,f[i]表示前i个元素是否可以构成连续子序列 f[0] = true; // 初始条件,前0个元素肯定可以构成连续子序列 for (int i = 1; i < n; i++) { if (f[i - 1] && nums[i] == nums[i - 1] || // 如果前i-1个元素可以构成连续子序列,并且当前元素与前一个元素相同 i > 1 && f[i - 2] && (nums[i] == nums[i - 1] && nums[i] == nums[i - 2] || // 或者前i-2个元素可以构成连续子序列,并且当前元素与前一个元素相同,并且当前元素与前两个元素相同 nums[i] == nums[i - 1] + 1 && nums[i] == nums[i - 2] + 2)) { // 或者当前元素与前一个元素相差1,与前两个元素相差2 f[i + 1] = true; // 则前i个元素可以构成连续子序列 } } return f[n]; // 返回前n个元素是否可以构成连续子序列 } }
2.受限条件下可到达节点的数目
题目链接:2368. 受限条件下可到达节点的数目
现有一棵由 n 个节点组成的无向树,节点编号从 0 到 n - 1 ,共有 n - 1 条边。
给你一个二维整数数组 edges ,长度为 n - 1 ,其中 edges[i] = [ai, bi] 表示树中节点 ai 和 bi 之间存在一条边。另给你一个整数数组 restricted 表示 受限 节点。
在不访问受限节点的前提下,返回你可以从节点 0 到达的 最多 节点数目。
注意,节点 0 不 会标记为受限节点。
示例 1:
输入:n = 7, edges = [[0,1],[1,2],[3,1],[4,0],[0,5],[5,6]], restricted =
[4,5]
输出:4
解释:上图所示正是这棵树。
在不访问受限节点的前提下,只有节点 [0,1,2,3] 可以从节点 0 到达。
示例 2:
输入:n = 7, edges = [[0,1],[0,2],[0,5],[0,4],[3,2],[6,5]], restricted =
[4,2,1]
输出:3
解释:上图所示正是这棵树。
在不访问受限节点的前提下,只有节点 [0,5,6] 可以从节点 0 到达。
提示:
2 <= n <= 15
edges.length == n - 1
edges[i].length == 2
0 <= ai, bi < n
ai != bi
edges 表示一棵有效的树
1 <= restricted.length < n
1 <= restricted[i] < n
restricted 中的所有值 互不相同
题解:
方法:树上 DFS
使用深度优先搜索来解决问题。首先,将受限制的节点转换成布尔数组,然后构建图的邻接表表示。接着,对每个节点进行深度优先搜索,计算以该节点为起点可以到达的节点数。最终返回以节点 0 为起点可以到达的节点数。
class Solution { /** * 计算在移除若干受限制的边之后,从节点 0 开始可以到达的节点数。 * @param n 图中节点的数量 * @param edges 图中的边 * @param restricted 受限制的节点数组 * @return 从节点 0 开始可以到达的节点数 */ public int reachableNodes(int n, int[][] edges, int[] restricted) { // 将受限制的节点转换成一个布尔数组,方便进行判断 boolean[] isRestricted = new boolean[n]; for (int x : restricted) { isRestricted[x] = true; } // 构建图的邻接表表示 List<Integer>[] g = new ArrayList[n]; Arrays.setAll(g, i -> new ArrayList<>()); // 遍历所有边,如果两个节点都不受限制,则在邻接表中添加边 for (int[] e : edges) { int x = e[0], y = e[1]; if (!isRestricted[x] && !isRestricted[y]) { g[x].add(y); g[y].add(x); } } // 进行深度优先搜索,计算可以到达的节点数 return dfs(0, -1, g); } /** * 深度优先搜索函数,计算以节点 x 为起点可以到达的节点数。 * @param x 当前处理的节点 * @param fa x 的父节点 * @param g 图的邻接表表示 * @return 以节点 x 为起点可以到达的节点数 */ private int dfs(int x, int fa, List<Integer>[] g) { int cnt = 1; // 从节点 x 出发,可以到达它自己 for (int y : g[x]) { // 遍历节点 x 的所有邻居节点 if (y != fa) { // 避免回到父节点,防止形成环 cnt += dfs(y, x, g); // 递归计算从邻居节点 y 出发可以到达的节点数 } } return cnt; // 返回以节点 x 为起点可以到达的节点数 } }
3.用队列实现栈
题目链接:225. 用队列实现栈
请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:
void push(int x) 将元素 x 压入栈顶。
int pop() 移除并返回栈顶元素。
int top() 返回栈顶元素。
boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。
注意:
你只能使用队列的基本操作 —— 也就是 push to back、peek/pop from front、size 和 is empty 这些操作。
你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。
示例:
输入:
[“MyStack”, “push”, “push”, “top”, “pop”, “empty”]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 2, 2, false]
解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False
提示:
1 <= x <= 9
最多调用100 次 push、pop、top 和 empty
每次调用 pop 和 top 都保证栈不为空
题解:
知识:用队列实现栈
方法:两个队列实现栈的操作
主要思路是将元素压入栈时,先将元素存入辅助队列,然后将主队列中的元素全部移到辅助队列,最后交换主队列和辅助队列。这样能够保证每次入栈的元素都位于主队列的队首,从而实现栈的后进先出特性。
import java.util.LinkedList; import java.util.Queue; class MyStack { Queue<Integer> queue1; // 主队列,用于存储栈中的元素 Queue<Integer> queue2; // 辅助队列,用于辅助元素入栈操作 /** Initialize your data structure here. */ public MyStack() { queue1 = new LinkedList<Integer>(); // 初始化主队列 queue2 = new LinkedList<Integer>(); // 初始化辅助队列 } /** Push element x onto stack. */ public void push(int x) { queue2.offer(x); // 将新元素入辅助队列 // 将主队列中的所有元素依次出队并入辅助队列,确保新元素位于队列头部 while (!queue1.isEmpty()) { queue2.offer(queue1.poll()); } // 交换主队列和辅助队列,确保主队列始终为非空队列 Queue<Integer> temp = queue1; queue1 = queue2; queue2 = temp; } /** Removes the element on top of the stack and returns that element. */ public int pop() { return queue1.poll(); // 从主队列中出队即可 } /** Get the top element. */ public int top() { return queue1.peek(); // 返回主队列的队首元素,即栈顶元素 } /** Returns whether the stack is empty. */ public boolean empty() { return queue1.isEmpty(); // 判断主队列是否为空即可 } }
方法:一个队列实现栈的操作
利用了队列先进先出(FIFO)的特性来实现栈的后进先出(LIFO)的操作。在每次入栈时,先将新元素入队,然后将队列中之前的元素依次出队再入队,以保证新入队的元素始终位于队首,从而实现栈的特性。
import java.util.LinkedList; import java.util.Queue; class MyStack { Queue<Integer> queue; // 使用队列实现栈 /** Initialize your data structure here. */ public MyStack() { queue = new LinkedList<Integer>(); // 初始化队列 } /** Push element x onto stack. */ public void push(int x) { int n = queue.size(); // 获取当前队列的大小 queue.offer(x); // 将元素入队 // 将之前的元素逐个出队再入队,使得新入队的元素位于队首,实现栈的特性 for (int i = 0; i < n; i++) { queue.offer(queue.poll()); } } /** Removes the element on top of the stack and returns that element. */ public int pop() { return queue.poll(); // 直接出队即可 } /** Get the top element. */ public int top() { return queue.peek(); // 获取队首元素,即栈顶元素 } /** Returns whether the stack is empty. */ public boolean empty() { return queue.isEmpty(); // 判断队列是否为空 } }
下接:【题解】—— LeetCode一周小结10