LeetCode 926-950. 题解合集
926. Flip String to Monotone Increasing(将字符串翻转到单调递增)
题目描述
如果一个二进制字符串,是以一些 0(可能没有 0)后面跟着一些 1(也可能没有 1)
的形式组成的,那么该字符串是单调递增的。
给你一个二进制字符串 s,你可以将任何 0 翻转为 1 或者将 1 翻转为 0。
返回使 s 单调递增的最小翻转次数。
示例 1
- 输入:
s= "00110" - 输出:1
- 解释:翻转最后一位得到 00111。
示例 2
- 输入:
s= "010110" - 输出:2
- 解释:翻转得到 011111,或者是 000111。
示例 3
- 输入:
s= "00011000" - 输出:2
- 解释:翻转得到 00000000。
提示: 1 <= s.length <= 10^5s[i]为'0'或'1'
解题思路
单调递增的最终形态必然是 0...01...1:存在一个分界点,分界点左侧全为 0、右侧全为
1。枚举分界点,统计需要翻转的字符数 = 左侧原本是 1 的个数 + 右侧原本是 0 的个数。
解法一(前缀 1 计数 + 枚举分界,主提交):预处理 pre1[i] = s[0..i-1] 中 1 的
个数。枚举分界 i(左侧 [0,i-1] 全 0、右侧 [i,n-1] 全 1):翻转数 =
pre1[i] + ((n - i) - (pre1[n] - pre1[i])),即左侧的 1 全翻 0 + 右侧的 0 全翻 1。
取最小。O(n)。
解法二(DP 状态机,对照实现):dp0[i] 表示 s[0..i] 以 0 结尾(即前段全 0)的
最小翻转,dp1[i] 表示以 1 结尾(可切换为 1 后全为 1)。按当前字符转移,滚动两个
变量即可,O(n)。
代码实现
class Solution {
// 解法一:前缀 1 计数 + 枚举分界(推荐提交)
public int minFlipsMonoIncr(String s) {
int n = s.length();
int[] pre1 = new int[n + 1];
for (int i = 0; i < n; ++i) {
pre1[i + 1] = pre1[i] + (s.charAt(i) == '1' ? 1 : 0);
}
int total1 = pre1[n];
int ans = n;
for (int i = 0; i <= n; ++i) { // 左侧 [0,i-1] 全 0,右侧 [i,n-1] 全 1
int leftOnes = pre1[i]; // 左侧的 1 需要翻成 0
int rightZeros = (n - i) - (total1 - pre1[i]); // 右侧的 0 需要翻成 1
ans = Math.min(ans, leftOnes + rightZeros);
}
return ans;
}
// 解法二:DP 状态机(对照实现)
public int minFlipsMonoIncr2(String s) {
// end0:当前处理前缀全为 0 的最小翻转;end1:当前处理前缀形如 0*1* 的最小翻转
int end0 = 0, end1 = 0;
for (char c : s.toCharArray()) {
int newEnd0 = end0 + (c == '1' ? 1 : 0);
int newEnd1 = Math.min(end0, end1) + (c == '0' ? 1 : 0);
end0 = newEnd0;
end1 = newEnd1;
}
return Math.min(end0, end1);
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[] ss = {"00110", "010110", "00011000", "111000", "0", "1", "1001010101"};
int[] exp = {1, 2, 2, 3, 0, 0, 4};
for (int t = 0; t < ss.length; ++t) {
++total;
int g1 = s.minFlipsMonoIncr(ss[t]);
int g2 = s.minFlipsMonoIncr2(ss[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 926-" + (t + 1) + ": " + ss[t] + " got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0926 PASS ==");
} else {
System.out.println("== 0926 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一(前缀 1 计数) | O(n) | O(n) |
| 解法二(DP 状态机) | O(n) | O(1) |
927. Three Equal Parts(三等分)
题目描述
给定一个由 0 和 1 组成的数组 arr,将数组分成 3 个非空的部分,使得所有这些部分表示相同的二进制值。
如果可以做到,请返回任何 [i, j],其中 i+1 < j,这样一来:
arr[0], arr[1], ..., arr[i]为第一部分;arr[i+1], arr[i+2], ..., arr[j-1]为第二部分;arr[j], arr[j+1], ..., arr[arr.length-1]为第三部分;- 这三个部分所表示的二进制值相等。
如果无法做到,就返回[-1, -1]。
注意,在考虑每个部分所表示的二进制时,应当将其看作一个整体。前导零是允许的,所以[0,1,1]和[1,1]表示相同的值。
示例 1
- 输入:
arr= [1,0,1,0,1] - 输出:[0,3]
示例 2
- 输入:
arr= [1,1,0,1,1] - 输出:[-1,-1]
示例 3
- 输入:
arr= [1,1,0,0,1] - 输出:[0,2]
提示: 3 <= arr.length <= 3 * 10^4arr[i]是0或1
解题思路
三个部分表示相同的二进制值。真正决定数值的是每个部分中 1 的个数与各 1 的相对位置。
解法一(计数 + 三指针,主提交):记数组中 1 的总数为 cnt。
- 若
cnt % 3 != 0,无法三等分,返回[-1, -1]; - 若
cnt == 0,全是 0,任意切分(三部分均为 0)都可,返回[0, n-1]; - 否则每部分应含
cnt/3个 1。用find(x)定位第 1 个、第cnt/3+1个、第2*cnt/3+1个 1 的位置i/j/k,
这三处分别是三段第一个 1 的位置(前导零不影响数值)。从i/j/k同步向右逐位比较,
只要arr[i]==arr[j]==arr[k]就一起右移,直到k走到数组末尾;若全程相等则合法,
返回切分点[i-1, j],否则返回[-1, -1]。时间复杂度 O(n),空间 O(1)。
解法二(后缀零处理对照):先定位三段第一个 1 的位置,然后选定第三段末尾,用第三段整体
(含后缀零)作为基准,逐位核对第一、二段与之完全一致;此实现显式处理每段等长(长度差为
后缀零)的约束,逻辑等价但更贴近二进制等值的定义。
代码实现
class Solution {
// 解法一:计数 + 三指针(推荐提交)
public int[] threeEqualParts(int[] arr) {
int n = arr.length;
int cnt = 0;
for (int v : arr) {
cnt += v;
}
if (cnt % 3 != 0) {
return new int[] {-1, -1};
}
if (cnt == 0) {
return new int[] {0, n - 1};
}
int per = cnt / 3;
int i = find(arr, 1); // 第一段第一个 1
int j = find(arr, per + 1); // 第二段第一个 1
int k = find(arr, per * 2 + 1);// 第三段第一个 1
while (k < n && arr[i] == arr[j] && arr[j] == arr[k]) {
++i;
++j;
++k;
}
if (k == n) {
return new int[] {i - 1, j};
}
return new int[] {-1, -1};
}
private int find(int[] arr, int x) {
int s = 0;
for (int i = 0; i < arr.length; ++i) {
s += arr[i];
if (s == x) {
return i;
}
}
return 0;
}
// 解法二:显式对齐每段(对照实现)
public int[] threeEqualParts2(int[] arr) {
int n = arr.length;
int cnt = 0;
for (int v : arr) {
cnt += v;
}
if (cnt % 3 != 0) {
return new int[] {-1, -1};
}
if (cnt == 0) {
return new int[] {0, n - 1};
}
int per = cnt / 3;
// 三段第一个 1 的位置
int a = find(arr, 1);
int b = find(arr, per + 1);
int c = find(arr, per * 2 + 1);
// 以第三段(含后缀零)为基准,其有效位数 len = n - c
int len = n - c;
// 第一、二段必须能容纳 len 位有效位,且三段互不重叠:
// 第一段有效位 [a, a+len),第二段有效位 [b, b+len),且 a+len <= b、b+len <= c
if (a + len > b || b + len > c) {
return new int[] {-1, -1};
}
for (int t = 0; t < len; ++t) {
if (arr[a + t] != arr[c + t] || arr[b + t] != arr[c + t]) {
return new int[] {-1, -1};
}
}
// 切分点:第一段结尾 = a+len-1,第二段结尾 = b+len-1,第三段起点 = b+len
return new int[] {a + len - 1, b + len};
}
private static void p(int[] a) {
System.out.println("[" + a[0] + "," + a[1] + "]");
}
// 校验切分 [i, j] 是否合法:三段非空、i+1<j,且三段二进制值相等
private static boolean valid(int[] arr, int i, int j) {
if (i < 0 || j < 0) {
return false;
}
int n = arr.length;
if (i + 1 >= j || j >= n || i >= n) {
return false;
}
long a = 0, b = 0, c = 0;
for (int t = 0; t <= i; ++t) {
a = (a << 1) | arr[t];
}
for (int t = i + 1; t <= j - 1; ++t) {
b = (b << 1) | arr[t];
}
for (int t = j; t < n; ++t) {
c = (c << 1) | arr[t];
}
return a == b && b == c;
}
// 预期无解的用例要求两解都返回 [-1,-1];预期有解的用例要求返回的切分合法
private static boolean matchEx(int[] arr, int[] g, int[] ex) {
if (ex[0] == -1) {
return g[0] == -1 && g[1] == -1;
}
return valid(arr, g[0], g[1]);
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] cs = {{1,0,1,0,1}, {1,1,0,1,1}, {1,1,0,0,1}, {0,0,0,0}, {1,0,0,1,0,1}, {1,1,0,1,0,0,1,0,0,1,1,0,0}};
int[][] ex = {{0,3}, {-1,-1}, {0,2}, {0,3}, {0,4}, {-1,-1}};
for (int t = 0; t < cs.length; ++t) {
++total;
int[] g1 = s.threeEqualParts(cs[t]);
int[] g2 = s.threeEqualParts2(cs[t]);
if (matchEx(cs[t], g1, ex[t]) && matchEx(cs[t], g2, ex[t])) {
++pass;
} else {
System.out.print("FAIL 927-" + (t + 1) + ": got ");
p(g1); System.out.print(" / "); p(g2);
}
}
if (pass == total) {
System.out.println("== 0927 PASS ==");
} else {
System.out.println("== 0927 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一(计数 + 三指针) | O(n) | O(1) |
| 解法二(后缀零显式对齐) | O(n) | O(1) |
928. Minimize Malware Spread II(尽量减少恶意软件的传播 II)
题目描述
给定一个由 n 个节点组成的网络,用 n x n 个邻接矩阵 graph 表示。在节点网络中,
只有当 graph[i][j] = 1 时,节点 i 能够直接连接到另一个节点 j。
一些节点 initial 最初被恶意软件感染。只要两个节点直接连接,且其中至少一个节点受到恶意软件
的感染,那么两个节点都将被恶意软件感染。这种恶意软件的传播将继续,直到没有更多的节点可以被
这种方式感染。
假设 M(initial) 是在恶意软件停止传播之后,整个网络中感染恶意软件的最终节点数。
我们可以从 initial 中删除一个节点,并完全移除该节点以及从该节点到任何其他节点的任何连接。
请返回移除后能够使 M(initial) 最小化的节点。如果有多个节点满足条件,返回索引最小的节点。
示例 1
- 输入:
graph= [[1,1,0],[1,1,0],[0,0,1]], initial = [0,1] - 输出:0
示例 2
- 输入:
graph= [[1,1,0],[1,1,1],[0,1,1]], initial = [0,1] - 输出:1
示例 3
- 输入:
graph= [[1,1,0,0],[1,1,1,0],[0,1,1,1],[0,0,1,1]], initial = [0,1] - 输出:1
提示: n == graph.lengthn == graph[i].length2 <= n <= 300graph[i][j]是0或1graph[i][j] == graph[j][i]graph[i][i] == 11 <= initial.length < ninitial中每个整数都不同
解题思路
本题与 924 的区别在于:删除节点会完全移除该节点,它不再向邻居传播;而其余初始节点仍会
继续感染各自的连通块。因此先把所有干净节点(不在 initial 中)按边合并成若干连通块,
再考察每个初始节点能接触到哪些干净块:若某块被多个初始节点同时接触,删掉其中任何一个,
该块仍会被其余初始节点感染,无法得救;只有被恰好一个初始节点接触的块,删掉那个节点
才能整块获救。
解法一(并查集 + 独占块累加,主提交):清点块归属后,对每个初始节点累加其"独占块"大小,
该值即删除它之后相比最坏情况减少的感染数,取能救最多(并列取编号最小)的节点。O(n^2 α(n))。
解法二(DFS 分量对照):不用并查集,直接用 DFS 从每个干净节点出发染色分量,等价统计。
代码实现
class Solution {
// 解法一:并查集(推荐提交)
public int minMalwareSpread(int[][] graph, int[] initial) {
int n = graph.length;
boolean[] bad = new boolean[n];
for (int x : initial) {
bad[x] = true;
}
UnionFind uf = new UnionFind(n);
for (int i = 0; i < n; ++i) {
if (bad[i]) {
continue;
}
for (int j = i + 1; j < n; ++j) {
if (!bad[j] && graph[i][j] == 1) {
uf.union(i, j);
}
}
}
// g[i]:初始节点 i 接触到的干净块根集合;cnt[root]:该干净块被几个初始节点接触
java.util.Set<Integer>[] g = new java.util.Set[n];
for (int i = 0; i < n; ++i) {
g[i] = new java.util.HashSet<>();
}
int[] cnt = new int[n];
for (int x : initial) {
for (int j = 0; j < n; ++j) {
if (!bad[j] && graph[x][j] == 1) {
g[x].add(uf.find(j));
}
}
for (int root : g[x]) {
++cnt[root];
}
}
int ans = initial[0], mx = -1;
for (int x : initial) {
int saved = 0;
for (int root : g[x]) {
if (cnt[root] == 1) {
saved += uf.size(root);
}
}
if (saved > mx || (saved == mx && x < ans)) {
ans = x;
mx = saved;
}
}
return ans;
}
// 解法二:DFS 分量统计(对照实现)
public int minMalwareSpread2(int[][] graph, int[] initial) {
int n = graph.length;
boolean[] bad = new boolean[n];
for (int x : initial) {
bad[x] = true;
}
int[] comp = new int[n];
java.util.Arrays.fill(comp, -1);
int compCnt = 0;
int[] compSize = new int[n];
for (int i = 0; i < n; ++i) {
if (!bad[i] && comp[i] == -1) {
dfsComp(graph, bad, comp, i, compCnt, n);
++compCnt;
}
}
// 统计每个分量大小
for (int i = 0; i < n; ++i) {
if (comp[i] != -1) {
++compSize[comp[i]];
}
}
// 接触计数
int[] touchCnt = new int[compCnt];
boolean[][] touched = new boolean[n][compCnt];
for (int x : initial) {
for (int j = 0; j < n; ++j) {
if (!bad[j] && graph[x][j] == 1) {
int c = comp[j];
if (!touched[x][c]) {
touched[x][c] = true;
++touchCnt[c];
}
}
}
}
int ans = initial[0], mx = -1;
for (int x : initial) {
int saved = 0;
for (int c = 0; c < compCnt; ++c) {
if (touched[x][c] && touchCnt[c] == 1) {
saved += compSize[c];
}
}
if (saved > mx || (saved == mx && x < ans)) {
ans = x;
mx = saved;
}
}
return ans;
}
private void dfsComp(int[][] graph, boolean[] bad, int[] comp, int u, int id, int n) {
comp[u] = id;
for (int v = 0; v < n; ++v) {
if (!bad[v] && graph[u][v] == 1 && comp[v] == -1) {
dfsComp(graph, bad, comp, v, id, n);
}
}
}
static class UnionFind {
int[] p;
int[] sz;
UnionFind(int n) {
p = new int[n];
sz = new int[n];
for (int i = 0; i < n; ++i) {
p[i] = i;
sz[i] = 1;
}
}
int find(int x) {
if (p[x] != x) {
p[x] = find(p[x]);
}
return p[x];
}
boolean union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) {
return false;
}
if (sz[ra] < sz[rb]) {
int t = ra;
ra = rb;
rb = t;
}
p[rb] = ra;
sz[ra] += sz[rb];
return true;
}
int size(int root) {
return sz[root];
}
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][][] graphs = {
{{1,1,0},{1,1,0},{0,0,1}},
{{1,1,0},{1,1,1},{0,1,1}},
{{1,1,0,0},{1,1,1,0},{0,1,1,1},{0,0,1,1}},
{{1,1,0},{1,1,0},{0,0,1}}
};
int[][] initials = {{0,1}, {0,1}, {0,1}, {0}};
int[] exp = {0, 1, 1, 0};
for (int t = 0; t < graphs.length; ++t) {
++total;
int g1 = s.minMalwareSpread(graphs[t], initials[t]);
int g2 = s.minMalwareSpread2(graphs[t], initials[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 928-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0928 PASS ==");
} else {
System.out.println("== 0928 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
929. Unique Email Addresses(独特的电子邮件地址)
题目描述
每个有效电子邮件地址都由一个本地名和一个域名组成,以 '@' 符号分隔。除小写字母之外,
电子邮件地址还可以含有一个或多个 '.' 或 '+'。
- 如果在电子邮件地址的本地名部分中的某些字符之间添加句点(
'.'),则发往那里的邮件将会 转发到本地名中没有点的同一地址。此规则不适用于域名。 - 如果在本地名中添加加号(
'+'),则会忽略第一个加号后面的所有内容。此规则不适用于域名。 给你一个字符串数组emails,我们会向每个emails[i]发送一封电子邮件。 返回实际收到邮件的不同地址数目。
示例 1
- 输入:emails = ["test.email+alex@leetcode.com","test.e.mail+bob.cathy@leetcode.com","testemail+david@lee.tcode.com"]
- 输出:2
示例 2
- 输入:emails = ["
a@leetcode.com","b@leetcode.com","c@leetcode.com"] - 输出:3 提示:
1 <= emails.length <= 1001 <= emails[i].length <= 100emails[i]由小写英文字母、'+'、'.'和'@'组成- 每个
emails[i]都包含有且仅有一个'@'字符
解题思路
规则只作用于 @ 左侧的本地名:忽略 .,并丢弃第一个 + 及其后的内容;域名原样保留。
规范化的地址(变换后本地名 + 原域名)相同的邮件实际送到同一收件人,去重后计数即为答案。
解法一(哈希表,主提交):逐封邮件解析出 @ 位置,遍历本地名字符:遇到 . 跳过,
遇到 + 提前终止,其余字符追加;拼回域名放入 Set,返回集合大小。O(L)。
解法二(split + StringBuilder 对照):用 split("@") 拆分,逻辑等价,写法更贴近题面。
代码实现
class Solution {
// 解法一:哈希表,手写解析(推荐提交)
public int numUniqueEmails(String[] emails) {
java.util.Set<String> set = new java.util.HashSet<>();
for (String email : emails) {
int at = email.indexOf('@');
StringBuilder sb = new StringBuilder();
for (int i = 0; i < at; ++i) {
char c = email.charAt(i);
if (c == '.') {
continue;
}
if (c == '+') {
break;
}
sb.append(c);
}
set.add(sb.toString() + email.substring(at));
}
return set.size();
}
// 解法二:split 拆分(对照实现)
public int numUniqueEmails2(String[] emails) {
java.util.Set<String> set = new java.util.HashSet<>();
for (String email : emails) {
String[] parts = email.split("@");
StringBuilder sb = new StringBuilder();
for (char c : parts[0].toCharArray()) {
if (c == '.') {
continue;
}
if (c == '+') {
break;
}
sb.append(c);
}
set.add(sb.toString() + "@" + parts[1]);
}
return set.size();
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[][] inputs = {
{"test.email+alex@leetcode.com","test.e.mail+bob.cathy@leetcode.com","testemail+david@lee.tcode.com"},
{"a@leetcode.com","b@leetcode.com","c@leetcode.com"},
{"test.email+alex@leetcode.com","test.e.mail+bob.cathy@leetcode.com","testemail+david@lee.tcode.com","a@leetcode.com"},
{"m.y+name@email.com","my@email.com"}
};
int[] exp = {2, 3, 3, 1};
for (int t = 0; t < inputs.length; ++t) {
++total;
int g1 = s.numUniqueEmails(inputs[t]);
int g2 = s.numUniqueEmails2(inputs[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 929-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0929 PASS ==");
} else {
System.out.println("== 0929 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
930. Binary Subarrays With Sum(和相同的二元子数组)
题目描述
给你一个二元数组 nums,和一个整数 goal,请你统计并返回有多少个和为 goal 的非空子数组。
子数组是数组的一段连续部分。
示例 1
- 输入:
nums= [1,0,1,0,1], goal = 2 - 输出:4
- 解释:有 4 个满足题目要求的子数组:[1,0,1]、[1,0,1,0]、[0,1,0,1]、[1,0,1]
示例 2
- 输入:
nums= [0,0,0,0,0], goal = 0 - 输出:15 提示:
1 <= nums.length <= 3 * 10^4nums[i]不是0就是10 <= goal <= nums.length
解题思路
求元素为 0/1 的数组中和为 goal 的子数组个数,n 较大不能枚举区间。
解法一(前缀和 + 计数,主提交):设 s 为当前前缀和,以当前位置为右端点的合法子数组数等于
之前前缀和为 s-goal 的出现次数。用计数数组维护前缀和出现次数,初始 cnt[0]=1,
边遍历边累加。O(n) 时间、O(n) 空间。
解法二(双指针):数组非负,窗口和随右端扩展单调不减。用 i1 维护和 <= goal 的最大左端
(即和恰大于 goal 的边界),i2 维护和 < goal 的最大左端(即和至少为 goal 的边界),
则右端固定时左端落在 [i1, i2) 的窗口和恰为 goal,个数为 i2 - i1。O(n) 时间、O(1) 空间。
代码实现
class Solution {
// 解法一:前缀和 + 计数(推荐提交)
public int numSubarraysWithSum(int[] nums, int goal) {
int n = nums.length;
int[] cnt = new int[n + 1];
cnt[0] = 1;
int ans = 0, s = 0;
for (int v : nums) {
s += v;
if (s - goal >= 0) {
ans += cnt[s - goal];
}
++cnt[s];
}
return ans;
}
// 解法二:双指针(对照实现)
public int numSubarraysWithSum2(int[] nums, int goal) {
int n = nums.length;
int i1 = 0, i2 = 0;
int s1 = 0, s2 = 0;
int ans = 0;
for (int j = 0; j < n; ++j) {
s1 += nums[j];
s2 += nums[j];
while (i1 <= j && s1 > goal) {
s1 -= nums[i1++];
}
while (i2 <= j && s2 >= goal) {
s2 -= nums[i2++];
}
ans += i2 - i1;
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] nums = {{1,0,1,0,1}, {0,0,0,0,0}, {0,0,0,0,0}, {1,1,1,1,1}};
int[] goals = {2, 0, 3, 2};
int[] exp = {4, 15, 0, 4};
for (int t = 0; t < nums.length; ++t) {
++total;
int g1 = s.numSubarraysWithSum(nums[t], goals[t]);
int g2 = s.numSubarraysWithSum2(nums[t], goals[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 930-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0930 PASS ==");
} else {
System.out.println("== 0930 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
931. Minimum Falling Path Sum(下降路径最小和)
题目描述
给你一个 n x n 的方形整数数组 matrix,请你找出并返回通过 matrix 的下降路径的最小和。
下降路径可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行
所选元素最多相隔一列(即位于正下方或者沿对角线向左或者向右的第一个元素)。具体来说,位置
(row, col) 的下一个元素应当是 (row+1, col-1)、(row+1, col) 或者 (row+1, col+1)。
示例 1
- 输入:
matrix= [[2,1,3],[6,5,4],[7,8,9]] - 输出:13
示例 2
- 输入:
matrix= [[-19,57],[-40,-5]] - 输出:-59 提示:
n == matrix.length == matrix[i].length1 <= n <= 100-100 <= matrix[i][j] <= 100
解题思路
到达 (i,j) 的最小路径和只依赖上一行 j-1, j, j+1 三个位置,是经典网格 DP。
解法一(滚动数组 DP,主提交):f[j] 表示从第一行下降到当前行第 j 列的最小和;
逐行推进时,新的 g[j] = min(f[j-1], f[j], f[j+1]) + matrix[i][j](越界列忽略),
用两个一维数组滚动,最后取 f 的最小值。O(n^2) 时间、O(n) 空间。
解法二(二维 DP 对照):直接开 n x n 表逐行填,语义更直观,空间 O(n^2)。
代码实现
class Solution {
// 解法一:滚动数组 DP(推荐提交)
public int minFallingPathSum(int[][] matrix) {
int n = matrix.length;
int[] f = new int[n];
for (int j = 0; j < n; ++j) {
f[j] = matrix[0][j];
}
for (int i = 1; i < n; ++i) {
int[] g = new int[n];
for (int j = 0; j < n; ++j) {
int best = f[j];
if (j > 0) {
best = Math.min(best, f[j - 1]);
}
if (j + 1 < n) {
best = Math.min(best, f[j + 1]);
}
g[j] = best + matrix[i][j];
}
f = g;
}
int ans = Integer.MAX_VALUE;
for (int v : f) {
ans = Math.min(ans, v);
}
return ans;
}
// 解法二:二维 DP(对照实现)
public int minFallingPathSum2(int[][] matrix) {
int n = matrix.length;
int[][] dp = new int[n][n];
for (int j = 0; j < n; ++j) {
dp[0][j] = matrix[0][j];
}
for (int i = 1; i < n; ++i) {
for (int j = 0; j < n; ++j) {
int best = dp[i - 1][j];
if (j > 0) {
best = Math.min(best, dp[i - 1][j - 1]);
}
if (j + 1 < n) {
best = Math.min(best, dp[i - 1][j + 1]);
}
dp[i][j] = best + matrix[i][j];
}
}
int ans = Integer.MAX_VALUE;
for (int j = 0; j < n; ++j) {
ans = Math.min(ans, dp[n - 1][j]);
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][][] mats = {
{{2,1,3},{6,5,4},{7,8,9}},
{{-19,57},{-40,-5}},
{{1}},
{{-1,-2},{-3,-4}}
};
int[] exp = {13, -59, 1, -6};
for (int t = 0; t < mats.length; ++t) {
++total;
int g1 = s.minFallingPathSum(mats[t]);
int g2 = s.minFallingPathSum2(mats[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 931-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0931 PASS ==");
} else {
System.out.println("== 0931 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
解法二(二维 DP 对照):直接开 n x n 表逐行填,语义更直观,空间 O(n^2)。
932. Beautiful Array(漂亮数组)
题目描述
如果长度为 n 的数组 nums 满足下述条件,则认为该数组是一个漂亮数组:
nums是由范围[1, n]的整数组成的一个排列。- 对于每个
0 <= i < j < n,均不存在下标k(i < k < j)使得2 * nums[k] == nums[i] + nums[j]。 给你整数n,返回长度为n的任一漂亮数组。本题保证对于给定的n至少存在一个有效答案。
示例 1
- 输入:
n= 4 - 输出:[2,1,4,3]
示例 2
- 输入:
n= 5 - 输出:[3,1,2,5,4] 提示:
1 <= n <= 1000
解题思路
漂亮数组禁止 2 * A[k] == A[i] + A[j](其中 i < k < j)。等式左侧为偶数,因此只要
A[i] 与 A[j] 一奇一偶,无论中间元素如何取值等式都不可能成立。
解法一(分治,主提交):将规模 n 拆成左半边 (n+1)/2(映射为奇数 2x-1)与右半边
n/2(映射为偶数 2x)。仿射变换 x -> kx+b 保持漂亮性,且奇数段、偶数段各自内部
不会有 A[i]、A[j] 同奇偶的冲突;拼接后任意跨越两段的对子一奇一偶,天然安全。
递归直到 n=1 返回 [1]。O(n log n)。
解法二(迭代构造对照):从 [1] 开始,每一轮把当前数组变换为奇数部分 + 偶数部分
(2x-1 与 2x),长度翻倍并始终漂亮,log n 轮后得到答案。与解法一殊途同归。
代码实现
class Solution {
// 解法一:分治递归(推荐提交)
public int[] beautifulArray(int n) {
if (n == 1) {
return new int[] {1};
}
int[] left = beautifulArray((n + 1) >> 1);
int[] right = beautifulArray(n >> 1);
int[] ans = new int[n];
int p = 0;
for (int x : left) {
ans[p++] = x * 2 - 1;
}
for (int x : right) {
ans[p++] = x * 2;
}
return ans;
}
// 解法二:迭代加倍构造(对照实现)
public int[] beautifulArray2(int n) {
int[] cur = new int[] {1};
while (cur.length < n) {
int[] next = new int[cur.length * 2];
int p = 0;
for (int x : cur) {
next[p++] = x * 2 - 1;
}
for (int x : cur) {
next[p++] = x * 2;
}
cur = next;
}
// 当前 cur 是 1..2^k 的漂亮排列;保留 <= n 的元素即得 1..n 的漂亮排列
//(子序列仍满足无不和谐三元组,且恰好含 1..n 各一次)
int[] ans = new int[n];
int q = 0;
for (int x : cur) {
if (x <= n) {
ans[q++] = x;
}
}
return ans;
}
// 校验数组是否为 1..n 的排列且满足漂亮条件
public boolean check(int[] a) {
int n = a.length;
boolean[] seen = new boolean[n + 1];
for (int x : a) {
if (x < 1 || x > n || seen[x]) {
return false;
}
seen[x] = true;
}
for (int i = 0; i < n; ++i) {
for (int k = i + 1; k < n; ++k) {
for (int j = k + 1; j < n; ++j) {
if (2L * a[k] == (long) a[i] + a[j]) {
return false;
}
}
}
}
return true;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[] ns = {1, 2, 3, 4, 5, 8, 10};
for (int t = 0; t < ns.length; ++t) {
++total;
int n = ns[t];
int[] g1 = s.beautifulArray(n);
int[] g2 = s.beautifulArray2(n);
if (s.check(g1) && s.check(g2)) {
++pass;
} else {
System.out.println("FAIL 932-" + (t + 1) + ": n=" + n);
}
}
if (pass == total) {
System.out.println("== 0932 PASS ==");
} else {
System.out.println("== 0932 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
933. Number of Recent Calls(最近的请求次数)
题目描述
写一个 RecentCounter 类来计算特定时间范围内最近的请求。
请你实现 RecentCounter 类:
RecentCounter()初始化计数器,请求数为 0。int ping(int t)在时间t添加一个新请求,其中t表示以毫秒为单位的某个时间, 然后返回在包含范围[t - 3000, t]内发生的请求数量,即新请求加上所有早于或等于 3000 毫秒前的请求。 保证每次对ping的调用都使用比之前更大的t值。
示例 1
- 输入:["RecentCounter", "ping", "ping", "ping", "ping"] [[], [1], [100], [3001], [3002]]
- 输出:[
null, 1, 2, 3, 3] - 解释:ping(1)=1, ping(100)=2, ping(3001)=3, ping(3002)=3 提示:
1 <= t <= 10^9- 保证每次对
ping调用所使用的t值都严格递增 - 至多调用
ping方法10^4次
解题思路
t 严格递增,早于 t-3000 的请求在后续查询中也不可能重新进入窗口,可以安全丢弃。
解法一(队列,主提交):维护一个单调递增队列,ping(t) 先入队,再从队头弹出所有
< t-3000 的元素,队列大小即窗口内请求数。每个请求至多入队出队一次,均摊 O(1)。
解法二(二分查找):将所有 t 存入数组,由于严格递增有序,用二分定位第一个
>= t-3000 的下标,size - idx 即为答案。单次查询 O(log n)。
代码实现
import java.util.ArrayDeque;
import java.util.Deque;
class RecentCounter {
private Deque<Integer> q = new ArrayDeque<>();
public RecentCounter() {
}
public int ping(int t) {
q.offer(t);
while (q.peek() < t - 3000) {
q.poll();
}
return q.size();
}
// ===== 本地测试(解法二对照)=====
private int[] s = new int[10010];
private int idx;
public int ping2(int t) {
s[idx++] = t;
return idx - lowerBound(t - 3000);
}
private int lowerBound(int x) {
int lo = 0, hi = idx;
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (s[mid] >= x) {
hi = mid;
} else {
lo = mid + 1;
}
}
return lo;
}
public static void main(String[] args) {
RecentCounter rc = new RecentCounter();
RecentCounter rc2 = new RecentCounter();
int pass = 0, total = 0;
int[] ts = {1, 100, 3001, 3002, 5000, 8000, 11000};
int[] exp = {1, 2, 3, 3, 3, 2, 2};
for (int i = 0; i < ts.length; ++i) {
++total;
int g1 = rc.ping(ts[i]);
int g2 = rc2.ping2(ts[i]);
if (g1 == exp[i] && g2 == exp[i]) {
++pass;
} else {
System.out.println("FAIL 933-" + (i + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[i]);
}
}
if (pass == total) {
System.out.println("== 0933 PASS ==");
} else {
System.out.println("== 0933 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
934. Shortest Bridge(最短的桥)
题目描述
给你一个大小为 n x n 的二元矩阵 grid,其中 1 表示陆地,0 表示水域。
岛是由四面相连的 1 形成的一个最大组,即不会与非组内的任何其他 1 相连。
grid 中恰好存在两座岛。
你可以将任意数量的 0 变为 1,以使两座岛连接起来,变成一座岛。
返回必须翻转的 0 的最小数目。
示例 1
- 输入:
grid= [[0,1],[1,0]] - 输出:1
示例 2
- 输入:
grid= [[0,1,0],[0,0,0],[0,0,1]] - 输出:2
示例 3
- 输入:
grid= [[1,1,1,1,1],[1,0,0,0,1],[1,0,1,0,1],[1,0,0,0,1],[1,1,1,1,1]] - 输出:1 提示:
n == grid.length == grid[i].length2 <= n <= 100grid[i][j]为0或1grid中恰有两个岛
解题思路
最少翻转次数等价于两座岛之间的最短距离(途经的水域格数,翻转它们即可连成桥)。
解法一(DFS + BFS,主提交):先找到第一座岛的任意一格,用 DFS 把整个岛标记为 2
并入队作为 BFS 起点;然后 BFS 逐层向外扩展,经过一格 0(标记为 2 防止回头计数),
当扩展到另一座岛(grid[x][y]==1)时,当前 BFS 层数即为答案。O(n^2)。
解法二(多源 BFS + 距离数组对照):把两座岛分别标记成 1/2,从其中一座岛的所有格子
同时出发做多源 BFS,记录每个格子到源岛的距离,第一次碰到另一座岛的格子即返回距离。
代码实现
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
private int[] dirs = {-1, 0, 1, 0, -1};
private int[][] grid;
private int n;
private Deque<int[]> q = new ArrayDeque<>();
// 解法一:DFS + BFS(推荐提交)
public int shortestBridge(int[][] grid) {
this.grid = grid;
n = grid.length;
q.clear();
boolean found = false;
for (int i = 0; i < n && !found; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == 1) {
dfsMark(i, j);
found = true;
break;
}
}
}
int ans = 0;
while (true) {
int sz = q.size();
for (int t = 0; t < sz; ++t) {
int[] p = q.poll();
for (int d = 0; d < 4; ++d) {
int x = p[0] + dirs[d], y = p[1] + dirs[d + 1];
if (x < 0 || x >= n || y < 0 || y >= n) {
continue;
}
if (grid[x][y] == 1) {
return ans;
}
if (grid[x][y] == 0) {
grid[x][y] = 2;
q.offer(new int[] {x, y});
}
}
}
++ans;
}
}
private void dfsMark(int i, int j) {
if (i < 0 || i >= n || j < 0 || j >= n || grid[i][j] != 1) {
return;
}
grid[i][j] = 2;
q.offer(new int[] {i, j});
for (int d = 0; d < 4; ++d) {
dfsMark(i + dirs[d], j + dirs[d + 1]);
}
}
// 解法二:多源 BFS(对照实现)
public int shortestBridge2(int[][] g) {
int m = g.length;
boolean[][] seen = new boolean[m][m];
Deque<int[]> queue = new ArrayDeque<>();
boolean found = false;
for (int i = 0; i < m && !found; ++i) {
for (int j = 0; j < m; ++j) {
if (g[i][j] == 1) {
dfsCollect(g, i, j, queue, m);
found = true;
break;
}
}
}
int ans = 0;
while (!queue.isEmpty()) {
int sz = queue.size();
for (int t = 0; t < sz; ++t) {
int[] p = queue.poll();
for (int d = 0; d < 4; ++d) {
int x = p[0] + dirs[d], y = p[1] + dirs[d + 1];
if (x < 0 || x >= m || y < 0 || y >= m || seen[x][y]) {
continue;
}
if (g[x][y] == 1) {
return ans; // 碰到另一座岛,层数即翻转数
}
seen[x][y] = true;
queue.offer(new int[] {x, y});
}
}
++ans;
}
return ans;
}
private void dfsCollect(int[][] g, int i, int j, Deque<int[]> queue, int m) {
if (i < 0 || i >= m || j < 0 || j >= m || g[i][j] != 1) {
return;
}
g[i][j] = 2;
queue.offer(new int[] {i, j});
for (int d = 0; d < 4; ++d) {
dfsCollect(g, i + dirs[d], j + dirs[d + 1], queue, m);
}
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][][] grids = {
{{0,1},{1,0}},
{{0,1,0},{0,0,0},{0,0,1}},
{{1,1,1,1,1},{1,0,0,0,1},{1,0,1,0,1},{1,0,0,0,1},{1,1,1,1,1}},
{{0,0,0,1},{0,0,0,0},{0,0,0,0},{1,0,0,0}}
};
int[] exp = {1, 2, 1, 5};
for (int t = 0; t < grids.length; ++t) {
++total;
int[][] copy = new int[grids[t].length][];
for (int r = 0; r < grids[t].length; ++r) {
copy[r] = grids[t][r].clone();
}
int g1 = s.shortestBridge(grids[t]);
int g2 = s.shortestBridge2(copy);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 934-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0934 PASS ==");
} else {
System.out.println("== 0934 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
935. Knight Dialer(骑士拨号器)
题目描述
象棋骑士有一个独特的移动方式,它可以垂直移动两个方格、水平移动一个方格,或者水平移动两个
方格、垂直移动一个方格(两者都形成一个 L 的形状)。
我们有一个象棋骑士和一个电话垫,骑士只能站在一个数字单元格上(数字 0~9 的 3x4 布局,
其中 1/2/3 为第一行,4/5/6 为第二行,7/8/9 为第三行,0 在第四行第二列)。
给定一个整数 n,返回我们可以拨多少个长度为 n 的不同电话号码。
你可以将骑士放置在任何数字单元格上,然后你应该执行 n-1 次移动来获得长度为 n 的号码。
因为答案可能很大,所以输出答案模 10^9 + 7。
示例 1
- 输入:
n= 1 - 输出:10
示例 2
- 输入:
n= 2 - 输出:20
示例 3
- 输入:
n= 3131 - 输出:136006598 提示:
1 <= n <= 5000
解题思路
骑士在每个数字上能跳到的下一格是固定的,定义"前驱":能一步跳到 d 的数字集合。
0←{4,6} 1←{6,8} 2←{7,9} 3←{4,8} 4←{0,3,9} 5←{} 6←{0,1,7} 7←{2,6} 8←{1,3} 9←{2,4}。
解法一(线性递推,主提交):f[d] 表示长度为当前轮次、且末位数字为 d 的方案数。
初始 f[d]=1(长度为 1);每跳一步(共 n-1 次),g[d] = sum(f[p], p 是 d 的前驱),
全程取模。最终 sum(f) % mod。O(n) 时间、O(1) 空间。
解法二(矩阵快速幂,对照):转移是常系数线性,写成 10x10 转移矩阵 base,
用快速幂计算 base^(n-1) 与全 1 初始向量相乘,把线性递推加速到 O(10^3 log n)。
代码实现
class Solution {
private static final int MOD = 1_000_000_007;
// 解法一:线性递推(推荐提交)
public int knightDialer(int n) {
long[] f = new long[10];
java.util.Arrays.fill(f, 1);
for (int step = 1; step < n; ++step) {
long[] g = new long[10];
g[0] = (f[4] + f[6]) % MOD;
g[1] = (f[6] + f[8]) % MOD;
g[2] = (f[7] + f[9]) % MOD;
g[3] = (f[4] + f[8]) % MOD;
g[4] = (f[0] + f[3] + f[9]) % MOD;
g[6] = (f[0] + f[1] + f[7]) % MOD;
g[7] = (f[2] + f[6]) % MOD;
g[8] = (f[1] + f[3]) % MOD;
g[9] = (f[2] + f[4]) % MOD;
f = g;
}
long ans = 0;
for (long v : f) {
ans = (ans + v) % MOD;
}
return (int) ans;
}
// 解法二:矩阵快速幂(对照实现)
private static final long[][] BASE = {
{0, 0, 0, 0, 1, 0, 1, 0, 0, 0},
{0, 0, 0, 0, 0, 0, 1, 0, 1, 0},
{0, 0, 0, 0, 0, 0, 0, 1, 0, 1},
{0, 0, 0, 0, 1, 0, 0, 0, 1, 0},
{1, 0, 0, 1, 0, 0, 0, 0, 0, 1},
{0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
{1, 1, 0, 0, 0, 0, 0, 1, 0, 0},
{0, 0, 1, 0, 0, 0, 1, 0, 0, 0},
{0, 1, 0, 1, 0, 0, 0, 0, 0, 0},
{0, 0, 1, 0, 1, 0, 0, 0, 0, 0}
};
public int knightDialer2(int n) {
long[] v = new long[10];
java.util.Arrays.fill(v, 1); // 长度 1 的初始向量
long[][] m = BASE;
int e = n - 1;
while (e > 0) {
if ((e & 1) == 1) {
v = mulVec(v, m);
}
m = mul(m, m);
e >>= 1;
}
long ans = 0;
for (long x : v) {
ans = (ans + x) % MOD;
}
return (int) ans;
}
private long[] mulVec(long[] v, long[][] m) {
long[] r = new long[10];
for (int j = 0; j < 10; ++j) {
long sum = 0;
for (int k = 0; k < 10; ++k) {
sum = (sum + v[k] * m[k][j]) % MOD;
}
r[j] = sum;
}
return r;
}
private long[][] mul(long[][] a, long[][] b) {
long[][] c = new long[10][10];
for (int i = 0; i < 10; ++i) {
for (int j = 0; j < 10; ++j) {
long sum = 0;
for (int k = 0; k < 10; ++k) {
sum = (sum + a[i][k] * b[k][j]) % MOD;
}
c[i][j] = sum;
}
}
return c;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[] ns = {1, 2, 3, 10, 50, 3131, 5000};
int[] exp = {10, 20, 46, 14912, 267287516, 136006598, 406880451};
for (int t = 0; t < ns.length; ++t) {
++total;
int g1 = s.knightDialer(ns[t]);
int g2 = s.knightDialer2(ns[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 935-" + (t + 1) + ": n=" + ns[t] + " got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0935 PASS ==");
} else {
System.out.println("== 0935 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
936. Stamping The Sequence(戳印序列)
题目描述
你想要用小写字母组成一个目标字符串 target。开始的时候,序列由 target.length 个
'?' 记号组成。而你有一个小写字母印章 stamp。
在每个回合,你可以将印章放在序列上,并将序列中的每个字母替换为印章上的相应字母。
你最多可以进行 10 * target.length 个回合。
举个例子,如果初始序列为 "?????",而你的印章 stamp 是 "abc",那么在第一回合,你可以
得到 "abc??"、"?abc?"、"??abc"。(请注意,印章必须完全包含在序列的边界内才能盖下去。)
如果可以印出序列,那么返回一个数组,该数组由每个回合中被印下的最左边字母的索引组成。
如果不能印出序列,就返回一个空数组。
示例 1
- 输入:stamp = "abc",
target= "ababc" - 输出:[0,2]
示例 2
- 输入:stamp = "abca",
target= "aabcaca" - 输出:[3,0,1] 提示:
1 <= stamp.length <= target.length <= 1000stamp和target只包含小写字母。
解题思路
正向盖章会被后续操作覆盖,难以直接判定;改用逆向思考:从 target 出发,逐步把窗口
"还原"成 ?,直到全部变成 ?,然后把操作顺序反转即为正向着印顺序。
解法一(逆向 + 拓扑排序,主提交):对每个起始位置 i(共 n-m+1 个窗口),初始
indeg[i] = m 表示窗口内尚未匹配的位置数;窗口与 target[i+j] 逐位对齐:若
target[i+j] == stamp[j] 则该位置天然匹配,indeg[i]--;否则该位置会被别的窗口覆盖,
建反向边 g[i+j] 记录覆盖它的窗口。入度为 0 的窗口(已可完全揭掉)入队:揭掉后把
窗口内每个未访问位置标记覆盖,并通知依赖这些位置的窗口 indeg--,新的入度 0 者继续。
若全部位置被覆盖,反转队列顺序即答案;否则返回空数组。O(n*(n-m+1))。
解法二(朴素模拟对照):用字符数组模拟,反复扫描所有窗口,凡是"能盖"(与目标一致或为 ?)
的窗口就盖章并记录下来,直到一轮无进展;若数组已完全等于目标则成功,否则失败。
代码实现
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;
class Solution {
// 解法一:逆向 + 拓扑排序(推荐提交)
public int[] movesToStamp(String stamp, String target) {
int m = stamp.length(), n = target.length();
int[] indeg = new int[n - m + 1];
Arrays.fill(indeg, m);
List<Integer>[] g = new List[n];
for (int i = 0; i < n; ++i) {
g[i] = new ArrayList<>();
}
Deque<Integer> q = new ArrayDeque<>();
for (int i = 0; i < n - m + 1; ++i) {
for (int j = 0; j < m; ++j) {
if (target.charAt(i + j) == stamp.charAt(j)) {
if (--indeg[i] == 0) {
q.offer(i);
}
} else {
g[i + j].add(i); // 位置 i+j 需要被窗口 i 覆盖
}
}
}
List<Integer> ans = new ArrayList<>();
boolean[] vis = new boolean[n];
while (!q.isEmpty()) {
int i = q.poll();
ans.add(i);
for (int j = 0; j < m; ++j) {
if (!vis[i + j]) {
vis[i + j] = true;
for (int k : g[i + j]) {
if (--indeg[k] == 0) {
q.offer(k);
}
}
}
}
}
for (int i = 0; i < n; ++i) {
if (!vis[i]) {
return new int[0];
}
}
java.util.Collections.reverse(ans);
int[] res = new int[ans.size()];
for (int i = 0; i < res.length; ++i) {
res[i] = ans.get(i);
}
return res;
}
// 解法二:逆向揭除贪心模拟(对照实现)
// 正向:在 ? 序列上盖章;逆向:把目标序列揭回全是 ?。
// 一个窗口可以揭除当且仅当窗口内每个位置要么已揭(?),要么与印章一致,
// 且窗口内至少有一个位置此前未被揭过(否则揭除无意义)。反复扫描揭除,
// 直到全部揭完(成功)或某轮无进展(失败)。
public int[] movesToStamp2(String stamp, String target) {
int m = stamp.length(), n = target.length();
char[] cur = target.toCharArray();
boolean[] done = new boolean[n]; // 该位置是否已揭成 ?
List<Integer> steps = new ArrayList<>();
boolean progressed = true;
int matched = 0; // 已揭 ? 的数量
while (matched < n) {
progressed = false;
for (int i = 0; i <= n - m; ++i) {
boolean ok = true, hasNew = false;
for (int j = 0; j < m; ++j) {
if (cur[i + j] != '?' && cur[i + j] != stamp.charAt(j)) {
ok = false;
break;
}
if (!done[i + j]) {
hasNew = true;
}
}
if (ok && hasNew) {
for (int j = 0; j < m; ++j) {
if (!done[i + j]) {
cur[i + j] = '?';
done[i + j] = true;
++matched;
}
}
steps.add(i);
progressed = true;
}
}
if (!progressed) {
return new int[0];
}
if (steps.size() > 10 * n) {
return new int[0];
}
}
// 揭除顺序需反转为正向盖章顺序(先揭的窗口最后盖)
java.util.Collections.reverse(steps);
int[] res = new int[steps.size()];
for (int i = 0; i < res.length; ++i) {
res[i] = steps.get(i);
}
return res;
}
// 验证正向操作能否得到 target
public boolean verify(int[] moves, String stamp, String target) {
int m = stamp.length(), n = target.length();
char[] cur = new char[n];
Arrays.fill(cur, '?');
for (int i : moves) {
for (int j = 0; j < m; ++j) {
cur[i + j] = stamp.charAt(j);
}
}
return new String(cur).equals(target);
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[] stamps = {"abc", "abca", "abc", "a", "abc", "ab"};
String[] targets = {"ababc", "aabcaca", "abcabc", "aaaa", "ababa", "abab"};
boolean[] exp = {true, true, true, true, false, true};
for (int t = 0; t < stamps.length; ++t) {
++total;
int[] g1 = s.movesToStamp(stamps[t], targets[t]);
int[] g2 = s.movesToStamp2(stamps[t], targets[t]);
boolean ok1 = g1.length > 0 && s.verify(g1, stamps[t], targets[t]);
boolean ok2 = g2.length > 0 && s.verify(g2, stamps[t], targets[t]);
if (ok1 == exp[t] && ok2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 936-" + (t + 1) + ": stamp=" + stamps[t] + " target=" + targets[t]
+ " g1=" + java.util.Arrays.toString(g1) + " g2=" + java.util.Arrays.toString(g2));
}
}
// 负例:无法印出
++total;
int[] bad = s.movesToStamp("abc", "axbxc");
if (bad.length == 0) {
++pass;
} else {
System.out.println("FAIL 936-bad: exp empty got " + java.util.Arrays.toString(bad));
}
if (pass == total) {
System.out.println("== 0936 PASS ==");
} else {
System.out.println("== 0936 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
937. Reorder Data in Log Files(重新排列日志文件)
题目描述
给你一个日志数组 logs。每条日志都是以空格分隔的字串,其第一个字为字母与数字混合的标识符。
有两种不同类型的日志:
- 字母日志:除标识符之外,所有字均由小写字母组成。
- 数字日志:除标识符之外,所有字均由数字组成。 请按下述规则将日志重新排序:
- 所有字母日志都排在数字日志之前。
- 字母日志在内容不同时,忽略标识符后,按内容字典顺序排序;在内容相同时,按标识符排序。
- 数字日志应该保留原来的相对顺序。 返回日志的最终顺序。
示例 1
- 输入:logs = ["dig1 8 1 5 1","let1 art can","dig2 3 6","let2 own kit dig","let3 art
zero"] - 输出:["let1 art can","let3 art
zero","let2 own kit dig","dig1 8 1 5 1","dig2 3 6"]
示例 2
- 输入:logs = ["a1 9 2 3 1","g1 act car","zo4 4 7","ab1 off
keydog","a8 act zoo"] - 输出:["g1 act car","a8 act zoo","ab1 off
keydog","a1 9 2 3 1","zo4 4 7"] 提示: 1 <= logs.length <= 1003 <= logs[i].length <= 100logs[i]中,字与字之间都用单个空格分隔- 题目数据保证
logs[i]都有一个标识符,并且在标识符之后至少存在一个字
解题思路
排序规则可归纳为一个稳定的自定义比较键:字母日志排在前面,且按 (内容, 标识符) 字典序;
数字日志排在后面且保持原相对顺序(稳定性由 Arrays.sort 的对象排序天然保证)。
解法一(自定义比较器,主提交):用 split(" ", 2) 把每条日志拆成 [id, content]。
两个都是字母日志时先比 content 再比 id;否则字母日志在前(返回 -1/1),两个数字日志
视为相等(返回 0,配合稳定排序保持原顺序)。O(n log n * L)。
解法二(分组 + 拼接对照):先扫描分拣出字母日志列表与数字日志列表,字母日志用比较器排序,
再顺序拼接。逻辑更直白,两种日志数量各为 a/d 时无需在比较器中判断类型。
代码实现
import java.util.Arrays;
import java.util.Comparator;
class Solution {
// 解法一:自定义比较器(推荐提交)
public String[] reorderLogFiles(String[] logs) {
Comparator<String> cmp = (log1, log2) -> {
String[] p1 = log1.split(" ", 2);
String[] p2 = log2.split(" ", 2);
boolean isLetter1 = Character.isLetter(p1[1].charAt(0));
boolean isLetter2 = Character.isLetter(p2[1].charAt(0));
if (isLetter1 && isLetter2) {
int c = p1[1].compareTo(p2[1]);
return c != 0 ? c : p1[0].compareTo(p2[0]);
}
return isLetter1 ? -1 : (isLetter2 ? 1 : 0);
};
Arrays.sort(logs, cmp);
return logs;
}
// 解法二:分拣 + 拼接(对照实现)
public String[] reorderLogFiles2(String[] logs) {
java.util.List<String> letters = new java.util.ArrayList<>();
java.util.List<String> digits = new java.util.ArrayList<>();
for (String log : logs) {
int sp = log.indexOf(' ');
if (Character.isLetter(log.charAt(sp + 1))) {
letters.add(log);
} else {
digits.add(log);
}
}
letters.sort((a, b) -> {
int sp1 = a.indexOf(' '), sp2 = b.indexOf(' ');
int c = a.substring(sp1 + 1).compareTo(b.substring(sp2 + 1));
return c != 0 ? c : a.substring(0, sp1).compareTo(b.substring(0, sp2));
});
String[] ans = new String[logs.length];
int p = 0;
for (String s : letters) {
ans[p++] = s;
}
for (String s : digits) {
ans[p++] = s;
}
return ans;
}
private static void p(String[] a) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < a.length; ++i) {
if (i > 0) {
sb.append(", ");
}
sb.append("\"").append(a[i]).append("\"");
}
sb.append("]");
System.out.println(sb);
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[][] inputs = {
{"dig1 8 1 5 1","let1 art can","dig2 3 6","let2 own kit dig","let3 art zero"},
{"a1 9 2 3 1","g1 act car","zo4 4 7","ab1 off key dog","a8 act zoo"},
{"a1 9 2 3 1","g1 act car"},
{"let2 art zero","let1 art can"}
};
String[][] exp = {
{"let1 art can","let3 art zero","let2 own kit dig","dig1 8 1 5 1","dig2 3 6"},
{"g1 act car","a8 act zoo","ab1 off key dog","a1 9 2 3 1","zo4 4 7"},
{"g1 act car","a1 9 2 3 1"},
{"let1 art can","let2 art zero"} // 内容 "art can" < "art zero"
};
for (int t = 0; t < inputs.length; ++t) {
++total;
String[] g1 = s.reorderLogFiles(inputs[t].clone());
String[] g2 = s.reorderLogFiles2(inputs[t].clone());
if (Arrays.equals(g1, exp[t]) && Arrays.equals(g2, exp[t])) {
++pass;
} else {
System.out.print("FAIL 937-" + (t + 1) + ": got ");
p(g1);
System.out.print(" exp ");
p(exp[t]);
}
}
if (pass == total) {
System.out.println("== 0937 PASS ==");
} else {
System.out.println("== 0937 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
938. Range Sum of BST(二叉搜索树的范围和)
题目描述
给定二叉搜索树的根结点 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 * 10^4]内 1 <= Node.val <= 10^51 <= low <= high <= 10^5- 所有
Node.val互不相同
解题思路
BST 满足左子树所有值 < 根值 < 右子树所有值,可利用有序性剪枝,不必遍历整棵树。
解法一(DFS 剪枝,主提交):递归访问根,若 low <= val <= high 计入;若 val > low,
左子树中才可能还有 ≥ low 的值,继续递归左子树;若 val < high,右子树中才可能还有
≤ high 的值,继续递归右子树。O(n)。
解法二(中序遍历 + 累加对照):对 BST 做中序遍历天然有序,收集位于 [low, high] 的
连续一段累加;为展示迭代写法,用栈模拟中序遍历,遇到进入区间的值即累加。
代码实现
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 {
// 解法一:DFS 剪枝(推荐提交)
public int rangeSumBST(TreeNode root, int low, int high) {
if (root == null) {
return 0;
}
int x = root.val;
int ans = (low <= x && x <= high) ? x : 0;
if (x > low) {
ans += rangeSumBST(root.left, low, high);
}
if (x < high) {
ans += rangeSumBST(root.right, low, high);
}
return ans;
}
// 解法二:迭代中序遍历(对照实现)
public int rangeSumBST2(TreeNode root, int low, int high) {
int sum = 0;
Deque<TreeNode> stk = new ArrayDeque<>();
TreeNode cur = root;
while (cur != null || !stk.isEmpty()) {
while (cur != null) {
stk.push(cur);
cur = cur.left;
}
cur = stk.pop();
if (cur.val >= low && cur.val <= high) {
sum += cur.val;
}
cur = cur.right;
}
return sum;
}
// 用层序数组建 BST(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 = {
{10, 5, 15, 3, 7, null, 18},
{10, 5, 15, 3, 7, 13, 18, 1, null, 6},
{1},
{5, 3, 7, 1, 4, 6, 9}
};
int[] lows = {7, 6, 1, 3};
int[] highs = {15, 10, 1, 7};
int[] exp = {32, 23, 1, 25};
for (int t = 0; t < trees.length; ++t) {
++total;
TreeNode r1 = build(trees[t]);
TreeNode r2 = build(trees[t]);
int g1 = s.rangeSumBST(r1, lows[t], highs[t]);
int g2 = s.rangeSumBST2(r2, lows[t], highs[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 938-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0938 PASS ==");
} else {
System.out.println("== 0938 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
939. Minimum Area Rectangle(最小面积矩形)
题目描述
给你一个 X-Y 平面上的点数组 points,其中 points[i] = [x_i, y_i]。
返回由这些点形成的矩形的最小面积,矩形的边与 X 轴和 Y 轴平行。如果不存在这样的矩形,
则返回 0。
示例 1
- 输入:points = [[1,1],[1,3],[3,1],[3,3],[2,2]]
- 输出:4
示例 2
- 输入:points = [[1,1],[1,3],[3,1],[3,3],[4,1],[4,3]]
- 输出:2 提示:
1 <= points.length <= 500points[i].length == 20 <= x_i, y_i <= 4 * 10^4- 所有给定的点都是唯一的。
解题思路
轴对齐矩形可由 "两对不同纵坐标" 决定:左右两条竖线各取两个点,且纵坐标相同。
n <= 500,枚举四个点不可行,但同竖线的点对枚举可行。
解法一(按 x 分组 + 记录 y 对最近 x,主提交):用 TreeMap 把点按 x 分组并对每组 y
排序,从左到右扫描每个 x:对当前竖线上每个点对 (y1, y2)(y1 < y2),若该点对在
更左的竖线出现过(记在 pos 中),则 (x - pos) * (y2 - y1) 是一个候选面积。
每次扫描后把当前 x 记为该点对"最近一次出现"的横坐标,保证与左侧最近的 x 配对以取小。
坐标编码用 y1 * 40001 + y2。O(n^2 log n)。
解法二(枚举对角点 + 集合查重,对照):面积由对角点唯一决定:对每一对点 (x1,y1),
(x2,y2)(x1 != x2 且 y1 != y2),若能找到对角 (x1,y2) 与 (x2,y1),则构成矩形,
用点集 O(1) 判定。O(n^2)。
代码实现
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
class Solution {
// 解法一:按 x 分组 + y 点对(推荐提交)
public int minAreaRect(int[][] points) {
TreeMap<Integer, List<Integer>> byX = new TreeMap<>();
for (int[] p : points) {
byX.computeIfAbsent(p[0], k -> new ArrayList<>()).add(p[1]);
}
Map<Integer, Integer> lastX = new HashMap<>();
int ans = Integer.MAX_VALUE;
for (Map.Entry<Integer, List<Integer>> e : byX.entrySet()) {
int x = e.getKey();
List<Integer> ys = e.getValue();
Collections.sort(ys);
int m = ys.size();
for (int i = 0; i < m; ++i) {
int y1 = ys.get(i);
for (int j = i + 1; j < m; ++j) {
int y2 = ys.get(j);
int key = y1 * 40001 + y2;
Integer prevX = lastX.get(key);
if (prevX != null) {
ans = Math.min(ans, (x - prevX) * (y2 - y1));
}
lastX.put(key, x);
}
}
}
return ans == Integer.MAX_VALUE ? 0 : ans;
}
// 解法二:对角点枚举(对照实现)
public int minAreaRect2(int[][] points) {
java.util.Set<Integer> set = new java.util.HashSet<>();
for (int[] p : points) {
set.add(p[0] * 40001 + p[1]);
}
int n = points.length;
int ans = Integer.MAX_VALUE;
for (int i = 0; i < n; ++i) {
int x1 = points[i][0], y1 = points[i][1];
for (int j = i + 1; j < n; ++j) {
int x2 = points[j][0], y2 = points[j][1];
if (x1 == x2 || y1 == y2) {
continue;
}
if (set.contains(x1 * 40001 + y2) && set.contains(x2 * 40001 + y1)) {
ans = Math.min(ans, Math.abs(x2 - x1) * Math.abs(y2 - y1));
}
}
}
return ans == Integer.MAX_VALUE ? 0 : ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][][] pts = {
{{1,1},{1,3},{3,1},{3,3},{2,2}},
{{1,1},{1,3},{3,1},{3,3},{4,1},{4,3}},
{{1,1},{2,2},{3,3}},
{{0,0},{0,1},{1,0},{1,1},{0,2},{2,2},{1,2},{2,1}}
};
int[] exp = {4, 2, 0, 1};
for (int t = 0; t < pts.length; ++t) {
++total;
int g1 = s.minAreaRect(pts[t]);
int g2 = s.minAreaRect2(pts[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 939-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0939 PASS ==");
} else {
System.out.println("== 0939 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
940. Distinct Subsequences II(不同的子序列 II)
题目描述
给定一个字符串 s,计算 s 的不同非空子序列的个数。因为结果可能很大,所以返回答案需要
对 10^9 + 7 取余。
字符串的子序列是经由原字符串删除一些(也可能不删除)字符但不改变剩余字符相对位置的一个
新字符串。例如,"ace" 是 "abcde" 的一个子序列,但 "aec" 不是。
示例 1
- 输入:
s= "abc" - 输出:7
- 解释:7 个不同的子序列分别是 "
a", "b", "c", "ab", "ac", "bc", 以及 "abc"。
示例 2
- 输入:
s= "aba" - 输出:6
- 解释:6 个不同的子序列分别是 "
a", "b", "ab", "ba", "aa" 以及 "aba"。
示例 3
- 输入:
s= "aaa" - 输出:3
- 解释:3 个不同的子序列分别是 "
a", "aa" 以及 "aaa"。 提示: 1 <= s.length <= 2000s仅由小写英文字母组成
解题思路
不同子序列按"末字符"分类去重:以字符 c 结尾的所有不同子序列,恰好是此前所有子序列
各追加一个 c 再加上 c 本身;若 f[c] 存以 c 结尾的不同子序列数,则每读到 c,
f[c] = sum(f) + 1,旧值被覆盖即完成去重(相同字符结尾的子序列集合不会重复计数)。
解法一(每次求和的 DP,主提交):维护 f[26],每次 f[c] = (sum(f) + 1) % mod,
答案 = sum(f) % mod。O(26n) 时间、O(26) 空间。
解法二(O(1) 增量 DP 对照):维护全局和 ans,新读到 c 时增量 add = ans + 1 - f[c],
再 ans += add、f[c] += add,单步 O(1),总 O(n)。
代码实现
class Solution {
private static final int MOD = 1_000_000_007;
// 解法一:f 数组 + 求和 DP(推荐提交)
public int distinctSubseqII(String s) {
int[] f = new int[26];
for (int i = 0; i < s.length(); ++i) {
int sum = 1; // 字符本身作为子序列
for (int v : f) {
sum = (sum + v) % MOD;
}
f[s.charAt(i) - 'a'] = sum;
}
int ans = 0;
for (int v : f) {
ans = (ans + v) % MOD;
}
return ans;
}
// 解法二:O(1) 增量 DP(对照实现)
public int distinctSubseqII2(String s) {
int[] f = new int[26];
int ans = 0;
for (int i = 0; i < s.length(); ++i) {
int c = s.charAt(i) - 'a';
int add = (ans + 1 + MOD - f[c]) % MOD;
ans = (ans + add) % MOD;
f[c] = (f[c] + add) % MOD;
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[] ss = {"abc", "aba", "aaa", "a", "ab", "abcabc", "abaabb"};
int[] exp = {7, 6, 3, 1, 3, 51, 25};
for (int t = 0; t < ss.length; ++t) {
++total;
int g1 = s.distinctSubseqII(ss[t]);
int g2 = s.distinctSubseqII2(ss[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 940-" + (t + 1) + ": " + ss[t] + " got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0940 PASS ==");
} else {
System.out.println("== 0940 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
941. Valid Mountain Array(有效的山脉数组)
题目描述
给定一个整数数组 arr,如果它是有效的山脉数组就返回 true,否则返回 false。
如果 arr 满足下述条件,那么它是一个山脉数组:
arr.length >= 3- 在
0 < i < arr.length - 1条件下,存在i使得: arr[0] < arr[1] < ... < arr[i-1] < arr[i]arr[i] > arr[i+1] > ... > arr[arr.length-1]
示例 1
- 输入:
arr= [2,1] - 输出:
false
示例 2
- 输入:
arr= [3,5,5] - 输出:
false
示例 3
- 输入:
arr= [0,3,2,1] - 输出:
true提示: 1 <= arr.length <= 10^40 <= arr[i] <= 10^4
解题思路
山脉数组须先严格递增再严格递减,长度至少 3,且峰不落在两端(否则只是单调段)。
解法一(双指针向中间夹逼,主提交):i 从 0 出发,只要 arr[i] < arr[i+1] 就右移;
j 从 n-1 出发,只要 arr[j-1] > arr[j] 就左移;最后 i == j(两段在山峰相遇)
且峰不在端点(i != 0 && j != n-1,由循环边界天然保证)即为山脉。O(n) 时间、O(1) 空间。
解法二(单侧爬升 + 检查下降,对照):从左爬升找到峰,然后必须严格下降到最后;峰不能是
首尾,且下降段必须走到末尾。逻辑分两步更直白。
代码实现
class Solution {
// 解法一:双指针夹逼(推荐提交)
public boolean validMountainArray(int[] arr) {
int n = arr.length;
if (n < 3) {
return false;
}
int i = 0, j = n - 1;
while (i + 1 < n && arr[i] < arr[i + 1]) {
++i;
}
while (j - 1 >= 0 && arr[j - 1] > arr[j]) {
--j;
}
return i == j && i != 0 && i != n - 1;
}
// 解法二:爬升 + 下降两段检查(对照实现)
public boolean validMountainArray2(int[] arr) {
int n = arr.length;
if (n < 3) {
return false;
}
int i = 0;
while (i + 1 < n && arr[i] < arr[i + 1]) {
++i;
}
if (i == 0 || i == n - 1) {
return false; // 峰在两端,无严格下降段或无严格上升段
}
while (i + 1 < n && arr[i] > arr[i + 1]) {
++i;
}
return i == n - 1;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] arrs = {
{2,1}, {3,5,5}, {0,3,2,1}, {0,1,2,3}, {3,2,1,0},
{1,2,3,4,5}, {5,4,3,2,1}, {1,2,2,1}, {0,1,2,3,2,1}, {1}
};
boolean[] exp = {false, false, true, false, false, false, false, false, true, false};
for (int t = 0; t < arrs.length; ++t) {
++total;
boolean g1 = s.validMountainArray(arrs[t]);
boolean g2 = s.validMountainArray2(arrs[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 941-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0941 PASS ==");
} else {
System.out.println("== 0941 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
942. DI String Match(增减字符串匹配)
题目描述
由范围 [0, n] 内所有整数组成的 n + 1 个整数的排列序列可以表示为长度为 n 的字符串 s,
其中:
- 如果
perm[i] < perm[i + 1],那么s[i] == 'I'; - 如果
perm[i] > perm[i + 1],那么s[i] == 'D'。 给定一个字符串s,重构排列perm并返回它。如果有多个有效排列 perm,则返回其中任何一个。
示例 1
- 输入:
s= "IDID" - 输出:[0,4,1,3,2]
示例 2
- 输入:
s= "III" - 输出:[0,1,2,3]
示例 3
- 输入:
s= "DDI" - 输出:[3,2,0,1] 提示:
1 <= s.length <= 10^5s只包含字符 "I" 或 "D"
解题思路
用 0..n 各一次构造排列,使相邻元素满足 'I' 升、'D' 降。n 可达 10^5,不可回溯。
贪心思路:遇到 'I' 时,放当前可用数字中的最小值(这样下一位无论多大都能满足严格上升);
遇到 'D' 时,放当前可用数字中的最大值(下一位无论多小都能满足严格下降)。
解法一(双端贪心,主提交):low、high 分别指向剩余最小/最大值,遍历 s,
'I' 取 low++ 放入结果,'D' 取 high-- 放入结果;最后补 low(此时 low==high)。
O(n) 时间、O(n) 空间。
解法二(预填区间对照):思路等价,先把 'I' 位置按升序依次填小值、'D' 位置按降序依次
填大值,再补剩余数,展示另一种实现方式。
代码实现
class Solution {
// 解法一:双端贪心(推荐提交)
public int[] diStringMatch(String s) {
int n = s.length();
int low = 0, high = n;
int[] ans = new int[n + 1];
for (int i = 0; i < n; ++i) {
if (s.charAt(i) == 'I') {
ans[i] = low++;
} else {
ans[i] = high--;
}
}
ans[n] = low; // 剩下唯一未用的数
return ans;
}
// 解法二:两遍预填(对照实现)
public int[] diStringMatch2(String s) {
int n = s.length();
int[] ans = new int[n + 1];
java.util.Arrays.fill(ans, -1);
int low = 0, high = n;
// 第一遍:从左往右填 'I',用当前剩余最小值(升序部分渐进)
for (int i = 0; i < n; ++i) {
if (s.charAt(i) == 'I') {
ans[i] = low++;
}
}
// 第二遍:从左往右填 'D',用当前剩余最大值(连续的 D 自然递减)
for (int i = 0; i < n; ++i) {
if (s.charAt(i) == 'D') {
ans[i] = high--;
}
}
for (int i = 0; i <= n; ++i) {
if (ans[i] == -1) {
ans[i] = low; // 剩余唯一值
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[] ss = {"IDID", "III", "DDI", "I", "D", "ID", "DI"};
for (int t = 0; t < ss.length; ++t) {
++total;
int[] g1 = s.diStringMatch(ss[t]);
int[] g2 = s.diStringMatch2(ss[t]);
if (check(ss[t], g1) && check(ss[t], g2)) {
++pass;
} else {
System.out.println("FAIL 942-" + (t + 1) + ": " + ss[t]
+ " g1=" + java.util.Arrays.toString(g1) + " g2=" + java.util.Arrays.toString(g2));
}
}
if (pass == total) {
System.out.println("== 0942 PASS ==");
} else {
System.out.println("== 0942 FAIL ==");
System.exit(1);
}
}
// 校验:g 是 0..len(s) 的排列且满足 s 规定的升降
private static boolean check(String s, int[] g) {
int n = s.length();
if (g.length != n + 1) {
return false;
}
boolean[] seen = new boolean[n + 1];
for (int x : g) {
if (x < 0 || x > n || seen[x]) {
return false;
}
seen[x] = true;
}
for (int i = 0; i < n; ++i) {
if (s.charAt(i) == 'I' && !(g[i] < g[i + 1])) {
return false;
}
if (s.charAt(i) == 'D' && !(g[i] > g[i + 1])) {
return false;
}
}
return true;
}
}
复杂度分析
943. Find the Shortest Superstring(最短超级串)
题目描述
给定一个字符串数组 words,找到以 words 中每个字符串作为子字符串的最短字符串。
如果有多个有效最短字符串满足题目条件,返回其中任意一个即可。
我们可以假设 words 中没有字符串是 words 中另一个字符串的子字符串。
示例 1
- 输入:
words= ["alex","loves","leetcode"] - 输出:"alexlovesleetcode"
示例 2
- 输入:
words= ["catg","ctaagt","gcta","ttca","atgcatc"] - 输出:"gctaagttcatgcatc" 提示:
1 <= words.length <= 121 <= words[i].length <= 20words[i]由小写英文字母组成words中的所有字符串互不相同
解题思路
最短超级串就是把所有单词排成一条链,相邻单词尽量重叠,等价于"最大化总重叠长度"的
哈密顿路径问题。n <= 12,可用状态压缩 DP。
解法一(状态压缩 DP,主提交):先预处理 g[i][j] = 把 words[j] 接在 words[i] 之后
能获得的最大重叠长度(枚举重叠长度 k 从 min(leni,lenj) 向下试,匹配后缀/前缀)。
定义 dp[mask][j] = 已用单词集合为 mask、链尾为 words[j] 时的最大重叠;
转移枚举上一个链尾 k(k 在 mask 中不含 j),dp[mask][j] = max(dp[mask^1<<j][k] + g[k][j])
并记录前驱 pre[mask][j] = k。取 dp[full][j] 最大者倒推链条,反转后拼接
(首单词整串,后续单词只接非重叠部分;未进链的单词按序补尾)。O(2^n * n^2)。
解法二(DFS 剪枝对照):实现等价的回溯搜索,按当前链尾寻找下一单词使重叠最大,
记录全局最优,用于小规模验证。
代码实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
class Solution {
// 解法一:状态压缩 DP(推荐提交)
public String shortestSuperstring(String[] words) {
int n = words.length;
int[][] g = new int[n][n];
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (i == j) {
continue;
}
int lim = Math.min(words[i].length(), words[j].length());
for (int k = lim; k > 0; --k) {
if (words[i].endsWith(words[j].substring(0, k))) {
g[i][j] = k;
break;
}
}
}
}
int full = (1 << n) - 1;
int[][] dp = new int[1 << n][n];
int[][] pre = new int[1 << n][n];
for (int[] row : pre) {
Arrays.fill(row, -1);
}
for (int mask = 1; mask <= full; ++mask) {
for (int j = 0; j < n; ++j) {
if ((mask & (1 << j)) == 0) {
continue;
}
int prev = mask ^ (1 << j);
for (int k = 0; k < n; ++k) {
if ((prev & (1 << k)) == 0) {
continue;
}
int v = dp[prev][k] + g[k][j];
if (v > dp[mask][j]) {
dp[mask][j] = v;
pre[mask][j] = k;
}
}
}
}
int last = 0;
for (int j = 1; j < n; ++j) {
if (dp[full][j] > dp[full][last]) {
last = j;
}
}
List<Integer> order = new ArrayList<>();
order.add(last);
int mask = full;
while (pre[mask][last] != -1) {
int k = pre[mask][last];
mask ^= (1 << last);
last = k;
order.add(last);
}
java.util.Collections.reverse(order);
Set<Integer> seen = new HashSet<>(order);
for (int i = 0; i < n; ++i) {
if (!seen.contains(i)) {
order.add(i);
}
}
StringBuilder sb = new StringBuilder(words[order.get(0)]);
for (int i = 1; i < order.size(); ++i) {
int a = order.get(i - 1), b = order.get(i);
sb.append(words[b].substring(g[a][b]));
}
return sb.toString();
}
// 解法二:DFS 回溯(对照实现,小规模验证)
private int bestOverlap = -1;
private List<Integer> bestOrder;
public String shortestSuperstring2(String[] words) {
int n = words.length;
int[][] g = new int[n][n];
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (i == j) {
continue;
}
int lim = Math.min(words[i].length(), words[j].length());
for (int k = lim; k > 0; --k) {
if (words[i].endsWith(words[j].substring(0, k))) {
g[i][j] = k;
break;
}
}
}
}
bestOverlap = -1;
bestOrder = null;
dfs(words, g, 0, 0, -1, new ArrayList<>(), new boolean[n]);
StringBuilder sb = new StringBuilder(words[bestOrder.get(0)]);
for (int i = 1; i < n; ++i) {
sb.append(words[bestOrder.get(i)].substring(g[bestOrder.get(i - 1)][bestOrder.get(i)]));
}
return sb.toString();
}
private void dfs(String[] words, int[][] g, int cnt, int overlap, int lastIdx,
List<Integer> cur, boolean[] used) {
int n = words.length;
if (cnt == n) {
if (overlap > bestOverlap) {
bestOverlap = overlap;
bestOrder = new ArrayList<>(cur);
}
return;
}
for (int i = 0; i < n; ++i) {
if (used[i]) {
continue;
}
used[i] = true;
cur.add(i);
int add = (lastIdx == -1) ? 0 : g[lastIdx][i];
dfs(words, g, cnt + 1, overlap + add, i, cur, used);
cur.remove(cur.size() - 1);
used[i] = false;
}
}
// 校验结果字符串确实包含每个 word
public boolean verify(String sup, String[] words) {
for (String w : words) {
if (!sup.contains(w)) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[][] inputs = {
{"alex","loves","leetcode"},
{"catg","ctaagt","gcta","ttca","atgcatc"},
{"a","b","c"},
{"ab","bc","cd","de"},
{"gcta","atgcatc","ctaagt","ttca","catg"}
};
for (int t = 0; t < inputs.length; ++t) {
++total;
String g1 = s.shortestSuperstring(inputs[t]);
String g2 = s.shortestSuperstring2(inputs[t]);
boolean ok1 = s.verify(g1, inputs[t]);
boolean ok2 = s.verify(g2, inputs[t]);
// 两种解法应得到相同总长度(最长重叠)
boolean sameLen = g1.length() == g2.length();
if (ok1 && ok2 && sameLen) {
++pass;
} else {
System.out.println("FAIL 943-" + (t + 1) + ": len " + g1.length() + " vs " + g2.length());
}
}
if (pass == total) {
System.out.println("== 0943 PASS ==");
} else {
System.out.println("== 0943 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
944. Delete Columns to Make Sorted(删列造序)
题目描述
给你由 n 个小写字母字符串组成的数组 strs,其中每个字符串长度相等。
这些字符串可以每个一行,排成一个网格。例如,strs = ["abc", "bce", "cae"] 可以排列为:
abc
bce
cae
你需要找出并删除不是按字典序非严格递增排列的列。在上面的例子(下标从 0 开始)中,列 0
('a', 'b', 'c')和列 2('c', 'e', 'e')都是按字典序非严格递增排列的,而列 1
('b', 'c', 'a')不是,所以要删除列 1。返回你需要删除的列数。
示例 1
- 输入:strs = ["cba","daf","ghi"]
- 输出:1
示例 2
- 输入:strs = ["
a","b"] - 输出:0
示例 3
- 输入:strs = ["zyx","wvu","tsr"]
- 输出:3 提示:
n == strs.length1 <= n <= 1001 <= strs[i].length <= 1000strs[i]由小写英文字母组成
解题思路
每一列是否非严格递增是相互独立的判断:只需逐列从上到下检查是否存在相邻行逆序。
解法一(逐列检查,主提交):列数 m = strs[0].length(),行数 n = strs.length。
对每一列 j,扫描 i = 1..n-1,一旦 strs[i].charAt(j) < strs[i-1].charAt(j) 即
逆序,该列必须删除,计数加一并跳到下一列。O(m * n) 时间、O(1) 空间。
解法二(转置 + 逐行检查对照):思路一致,但把网格转置后对每行做有序性检查,
便于理解"列变成行"的等价性。
代码实现
class Solution {
// 解法一:逐列检查(推荐提交)
public int minDeletionSize(String[] strs) {
int m = strs[0].length();
int n = strs.length;
int ans = 0;
for (int j = 0; j < m; ++j) {
for (int i = 1; i < n; ++i) {
if (strs[i].charAt(j) < strs[i - 1].charAt(j)) {
++ans;
break;
}
}
}
return ans;
}
// 解法二:转置后检查(对照实现)
public int minDeletionSize2(String[] strs) {
int m = strs[0].length();
int n = strs.length;
char[][] grid = new char[n][m];
for (int i = 0; i < n; ++i) {
grid[i] = strs[i].toCharArray();
}
int ans = 0;
for (int j = 0; j < m; ++j) {
for (int i = 1; i < n; ++i) {
if (grid[i][j] < grid[i - 1][j]) {
++ans;
break;
}
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[][] strs = {
{"cba","daf","ghi"},
{"a","b"},
{"zyx","wvu","tsr"},
{"abc","bce","cae"},
{"a"}
};
int[] exp = {1, 0, 3, 1, 0};
for (int t = 0; t < strs.length; ++t) {
++total;
int g1 = s.minDeletionSize(strs[t]);
int g2 = s.minDeletionSize2(strs[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 944-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0944 PASS ==");
} else {
System.out.println("== 0944 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
945. Minimum Increment to Make Array Unique(使数组唯一的最小增量)
题目描述
给你一个整数数组 nums。每次 move 操作将会选择任意一个满足 0 <= i < nums.length
的下标 i,并将 nums[i] 递增 1。
返回使 nums 中的每个值都变成唯一的所需要的最少操作次数。
生成的测试用例保证答案在 32 位整数范围内。
示例 1
- 输入:
nums= [1,2,2] - 输出:1
- 解释:经过一次 move 操作,数组将变为 [1, 2, 3]。
示例 2
- 输入:
nums= [3,2,1,2,1,7] - 输出:6
- 解释:经过 6 次 move 操作,数组将变为 [3, 4, 1, 2, 5, 7]。 提示:
1 <= nums.length <= 10^50 <= nums[i] <= 10^5
解题思路
每次只能把数加一,目标是最终互不相同,最小化总增量。
解法一(排序 + 贪心,主提交):排序后,用 y 记录"当前已占用的下一个最小可用值",
对每个 x,把它抬升到 max(y+1, x)(y+1 确保不与前一个数冲突,x 保留原值下界),
代价 y - x 累加。O(n log n) 时间、O(log n) 空间(排序栈)。
解法二(计数 + 线性扫描,对照):结果数组最大值不超过 max(nums) + len(nums),
用计数数组记录每个值出现次数,从 0 起:cnt[i] > 1 时把多出的 cnt[i]-1 个元素
全部推向 i+1(每个一步),累加成本 cnt[i]-1。O(n + max) 时间、O(n + max) 空间。
代码实现
import java.util.Arrays;
class Solution {
// 解法一:排序 + 贪心(推荐提交)
public int minIncrementForUnique(int[] nums) {
Arrays.sort(nums);
int ans = 0;
int y = -1;
for (int x : nums) {
y = Math.max(y + 1, x);
ans += y - x;
}
return ans;
}
// 解法二:计数 + 线性扫描(对照实现)
public int minIncrementForUnique2(int[] nums) {
int max = 0;
for (int x : nums) {
max = Math.max(max, x);
}
int size = max + nums.length;
int[] cnt = new int[size];
for (int x : nums) {
++cnt[x];
}
int ans = 0;
for (int i = 0; i < size - 1; ++i) {
if (cnt[i] > 1) {
int diff = cnt[i] - 1;
cnt[i + 1] += diff;
ans += diff;
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] nums = {
{1,2,2},
{3,2,1,2,1,7},
{1},
{1,1,1},
{0,0,0,0},
{2,2,2,2,2}
};
int[] exp = {1, 6, 0, 3, 6, 10};
for (int t = 0; t < nums.length; ++t) {
++total;
int g1 = s.minIncrementForUnique(nums[t].clone());
int g2 = s.minIncrementForUnique2(nums[t].clone());
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 945-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0945 PASS ==");
} else {
System.out.println("== 0945 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
946. Validate Stack Sequences(验证栈序列)
题目描述
给定 pushed 和 popped 两个序列,每个序列中的值都不重复,只有当它们可能是在最初空栈上
进行的推入 push 和弹出 pop 操作序列的结果时,返回 true;否则,返回 false。
示例 1
- 输入:pushed = [1,2,3,4,5], popped = [4,5,3,2,1]
- 输出:
true
示例 2
- 输入:pushed = [1,2,3,4,5], popped = [4,3,5,1,2]
- 输出:
false - 解释:1 不能在 2 之前弹出。 提示:
1 <= pushed.length <= 10000 <= pushed[i] <= 1000pushed的所有元素互不相同popped.length == pushed.lengthpopped是pushed的一个排列
解题思路
判断一个出栈序列能否由依次按 pushed 入栈(必要时随时弹出)得到。
解法一(栈模拟,主提交):用栈模拟,按 pushed 依次入栈,每次入栈后检查栈顶是否等于
popped 中下一个待出元素,是则弹出并推进 popped 下标;处理完所有 pushed 后,
若 popped 下标已到末尾说明全部弹出成功,返回 true。O(n) 时间、O(n) 空间。
解法二(数组模拟栈对照):用 pushed 数组本身作栈底复用,双指针模拟,节省 Stack 开销,
O(n) 时间、O(1) 额外空间。
代码实现
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
// 解法一:栈模拟(推荐提交)
public boolean validateStackSequences(int[] pushed, int[] popped) {
Deque<Integer> stk = new ArrayDeque<>();
int i = 0;
for (int x : pushed) {
stk.push(x);
while (!stk.isEmpty() && stk.peek() == popped[i]) {
stk.pop();
++i;
}
}
return i == popped.length;
}
// 解法二:数组模拟栈(对照实现)
public boolean validateStackSequences2(int[] pushed, int[] popped) {
int n = pushed.length;
int[] stk = new int[n];
int top = -1;
int i = 0;
for (int x : pushed) {
stk[++top] = x;
while (top >= 0 && stk[top] == popped[i]) {
--top;
++i;
}
}
return i == n;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] pusheds = {
{1,2,3,4,5},
{1,2,3,4,5},
{1},
{2,1,0},
{0,1,2,3}
};
int[][] poppeds = {
{4,5,3,2,1},
{4,3,5,1,2},
{1},
{2,1,0},
{3,2,1,0}
};
boolean[] exp = {true, false, true, true, true};
for (int t = 0; t < pusheds.length; ++t) {
++total;
boolean g1 = s.validateStackSequences(pusheds[t], poppeds[t]);
boolean g2 = s.validateStackSequences2(pusheds[t], poppeds[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 946-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0946 PASS ==");
} else {
System.out.println("== 0946 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
947. Most Stones Removed with Same Row or Column(移除最多的同行或同列石头)
题目描述
n 块石头放置在二维平面中的一些整数坐标点上。每个坐标点上最多只能有一块石头。
如果一块石头的同行或者同列上有其他石头存在,那么就可以移除这块石头。
给你一个长度为 n 的数组 stones,其中 stones[i] = [x_i, y_i] 表示第 i 块石头的
位置,返回可以移除的石子的最大数量。
示例 1
- 输入:stones = [[0,0],[0,1],[1,0],[1,2],[2,1],[2,2]]
- 输出:5
示例 2
- 输入:stones = [[0,0],[0,2],[1,1],[2,0],[2,2]]
- 输出:3
示例 3
- 输入:stones = [[0,0]]
- 输出:0 提示:
1 <= stones.length <= 10000 <= x_i, y_i <= 10^4- 不会有两块石头放在同一个坐标点上
解题思路
同行或同列的石头构成一个连通关系:从一块石头出发,可以经由"同行的石头→同列的石头"
两两传递,因此属于同一连通分量。每个连通分量无论多大,最后只能保留一块石头;
故最大可移除数 = 石头总数 - 连通分量数。
解法一(并查集 + 坐标偏移,主提交):把每个横坐标视为节点,每个纵坐标加上偏移量
10001 视为另一组节点;对每块石头 union(x, y + OFFSET),同行/同列自动连通。
最后统计每个石头的横坐标根节点去重个数(即分量数),返回 n - 分量子。
时间 O(n log m),空间 O(m)。
解法二(两两合并的并查集对照):对石头两两检查是否同行或同列,成功合并一次计数加一,
该计数即为可移除数;O(n^2 α(n)),实现直观但平方复杂度。
代码实现
import java.util.HashSet;
import java.util.Set;
class Solution {
// 解法一:并查集 + 坐标偏移(推荐提交)
public int removeStones(int[][] stones) {
int offset = 10001;
int max = offset * 2;
UnionFind uf = new UnionFind(max);
for (int[] st : stones) {
uf.union(st[0], st[1] + offset);
}
Set<Integer> roots = new HashSet<>();
for (int[] st : stones) {
roots.add(uf.find(st[0]));
}
return stones.length - roots.size();
}
// 解法二:石头两两合并(对照实现)
public int removeStones2(int[][] stones) {
int n = stones.length;
UnionFind uf = new UnionFind(n);
int merged = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (stones[i][0] == stones[j][0] || stones[i][1] == stones[j][1]) {
if (uf.union(i, j)) {
++merged;
}
}
}
}
return merged;
}
static class UnionFind {
int[] p;
int[] sz;
UnionFind(int n) {
p = new int[n];
sz = new int[n];
for (int i = 0; i < n; ++i) {
p[i] = i;
sz[i] = 1;
}
}
int find(int x) {
if (p[x] != x) {
p[x] = find(p[x]);
}
return p[x];
}
boolean union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) {
return false;
}
if (sz[ra] < sz[rb]) {
int t = ra;
ra = rb;
rb = t;
}
p[rb] = ra;
sz[ra] += sz[rb];
return true;
}
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][][] stones = {
{{0,0},{0,1},{1,0},{1,2},{2,1},{2,2}},
{{0,0},{0,2},{1,1},{2,0},{2,2}},
{{0,0}},
{{0,0},{0,1},{1,0},{1,1}},
{{0,0},{0,1},{0,2},{0,3}}
};
int[] exp = {5, 3, 0, 3, 3};
for (int t = 0; t < stones.length; ++t) {
++total;
int g1 = s.removeStones(stones[t]);
int g2 = s.removeStones2(stones[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 947-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0947 PASS ==");
} else {
System.out.println("== 0947 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
时间 O(n log m),空间 O(m)。
948. Bag of Tokens(令牌放置)
题目描述
你的初始能量为 power,初始分数为 0,只有一包令牌以整数数组 tokens 给出。
你的目标是通过有策略地使用这些令牌以最大化总分数。在一次行动中,你可以用两种方式中的一种
来使用一个未被使用的令牌(但不是对同一个令牌使用两种方式):
- 朝上:如果你当前至少有
tokens[i]点能量,可以使用令牌i,失去tokens[i]点能量, 并得到 1 分。 - 朝下:如果你当前至少有 1 分,可以使用令牌
i,获得tokens[i]点能量,并失去 1 分。 在使用任意数量的令牌后,返回我们可以得到的最大分数。
示例 1
- 输入:tokens = [100], power = 50
- 输出:0
示例 2
- 输入:tokens = [200,100], power = 150
- 输出:1
示例 3
- 输入:tokens = [100,200,300,400], power = 200
- 输出:2 提示:
0 <= tokens.length <= 10000 <= tokens[i], power < 10^4
解题思路
为了最大化分数,应该"用尽可能少的能量换分",即总是正面使用剩余能量能买得起的最小令牌;
当能量不足以买任何令牌时,若手里有分数,则反面卖出最大令牌以换取最多能量。
解法一(排序 + 双指针贪心,主提交):排序后用 i 指向最便宜的未用令牌、j 指向最贵的。
能量够就 i 正面买下(分数 +1,记录峰值);不够但有分就卖 j(分数 -1,能量 += tokens[j]);
两者都不可则结束。O(n log n)。
解法二(计数贪心对照):值域 [0, 10^4) 较小,用频数数组按值从小到大"买分"、从大到小
"回购能量",体现同一种贪心策略的另一种实现。
代码实现
import java.util.Arrays;
class Solution {
// 解法一:排序 + 双指针(推荐提交)
public int bagOfTokensScore(int[] tokens, int power) {
Arrays.sort(tokens);
int n = tokens.length;
int ans = 0, score = 0;
int i = 0, j = n - 1;
while (i <= j) {
if (power >= tokens[i]) {
power -= tokens[i++];
ans = Math.max(ans, ++score);
} else if (score > 0) {
power += tokens[j--];
--score;
} else {
break;
}
}
return ans;
}
// 解法二:频数计数贪心(对照实现)
public int bagOfTokensScore2(int[] tokens, int power) {
if (tokens.length == 0) {
return 0;
}
int maxV = 0;
for (int v : tokens) {
maxV = Math.max(maxV, v);
}
int[] cnt = new int[maxV + 1];
for (int v : tokens) {
++cnt[v];
}
int ans = 0, score = 0;
int lo = 0, hi = maxV;
int used = 0, n = tokens.length;
while (used < n && lo <= hi) {
// 跳过已用完的低值区
while (lo <= hi && cnt[lo] == 0) {
++lo;
}
if (lo > hi) {
break;
}
if (power >= lo) {
--cnt[lo];
power -= lo;
++score;
++used;
ans = Math.max(ans, score);
} else if (score > 0) {
// 能量不够,卖出最贵令牌换能量
while (hi >= lo && cnt[hi] == 0) {
--hi;
}
--cnt[hi];
power += hi;
--score;
++used;
} else {
break;
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] tokens = {
{100},
{200, 100},
{100, 200, 300, 400},
{},
{300, 200, 100},
{10, 20, 30, 40, 50}
};
int[] powers = {50, 150, 200, 100, 150, 45};
int[] exp = {0, 1, 2, 0, 1, 2};
for (int t = 0; t < tokens.length; ++t) {
++total;
int g1 = s.bagOfTokensScore(tokens[t].clone(), powers[t]);
int g2 = s.bagOfTokensScore2(tokens[t].clone(), powers[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 948-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0948 PASS ==");
} else {
System.out.println("== 0948 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
949. Largest Time for Given Digits(给定数字能组成的最大时间)
题目描述
给定一个由 4 位数字组成的数组,返回可以设置的符合 24 小时制的最大时间。
24 小时格式为 "HH:MM",其中 HH 在 00 到 23 之间,MM 在 00 到 59 之间。
最小的 24 小时制时间是 00:00,而最大的是 23:59。从 00:00(午夜)开始算起,过得越久,
时间越大。以长度为 5 的字符串,按 "HH:MM" 格式返回答案。如果不能确定有效时间,
则返回空字符串。
示例 1
- 输入:
arr= [1,2,3,4] - 输出:"23:41"
示例 2
- 输入:
arr= [5,5,5,5] - 输出:""
示例 3
- 输入:
arr= [0,0,0,0] - 输出:"00:00"
示例 4
- 输入:
arr= [0,0,1,0] - 输出:"10:00" 提示:
arr.length == 40 <= arr[i] <= 9
解题思路
4 个数字组成的合法时刻数量很少(4 个位置全排列 24 种,或从大到小枚举 2460 个时刻),
只需找其中最大者。
解法一(从大到小枚举时分 + 计数匹配,主提交):小时 h 从 23 到 0、分钟 m 从 59 到 0
枚举,对每个候选 (h, m) 统计它所需数字的频数是否恰等于 arr 的频数;第一个命中即为
最大合法时间。O(2460*4)。
解法二(全排列枚举对比):枚举 4 个下标的排列,构造 HH 与 MM,合法者取
h*60+m 最大;用 (1<<4-1) 位级去重避免重复下标组合。O(4^3)。
代码实现
class Solution {
// 解法一:枚举时分 + 计数(推荐提交)
public String largestTimeFromDigits(int[] arr) {
int[] cnt = new int[10];
for (int v : arr) {
++cnt[v];
}
for (int h = 23; h >= 0; --h) {
for (int m = 59; m >= 0; --m) {
int[] need = new int[10];
++need[h / 10];
++need[h % 10];
++need[m / 10];
++need[m % 10];
boolean ok = true;
for (int d = 0; d < 10; ++d) {
if (need[d] != cnt[d]) {
ok = false;
break;
}
}
if (ok) {
return String.format("%02d:%02d", h, m);
}
}
}
return "";
}
// 解法二:全排列枚举(对照实现)
public String largestTimeFromDigits2(int[] arr) {
int ans = -1;
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 4; ++j) {
for (int k = 0; k < 4; ++k) {
if (i == j || i == k || j == k) {
continue;
}
int l = 6 - i - j - k;
int h = arr[i] * 10 + arr[j];
int m = arr[k] * 10 + arr[l];
if (h < 24 && m < 60) {
ans = Math.max(ans, h * 60 + m);
}
}
}
}
return ans < 0 ? "" : String.format("%02d:%02d", ans / 60, ans % 60);
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] arrs = {
{1, 2, 3, 4},
{5, 5, 5, 5},
{0, 0, 0, 0},
{0, 0, 1, 0},
{2, 0, 6, 6},
{9, 8, 7, 6},
{0, 4, 0, 0}
};
String[] exp = {"23:41", "", "00:00", "10:00", "06:26", "", "04:00"};
for (int t = 0; t < arrs.length; ++t) {
++total;
String g1 = s.largestTimeFromDigits(arrs[t]);
String g2 = s.largestTimeFromDigits2(arrs[t]);
if (g1.equals(exp[t]) && g2.equals(exp[t])) {
++pass;
} else {
System.out.println("FAIL 949-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0949 PASS ==");
} else {
System.out.println("== 0949 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
950. Reveal Cards In Increasing Order(按递增顺序显示卡牌)
题目描述
牌组中的每张卡牌都对应有一个唯一的整数。你可以按你想要的顺序对这套卡片进行排序。 最初,这些卡牌在牌组里是正面朝下的(即,未显示状态)。 现在,重复执行以下步骤,直到显示所有卡牌为止:
- 从牌组顶部抽一张牌,显示它,然后将其从牌组中移出。
- 如果牌组中仍有牌,则将下一张处于牌组顶部的牌放在牌组的底部。
- 如果仍有未显示的牌,那么返回步骤 1。否则,停止行动。 返回能以递增顺序显示卡牌的牌组顺序。答案中的第一张牌被认为处于牌堆顶部。
示例
- 输入:
deck= [17,13,11,2,3,5,7] - 输出:[2,13,3,11,5,17,7] 提示:
1 <= deck.length <= 10001 <= deck[i] <= 10^6- 对于所有的
i != j,deck[i] != deck[j] - 所有卡牌都互不相同,保证答案唯一
解题思路
揭示过程是"取出队首展示、再把新队首挪到队尾",目标是从小到大展示。
正向模拟需要提前知道初始排列,反过来做反而简单:对 deck 从小到大排序后,从大到小
依次把当前元素压回队列,同时逆推"挪到底部"这一步。
解法一(队列逆向构造,主提交):排序 deck 后从大到小遍历:若队列非空,先把队尾元素
移到队首(这是正向"放到底部"的逆操作),再把当前元素插到队首;最后队列从前到后即为
初始牌组顺序。O(n log n)。
解法二(数组下标模拟对照):不用队列,改用布尔数组 + 线性扫描,按"跳过已填位置"的方式
模拟揭示过程来填值,与正向步骤一一对应,O(n^2)。
代码实现
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
class Solution {
// 解法一:队列逆向构造(推荐提交)
public int[] deckRevealedIncreasing(int[] deck) {
Arrays.sort(deck);
Deque<Integer> q = new ArrayDeque<>();
for (int i = deck.length - 1; i >= 0; --i) {
if (!q.isEmpty()) {
q.offerFirst(q.pollLast());
}
q.offerFirst(deck[i]);
}
int[] ans = new int[deck.length];
int p = 0;
for (int v : q) {
ans[p++] = v;
}
return ans;
}
// 解法二:模拟揭示过程填值(对照实现)
public int[] deckRevealedIncreasing2(int[] deck) {
Arrays.sort(deck);
int n = deck.length;
int[] ans = new int[n];
boolean[] filled = new boolean[n];
int idx = 0; // 当前应填入的牌(从小到大)
int pos = 0;
int filledCnt = 0;
while (filledCnt < n) {
// 找到下一张要填的牌
ans[pos] = deck[idx++];
filled[pos] = true;
++filledCnt;
if (filledCnt == n) {
break;
}
// 模拟"把下一张移到队尾":跳过已填位置,找下一个空位
pos = nextEmpty(filled, pos, n);
pos = nextEmpty(filled, pos, n);
}
return ans;
}
private int nextEmpty(boolean[] filled, int pos, int n) {
do {
pos = (pos + 1) % n;
} while (filled[pos]);
return pos;
}
// 校验:模拟揭示过程,确认展示顺序递增
public boolean check(int[] deck) {
Deque<Integer> q = new ArrayDeque<>();
for (int v : deck) {
q.offer(v);
}
int prev = -1;
while (!q.isEmpty()) {
int cur = q.poll();
if (cur < prev) {
return false;
}
prev = cur;
if (!q.isEmpty()) {
q.offer(q.poll());
}
}
return true;
}
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] decks = {
{17, 13, 11, 2, 3, 5, 7},
{1},
{1, 2},
{1, 2, 3},
{5, 4, 3, 2, 1},
{1, 3, 2, 4}
};
for (int t = 0; t < decks.length; ++t) {
++total;
int[] g1 = s.deckRevealedIncreasing(decks[t].clone());
int[] g2 = s.deckRevealedIncreasing2(decks[t].clone());
// g1 是标准答案,直接比较两者与验证
if (Arrays.equals(g1, g2) && s.check(g1)) {
++pass;
} else {
System.out.println("FAIL 950-" + (t + 1) + ": got "
+ Arrays.toString(g1) + " vs " + Arrays.toString(g2));
}
}
if (pass == total) {
System.out.println("== 0950 PASS ==");
} else {
System.out.println("== 0950 FAIL ==");
System.exit(1);
}
}
}