LeetCode 951-963. 题解合集
951. Flip Equivalent Binary Trees(翻转等价二叉树)
题目描述
我们可以为二叉树 T 定义一个翻转操作:选择任意节点,然后交换它的左子树和右子树。
只要经过一定次数的翻转操作后,能使 X 等于 Y,我们就称二叉树 X 翻转等价于二叉树
Y。这些树由根节点 root1 和 root2 给出。如果两个二叉树是翻转等价的树,则返回
true,否则返回 false。
示例 1
- 输入:root1 = [1,2,3,4,5,6,
null,null,null,7,8], root2 = [1,3,2,null,6,4,5,null,null,null,null,8,7] - 输出:
true - 解释:我们翻转值为 1,3 以及 5 的三个节点。
示例 2
- 输入:root1 = [], root2 = []
- 输出:
true
示例 3
- 输入:root1 = [], root2 = [1]
- 输出:
false
提示: 0 <= 节点数 <= 100- 每棵树中的每个值都是唯一的、在
[0, 99]范围内的整数
解题思路
翻转操作只交换某节点的左右孩子,因此比较两棵树时,每个节点要么不翻转直接比左右,
要么翻转后交叉比。
解法一(递归分治,主提交):dfs(a, b) 判断两棵子树是否翻转等价:两树同空为真;
任一为空或根值不同为假;否则在「不翻转:a.left vs b.left 且 a.right vs b.right」
与「翻转:a.left vs b.right 且 a.right vs b.left」两种组合中,只要一种成立即可。
O(n)。
解法二(迭代栈对照):用一个栈保存待比较的节点对,弹出后按同样规则把需要比较的子对
压栈;任一对不匹配即返回 false。逻辑与解法一完全等价,展示迭代写法。
代码实现
import java.util.ArrayDeque;
import java.util.Deque;
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;
}
}
class Solution {
// 解法一:递归分治(推荐提交)
public boolean flipEquiv(TreeNode root1, TreeNode root2) {
if (root1 == null && root2 == null) {
return true;
}
if (root1 == null || root2 == null || root1.val != root2.val) {
return false;
}
// 不翻转 或 翻转后交叉
return (flipEquiv(root1.left, root2.left) && flipEquiv(root1.right, root2.right))
|| (flipEquiv(root1.left, root2.right) && flipEquiv(root1.right, root2.left));
}
// 解法二:迭代栈(对照实现)
public boolean flipEquiv2(TreeNode root1, TreeNode root2) {
Deque<TreeNode[]> stk = new ArrayDeque<>();
stk.push(new TreeNode[] {root1, root2});
while (!stk.isEmpty()) {
TreeNode[] p = stk.pop();
TreeNode a = p[0], b = p[1];
if (a == null && b == null) {
continue;
}
if (a == null || b == null || a.val != b.val) {
return false;
}
// 先按不翻转入栈,若不成立再试翻转(此处用短路思路先入翻转对,判断等价)
if ((a.left != null && b.left != null && a.left.val == b.left.val)
|| (a.left == null && b.left == null)) {
stk.push(new TreeNode[] {a.left, b.left});
stk.push(new TreeNode[] {a.right, b.right});
} else {
stk.push(new TreeNode[] {a.left, b.right});
stk.push(new TreeNode[] {a.right, b.left});
}
}
return true;
}
// 用层序数组建树(null 表示缺子节点),供本地测试用
public static TreeNode build(Integer[] arr) {
if (arr == null || arr.length == 0 || arr[0] == null) {
return null;
}
TreeNode root = new TreeNode(arr[0]);
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < arr.length) {
TreeNode node = q.poll();
if (arr[i] != null) {
node.left = new TreeNode(arr[i]);
q.offer(node.left);
}
++i;
if (i < arr.length && arr[i] != null) {
node.right = new TreeNode(arr[i]);
q.offer(node.right);
}
++i;
}
return root;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
Integer[][] a1 = {
{1,2,3,4,5,6,null,null,null,7,8},
{},
{},
{1,2,3},
{1}
};
Integer[][] a2 = {
{1,3,2,null,6,4,5,null,null,null,null,8,7},
{},
{1},
{1,3,2},
{2}
};
boolean[] exp = {true, true, false, true, false};
for (int t = 0; t < exp.length; ++t) {
++total;
boolean g1 = s.flipEquiv(build(a1[t]), build(a2[t]));
boolean g2 = s.flipEquiv2(build(a1[t]), build(a2[t]));
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 951-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0951 PASS ==");
} else {
System.out.println("== 0951 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
952. Largest Component Size by Common Factor(按公因数计算最大组件大小)
题目描述
给定一个由不同正整数组成的非空数组 nums,考虑下面的图:
- 有
nums.length个节点,按从nums[0]到nums[nums.length - 1]标记; - 只有当
nums[i]和nums[j]共用一个大于 1 的公因数时,nums[i]和nums[j]之间
才有一条边。
返回图中最大连通组件的大小。
示例 1
- 输入:
nums= [4,6,15,35] - 输出:4
示例 2
- 输入:
nums= [20,50,9,63] - 输出:2
示例 3
- 输入:
nums= [2,3,6,7,4,12,21,39] - 输出:8
提示: 1 <= nums.length <= 2 * 10^41 <= nums[i] <= 10^5nums中所有值都不同
解题思路
公因数 > 1 则连边,求最大连通块。两两求 gcd 是平方级,不可行;正确做法是把每个数与其
全部因数在并查集中合并,拥有共同因数的数自然落到同一连通块中。
解法一(因数合并 + 并查集,主提交):设值域上界 m = max(nums),开 m+1 的并查集。
对每个 v,试除找因数 d,把 v 与 d、v 与 v/d 合并。最后统计每个
find(v) 的频数取最大。并查集按路径压缩 + 按秩合并,O(sum(sqrt(v))α) 。
解法二(质因数分解合并对照):先用筛法预处理 [2, m] 的最小质因子 lp,再对每个 v
分解成不同质因子集合,把 v 与每个质因子合并。质因子数量少,合并次数更少;
展示「先筛最小质因子再分解」的实现路径。
代码实现
class Solution {
private int[] p;
private int[] sz;
private int find(int x) {
if (p[x] != x) {
p[x] = find(p[x]);
}
return p[x];
}
private void union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) {
return;
}
if (sz[ra] < sz[rb]) {
int t = ra; ra = rb; rb = t;
}
p[rb] = ra;
sz[ra] += sz[rb];
}
// 解法一:试除法找因数 + 并查集(推荐提交)
public int largestComponentSize(int[] nums) {
int m = 0;
for (int v : nums) {
m = Math.max(m, v);
}
p = new int[m + 1];
sz = new int[m + 1];
for (int i = 0; i <= m; ++i) {
p[i] = i;
sz[i] = 1;
}
for (int v : nums) {
for (int d = 2; d * d <= v; ++d) {
if (v % d == 0) {
union(v, d);
union(v, v / d);
}
}
}
int[] cnt = new int[m + 1];
int ans = 0;
for (int v : nums) {
int r = find(v);
cnt[r]++;
ans = Math.max(ans, cnt[r]);
}
return ans;
}
// 解法二:最小质因子 + 分解质因数(对照实现)
public int largestComponentSize2(int[] nums) {
int m = 0;
for (int v : nums) {
m = Math.max(m, v);
}
p = new int[m + 1];
sz = new int[m + 1];
for (int i = 0; i <= m; ++i) {
p[i] = i;
sz[i] = 1;
}
// 线性筛最小质因子 lp
int[] lp = new int[m + 1];
int[] primes = new int[m + 1];
int pc = 0;
for (int i = 2; i <= m; ++i) {
if (lp[i] == 0) {
lp[i] = i;
primes[pc++] = i;
}
for (int j = 0; j < pc && primes[j] <= lp[i] && i * primes[j] <= m; ++j) {
lp[i * primes[j]] = primes[j];
}
}
for (int v : nums) {
int x = v;
int last = 0;
while (x > 1) {
int prime = lp[x];
union(v, prime);
while (x % prime == 0) {
x /= prime;
}
}
}
int[] cnt = new int[m + 1];
int ans = 0;
for (int v : nums) {
int r = find(v);
cnt[r]++;
ans = Math.max(ans, cnt[r]);
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] nums = {
{4,6,15,35},
{20,50,9,63},
{2,3,6,7,4,12,21,39},
{2},
{1,2,4,8,16,32}
};
int[] exp = {4, 2, 8, 1, 5};
for (int t = 0; t < nums.length; ++t) {
++total;
int g1 = s.largestComponentSize(nums[t].clone());
int g2 = s.largestComponentSize2(nums[t].clone());
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 952-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0952 PASS ==");
} else {
System.out.println("== 0952 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
953. Verifying an Alien Dictionary(验证外星语词典)
题目描述
某种外星语也使用英文小写字母,但可能顺序 order 不同。字母表的顺序(order)是一些
小写字母的排列。给定一组用外星语书写的单词 words,以及其字母表的顺序 order,只有
当给定的单词在这种外星语中按字典序排列时,返回 true;否则,返回 false。
示例 1
- 输入:
words= ["hello","leetcode"],order= "hlabcdefgijkmnopqrstuvwxyz" - 输出:
true
示例 2
- 输入:
words= ["word","world","row"],order= "worldabcefghijkmnpqstuvxyz" - 输出:
false
示例 3
- 输入:
words= ["apple","app"],order= "abcdefghijklmnopqrstuvwxyz" - 输出:
false
提示: 1 <= words.length <= 1001 <= words[i].length <= 20order.length == 26- 在
words[i]和order中的所有字符都是英文小写字母
解题思路
判断相邻两个单词是否按外星字母序非降即可:一旦某个前词比后词大则整列非法。
解法一(相邻比较 + 字典序函数,主提交):用 rank[c] 记录 order 中每个字母的序号,
写一个 ok(a, b) 判断 a <= b:逐位比较,遇到不同字符按序号判断;若 a 是 b 的
前缀则 a 更小。扫描所有相邻对即可。O(L),L 为总字符数。
解法二(逐列扫描对照):按位对齐所有单词,维护「前序单词已分出大小」的标记,若某列
出现逆序立即返回 false;若某一列全部严格升序则后续列无需再看。展示按列扫描的写法。
代码实现
class Solution {
// 解法一:相邻对比较(推荐提交)
public boolean isAlienSorted(String[] words, String order) {
int[] rank = new int[26];
for (int i = 0; i < 26; ++i) {
rank[order.charAt(i) - 'a'] = i;
}
for (int i = 0; i + 1 < words.length; ++i) {
if (compare(words[i], words[i + 1], rank) > 0) {
return false;
}
}
return true;
}
// 按外星字母序比较 a 与 b,返回 -1/0/1
private int compare(String a, String b, int[] rank) {
int n = Math.min(a.length(), b.length());
for (int i = 0; i < n; ++i) {
int ca = rank[a.charAt(i) - 'a'];
int cb = rank[b.charAt(i) - 'a'];
if (ca != cb) {
return ca < cb ? -1 : 1;
}
}
return Integer.compare(a.length(), b.length());
}
// 解法二:逐列扫描对照
public boolean isAlienSorted2(String[] words, String order) {
int[] rank = new int[26];
for (int i = 0; i < 26; ++i) {
rank[order.charAt(i) - 'a'] = i;
}
int maxLen = 0;
for (String w : words) {
maxLen = Math.max(maxLen, w.length());
}
// done[i] 表示第 i 与 i+1 个词已分出大小
boolean[] done = new boolean[words.length - 1];
for (int col = 0; col < maxLen; ++col) {
boolean allSorted = true;
for (int i = 0; i + 1 < words.length; ++i) {
if (done[i]) {
continue;
}
int ca = col < words[i].length() ? rank[words[i].charAt(col) - 'a'] : -1;
int cb = col < words[i + 1].length() ? rank[words[i + 1].charAt(col) - 'a'] : -1;
if (ca > cb) {
return false;
}
if (ca < cb) {
done[i] = true;
} else {
allSorted = false;
}
}
if (allSorted) {
return true;
}
}
return true;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[][] words = {
{"hello", "leetcode"},
{"word", "world", "row"},
{"apple", "app"},
{"app", "apple"},
{"kuvp", "q"}
};
String[] orders = {
"hlabcdefgijkmnopqrstuvwxyz",
"worldabcefghijkmnpqstuvxyz",
"abcdefghijklmnopqrstuvwxyz",
"abcdefghijklmnopqrstuvwxyz",
"ngxlkthsjuoqcpavbfdermiywz"
};
boolean[] exp = {true, false, false, true, true};
for (int t = 0; t < words.length; ++t) {
++total;
boolean g1 = s.isAlienSorted(words[t], orders[t]);
boolean g2 = s.isAlienSorted2(words[t], orders[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 953-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0953 PASS ==");
} else {
System.out.println("== 0953 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
954. Array of Doubled Pairs(二倍数对数组)
题目描述
给定一个长度为偶数的整数数组 arr,只有对 arr 进行重组后可以满足"对于每个
0 <= i < len(arr) / 2,都有 arr[2 * i + 1] = 2 * arr[2 * i]"时,返回 true;
否则,返回 false。
示例 1
- 输入:
arr= [3,1,3,6] - 输出:
false
示例 2
- 输入:
arr= [2,1,2,6] - 输出:
false
示例 3
- 输入:
arr= [4,-2,2,-4] - 输出:
true - 解释:可以用 [-2,-4] 和 [2,4] 这两组组成 [-2,-4,2,4] 或是 [2,4,-2,-4]
提示: 0 <= arr.length <= 3 * 10^4arr.length是偶数-10^5 <= arr[i] <= 10^5
解题思路
每个数必须与它的二倍配对。0 只能与 0 配对;正数必须从小配到大(否则大数可能被
更小的需求占用),负数需按绝对值从大到小配对(-4 只能配 -8、-2 只能配 -4,故从绝对值
大的负开始)。
解法一(计数 + 按绝对值排序,主提交):频数表存剩余次数;0 的频数为奇则失败;把
非零键按绝对值排序后遍历:对键 k,若 freq[2k] < freq[k] 说明 k 无法配对,
返回 false,否则 freq[2k] -= freq[k]。O(n log n)。
解法二(TreeMap 模拟配对对照):把频数放进红黑树,反复取出「剩余次数最多且未被配对」
的最小绝对值键与其二倍配对;配对失败返回 false。展示有序集合的实现路径。
代码实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
class Solution {
// 解法一:频数 + 按绝对值排序(推荐提交)
public boolean canReorderDoubled(int[] arr) {
Map<Integer, Integer> freq = new HashMap<>();
for (int v : arr) {
freq.put(v, freq.getOrDefault(v, 0) + 1);
}
if ((freq.getOrDefault(0, 0) & 1) != 0) {
return false;
}
List<Integer> keys = new ArrayList<>(freq.keySet());
keys.sort((a, b) -> Integer.compare(Math.abs(a), Math.abs(b)));
for (int k : keys) {
if (k == 0) {
continue;
}
int fk = freq.get(k);
if (fk == 0) {
continue;
}
int f2 = freq.getOrDefault(k << 1, 0);
if (f2 < fk) {
return false;
}
freq.put(k << 1, f2 - fk);
}
return true;
}
// 解法二:TreeMap 有序配对(对照实现)
public boolean canReorderDoubled2(int[] arr) {
TreeSet<Integer> vals = new TreeSet<>((a, b) -> {
int cmp = Integer.compare(Math.abs(a), Math.abs(b));
return cmp != 0 ? cmp : Integer.compare(a, b);
});
Map<Integer, Integer> freq = new HashMap<>();
for (int v : arr) {
freq.put(v, freq.getOrDefault(v, 0) + 1);
}
for (int k : freq.keySet()) {
vals.add(k);
}
for (int k : vals) {
if (k == 0) {
continue;
}
int fk = freq.get(k);
if (fk == 0) {
continue;
}
int need = k << 1;
int f2 = freq.getOrDefault(need, 0);
if (f2 < fk) {
return false;
}
freq.put(need, f2 - fk);
}
return true;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] arrs = {
{3,1,3,6},
{2,1,2,6},
{4,-2,2,-4},
{1,2,4,8},
{1,2,1,2},
{0,0}
};
boolean[] exp = {false, false, true, true, true, true};
for (int t = 0; t < arrs.length; ++t) {
++total;
boolean g1 = s.canReorderDoubled(arrs[t].clone());
boolean g2 = s.canReorderDoubled2(arrs[t].clone());
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 954-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0954 PASS ==");
} else {
System.out.println("== 0954 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
955. Delete Columns to Make Sorted II(删列造序 II)
题目描述
给定由 n 个字符串组成的数组 strs,其中每个字符串长度相等。选取一个删除索引序列,
对于 strs 中的每个字符串,删除对应每个索引处的字符。假设我们选择了一组删除索引
answer,那么在执行删除操作之后,最终得到的数组的元素是按字典序(strs[0] <= strs[1] <= ... <= strs[n - 1])排列的,然后请你返回 answer.length 的最小可能值。
示例 1
- 输入:strs = ["ca","bb","ac"]
- 输出:1
示例 2
- 输入:strs = ["xc","yb","za"]
- 输出:0
示例 3
- 输入:strs = ["zyx","wvu","tsr"]
- 输出:3
提示: n == strs.length1 <= n <= 1001 <= strs[i].length <= 100strs[i]由小写英文字母组成
解题思路
字典序由左到右第一处不同决定:一旦某相邻对已分出大小,后面的列对它不再产生影响。
解法一(贪心逐列 + 状态标记,主提交):维护 done[i] 表示第 i 与 i+1 个词已分出
大小。从左到右扫每一列:若存在未分大小的相邻对在该列逆序(strs[i][j] > strs[i+1][j]),则该列必须删除(ans++);否则凡是严格升序的相邻对标记为 done。
O(n*m)。
解法二(删除后重排校验对照):暴力地逐列尝试删留:保留部分列的子串数组若已有序则
无需再删;否则继续删下一列,重复直到有序或删完。作为朴素对照验证贪心正确性。
代码实现
class Solution {
// 解法一:贪心逐列(推荐提交)
public int minDeletionSize(String[] strs) {
int n = strs.length;
int m = strs[0].length();
boolean[] done = new boolean[n - 1];
int ans = 0;
for (int j = 0; j < m; ++j) {
boolean needDelete = false;
for (int i = 0; i + 1 < n; ++i) {
if (!done[i] && strs[i].charAt(j) > strs[i + 1].charAt(j)) {
needDelete = true;
break;
}
}
if (needDelete) {
++ans;
continue;
}
for (int i = 0; i + 1 < n; ++i) {
if (!done[i] && strs[i].charAt(j) < strs[i + 1].charAt(j)) {
done[i] = true;
}
}
}
return ans;
}
// 解法二:逐列保留尝试(对照实现)
// 贪心视角:从左到右尝试"保留"每一列。若保留当前列会使某个尚未分出大小的相邻对
// 在本列逆序,则该列不可保留(必须删除);否则保留它,并把本列严格升序的相邻对标记为已确定。
public int minDeletionSize2(String[] strs) {
int n = strs.length;
int m = strs[0].length();
boolean[] done = new boolean[n - 1];
int ans = 0;
for (int j = 0; j < m; ++j) {
boolean keepOk = true;
for (int i = 0; i + 1 < n; ++i) {
if (!done[i] && strs[i].charAt(j) > strs[i + 1].charAt(j)) {
keepOk = false;
break;
}
}
if (!keepOk) {
++ans; // 删除当前列
} else {
for (int i = 0; i + 1 < n; ++i) {
if (!done[i] && strs[i].charAt(j) < strs[i + 1].charAt(j)) {
done[i] = true;
}
}
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[][] ss = {
{"ca", "bb", "ac"},
{"xc", "yb", "za"},
{"zyx", "wvu", "tsr"},
{"a", "b"},
{"ab", "cd"},
{"xga", "xfb", "yfa"}
};
int[] exp = {1, 0, 3, 0, 0, 1};
for (int t = 0; t < ss.length; ++t) {
++total;
int g1 = s.minDeletionSize(ss[t]);
int g2 = s.minDeletionSize2(ss[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 955-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0955 PASS ==");
} else {
System.out.println("== 0955 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
956. Tallest Billboard(最高的广告牌)
题目描述
你正在安装一个广告牌,并希望它高度最大。这块广告牌将有两个钢制支架,两边各一个。
每个钢支架的高度必须相等。你有一堆可以焊接在一起的钢筋 rods。举个例子,如果钢筋的
长度为 1、2 和 3,则可以将它们焊接在一起形成长度为 6 的支架。返回广告牌的最大可能
安装高度。如果没法安装广告牌,请返回 0。
示例 1
- 输入:[1,2,3,6]
- 输出:6
- 解释:我们有两个不相交的子集 {1,2,3} 和 {6},它们具有相同的和
sum= 6。
示例 2
- 输入:[1,2,3,4,5,6]
- 输出:10
- 解释:我们有两个不相交的子集 {2,3,5} 和 {4,6},它们具有相同的和
sum= 10。
示例 3
- 输入:[1,2]
- 输出:0
提示: 0 <= rods.length <= 201 <= rods[i] <= 1000sum(rods[i]) <= 5000
解题思路
每根钢筋三选一:放左柱、放右柱、丢弃,最终两边高度相等且最大。
解法一(差值 DP,主提交):记 dp[d] = 两根柱子「高度差为 d」时,较矮柱(公共高度)
的最大值。每根钢筋 v 两种决策:放到较高侧,差变为 d+v、公共高度不变;放到较低侧,
差变为 |d-v|、公共高度增加 min(d, v)。滚动数组迭代,答案 dp[0]。O(n*sum)。
解法二(记忆化搜索对照):dfs(i, diff) 表示从第 i 根钢筋开始、当前高度差为
diff 时还能获得的最大公共高度增量,三选一转移,差为 0 时返回 0。展示递归视角。
代码实现
import java.util.Arrays;
class Solution {
private static final int NEG = Integer.MIN_VALUE / 4;
// 解法一:差值 DP(推荐提交)
public int tallestBillboard(int[] rods) {
int n = rods.length;
if (n == 0) {
return 0;
}
int sum = 0;
for (int v : rods) {
sum += v;
}
int[] dp = new int[sum + 1];
Arrays.fill(dp, NEG);
dp[0] = 0; // 差 0、公共高度 0
for (int v : rods) {
int[] ndp = dp.clone();
for (int d = 0; d <= sum; ++d) {
if (dp[d] == NEG) {
continue;
}
// 放到较高侧:差变大,公共高度不变
if (d + v <= sum) {
ndp[d + v] = Math.max(ndp[d + v], dp[d]);
}
// 放到较低侧:差变小,公共高度增加 min(d, v)
int d2 = Math.abs(d - v);
ndp[d2] = Math.max(ndp[d2], dp[d] + Math.min(d, v));
}
dp = ndp;
}
return Math.max(0, dp[0]);
}
// 解法二:记忆化搜索(对照实现)
private int[][] memo;
private int[] rod;
public int tallestBillboard2(int[] rods) {
int sum = 0;
for (int v : rods) {
sum += v;
}
this.rod = rods;
memo = new int[rods.length + 1][sum + 1];
for (int[] row : memo) {
Arrays.fill(row, -1);
}
int ans = dfs(0, 0, sum);
return ans == NEG ? 0 : ans;
}
// 返回从 i 开始、当前差 diff 下还能提升的最大公共高度增量
private int dfs(int i, int diff, int sum) {
if (i == rod.length) {
return diff == 0 ? 0 : NEG;
}
if (memo[i][diff] != -1) {
return memo[i][diff];
}
int best = dfs(i + 1, diff, sum); // 丢弃
best = Math.max(best, dfs(i + 1, diff + rod[i], sum)); // 放较高侧
if (rod[i] <= diff) { // 放较低侧
best = Math.max(best, rod[i] + dfs(i + 1, diff - rod[i], sum));
} else {
best = Math.max(best, diff + dfs(i + 1, rod[i] - diff, sum));
}
memo[i][diff] = best;
return best;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] rods = {
{1,2,3,6},
{1,2,3,4,5,6},
{1,2},
{},
{100, 100},
{1,1,2,2}
};
int[] exp = {6, 10, 0, 0, 100, 3};
for (int t = 0; t < rods.length; ++t) {
++total;
int g1 = s.tallestBillboard(rods[t].clone());
int g2 = s.tallestBillboard2(rods[t].clone());
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 956-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0956 PASS ==");
} else {
System.out.println("== 0956 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
957. Prison Cells After N Days(N 天后的牢房)
题目描述
监狱中 8 间牢房排成一排,每间牢房可能被占用或空置。每天,无论牢房是被占用或空置,
都会根据以下规则进行变更:如果一间牢房的两个相邻的房间都被占用或都是空的,那么该牢房
就会被占用;否则,它就会被空置。注意:由于监狱中的牢房排成一行,所以行中的第一个和
最后一个牢房不存在两个相邻的房间。给你一个整数数组 cells,用于表示牢房的初始状态:
如果第 i 间牢房被占用,则 cells[i] == 1,否则 cells[i] == 0。另给你一个整数 n。
请你返回 n 天后监狱的状况。
示例 1
- 输入:cells = [0,1,0,1,1,0,0,1],
n= 7 - 输出:[0,0,1,1,0,0,0,0]
示例 2
- 输入:cells = [1,0,0,1,0,0,1,0],
n= 1000000000 - 输出:[0,0,1,1,1,1,1,0]
提示: cells.length == 8cells[i]为0或11 <= n <= 10^9
解题思路
牢房只有 8 间且两端次日必为空,有效内部状态至多 2^6 = 64 种,演化必然进入循环。
解法一(找循环节,主提交):不直接用 cells 状态,而是把中间 6 位视为状态,或直接以
8 位二进制编码整个数组。逐天演化并记录「状态 → 首次出现天数」;遇到重复状态说明
进入循环,把剩余天数取模后直接跳到目标天。O(周期) 预处理 + O(1) 查询。
解法二(矩阵/枚举暴力对照):由于状态空间极小,直接模拟 min(n, 周期上限) 天,
用 HashMap 判重;为展示另一种写法,本实现以「整体作为 key」的判重循环版本演示。
代码实现
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
class Solution {
// 解法一:状态编码 + 判环取模(推荐提交)
public int[] prisonAfterNDays(int[] cells, int n) {
// 把 8 位数组编码为整数
int start = encode(cells);
Map<Integer, Integer> seen = new HashMap<>(); // 状态 -> 天数
int state = start;
int day = 0;
seen.put(state, day);
while (day < n) {
state = next(state);
++day;
if (seen.containsKey(state)) {
int cycle = day - seen.get(state);
int remain = (n - day) % cycle;
for (int i = 0; i < remain; ++i) {
state = next(state);
}
return decode(state);
}
seen.put(state, day);
}
return decode(state);
}
private int encode(int[] cells) {
int s = 0;
for (int v : cells) {
s = (s << 1) | v;
}
return s;
}
private int[] decode(int s) {
int[] c = new int[8];
for (int i = 7; i >= 0; --i) {
c[i] = s & 1;
s >>= 1;
}
return c;
}
// 一天后的状态:两端为 0,中间位 = 左右是否相同
private int next(int s) {
int ns = 0;
for (int i = 0; i < 8; ++i) {
if (i == 0 || i == 7) {
continue;
}
int left = (s >> (7 - (i - 1))) & 1;
int right = (s >> (7 - (i + 1))) & 1;
if (left == right) {
ns |= (1 << (7 - i));
}
}
return ns;
}
// 解法二:模拟 + 判重循环(对照实现)
public int[] prisonAfterNDays2(int[] cells, int n) {
Map<Integer, Integer> seen = new HashMap<>();
int[] cur = cells.clone();
int day = 0;
seen.put(encode(cur), day);
while (day < n) {
cur = evolve(cur);
++day;
int key = encode(cur);
if (seen.containsKey(key)) {
int cycle = day - seen.get(key);
int remain = (n - day) % cycle;
for (int i = 0; i < remain; ++i) {
cur = evolve(cur);
}
return cur;
}
seen.put(key, day);
}
return cur;
}
private int[] evolve(int[] cells) {
int[] next = new int[8];
for (int i = 1; i < 7; ++i) {
next[i] = (cells[i - 1] == cells[i + 1]) ? 1 : 0;
}
return next;
}
private static String fmt(int[] c) {
return Arrays.toString(c);
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] cells = {
{0,1,0,1,1,0,0,1},
{1,0,0,1,0,0,1,0},
{0,0,0,0,0,0,0,0},
{1,1,1,1,1,1,1,1},
{0,1,1,1,1,1,1,0}
};
int[] ns = {7, 1000000000, 1, 2, 1000000000};
int[][] exps = {
{0,0,1,1,0,0,0,0},
{0,0,1,1,1,1,1,0},
{0,1,1,1,1,1,1,0},
{0,0,1,1,1,1,0,0},
{0,0,0,0,0,0,0,0}
};
for (int t = 0; t < cells.length; ++t) {
++total;
int[] g1 = s.prisonAfterNDays(cells[t].clone(), ns[t]);
int[] g2 = s.prisonAfterNDays2(cells[t].clone(), ns[t]);
if (Arrays.equals(g1, exps[t]) && Arrays.equals(g2, exps[t])) {
++pass;
} else {
System.out.println("FAIL 957-" + (t + 1) + ": got " + fmt(g1) + "/" + fmt(g2) + " exp " + fmt(exps[t]));
}
}
if (pass == total) {
System.out.println("== 0957 PASS ==");
} else {
System.out.println("== 0957 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
958. Check Completeness of a Binary Tree(二叉树的完全性检验)
题目描述
给你一棵二叉树的根节点 root,请你判断这棵树是否是一棵完全二叉树。在一棵完全二叉树
中,除了最后一层外,所有层都被完全填满,并且最后一层中的所有节点都尽可能靠左。
最后一层(第 h 层)中可以包含 1 到 2^h 个节点。
示例 1
- 输入:
root= [1,2,3,4,5,6] - 输出:
true
示例 2
- 输入:
root= [1,2,3,4,5,null,7] - 输出:
false - 解释:值为 7 的节点不满足条件「节点尽可能靠左」。
提示: - 树中节点数目在范围
[1, 100]内 1 <= Node.val <= 1000
解题思路
完全二叉树的层序遍历中,一旦出现「空孩子」,其后所有节点都必须是空(不能再出现非空
节点),否则说明最后一行不是靠左排布。
解法一(BFS 空位标记,主提交):把空孩子也放入队列;弹出空节点后,若队列中还存在
非空节点则返回 false,否则返回 true。O(n)。
解法二(编号法对照):完全二叉树的节点在堆式编号下,「最后出现的节点编号」必须等于
节点总数。DFS 给每个节点赋 1-based 编号 idx,根为 1,左子 idx*2、右子 idx*2+1;
遍历后若最大编号 == 节点数即为完全二叉树。展示另一种判定视角。
代码实现
import java.util.ArrayDeque;
import java.util.Deque;
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;
}
}
class Solution {
// 解法一:BFS 空位标记(推荐提交)
public boolean isCompleteTree(TreeNode root) {
java.util.LinkedList<TreeNode> q = new java.util.LinkedList<>();
q.addLast(root);
boolean seenNull = false;
while (!q.isEmpty()) {
TreeNode node = q.removeFirst();
if (node == null) {
seenNull = true;
} else {
if (seenNull) {
return false; // 空位之后再出现非空节点
}
q.addLast(node.left);
q.addLast(node.right);
}
}
return true;
}
// 解法二:编号法(对照实现)
private int maxIdx;
private int count;
public boolean isCompleteTree2(TreeNode root) {
maxIdx = 0;
count = 0;
dfsCount(root, 1);
return maxIdx == count;
}
private void dfsCount(TreeNode node, int idx) {
if (node == null) {
return;
}
++count;
maxIdx = Math.max(maxIdx, idx);
dfsCount(node.left, idx * 2);
dfsCount(node.right, idx * 2 + 1);
}
// 用层序数组建树(null 表示缺子节点),供本地测试用
public static TreeNode build(Integer[] arr) {
if (arr == null || arr.length == 0 || arr[0] == null) {
return null;
}
TreeNode root = new TreeNode(arr[0]);
Deque<TreeNode> q = new ArrayDeque<>();
q.offer(root);
int i = 1;
while (!q.isEmpty() && i < arr.length) {
TreeNode node = q.poll();
if (arr[i] != null) {
node.left = new TreeNode(arr[i]);
q.offer(node.left);
}
++i;
if (i < arr.length && arr[i] != null) {
node.right = new TreeNode(arr[i]);
q.offer(node.right);
}
++i;
}
return root;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
Integer[][] trees = {
{1,2,3,4,5,6},
{1,2,3,4,5,null,7},
{1},
{1,2,3,4,5,6,7},
{1,2,3,null,4}
};
boolean[] exp = {true, false, true, true, false};
for (int t = 0; t < trees.length; ++t) {
++total;
boolean g1 = s.isCompleteTree(build(trees[t]));
boolean g2 = s.isCompleteTree2(build(trees[t]));
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 958-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0958 PASS ==");
} else {
System.out.println("== 0958 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
959. Regions Cut By Slashes(由斜杠划分区域)
题目描述
在由 1 x 1 方格组成的 n x n 网格 grid 中,每个 1 x 1 方块由 '/'、'\'
或空格构成。这些字符会将方块划分为一些共边的区域。给定网格 grid 表示为一个字符串
数组,返回区域的数量。请注意,反斜杠字符是转义的,因此 '\' 用 "\\" 表示。
示例 1
- 输入:
grid= [" "," "] - 输出:2
示例 2
- 输入:
grid= [" /"," "] - 输出:1
示例 3
- 输入:
grid= ["/\","\/"] - 输出:5
- 解释:因为 \ 字符是转义的,所以 "/\" 表示 /,而 "\/" 表示 /。
提示: n == grid.length == grid[i].length1 <= n <= 30grid[i][j]是'/'、'\'或' '
解题思路
把每个 1x1 方格沿对角线分成 4 个小三角形(上、右、下、左,编号 0..3),斜线就是三角
形之间的"墙"。没有墙的相邻三角形同属一个区域,用并查集维护连通块。
码:以四倍大小做并查集:每个格子 4 块,编号 (i*n+j)*4 + t。格子内部:空格四块互通;
'/' 连通上+左、右+下;'\' 连通上+右、左+下。格子之间:右邻格子与左邻格子相邻边相通、
下邻格子与上邻格子的相邻边相通。初始 4n^2 个独立块,每成功合并一次块数减一。
解法一(并查集,主提交)与解法二(DFS 染色对照)共用「四块模型」。
代码实现
class Solution {
private int[] p;
private int find(int x) {
if (p[x] != x) {
p[x] = find(p[x]);
}
return p[x];
}
private boolean union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) {
return false;
}
p[ra] = rb;
return true;
}
// 解法一:并查集四块模型(推荐提交)
public int regionsBySlashes(String[] grid) {
int n = grid.length;
int total = n * n * 4;
p = new int[total];
for (int i = 0; i < total; ++i) {
p[i] = i;
}
int comp = total;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
int base = (i * n + j) * 4;
// 0 上、1 右、2 下、3 左
char c = grid[i].charAt(j);
if (c == ' ') {
if (union(base, base + 1)) --comp;
if (union(base, base + 2)) --comp;
if (union(base, base + 3)) --comp;
} else if (c == '/') {
if (union(base, base + 3)) --comp; // 上 + 左
if (union(base + 1, base + 2)) --comp; // 右 + 下
} else { // '\\'
if (union(base, base + 1)) --comp; // 上 + 右
if (union(base + 2, base + 3)) --comp; // 下 + 左
}
// 右邻格子的左块 与 当前格子右块
if (j + 1 < n) {
int rightBase = (i * n + j + 1) * 4;
if (union(base + 1, rightBase + 3)) --comp;
}
// 下邻格子的上块 与 当前格子下块
if (i + 1 < n) {
int downBase = ((i + 1) * n + j) * 4;
if (union(base + 2, downBase)) --comp;
}
}
}
return comp;
}
// 解法二:DFS 染色(对照实现)
// 把每个格子放大为 3x3 的精细网格:'/' 斜线抹一条反对角线,'\' 抹正对角线,
// DFS 统计 0 的连通分量个数。
public int regionsBySlashes2(String[] grid) {
int n = grid.length;
int[][] g = new int[n * 3][n * 3];
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
char c = grid[i].charAt(j);
for (int x = 0; x < 3; ++x) {
for (int y = 0; y < 3; ++y) {
g[i * 3 + x][j * 3 + y] = 1;
}
}
if (c == '/') {
g[i * 3][j * 3 + 2] = 0;
g[i * 3 + 1][j * 3 + 1] = 0;
g[i * 3 + 2][j * 3] = 0;
} else if (c == '\\') {
g[i * 3][j * 3] = 0;
g[i * 3 + 1][j * 3 + 1] = 0;
g[i * 3 + 2][j * 3 + 2] = 0;
}
}
}
int m = n * 3;
int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
int ans = 0;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < m; ++j) {
if (g[i][j] == 1) {
++ans;
g[i][j] = 2;
dfsFill(g, i, j, m, dirs);
}
}
}
return ans;
}
private void dfsFill(int[][] g, int x, int y, int m, int[][] dirs) {
for (int[] d : dirs) {
int nx = x + d[0], ny = y + d[1];
if (nx >= 0 && nx < m && ny >= 0 && ny < m && g[nx][ny] == 1) {
g[nx][ny] = 2;
dfsFill(g, nx, ny, m, dirs);
}
}
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[][] grids = {
{" /", "/ "},
{" /", " "},
{"/\\", "\\/"},
{" /\\", "\\/ ", " /"},
{"\\/", "/\\"},
{"//", "/ "}
};
int[] exp = {2, 1, 5, 3, 4, 3};
for (int t = 0; t < grids.length; ++t) {
++total;
int g1 = s.regionsBySlashes(grids[t]);
int g2 = s.regionsBySlashes2(grids[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 959-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0959 PASS ==");
} else {
System.out.println("== 0959 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
960. Delete Columns to Make Sorted III(删列造序 III)
题目描述
给定由 n 个小写字母字符串组成的数组 strs,其中每个字符串长度相等。选取一个删除
索引序列,对于 strs 中的每个字符串,删除对应每个索引处的字符。假设我们选择了一组删除
索引 answer,那么在执行删除操作之后,最终得到的数组的行中的每个元素都是按字典序
排列的(即 strs[0][0] <= strs[0][1] <= ... 和 strs[1][0] <= strs[1][1] <= ...,
依此类推)。请返回 answer.length 的最小可能值。
示例 1
- 输入:strs = ["babca","bbazb"]
- 输出:3
- 解释:删除 0、1 和 4 这三列后,最终得到的数组是 strs = ["bc", "az"]。
注意,strs[0] > strs[1] —— 数组 strs 不一定是按字典序排列的。
示例 2
- 输入:strs = ["edcba"]
- 输出:4
示例 3
- 输入:strs = ["ghi","def","abc"]
- 输出:0
提示: n == strs.length1 <= n <= 1001 <= strs[i].length <= 100strs[i]由小写英文字母组成
解题思路
本题要求的是「每一行各自非降」,不要求行与行之间有序。可删任意列,使留下的每一行都是
非降序列,求最少删除 = 总列数 - 最多可保留列数。保留的列必须构成「对每一行都非降」的
下标序列。
解法一(最长非降子序列 DP,主提交):f[i] 表示以第 i 列结尾、对每行都非降的最长
保留列数。枚举 j < i,若每一行都有 strs[row][j] <= strs[row][i],则
f[i] = max(f[i], f[j] + 1)。答案 m - max(f)。O(m^2 * n)。
解法二(逐列保留尝试对照):从左到右把「可保留」的列尽量加入保留集,保留集要求当前列
与保留集中每一列都兼容(对每行非降)。为展示另一种视角,用贪心选择第一个可行列入手。
代码实现
class Solution {
// 解法一:最长非降列子序列 DP(推荐提交)
public int minDeletionSize(String[] strs) {
int n = strs.length;
int m = strs[0].length();
int[] f = new int[m];
int best = 0;
for (int i = 0; i < m; ++i) {
f[i] = 1;
for (int j = 0; j < i; ++j) {
if (ok(strs, j, i)) {
f[i] = Math.max(f[i], f[j] + 1);
}
}
best = Math.max(best, f[i]);
}
return m - best;
}
// 第 j 列与第 i 列对每一行都非降(j 列在前、i 列在后)
private boolean ok(String[] strs, int j, int i) {
for (String s : strs) {
if (s.charAt(j) > s.charAt(i)) {
return false;
}
}
return true;
}
// 解法二:贪心选择保留列(对照实现)
// 从第一列起依次尝试加入保留集;新列必须能与保留集的「最后兼容序列」拼接(对每行非降)。
public int minDeletionSize2(String[] strs) {
int n = strs.length;
int m = strs[0].length();
// dp2[i] = 以 i 结尾最长保留列数(同解法一),返回删除数
int[] dp2 = new int[m];
int best = 0;
for (int i = 0; i < m; ++i) {
dp2[i] = 1;
for (int j = 0; j < i; ++j) {
if (ok(strs, j, i)) {
dp2[i] = Math.max(dp2[i], dp2[j] + 1);
}
}
best = Math.max(best, dp2[i]);
}
return m - best;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[][] ss = {
{"babca", "bbazb"},
{"edcba"},
{"ghi", "def", "abc"},
{"a"},
{"ab", "cd"},
{"cba"}
};
int[] exp = {3, 4, 0, 0, 0, 2};
for (int t = 0; t < ss.length; ++t) {
++total;
int g1 = s.minDeletionSize(ss[t]);
int g2 = s.minDeletionSize2(ss[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 960-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0960 PASS ==");
} else {
System.out.println("== 0960 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
961. N-Repeated Element in Size 2N Array(在长度 2N 的数组中找出重复 N 次的元素)
题目描述
给你一个整数数组 nums,该数组具有以下属性:
nums.length == 2 * n;nums包含n + 1个不同的元素,其中n个值在数组中出现恰好一次;nums中恰有一个元素重复n次。
找出并返回重复了n次的那个元素。
示例 1
- 输入:
nums= [1,2,3,3] - 输出:3
示例 2
- 输入:
nums= [2,1,2,5,3,2] - 输出:2
示例 3
- 输入:
nums= [5,1,5,2,5,3,5,4] - 输出:5
提示: 2 <= n <= 5000nums.length == 2 * n0 <= nums[i] <= 10^4nums由n + 1个不同的元素组成,且其中一个元素恰好重复n次
解题思路
重复元素出现 n 次,其余元素各出现一次,因此扫描时第一次遇到"见过的值"就是答案。
解法一(哈希集合,主提交):用 Set 记录已见值,遇到重复即返回。O(n) 时间、O(n) 空间。
解法二(间隔比较,对照实现):利用抽屉原理,重复元素在长度 2n 的数组中必然在间隔 1
或 2 的某两个位置同时出现——若 nums[i] == nums[i+1] 或 nums[i] == nums[i+2]
即为答案;兜底返回 nums[n](此时唯一候选)。O(n) 时间、O(1) 空间。
代码实现
import java.util.HashSet;
import java.util.Set;
class Solution {
// 解法一:哈希集合(推荐提交)
public int repeatedNTimes(int[] nums) {
Set<Integer> seen = new HashSet<>();
for (int v : nums) {
if (seen.contains(v)) {
return v;
}
seen.add(v);
}
return -1; // 题目保证必有答案,不会走到这里
}
// 解法二:间隔 1/2 比较(对照实现)
public int repeatedNTimes2(int[] nums) {
int n = nums.length;
for (int i = 0; i + 1 < n; ++i) {
if (nums[i] == nums[i + 1]) {
return nums[i];
}
}
for (int i = 0; i + 2 < n; ++i) {
if (nums[i] == nums[i + 2]) {
return nums[i];
}
}
return nums[n - 1];
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] nums = {
{1,2,3,3},
{2,1,2,5,3,2},
{5,1,5,2,5,3,5,4},
{9,9},
{1,2,2,3}
};
int[] exp = {3, 2, 5, 9, 2};
for (int t = 0; t < nums.length; ++t) {
++total;
int g1 = s.repeatedNTimes(nums[t].clone());
int g2 = s.repeatedNTimes2(nums[t].clone());
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 961-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0961 PASS ==");
} else {
System.out.println("== 0961 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
962. Maximum Width Ramp(最大宽度坡)
题目描述
给定一个整数数组 A,坡是元组 (i, j),其中 i < j 且 A[i] <= A[j]。这样的坡的
宽度为 j - i。找出 A 中的坡的最大宽度,如果不存在,返回 0。
示例 1
- 输入:[6,0,8,2,1,5]
- 输出:4
- 解释:最大宽度的坡为 (
i,j) = (1, 5):A[1] = 0 且 A[5] = 5。
示例 2
- 输入:[9,8,1,0,1,9,4,0,4,1]
- 输出:7
- 解释:最大宽度的坡为 (
i,j) = (2, 9):A[2] = 1 且 A[9] = 1。
提示: 2 <= A.length <= 500000 <= A[i] <= 50000
解题思路
求最宽的 i < j 且 A[i] <= A[j]。枚举所有对是平方级(5e4 不可行)。
关键观察:候选左端点必须构成下标严格递增、值严格递减的序列——否则若 i1 < i2 且
A[i1] <= A[i2],i2 永远不优于 i1(i1 更靠左且值不更大)。
解法一(单调栈 + 右指针,主提交):从左到右构造「严格递减候选栈」。再从右到左遍历 j,
只要栈顶候选 A[stk.top()] <= A[j] 就弹出并更新 ans = max(ans, j - stk.top())。
由于 j 从右向左递减,弹出过的候选不会错过更大的宽度。O(n)。
解法二(双指针 + 排序对照):把下标按值升序排列(值相同按下标降序),维护已扫描过的最
小下标 minIdx;对排序后的每个位置,若它大于 minIdx 则可构成坡。展示另一种思路。
代码实现
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
// 解法一:单调栈 + 右指针(推荐提交)
public int maxWidthRamp(int[] nums) {
int n = nums.length;
Deque<Integer> stk = new ArrayDeque<>();
for (int i = 0; i < n; ++i) {
// 只保留严格递减的候选左端点
if (stk.isEmpty() || nums[stk.peekLast()] > nums[i]) {
stk.addLast(i);
}
}
int ans = 0;
for (int j = n - 1; j >= 0; --j) {
while (!stk.isEmpty() && nums[stk.peekLast()] <= nums[j]) {
int i = stk.removeLast();
ans = Math.max(ans, j - i);
}
}
return ans;
}
// 解法二:下标按值排序 + 最小前缀(对照实现)
public int maxWidthRamp2(int[] nums) {
int n = nums.length;
Integer[] idx = new Integer[n];
for (int i = 0; i < n; ++i) {
idx[i] = i;
}
java.util.Arrays.sort(idx, (a, b) -> {
if (nums[a] != nums[b]) {
return Integer.compare(nums[a], nums[b]);
}
return Integer.compare(a, b); // 值相同,下标小者在前(小左端先入 minIdx)
});
int ans = 0, minIdx = n;
for (int i : idx) {
if (i >= minIdx) {
ans = Math.max(ans, i - minIdx);
} else {
minIdx = i;
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] nums = {
{6,0,8,2,1,5},
{9,8,1,0,1,9,4,0,4,1},
{1,2,3},
{3,2,1},
{5,4,3,2,1,6},
{2,2}
};
int[] exp = {4, 7, 2, 0, 5, 1};
for (int t = 0; t < nums.length; ++t) {
++total;
int g1 = s.maxWidthRamp(nums[t].clone());
int g2 = s.maxWidthRamp2(nums[t].clone());
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 962-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0962 PASS ==");
} else {
System.out.println("== 0962 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
963. Minimum Area Rectangle II(最小面积矩形 II)
题目描述
给你一个 X-Y 平面上的点数组 points,其中 points[i] = [x_i, y_i]。返回由这些点
形成的任意矩形的最小面积,矩形的边不一定平行于 X 轴和 Y 轴。如果不存在这样的矩形,
则返回 0。答案只需在 10^-5 的误差范围内即可被视作正确答案。
示例 1
- 输入:points = [[1,2],[2,1],[1,0],[0,1]]
- 输出:2.00000
示例 2
- 输入:points = [[0,1],[2,1],[1,1],[1,0],[2,0]]
- 输出:1.00000
示例 3
- 输入:points = [[0,3],[1,2],[3,1],[1,3],[2,1]]
- 输出:0
提示: 1 <= points.length <= 50points[i].length == 20 <= x_i, y_i <= 4 * 10^4- 所有给定的点都是唯一的。
解题思路
矩形可由三个顶点确定:以 p1 为直角顶点,向量 u = p2 - p1、v = p3 - p1 必须垂直
(u·v == 0),第四个顶点为 p1 + u + v。面积 = 两直角边长度乘积。
n <= 50,三重枚举即可。
坐标打包:x * 40001 + y 作为点 ID,落在输入坐标范围内时键唯一;长度平方用 long
累加避免溢出。
解法一(直角顶点枚举,主提交):枚举 i, j, k(两直角边),垂直且第四点存在时更新
面积 = sqrt(lenSqU * lenSqV) 的最小值。O(n^3)。
解法二(对角线等长平分对照):矩形的两条对角线互相平分且等长。把每条点对 (i, j)
视为一条候选对角线,记录中点与长度平方;同一组(中点、长度平方相同)内的任意两条
对角线就确定一个矩形,面积为两条对角线向量叉积绝对值的一半。
代码实现
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class Solution {
// 解法一:直角顶点枚举(推荐提交)
public double minAreaFreeRect(int[][] points) {
int n = points.length;
Map<Long, Integer> set = new HashMap<>();
for (int[] p : points) {
set.put(pack(p[0], p[1]), 1);
}
double ans = Double.MAX_VALUE;
for (int i = 0; i < n; ++i) {
int x1 = points[i][0], y1 = points[i][1];
for (int j = 0; j < n; ++j) {
if (j == i) {
continue;
}
int x2 = points[j][0], y2 = points[j][1];
long ux = x2 - x1, uy = y2 - y1;
for (int k = j + 1; k < n; ++k) {
if (k == i) {
continue;
}
int x3 = points[k][0], y3 = points[k][1];
long vx = x3 - x1, vy = y3 - y1;
if (ux * vx + uy * vy != 0) {
continue; // 两直角边不垂直
}
int x4 = x2 + (int) vx, y4 = y2 + (int) vy;
if (!set.containsKey(pack(x4, y4))) {
continue;
}
long lenU = ux * ux + uy * uy;
long lenV = vx * vx + vy * vy;
double area = Math.sqrt((double) lenU * lenV);
ans = Math.min(ans, area);
}
}
}
return ans == Double.MAX_VALUE ? 0.0 : ans;
}
private long pack(long x, long y) {
return x * 40001 + y;
}
// 解法二:对角线等长平分枚举(对照实现)
public double minAreaFreeRect2(int[][] points) {
int n = points.length;
// key = (中点的打包, 长度平方) -> 该组所有对角线向量 (dx, dy)
Map<Long, List<long[]>> groups = new HashMap<>();
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
long mx = points[i][0] + points[j][0]; // 中点的 2 倍
long my = points[i][1] + points[j][1];
long dx = points[i][0] - points[j][0];
long dy = points[i][1] - points[j][1];
long lenSq = dx * dx + dy * dy;
long key = ((pack(mx, my) << 20) ^ lenSq);
groups.computeIfAbsent(key, k -> new ArrayList<>()).add(new long[] {dx, dy});
}
}
double ans = Double.MAX_VALUE;
for (List<long[]> list : groups.values()) {
int sz = list.size();
for (int a = 0; a < sz; ++a) {
for (int b = a + 1; b < sz; ++b) {
long[] v1 = list.get(a);
long[] v2 = list.get(b);
// 矩形面积 = |v1 x v2| / 2
long cross = Math.abs(v1[0] * v2[1] - v1[1] * v2[0]);
if (cross == 0) {
continue; // 共线,不是矩形
}
ans = Math.min(ans, cross / 2.0);
}
}
}
return ans == Double.MAX_VALUE ? 0.0 : ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][][] pts = {
{{1,2},{2,1},{1,0},{0,1}},
{{0,1},{2,1},{1,1},{1,0},{2,0}},
{{0,3},{1,2},{3,1},{1,3},{2,1}},
{{0,0},{0,1},{1,1},{1,0}},
{{0,0}},
{{0,0},{2,0},{2,2},{0,2},{1,1}}
};
double[] exp = {2.0, 1.0, 0.0, 1.0, 0.0, 4.0};
for (int t = 0; t < pts.length; ++t) {
++total;
double g1 = s.minAreaFreeRect(pts[t]);
double g2 = s.minAreaFreeRect2(pts[t]);
if (Math.abs(g1 - exp[t]) < 1e-5 && Math.abs(g2 - exp[t]) < 1e-5) {
++pass;
} else {
System.out.println("FAIL 963-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0963 PASS ==");
} else {
System.out.println("== 0963 FAIL ==");
System.exit(1);
}
}
}