LeetCode 989-1000. 题解合集
989. Add to Array-Form of Integer(数组形式的整数加法)
题目描述
整数的数组形式 num 是按照从左到右的顺序表示其数字的数组。
例如,对于 num = 1321,数组形式是 [1,3,2,1]。
给定 num,整数的数组形式,和整数 k,返回整数 num + k 的数组形式。
示例 1
- 输入:
num= [1,2,0,0],k= 34 - 输出:[1,2,3,4]
- 解释:1200 + 34 = 1234
示例 2
- 输入:
num= [2,7,4],k= 181 - 输出:[4,5,5]
示例 3
- 输入:
num= [2,1,5],k= 806 - 输出:[1,0,2,1]
- 解释:215 + 806 = 1021
提示: - 1 <= num.length <= 10^4- 0 <= num[i] <= 9- 1 <= k <= 10^4
解题思路
解法一(逐位模拟):
从 num 的最低位(数组末尾)开始,把 k 加到当前位:
while i >= 0 或 k > 0:
若 i >= 0,则 k += num[i];
结果位为 k % 10,累进 k /= 10,--i。
最后把结果反转(从低到高存 → 从高到低)。
注意 k 可能有多位(>10^4 时不会,但 k 可能比 num 长,循环条件保证覆盖)。
解法二(字符串/大数对照,供对照):
从最低位同步推进 num 与 k 的十进制表示,两数相加模拟,处理进位。
与解法一结果一致。
代码实现
public class Solution {
// 解法一:逐位模拟(主解法,提交 LeetCode)
public java.util.List<Integer> addToArrayForm(int[] num, int k) {
java.util.List<Integer> ans = new java.util.ArrayList<>();
int i = num.length - 1;
while (i >= 0 || k > 0) {
if (i >= 0) {
k += num[i];
}
ans.add(k % 10);
k /= 10;
--i;
}
java.util.Collections.reverse(ans);
return ans;
}
// 解法二:数组推进对照(独立实现,供对照验证)
public java.util.List<Integer> addToArrayForm2(int[] num, int k) {
// 把 k 拆成十进制位数组
java.util.List<Integer> kk = new java.util.ArrayList<>();
int t = k;
while (t > 0) {
kk.add(t % 10);
t /= 10;
}
// 低位在前
java.util.List<Integer> res = new java.util.ArrayList<>();
int carry = 0;
int p = num.length - 1, q = 0;
while (p >= 0 || q < kk.size() || carry > 0) {
int sum = carry;
if (p >= 0) {
sum += num[p];
--p;
}
if (q < kk.size()) {
sum += kk.get(q);
++q;
}
res.add(sum % 10);
carry = sum / 10;
}
java.util.Collections.reverse(res);
return res;
}
private static void check(int[] num, int k, int[] expected, int caseId) {
java.util.List<Integer> got1 = new Solution().addToArrayForm(num.clone(), k);
java.util.List<Integer> got2 = new Solution().addToArrayForm2(num.clone(), k);
java.util.List<Integer> exp = new java.util.ArrayList<>();
for (int x : expected) {
exp.add(x);
}
if (!got1.equals(exp) || !got2.equals(exp)) {
System.out.println("FAIL case" + caseId + " expected=" + exp
+ " got1=" + got1 + " got2=" + got2);
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new int[] {1, 2, 0, 0}, 34, new int[] {1, 2, 3, 4}, 1);
check(new int[] {2, 7, 4}, 181, new int[] {4, 5, 5}, 2);
check(new int[] {2, 1, 5}, 806, new int[] {1, 0, 2, 1}, 3);
check(new int[] {9, 9, 9}, 1, new int[] {1, 0, 0, 0}, 4);
check(new int[] {0}, 10000, new int[] {1, 0, 0, 0, 0}, 5);
check(new int[] {1, 2, 3}, 9, new int[] {1, 3, 2}, 6);
check(new int[] {0}, 0, new int[] {0}, 7);
System.out.println("== 0989 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(max(n, log k));
- 空间复杂度:O(max(n, log k))(答案)。
解法二: - 时间复杂度:O(max(n, log k));
- 空间复杂度:O(max(n, log k))。
990. Satisfiability of Equality Equations(等式方程的可满足性)
题目描述
给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,
并采用两种不同的形式之一:"a==b" 或 "a!=b"。在这里,a 和 b 是小写字母(不一定不同),
表示单字母变量名。
只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。
示例 1
- 输入:["
a==b","b!=a"] - 输出:
false
示例 2
- 输入:["
b==a","a==b"] - 输出:
true
示例 3
- 输入:["
a==b","b==c","a==c"] - 输出:
true
示例 4
- 输入:["
a==b","b!=c","c==a"] - 输出:
false
示例 5
- 输入:["
c==c","b==d","x!=z"] - 输出:
true
提示: - 1 <= equations.length <= 500- equations[i].length == 4
解题思路
解法一(并查集):
相等关系具有传递性,先把所有 "==" 方程涉及的变量合并到同一连通分量;
再逐条检查 "!=" 方程,若两变量已在同一连通分量则矛盾,返回 false;
全部检查通过返回 true。
变量只有 26 个小写字母,用数组实现路径压缩并查集即可。
解法二(BFS 连通性对照):
先用 "==" 建图(26 节点邻接表),再对每条 "!=" 用 BFS 判断两端是否连通,
若连通则矛盾。与解法一结果一致。
代码实现
public class Solution {
// 解法一:并查集(主解法,提交 LeetCode)
private int[] p;
public boolean equationsPossible(String[] equations) {
p = new int[26];
for (int i = 0; i < 26; ++i) {
p[i] = i;
}
for (String e : equations) {
if (e.charAt(1) == '=') {
int a = e.charAt(0) - 'a';
int b = e.charAt(3) - 'a';
union(a, b);
}
}
for (String e : equations) {
if (e.charAt(1) == '!') {
int a = e.charAt(0) - 'a';
int b = e.charAt(3) - 'a';
if (find(a) == find(b)) {
return false;
}
}
}
return true;
}
private int find(int x) {
if (p[x] != x) {
p[x] = find(p[x]);
}
return p[x];
}
private void union(int a, int b) {
p[find(a)] = find(b);
}
// 解法二:BFS 连通性(对照验证)
public boolean equationsPossibleBFS(String[] equations) {
@SuppressWarnings("unchecked")
java.util.List<Integer>[] g = new java.util.List[26];
for (int i = 0; i < 26; ++i) {
g[i] = new java.util.ArrayList<>();
}
for (String e : equations) {
if (e.charAt(1) == '=') {
int a = e.charAt(0) - 'a';
int b = e.charAt(3) - 'a';
g[a].add(b);
g[b].add(a);
}
}
for (String e : equations) {
if (e.charAt(1) == '!') {
int a = e.charAt(0) - 'a';
int b = e.charAt(3) - 'a';
if (connected(g, a, b)) {
return false;
}
}
}
return true;
}
private boolean connected(java.util.List<Integer>[] g, int s, int t) {
boolean[] vis = new boolean[26];
java.util.Queue<Integer> q = new java.util.LinkedList<>();
q.offer(s);
vis[s] = true;
while (!q.isEmpty()) {
int u = q.poll();
if (u == t) {
return true;
}
for (int v : g[u]) {
if (!vis[v]) {
vis[v] = true;
q.offer(v);
}
}
}
return false;
}
private static void check(String[] equations, boolean expected, int caseId) {
boolean got1 = new Solution().equationsPossible(equations);
boolean got2 = new Solution().equationsPossibleBFS(equations);
if (got1 != expected || got2 != expected) {
System.out.println("FAIL case" + caseId + " expected=" + expected
+ " got1=" + got1 + " got2=" + got2);
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new String[] {"a==b", "b!=a"}, false, 1);
check(new String[] {"b==a", "a==b"}, true, 2);
check(new String[] {"a==b", "b==c", "a==c"}, true, 3);
check(new String[] {"a==b", "b!=c", "c==a"}, false, 4);
check(new String[] {"c==c", "b==d", "x!=z"}, true, 5);
check(new String[] {"a!=a"}, false, 6);
check(new String[] {"a!=b", "b!=c", "a==c"}, true, 7);
check(new String[] {"a==b"}, true, 8);
System.out.println("== 0990 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(n * α(26)) ≈ O(n),n 为方程数;
- 空间复杂度:O(26)。
解法二: - 时间复杂度:O(n * 26);每次 BFS 都访问邻接表;
- 空间复杂度:O(26^2)。
991. Broken Calculator(坏了的计算器)
题目描述
在显示着数字 startValue 的坏计算器上,我们可以执行以下两种操作:
- 双倍(
Double):将显示屏上的数字乘 2; - 递减(Decrement):将显示屏上的数字减 1。
给定两个整数startValue和target。返回显示数字target所需的最小操作数。
示例 1
- 输入:
startValue= 2,target= 3 - 输出:2
- 解释:先进行双倍运算,然后再进行递减运算 {2 -> 4 -> 3}。
示例 2
- 输入:
startValue= 5,target= 8 - 输出:2
- 解释:先递减,再双倍 {5 -> 4 -> 8}。
示例 3
- 输入:
startValue= 3,target= 10 - 输出:3
- 解释:先双倍,然后递减,再双倍 {3 -> 6 -> 5 -> 10}。
提示: - 1 <= startValue, target <= 10^9
解题思路
解法一(逆向贪心):
正向同时有乘 2 和减 1,dp/BFS 都会超时。逆向思考:从 target 变回 startValue,
逆向操作是「除以 2」(当 target 为偶数)与「加 1」。
贪心策略:
- 若 target 为奇数,则最后一次操作必是「减 1」(正向),逆向为 target+1;
- 若 target 为偶数,优先 target/2(除以 2 比逐个加 1 减少操作更快)。
循环直到 target <= startValue,之后只需逐个减 1,操作数为 startValue - target。
累计操作次数即为答案。
解法二(BFS 小数据对照):
对 startValue 做 BFS(乘 2 / 减 1),记录到 target 的最短操作数。仅用于小数据对照验证。
代码实现
public class Solution {
// 解法一:逆向贪心(主解法,提交 LeetCode)
public int brokenCalc(int startValue, int target) {
int ans = 0;
while (startValue < target) {
if ((target & 1) == 1) {
++target;
} else {
target >>= 1;
}
++ans;
}
ans += startValue - target;
return ans;
}
// 解法二:BFS(对照验证,仅限小数据)
public int brokenCalcBFS(int startValue, int target) {
java.util.Queue<int[]> q = new java.util.LinkedList<>();
java.util.Set<Integer> vis = new java.util.HashSet<>();
q.offer(new int[] {startValue, 0});
vis.add(startValue);
while (!q.isEmpty()) {
int[] cur = q.poll();
int v = cur[0], d = cur[1];
if (v == target) {
return d;
}
// 乘 2:过快超出 target 时剪枝(target*2 内足够)
if (v < target * 2 && !vis.contains(v * 2)) {
vis.add(v * 2);
q.offer(new int[] {v * 2, d + 1});
}
// 减 1
if (v - 1 >= 0 && !vis.contains(v - 1)) {
vis.add(v - 1);
q.offer(new int[] {v - 1, d + 1});
}
}
return -1;
}
private static void check(int s, int t, int expected, int caseId) {
int got1 = new Solution().brokenCalc(s, t);
if (got1 != expected) {
System.out.println("FAIL case" + caseId + " expected=" + expected + " got1=" + got1);
return;
}
if (t <= 40) {
int got2 = new Solution().brokenCalcBFS(s, t);
if (got2 != expected) {
System.out.println("FAIL-BFS case" + caseId + " expected=" + expected + " got2=" + got2);
return;
}
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(2, 3, 2, 1);
check(5, 8, 2, 2);
check(3, 10, 3, 3);
check(1, 1000000000, 39, 4);
check(1024, 1, 1023, 5);
check(1, 2, 1, 6);
check(3, 3, 0, 7);
System.out.println("== 0991 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(log target),每次至少减半或加一后减半;
- 空间复杂度:O(1)。 解法二:
- 时间复杂度:O(2^d),d 为最短操作数(仅限小数据);
- 空间复杂度:O(2^d)。
992. Subarrays with K Different Integers(K 个不同整数的子数组)
题目描述
给定一个正整数数组 nums 和一个整数 k,返回 nums 中「好子数组」的数目。
如果 nums 的某个子数组中不同整数的个数恰好为 k,则称 nums 的这个连续、不一定不同的子数组为「好子数组」。
例如,[1,2,3,1,2] 中有 3 个不同的整数:1,2,以及 3。
子数组是数组的连续部分。
示例 1
- 输入:
nums= [1,2,1,2,3],k= 2 - 输出:7
- 解释:恰好由 2 个不同整数组成的子数组:[1,2], [2,1], [1,2], [2,3], [1,2,1], [2,1,2], [1,2,1,2]
示例 2
- 输入:
nums= [1,2,1,3,4],k= 3 - 输出:3
- 解释:[1,2,1,3], [2,1,3], [1,3,4] 提示:
- 1 <= nums.length <= 2 * 10^4- 1 <= nums[i], k <= nums.length
解题思路
解法一(双指针 + 「至多 k」差分): 恰好 k 种 = 至多 k 种 - 至多 k-1 种。 定义 f(k) 返回数组 pos:pos[i] 表示以 i 结尾、不同整数种数不超过 k 的最短窗口左端点 j。 滑动窗口维护 cnt[] 与当前窗口不同种数 s,当 s > k 时右移左指针直到 s <= k。 对每个右端点,好子数组数量 = withAtMost(k-1) 的左端点 - withAtMost(k) 的左端点, 求和即为答案。 解法二(暴力枚举对照): 枚举所有子数组,统计不同元素恰为 k 的数量。仅用于小数据对照验证。
代码实现
public class Solution {
// 解法一:双指针 + 至多 k 差分(主解法,提交 LeetCode)
public int subarraysWithKDistinct(int[] nums, int k) {
return atMost(nums, k) - atMost(nums, k - 1);
}
// 不同整数种数不超过 k 的子数组数量
private int atMost(int[] nums, int k) {
int n = nums.length;
int[] cnt = new int[n + 1];
int s = 0; // 当前窗口不同种数
int ans = 0;
for (int i = 0, j = 0; i < n; ++i) {
if (cnt[nums[i]]++ == 0) {
++s;
}
while (s > k) {
if (--cnt[nums[j]] == 0) {
--s;
}
++j;
}
ans += i - j + 1;
}
return ans;
}
// 解法二:暴力枚举(对照验证)
public int subarraysWithKDistinctBrute(int[] nums, int k) {
int n = nums.length;
int ans = 0;
for (int i = 0; i < n; ++i) {
java.util.Set<Integer> set = new java.util.HashSet<>();
for (int j = i; j < n; ++j) {
set.add(nums[j]);
if (set.size() == k) {
++ans;
} else if (set.size() > k) {
break;
}
}
}
return ans;
}
private static void check(int[] nums, int k, int expected, int caseId) {
int got1 = new Solution().subarraysWithKDistinct(nums.clone(), k);
int got2 = new Solution().subarraysWithKDistinctBrute(nums.clone(), k);
if (got1 != expected || got2 != expected) {
System.out.println("FAIL case" + caseId + " expected=" + expected
+ " got1=" + got1 + " got2=" + got2);
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new int[] {1, 2, 1, 2, 3}, 2, 7, 1);
check(new int[] {1, 2, 1, 3, 4}, 3, 3, 2);
check(new int[] {1, 1, 1, 1}, 1, 10, 3);
check(new int[] {1, 2}, 1, 2, 4);
check(new int[] {1, 2, 1, 2}, 2, 6, 5);
check(new int[] {1, 2, 3}, 3, 1, 6);
check(new int[] {1, 1, 2, 2}, 2, 4, 7);
System.out.println("== 0992 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(n);
- 空间复杂度:O(n)。 解法二:
- 时间复杂度:O(n^2);
- 空间复杂度:O(n)。
993. Cousins in Binary Tree(二叉树的堂兄弟节点)
题目描述
在二叉树中,根节点位于深度 0 处,每个深度为 k 的节点的子节点位于深度 k+1 处。
如果二叉树的两个节点深度相同,但父节点不同,则它们是一对堂兄弟节点。
我们给出了具有唯一值的二叉树的根节点 root,以及树中两个不同节点的值 x 和 y。
只有与值 x 和 y 对应的节点是堂兄弟节点时,才返回 true。否则,返回 false。
示例 1
- 输入:
root= [1,2,3,4],x= 4,y= 3 - 输出:
false
示例 2
- 输入:
root= [1,2,3,null,4,null,5],x= 5,y= 4 - 输出:
true
示例 3
- 输入:
root= [1,2,3,null,4],x= 2,y= 3 - 输出:
false提示: -
- 二叉树的节点数介于 2 到 100 之间
-
- 每个节点的值都是唯一的、范围为 1 到 100 的整数
解题思路
解法一(BFS 层序): 堂兄弟 = 深度相同且父节点不同。层序遍历时同时记录每个节点的父节点与深度: 把 (node, parent) 入队,按层推进;遇到值 x 或 y 时记录 (depth, parent)。 遍历结束后比较两者的深度与父节点。 解法二(DFS 递归,供对照): 从根递归,携带当前深度与父节点;遇到 x/y 记录;遍历完整棵树后比较。
代码实现
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
public class Solution {
// 解法一:BFS 层序(主解法,提交 LeetCode)
public boolean isCousins(TreeNode root, int x, int y) {
java.util.Queue<TreeNode[]> q = new java.util.LinkedList<>();
q.offer(new TreeNode[] {root, null});
int d1 = -1, d2 = -1;
TreeNode p1 = null, p2 = null;
int depth = 0;
while (!q.isEmpty()) {
int size = q.size();
for (int t = 0; t < size; ++t) {
TreeNode[] cur = q.poll();
TreeNode node = cur[0], parent = cur[1];
if (node.val == x) {
d1 = depth;
p1 = parent;
} else if (node.val == y) {
d2 = depth;
p2 = parent;
}
if (node.left != null) {
q.offer(new TreeNode[] {node.left, node});
}
if (node.right != null) {
q.offer(new TreeNode[] {node.right, node});
}
}
++depth;
}
return d1 == d2 && p1 != p2;
}
// 解法二:DFS 递归(对照验证)
private int xv, yv;
private int dx, dy;
private TreeNode px, py;
public boolean isCousinsDFS(TreeNode root, int x, int y) {
xv = x;
yv = y;
dfs(root, null, 0);
return dx == dy && px != py;
}
private void dfs(TreeNode node, TreeNode parent, int depth) {
if (node == null) {
return;
}
if (node.val == xv) {
dx = depth;
px = parent;
} else if (node.val == yv) {
dy = depth;
py = parent;
}
dfs(node.left, node, depth + 1);
dfs(node.right, node, depth + 1);
}
private static TreeNode build(Integer[] arr) {
if (arr == null || arr.length == 0 || arr[0] == null) {
return null;
}
TreeNode root = new TreeNode(arr[0]);
java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < arr.length) {
TreeNode cur = q.poll();
if (arr[i] != null) {
cur.left = new TreeNode(arr[i]);
q.offer(cur.left);
}
++i;
if (i < arr.length && arr[i] != null) {
cur.right = new TreeNode(arr[i]);
q.offer(cur.right);
}
++i;
}
return root;
}
private static void check(Integer[] arr, int x, int y, boolean expected, int caseId) {
TreeNode r1 = build(arr);
TreeNode r2 = build(arr);
boolean got1 = new Solution().isCousins(r1, x, y);
boolean got2 = new Solution().isCousinsDFS(r2, x, y);
if (got1 != expected || got2 != expected) {
System.out.println("FAIL case" + caseId + " expected=" + expected
+ " got1=" + got1 + " got2=" + got2);
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new Integer[] {1, 2, 3, 4}, 4, 3, false, 1);
check(new Integer[] {1, 2, 3, null, 4, null, 5}, 5, 4, true, 2);
check(new Integer[] {1, 2, 3, null, 4}, 2, 3, false, 3);
check(new Integer[] {1, 2, 3, null, 4, null, 5}, 2, 3, false, 4); // 2 与 3 是兄弟
check(new Integer[] {1, 2, 3, 4, 5, 6, 7}, 4, 7, true, 5);
System.out.println("== 0993 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(n);
- 空间复杂度:O(n),队列。 解法二:
- 时间复杂度:O(n);
- 空间复杂度:O(h),递归栈,h 为树高。
994. Rotting Oranges(腐烂的橘子)
题目描述
在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:
- 值 0 代表空单元格;
- 值 1 代表新鲜橘子;
- 值 2 代表腐烂的橘子。 每分钟,腐烂的橘子周围 4 个方向上相邻的新鲜橘子都会腐烂。 返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1。
示例 1
- 输入:
grid= [[2,1,1],[1,1,0],[0,1,1]] - 输出:4
示例 2
- 输入:
grid= [[2,1,1],[0,1,1],[1,0,1]] - 输出:-1
- 解释:左下角的橘子永远不会腐烂……
示例 3
- 输入:
grid= [[0,2]] - 输出:0 提示:
- m == grid.length- n == grid[i].length- 1 <= m, n <= 10-
grid[i][j]仅为 0、1 或 2
解题思路
解法一(多源 BFS): 所有腐烂橘子同时向外扩散,每分钟感染四邻的新鲜橘子。这是无权图多源最短路径问题:
- 先统计新鲜橘子数 cnt,并把所有腐烂橘子坐标入队;
- 按层 BFS:轮数 ans 从 0 递增,每轮扩张队列中所有腐烂橘子的四邻; 感染一个新鲜橘子就把 cnt 减 1,若 cnt 变为 0 立即返回当前轮数;
- BFS 结束后若 cnt > 0,说明有新鲜橘子永远无法被感染,返回 -1,否则返回 0。 解法二(DFS 逐分钟模拟,供对照): 每分钟扫描网格把可被感染的橘子标记为腐烂,检查是否有新感染;反复直到无新感染 或新鲜橘已清零。与解法一结果一致,仅限小网格。
代码实现
public class Solution {
// 解法一:多源 BFS(主解法,提交 LeetCode)
public int orangesRotting(int[][] grid) {
int m = grid.length, n = grid[0].length;
java.util.Queue<int[]> q = new java.util.LinkedList<>();
int fresh = 0;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == 1) {
++fresh;
} else if (grid[i][j] == 2) {
q.offer(new int[] {i, j});
}
}
}
if (fresh == 0) {
return 0;
}
int[] dx = {-1, 0, 1, 0};
int[] dy = {0, 1, 0, -1};
int minutes = 0;
while (!q.isEmpty()) {
int size = q.size();
for (int t = 0; t < size; ++t) {
int[] cur = q.poll();
for (int d = 0; d < 4; ++d) {
int nx = cur[0] + dx[d];
int ny = cur[1] + dy[d];
if (nx >= 0 && nx < m && ny >= 0 && ny < n && grid[nx][ny] == 1) {
grid[nx][ny] = 2;
--fresh;
q.offer(new int[] {nx, ny});
}
}
}
++minutes;
if (fresh == 0) {
return minutes;
}
}
return -1;
}
// 解法二:逐分钟模拟(对照验证)
public int orangesRottingSim(int[][] grid) {
int m = grid.length, n = grid[0].length;
int fresh = 0;
for (int[] row : grid) {
for (int x : row) {
if (x == 1) {
++fresh;
}
}
}
if (fresh == 0) {
return 0;
}
int[] dx = {-1, 0, 1, 0};
int[] dy = {0, 1, 0, -1};
int minutes = 0;
while (fresh > 0) {
boolean changed = false;
boolean[][] willRot = new boolean[m][n];
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == 2) {
for (int d = 0; d < 4; ++d) {
int nx = i + dx[d];
int ny = j + dy[d];
if (nx >= 0 && nx < m && ny >= 0 && ny < n && grid[nx][ny] == 1) {
willRot[nx][ny] = true;
}
}
}
}
}
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (willRot[i][j]) {
grid[i][j] = 2;
--fresh;
changed = true;
}
}
}
if (!changed) {
return -1;
}
++minutes;
}
return minutes;
}
private static int[][] cloneGrid(int[][] g) {
int[][] c = new int[g.length][];
for (int i = 0; i < g.length; ++i) {
c[i] = g[i].clone();
}
return c;
}
private static void check(int[][] g, int expected, int caseId) {
int got1 = new Solution().orangesRotting(cloneGrid(g));
int got2 = new Solution().orangesRottingSim(cloneGrid(g));
if (got1 != expected || got2 != expected) {
System.out.println("FAIL case" + caseId + " expected=" + expected
+ " got1=" + got1 + " got2=" + got2);
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new int[][] {{2, 1, 1}, {1, 1, 0}, {0, 1, 1}}, 4, 1);
check(new int[][] {{2, 1, 1}, {0, 1, 1}, {1, 0, 1}}, -1, 2);
check(new int[][] {{0, 2}}, 0, 3);
check(new int[][] {{1}}, -1, 4);
check(new int[][] {{2}}, 0, 5);
check(new int[][] {{1, 2}}, 1, 6);
check(new int[][] {{0, 0, 0}}, 0, 7);
System.out.println("== 0994 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(m*n),每格至多入队一次;
- 空间复杂度:O(m*n),队列。 解法二:
- 时间复杂度:O(T * m * n),T 为分钟数;
- 空间复杂度:O(m*n)。
995. Minimum Number of K Consecutive Bit Flips(K 连续位的最小翻转次数)
题目描述
给定一个二进制数组 nums 和一个整数 k。
k 位翻转就是从 nums 中选择一个长度为 k 的子数组,同时把子数组中的每一个 0 都改成 1,
把子数组中的每一个 1 都改成 0。
返回数组中不存在 0 所需的最小 k 位翻转次数。如果不可能,则返回 -1。
子数组是数组的连续部分。
示例 1
- 输入:
nums= [0,1,0], K = 1 - 输出:2
- 解释:先翻转 A[0],然后翻转 A[2]。
示例 2
- 输入:
nums= [1,1,0], K = 2 - 输出:-1
示例 3
- 输入:
nums= [0,0,0,1,0,1,1,0], K = 3 - 输出:3 提示:
- 1 <= nums.length <= 10^5- 1 <= k <= nums.length
解题思路
解法一(差分数组 + 贪心): 翻转顺序不影响最终结果。从左到右贪心:对于位置 i,若此前叠加的翻转次数与 nums[i] 的奇偶性相同,则 nums[i] 最终仍是 0,必须在 i 处开启一次长度为 k 的翻转。 用差分数组 d 维护「从 i 开始叠加的翻转次数」:开启翻转时 d[i]++、d[i+k]--, 当前位置累计次数 s += d[i]。若开启翻转后 i+k 越界则返回 -1。 时间复杂度 O(n)。 解法二(O(1) 空间滑动窗口,供对照): 与差分等价:用变量 flipped 记录当前位置是否被奇数个翻转覆盖。当 i >= k 且 nums[i-k] == -1(标记在 i-k 处开启过翻转)时,flipped ^= 1。若 flipped == nums[i] 则 当前位仍为 0,必须开翻:flipped ^= 1、标记 nums[i] = -1、答案 +1;i+k 越界返回 -1。
代码实现
public class Solution {
// 解法一:差分数组(主解法,提交 LeetCode)
public int minKBitFlips(int[] nums, int k) {
int n = nums.length;
int[] d = new int[n + 1];
int ans = 0, s = 0;
for (int i = 0; i < n; ++i) {
s += d[i];
if (s % 2 == nums[i]) {
if (i + k > n) {
return -1;
}
++d[i];
--d[i + k];
++s;
++ans;
}
}
return ans;
}
// 解法二:原地滑动窗口(对照验证)
public int minKBitFlipsO1(int[] nums, int k) {
int n = nums.length;
int ans = 0, flipped = 0;
for (int i = 0; i < n; ++i) {
if (i >= k && nums[i - k] == -1) {
flipped ^= 1;
}
if (flipped == nums[i]) {
if (i + k > n) {
return -1;
}
flipped ^= 1;
++ans;
nums[i] = -1;
}
}
return ans;
}
private static void check(int[] nums, int k, int expected, int caseId) {
int[] n1 = nums.clone();
int[] n2 = nums.clone();
int got1 = new Solution().minKBitFlips(n1, k);
int got2 = new Solution().minKBitFlipsO1(n2, k);
if (got1 != expected || got2 != expected) {
System.out.println("FAIL case" + caseId + " expected=" + expected
+ " got1=" + got1 + " got2=" + got2);
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new int[] {0, 1, 0}, 1, 2, 1);
check(new int[] {1, 1, 0}, 2, -1, 2);
check(new int[] {0, 0, 0, 1, 0, 1, 1, 0}, 3, 3, 3);
check(new int[] {0}, 1, 1, 4);
check(new int[] {1}, 1, 0, 5);
check(new int[] {0, 0, 0}, 2, -1, 6);
check(new int[] {1, 0, 0, 1}, 2, 1, 7);
System.out.println("== 0995 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(n);
- 空间复杂度:O(n),差分数组。 解法二:
- 时间复杂度:O(n);
- 空间复杂度:O(1),原地标记。
996. Number of Squareful Arrays(平方数组的数目)
题目描述
如果一个数组的任意两个相邻元素之和都是完全平方数,则该数组称为平方数组。
给定一个整数数组 nums,返回所有属于平方数组的 nums 的排列数量。
如果存在某个索引 i 使得 perm1[i] != perm2[i],则认为两个排列 perm1 和 perm2 不同。
示例 1
- 输入:
nums= [1,17,8] - 输出:2
- 解释:[1,8,17] 和 [17,8,1] 是有效的排列。
示例 2
- 输入:
nums= [2,2,2] - 输出:1 提示:
- 1 <= nums.length <= 12- 0 <= nums[i] <= 10^9
解题思路
解法一(状态压缩 DP): n <= 12,可以用位掩码表示已使用的数字。f[i][j] 表示已选集合为 i(二进制第 b 位为 1 表示已选 nums[b])、且当前排列最后一个下标是 j 的排列数。 初始 f[1<<j][j] = 1;转移:枚举下一个要追加的下标 k(不在集合内),若 nums[j] + nums[k] 是完全平方数,则 f[i|(1<<k)][k] += f[i][j]。 最终答案是 sum(f[(1<<n)-1][j])。由于相同数字的不同下标会产生重复排列, 需除以各数值出现次数的阶乘(乘法原理去重)。 解法二(回溯 + 集合去重,供对照): 从空排列开始 DFS 构造,每次挑选「与前一个数之和为平方数」且未被使用的下标; 相同数值在同一层只尝试第一个可用下标(用 Set 记录本层已尝试值)以达到去重。 计数即答案。仅适用于 n 较小。
代码实现
public class Solution {
// 解法一:状态压缩 DP(主解法,提交 LeetCode)
public int numSquarefulPerms(int[] nums) {
int n = nums.length;
int full = 1 << n;
int[][] f = new int[full][n];
for (int j = 0; j < n; ++j) {
f[1 << j][j] = 1;
}
for (int mask = 1; mask < full; ++mask) {
for (int j = 0; j < n; ++j) {
if ((mask & (1 << j)) == 0 || f[mask][j] == 0) {
continue;
}
for (int k = 0; k < n; ++k) {
if ((mask & (1 << k)) != 0) {
continue;
}
long sum = (long) nums[j] + nums[k];
int root = (int) Math.sqrt(sum);
if ((long) root * root == sum) {
f[mask | (1 << k)][k] += f[mask][j];
}
}
}
}
long ans = 0;
for (int j = 0; j < n; ++j) {
ans += f[full - 1][j];
}
// 除以相同数值出现次数的阶乘(每种数值的重复下标排列属同一序列)
java.util.Map<Integer, Integer> cnt = new java.util.HashMap<>();
for (int x : nums) {
cnt.merge(x, 1, Integer::sum);
}
int[] fact = new int[n + 1];
fact[0] = 1;
for (int i = 1; i <= n; ++i) {
fact[i] = fact[i - 1] * i;
}
for (int v : cnt.values()) {
ans /= fact[v];
}
return (int) ans;
}
// 解法二:回溯 + 集合去重(对照验证)
private int n;
private int[] a;
private int count;
public int numSquarefulPermsBacktrack(int[] nums) {
n = nums.length;
a = nums.clone();
count = 0;
java.util.Arrays.sort(a);
boolean[] used = new boolean[n];
dfs(0, -1, used);
return count;
}
private void dfs(int depth, int last, boolean[] used) {
if (depth == n) {
++count;
return;
}
java.util.Set<Integer> seen = new java.util.HashSet<>();
for (int i = 0; i < n; ++i) {
if (used[i] || seen.contains(a[i])) {
continue;
}
if (last != -1) {
long sum = (long) last + a[i];
int root = (int) Math.sqrt(sum);
if ((long) root * root != sum) {
continue;
}
}
seen.add(a[i]);
used[i] = true;
dfs(depth + 1, a[i], used);
used[i] = false;
}
}
private static void check(int[] nums, int expected, int caseId) {
int got1 = new Solution().numSquarefulPerms(nums.clone());
int got2 = new Solution().numSquarefulPermsBacktrack(nums.clone());
if (got1 != expected || got2 != expected) {
System.out.println("FAIL case" + caseId + " expected=" + expected
+ " got1=" + got1 + " got2=" + got2);
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new int[] {1, 17, 8}, 2, 1);
check(new int[] {2, 2, 2}, 1, 2);
check(new int[] {1}, 1, 3);
check(new int[] {0, 0}, 1, 4);
check(new int[] {1, 1, 8, 1, 8}, 1, 5);
check(new int[] {2, 2, 2, 2, 2}, 1, 6);
check(new int[] {1, 8, 17, 8}, 6, 7);
System.out.println("== 0996 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(2^n * n^2);
- 空间复杂度:O(2^n * n)。 解法二:
- 时间复杂度:O(n!),最坏全排列数;
- 空间复杂度:O(n)。
997. Find the Town Judge(找到小镇的法官)
题目描述
小镇里有 n 个人,按从 1 到 n 的顺序编号。传言称,这些人中有一个暗地里是小镇法官。
如果小镇法官真的存在,那么:
- 小镇法官不会信任任何人;
- 每个人(除了小镇法官)都信任这位小镇法官;
- 只有一个人同时满足属性 1 和属性 2。
给你一个数组 trust,其中 trust[i] = [
a_i,b_i] 表示编号为a_i的人信任编号为b_i的人。 如果小镇法官存在并且可以确定他的身份,请返回该法官的编号;否则,返回 -1。
示例 1
- 输入:
n= 2, trust = [[1,2]] - 输出:2
示例 2
- 输入:
n= 3, trust = [[1,3],[2,3]] - 输出:3
示例 3
- 输入:
n= 3, trust = [[1,3],[2,3],[3,1]] - 输出:-1 提示:
- 1 <= n <= 1000- 0 <= trust.length <= 10^4- trust[i].length == 2-
- trust 中的所有 trust[i] = [
a_i,b_i] 互不相同
- trust 中的所有 trust[i] = [
- a_i != b_i- 1 <= a_i, b_i <= n
解题思路
解法一(出入度计数): 法官的特征是「不信任任何人」且「被其他所有 n-1 人信任」。 用 cnt1[i] 记录 i 信任的人数(出度),cnt2[i] 记录信任 i 的人数(入度)。 扫描 trust 后,若存在编号 i 满足 cnt1[i] == 0 且 cnt2[i] == n - 1,则 i 是法官。 由于「只有一个人同时满足属性 1 和属性 2」,满足条件的编号至多一个。 解法二(差值计数,供对照): 记 diff[i] = 被信任数 - 信任数(入度减出度)。法官满足 diff[i] == n - 1。 与解法一结果一致。
代码实现
public class Solution {
// 解法一:出入度计数(主解法,提交 LeetCode)
public int findJudge(int n, int[][] trust) {
int[] cnt1 = new int[n + 1]; // 信任人数(出度)
int[] cnt2 = new int[n + 1]; // 被信任人数(入度)
for (int[] t : trust) {
++cnt1[t[0]];
++cnt2[t[1]];
}
for (int i = 1; i <= n; ++i) {
if (cnt1[i] == 0 && cnt2[i] == n - 1) {
return i;
}
}
return -1;
}
// 解法二:差值计数(对照验证)
public int findJudgeDiff(int n, int[][] trust) {
int[] diff = new int[n + 1];
for (int[] t : trust) {
--diff[t[0]]; // 信任别人:出度 +
++diff[t[1]]; // 被别人信任:入度 +
}
for (int i = 1; i <= n; ++i) {
if (diff[i] == n - 1) {
return i;
}
}
return -1;
}
private static void check(int n, int[][] trust, int expected, int caseId) {
int got1 = new Solution().findJudge(n, trust);
int got2 = new Solution().findJudgeDiff(n, trust);
if (got1 != expected || got2 != expected) {
System.out.println("FAIL case" + caseId + " expected=" + expected
+ " got1=" + got1 + " got2=" + got2);
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(2, new int[][] {{1, 2}}, 2, 1);
check(3, new int[][] {{1, 3}, {2, 3}}, 3, 2);
check(3, new int[][] {{1, 3}, {2, 3}, {3, 1}}, -1, 3);
check(1, new int[][] {}, 1, 4);
check(3, new int[][] {{1, 2}, {2, 3}}, -1, 5);
check(3, new int[][] {{1, 3}, {2, 3}, {3, 2}}, -1, 6);
check(4, new int[][] {{1, 3}, {1, 4}, {2, 3}, {2, 4}, {4, 3}}, 3, 7);
System.out.println("== 0997 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(n + m),m 为 trust 长度;
- 空间复杂度:O(n)。 解法二:
- 时间复杂度:O(n + m);
- 空间复杂度:O(n)。
998. Maximum Binary Tree II(最大二叉树 II)
题目描述
最大树定义:一棵树,并满足:其中每个节点的值都大于其子树中的任何其他值。
给你最大树的根节点 root 和一个整数 val。
就像之前的问题那样,给定的树是利用 Construct(a) 例程从列表 a(root = Construct(a))递归地构建的:
- 如果
a为空,返回null; - 否则,令
a[i]作为a的最大元素。创建一个值为a[i]的根节点root; root的左子树将被构建为 Construct([a[0],a[1], ...,a[i-1]]);root的右子树将被构建为 Construct([a[i+1],a[i+2], ...,a[a.length-1]]);- 返回
root。 请注意,题目没有直接给出a,只是给出一个根节点root= Construct(a)。 假设b是a的副本,并在末尾附加值val。题目数据保证b中的值互不相同。 返回 Construct(b)。
示例 1
- 输入:
root= [4,1,3,null,null,2],val= 5 - 输出:[5,4,
null,1,3,null,null,2] - 解释:
a= [1,4,2,3],b= [1,4,2,3,5]
示例 2
- 输入:
root= [5,2,4,null,1],val= 3 - 输出:[5,2,4,
null,1,null,3] - 解释:
a= [2,1,5,4],b= [2,1,5,4,3]
示例 3
- 输入:
root= [5,2,3,null,1],val= 4 - 输出:[5,2,4,
null,1,3] - 解释:
a= [2,1,5,3],b= [2,1,5,3,4] 提示: -
- 树中节点数目在范围 [1, 100] 内
- 1 <= Node.val <= 100-
- 树中的所有值互不相同
- 1 <= val <= 100
解题思路
解法一(递归): 在最大树对应的数组末尾追加 val,再重建最大树。val 是最后追加的元素, 因此它要么成为新根(val 比根大),要么插入右子树,绝不进入左子树:
- 若 root == null 或 root.val < val:新节点为根,原树作为左子树;
- 否则 val 仍应插到右子树:root.right = insertIntoMaxTree(root.right, val)。 解法二(迭代沿右脊,供对照): 沿右子树找到「当前节点值 > val > 右孩子值(或右孩子为空)」的位置, 在该处把新节点挂上:新节点左孩子 = 当前位置的右节点,当前节点右孩子 = 新节点。 与递归结果一致,且无需递归栈。
代码实现
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}
public class Solution {
// 解法一:递归(主解法,提交 LeetCode)
public TreeNode insertIntoMaxTree(TreeNode root, int val) {
if (root == null || root.val < val) {
return new TreeNode(val, root, null);
}
root.right = insertIntoMaxTree(root.right, val);
return root;
}
// 解法二:迭代沿右脊(对照验证)
public TreeNode insertIntoMaxTreeIter(TreeNode root, int val) {
if (root.val < val) {
return new TreeNode(val, root, null);
}
TreeNode cur = root;
while (cur.right != null && cur.right.val > val) {
cur = cur.right;
}
TreeNode node = new TreeNode(val);
node.left = cur.right;
cur.right = node;
return root;
}
private static TreeNode build(Integer[] arr) {
if (arr == null || arr.length == 0 || arr[0] == null) {
return null;
}
TreeNode root = new TreeNode(arr[0]);
java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < arr.length) {
TreeNode cur = q.poll();
if (arr[i] != null) {
cur.left = new TreeNode(arr[i]);
q.offer(cur.left);
}
++i;
if (i < arr.length && arr[i] != null) {
cur.right = new TreeNode(arr[i]);
q.offer(cur.right);
}
++i;
}
return root;
}
private static Integer[] toLevel(TreeNode root) {
java.util.List<Integer> res = new java.util.ArrayList<>();
java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
q.offer(root);
while (!q.isEmpty()) {
TreeNode cur = q.poll();
if (cur == null) {
res.add(null);
continue;
}
res.add(cur.val);
q.offer(cur.left);
q.offer(cur.right);
}
// 去掉尾部空位
while (!res.isEmpty() && res.get(res.size() - 1) == null) {
res.remove(res.size() - 1);
}
return res.toArray(new Integer[0]);
}
// 结构比较:两棵树逐节点判断值一致且左右孩子结构一致
private static boolean sameTree(TreeNode a, TreeNode b) {
if (a == null || b == null) {
return a == b;
}
return a.val == b.val && sameTree(a.left, b.left) && sameTree(a.right, b.right);
}
private static void check(Integer[] arr, int val, Integer[] expected, int caseId) {
TreeNode r1 = build(arr);
TreeNode r2 = build(arr);
TreeNode t1 = new Solution().insertIntoMaxTree(r1, val);
TreeNode t2 = new Solution().insertIntoMaxTreeIter(r2, val);
TreeNode exp = build(expected);
boolean ok1 = sameTree(t1, exp);
boolean ok2 = sameTree(t2, exp);
if (!ok1 || !ok2) {
System.out.println("FAIL case" + caseId + " expected=" + java.util.Arrays.toString(toLevel(exp))
+ " got1=" + java.util.Arrays.toString(toLevel(t1))
+ " got2=" + java.util.Arrays.toString(toLevel(t2)));
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new Integer[] {4, 1, 3, null, null, 2}, 5,
new Integer[] {5, 4, null, 1, 3, null, null, 2}, 1);
check(new Integer[] {5, 2, 4, null, 1}, 3,
new Integer[] {5, 2, 4, null, 1, null, 3}, 2);
check(new Integer[] {5, 2, 3, null, 1}, 4,
new Integer[] {5, 2, 4, null, 1, 3}, 3);
check(new Integer[] {1}, 2, new Integer[] {2, 1}, 4);
check(new Integer[] {3, 1, 2}, 0, new Integer[] {3, 1, 2, null, null, null, 0}, 5);
System.out.println("== 0998 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(n),最坏访问右脊全部节点;
- 空间复杂度:O(h),递归栈,h 为右脊长度(最坏 O(n))。 解法二:
- 时间复杂度:O(n);
- 空间复杂度:O(1)。
999. Available Captures for Rook(可以被一步捕获的棋子数)
题目描述
给定一个 8 x 8 的棋盘,只有一个白色的车,用字符 'R' 表示。棋盘上还可能存在白色的象 'B'
以及黑色的卒 'p'。空方块用字符 '.' 表示。
车可以按水平或竖直方向(上,下,左,右)移动任意个方格直到它遇到另一个棋子或棋盘的边界。
如果它能够在一次移动中移动到棋子的方格,则能够吃掉棋子。
注意:车不能穿过其它棋子,比如象和卒。这意味着如果有其它棋子挡住了路径,车就不能够吃掉棋子。
返回白车攻击范围内兵的数量。
示例 1
- 输入:[[".",".",".",".",".",".",".","."],
[".",".",".","
p",".",".",".","."], [".",".",".","R",".",".",".","p"], [".",".",".",".",".",".",".","."], [".",".",".",".",".",".",".","."], [".",".",".","p",".",".",".","."], [".",".",".",".",".",".",".","."], [".",".",".",".",".",".",".","."]] - 输出:3
示例 2
- 输入:[[".",".",".",".",".",".",".","."],
[".","
p","p","p","p","p",".","."], [".","p","p","B","p","p",".","."], [".","p","B","R","B","p",".","."], [".","p","p","B","p","p",".","."], [".","p","p","p","p","p",".","."], [".",".",".",".",".",".",".","."], [".",".",".",".",".",".",".","."]] - 输出:0
示例 3
- 输入:[[".",".",".",".",".",".",".","."],
[".",".",".","
p",".",".",".","."], [".",".",".","p",".",".",".","."], ["p","p",".","R",".","p","B","."], [".",".",".",".",".",".",".","."], [".",".",".","B",".",".",".","."], [".",".",".","p",".",".",".","."], [".",".",".",".",".",".",".","."]] - 输出:3 提示:
- board.length == 8- board[i].length == 8-
board[i][j]可以是 'R','.','B' 或 'p'
-
- 只有一个格子上存在
board[i][j]== 'R'
- 只有一个格子上存在
解题思路
解法一(四向模拟): 车可以沿上下左右四个方向直线移动,遇象 'B' 立即停下(象挡路),遇卒 'p' 可以吃掉 并停止该方向(吃到卒后不能再继续穿透)。步骤如下:
- 找到 'R' 的位置;
- 对四个方向分别沿边步进:越界或遇 'B' 停止;遇 'p' 计数 +1 并停止; '.' 则继续。 返回值即四个方向吃卒总数。 解法二(BFS 一步可达,供对照): 车在一次移动中能吃到兵等价于该兵在车所在行或列的直线上、且两者之间没有其他棋子。 对每个卒检查是否与车同行/同列且路径上无 'B'/'p' 阻挡。与解法一结果一致。
代码实现
public class Solution {
// 解法一:四向模拟(主解法,提交 LeetCode)
public int numRookCaptures(char[][] board) {
int n = board.length;
int rx = 0, ry = 0;
outer:
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (board[i][j] == 'R') {
rx = i;
ry = j;
break outer;
}
}
}
int[] dx = {-1, 0, 1, 0};
int[] dy = {0, 1, 0, -1};
int ans = 0;
for (int d = 0; d < 4; ++d) {
int x = rx + dx[d];
int y = ry + dy[d];
while (x >= 0 && x < n && y >= 0 && y < n) {
char c = board[x][y];
if (c == 'B') {
break;
}
if (c == 'p') {
++ans;
break;
}
x += dx[d];
y += dy[d];
}
}
return ans;
}
// 解法二:逐卒检查直线(对照验证)
public int numRookCapturesCheck(char[][] board) {
int n = board.length;
int rx = 0, ry = 0;
outer:
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (board[i][j] == 'R') {
rx = i;
ry = j;
break outer;
}
}
}
int ans = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (board[i][j] != 'p') {
continue;
}
if (i == rx) {
// 同行:检查 (rx, min(ry,j)+1 .. max(ry,j)-1) 是否有棋子,且没有越过车时……车在端点
// 逻辑:直接判断与车之间无障碍
if (clearRow(board, rx, ry, j)) {
++ans;
}
} else if (j == ry) {
if (clearCol(board, ry, rx, i)) {
++ans;
}
}
}
}
return ans;
}
private boolean clearRow(char[][] board, int row, int a, int b) {
int lo = Math.min(a, b), hi = Math.max(a, b);
for (int j = lo + 1; j < hi; ++j) {
if (board[row][j] != '.') {
return false;
}
}
return true;
}
private boolean clearCol(char[][] board, int col, int a, int b) {
int lo = Math.min(a, b), hi = Math.max(a, b);
for (int i = lo + 1; i < hi; ++i) {
if (board[i][col] != '.') {
return false;
}
}
return true;
}
private static char[][] str2board(String[] rows) {
char[][] b = new char[8][8];
for (int i = 0; i < 8; ++i) {
b[i] = rows[i].toCharArray();
}
return b;
}
private static void check(String[] rows, int expected, int caseId) {
char[][] b1 = str2board(rows);
char[][] b2 = str2board(rows);
int got1 = new Solution().numRookCaptures(b1);
int got2 = new Solution().numRookCapturesCheck(b2);
if (got1 != expected || got2 != expected) {
System.out.println("FAIL case" + caseId + " expected=" + expected
+ " got1=" + got1 + " got2=" + got2);
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new String[] {
"........",
"...p....",
"...R...p",
"........",
"........",
"...p....",
"........",
"........"
}, 3, 1);
check(new String[] {
"........",
".ppppp..",
".ppBpp..",
".pBRBp..",
".ppBpp..",
".ppppp..",
"........",
"........"
}, 0, 2);
check(new String[] {
"........",
"...p....",
"...p....",
"pp.R.pB.",
"........",
"...B....",
"...p....",
"........"
}, 3, 3);
check(new String[] {
"........",
"........",
"........",
"...R....",
"........",
"........",
"...p....",
"........"
}, 1, 4);
check(new String[] {
"........",
"........",
"........",
"...R....",
"........",
"...B....",
"...p....",
"........"
}, 0, 5);
check(new String[] {
"........",
"........",
"........",
"...R....",
"........",
"........",
"........",
"........"
}, 0, 6);
System.out.println("== 0999 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(88 + 48) = O(1),棋盘固定大小;
- 空间复杂度:O(1)。 解法二:
- 时间复杂度:O(8*8 * 8) = O(1);
- 空间复杂度:O(1)。
1000. Minimum Cost to Merge Stones(合并石头的最低成本)
题目描述
有 n 堆石头排成一排,第 i 堆中有 stones[i] 块石头。
每次移动需要将连续的 k 堆石头合并为一堆,而这次移动的成本为这 k 堆中石头的总数。
返回把所有石头合并成一堆的最低成本。如果无法合并成一堆,返回 -1。
示例 1
- 输入:stones = [3,2,4,1], K = 2
- 输出:20
- 解释:合并 [3,2] 成本 5 → [5,4,1];合并 [4,1] 成本 5 → [5,5];合并 [5,5] 成本 10 → [10]。 总成本 20。
示例 2
- 输入:stones = [3,2,4,1], K = 3
- 输出:-1
示例 3
- 输入:stones = [3,5,1,2,6], K = 3
- 输出:25 提示:
- n == stones.length- 1 <= n <= 30- 1 <= stones[i] <= 100- 2 <= k <= 30
解题思路
解法一(区间 DP + 前缀和): 每次把「连续 K 堆」合并为一堆,合并后总堆数减少 K-1。
- 可行性:最终合并成一堆 ⇔ (n-1) % (K-1) == 0,否则返回 -1。
- 定义 f[i][j][k] = 把区间 [i, j] 合并成 k 堆的最小成本,初始 f[i][i][1] = 0,其余 +∞。 转移(枚举断点 h):f[i][j][k] = min(f[i][h][1] + f[h+1][j][k-1])。 因为每次只能合并连续段,先使左段并成 1 堆、右段并成 k-1 堆,随后一并用掉一次合并。
- 当 k == K 时,把 K 堆并成 1 堆需要付出区间总和: f[i][j][1] = f[i][j][K] + (s[j] - s[i-1])。 按区间长度递增计算,答案为 f[1][n][1]。 解法二(记忆化搜索,供对照): 与解法一相同状态定义,用递归 + memo 实现,枚举断点方式一致,结果应与 DP 相同。
代码实现
public class Solution {
// 解法一:区间 DP(主解法,提交 LeetCode)
public int mergeStones(int[] stones, int K) {
int n = stones.length;
if ((n - 1) % (K - 1) != 0) {
return -1;
}
int[] s = new int[n + 1];
for (int i = 1; i <= n; ++i) {
s[i] = s[i - 1] + stones[i - 1];
}
final int INF = 1 << 20;
int[][][] f = new int[n + 1][n + 1][K + 1];
for (int[][] g : f) {
for (int[] row : g) {
java.util.Arrays.fill(row, INF);
}
}
for (int i = 1; i <= n; ++i) {
f[i][i][1] = 0;
}
for (int len = 2; len <= n; ++len) {
for (int i = 1; i + len - 1 <= n; ++i) {
int j = i + len - 1;
for (int k = 2; k <= K; ++k) {
for (int h = i; h < j; ++h) {
f[i][j][k] = Math.min(f[i][j][k],
f[i][h][1] + f[h + 1][j][k - 1]);
}
}
f[i][j][1] = f[i][j][K] + (s[j] - s[i - 1]);
}
}
return f[1][n][1];
}
// 解法二:记忆化搜索(对照验证)
private int K;
private int[] ps;
private int[][][] memo;
public int mergeStonesMemo(int[] stones, int k) {
int n = stones.length;
if ((n - 1) % (k - 1) != 0) {
return -1;
}
K = k;
ps = new int[n + 1];
for (int i = 1; i <= n; ++i) {
ps[i] = ps[i - 1] + stones[i - 1];
}
memo = new int[n + 1][n + 1][K + 1];
for (int[][] g : memo) {
for (int[] row : g) {
java.util.Arrays.fill(row, -1);
}
}
return dfs(1, n, 1);
}
private int dfs(int i, int j, int piles) {
if (i == j) {
return piles == 1 ? 0 : INF;
}
if (piles < 1 || piles > K) {
return INF;
}
if (memo[i][j][piles] != -1) {
return memo[i][j][piles];
}
int best = INF;
if (piles == 1) {
// 先得到 K 堆,再合并为 1 堆
int cost = dfs(i, j, K);
best = cost + (ps[j] - ps[i - 1]);
} else {
for (int h = i; h < j; ++h) {
int left = dfs(i, h, 1);
int right = dfs(h + 1, j, piles - 1);
best = Math.min(best, left + right);
}
}
memo[i][j][piles] = best;
return best;
}
private static final int INF = 1 << 20;
private static void check(int[] stones, int k, int expected, int caseId) {
int got1 = new Solution().mergeStones(stones.clone(), k);
int got2 = new Solution().mergeStonesMemo(stones.clone(), k);
if (got1 != expected || got2 != expected) {
System.out.println("FAIL case" + caseId + " expected=" + expected
+ " got1=" + got1 + " got2=" + got2);
return;
}
System.out.println("ok case" + caseId);
}
public static void main(String[] args) {
check(new int[] {3, 2, 4, 1}, 2, 20, 1);
check(new int[] {3, 2, 4, 1}, 3, -1, 2);
check(new int[] {3, 5, 1, 2, 6}, 3, 25, 3);
check(new int[] {1}, 2, 0, 4);
check(new int[] {1, 2}, 2, 3, 5);
check(new int[] {1, 2, 3}, 2, 9, 6);
check(new int[] {7}, 2, 0, 7);
check(new int[] {1, 2, 3, 4}, 3, -1, 8); // (4-1)%(3-1)=3%2=1 → -1
System.out.println("== 1000 PASS ==");
}
}
复杂度分析
解法一:
- 时间复杂度:O(n^3 * K);
- 空间复杂度:O(n^2 * K)。 解法二:
- 时间复杂度:O(n^3 * K)(状态同 DP);
- 空间复杂度:O(n^2 * K)。