LeetCode 751-775. 题解合集
751. IP to CIDR(IP 到 CIDR)
题目描述
IP 地址是一个格式化的 32 位无符号整数,每组 8 位被打印为一个十进制数字,并用点字符 '.'
分组。CIDR 块由基础 IP 地址、斜杠和前缀长度 k 组成,覆盖所有「前 k 位」与基础 IP 相同
的地址。例如,"123.45.67.89/20" 是一个前缀长度为 20 的 CIDR 块。
给你一个起始 IP 地址 ip 和需要覆盖的 IP 地址数量 n。目标是使用尽可能少的 CIDR 块来
覆盖范围 [ip, ip + n - 1] 内的所有 IP 地址,且不应覆盖范围之外的其他 IP 地址。
返回包含 IP 地址范围的 CIDR 块的最短列表。如果有多个答案,返回其中任何一个。
示例 1
- 输入:ip = "255.0.0.7", n = 10
- 输出:["255.0.0.7/32","255.0.0.8/29","255.0.0.16/32"]
示例 2
- 输入:ip = "117.145.102.62", n = 8
- 输出:["117.145.102.62/31","117.145.102.64/30","117.145.102.68/31"]
提示: ip是一个有效的 IPv4 地址1 <= n <= 1000- 给定范围不会超过可表示的 IP 范围
解题思路
解法一(位运算贪心,主提交):把 IP 折算成 32 位整数 cur,目标终点 end = cur + n - 1。
循环时确定当前地址在二进制上「后缀有多少个 0」(lowbit = cur & -cur 表示当前能覆盖的
最大块大小),再考虑剩余数量 n 的限制,取 max 块作为本次 CIDR:前缀长度
prefixLen = 32 - log2(blockSize),输出后 cur += blockSize, n -= blockSize。
时间复杂度 O(分段数) <= O(32),空间复杂度 O(分段数)。
解法二(对照暴力块枚举):不理解位运算时,从 32 递减尝试每块大小,逻辑等价。
代码实现
import java.util.ArrayList;
import java.util.List;
class Solution {
// 解法一:位运算贪心
public List<String> ipToCIDR(String ip, int n) {
long cur = ipToLong(ip);
long end = cur + n - 1;
List<String> ans = new ArrayList<>();
while (cur <= end) {
long lowbit = cur & -cur; // 当前地址后缀 0 的个数决定的块大小
// 剩余需要覆盖的数量
long remain = end - cur + 1;
long block = 1;
int bits = 0;
// 块大小必须能被 cur 的 2 的幂次整除,且不能超过 remain
while (block * 2 <= lowbit && block * 2 <= remain) {
block <<= 1;
++bits;
}
// 但 lowbit 可能超过 remain,需要按 remain 的最高 2 的幂来裁剪
while (block > remain) {
block >>= 1;
--bits;
}
if (block == 0) {
block = 1;
bits = 0;
}
int prefixLen = 32 - bits; // 实际应基于 block 的位数修正
// 重算:block 是 2 的幂,其 log2 即 bits
prefixLen = 32 - bits;
ans.add(longToIp(cur) + "/" + prefixLen);
cur += block;
}
return ans;
}
private long ipToLong(String ip) {
String[] parts = ip.split("\\.");
long v = 0;
for (String p : parts) {
v = (v << 8) | Long.parseLong(p);
}
return v;
}
private String longToIp(long v) {
return ((v >> 24) & 255) + "." + ((v >> 16) & 255) + "."
+ ((v >> 8) & 255) + "." + (v & 255);
}
// 修正版:优先用 (cur & -cur) 与 remain 的最低 2 幂
public List<String> ipToCIDR2(String ip, int n) {
long cur = ipToLong(ip);
long remain = n;
List<String> ans = new ArrayList<>();
while (remain > 0) {
long maxBlock = remain & -remain; // remain 最低位的 1 对应块大小
long curLow = (cur == 0) ? (1L << 31) : (cur & -cur); // cur 后缀 0 决定的对齐块大小
long block = Math.min(maxBlock, curLow);
int bits = 0;
long b = block;
while (b > 1) {
b >>= 1;
++bits;
}
ans.add(longToIp(cur) + "/" + (32 - bits));
cur += block;
remain -= block;
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
List<String> r1 = sol.ipToCIDR2("255.0.0.7", 10);
++total; if (check(r1, "255.0.0.7", 10)) ++pass; else System.out.println("FAIL test1 got " + r1);
List<String> r2 = sol.ipToCIDR2("117.145.102.62", 8);
++total; if (check(r2, "117.145.102.62", 8)) ++pass; else System.out.println("FAIL test2 got " + r2);
List<String> r3 = sol.ipToCIDR2("0.0.0.0", 1);
++total; if (check(r3, "0.0.0.0", 1)) ++pass; else System.out.println("FAIL test3 got " + r3);
if (pass == total) {
System.out.println("== 0751 PASS ==");
} else {
System.out.println("== 0751 FAIL ==");
System.exit(1);
}
}
// 校验每个解析的 CIDR 块逐地址不越界
private static boolean check(List<String> blocks, String ip, int n) {
long cur = new Solution().ipToLong(ip);
long end = cur + n - 1;
for (String b : blocks) {
String[] parts = b.split("/");
long start = new Solution().ipToLong(parts[0]);
int prefix = Integer.parseInt(parts[1]);
int hostBits = 32 - prefix;
long count = 1L << hostBits;
long blockEnd = start + count - 1;
if (start != cur) {
return false;
}
if (blockEnd > end) {
return false;
}
cur = blockEnd + 1;
}
return cur == end + 1;
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(32) | O(32) |
752. Open the Lock(打开转盘锁)
题目描述
你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有 10 个数字:'0' 到 '9'。每个拨轮可以
自由旋转:例如把 '9' 变为 '0','0' 变为 '9'。每次旋转都只能旋转一个拨轮的一位数字。
锁的初始数字为 '0000',一个代表四个拨轮的数字的字符串。列表 deadends 包含了一组死亡数字,
一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。
字符串 target 代表可以解锁的数字,你需要给出解锁需要的最小旋转次数,如果无论如何不能解锁,
返回 -1。
示例 1
- 输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202"
- 输出:6
示例 2
- 输入:deadends = ["8888"], target = "0009"
- 输出:1
示例 3
- 输入:deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888"
- 输出:-1
提示: - 死亡列表
deadends的长度范围为[1, 500] - 目标数字
target不会在deadends之中 - 每个
deadends和target中的字符串的数字会在 10 个'0'到'9'之中产生
解题思路
解法一(BFS 最短路,主提交):把 4 位数字串看作节点,每次旋转改变一位(±1 模 10),共
8 个邻居。从 "0000" 出发 BFS 求到 target 的最短路径(即最少旋转次数)。死亡数字直接
当作不可达节点跳过。
状态数至多 10000(4 位 10 进制),BFS 复杂度 O(10^4 · 8),空间 O(10^4)。
解法二(双向 BFS):从 "0000" 与 target 同时扩展,交替推进,直到两方向访问集合相交。
在大状态空间中更快,此处状态数固定 10^4,双向 BFS 仍可显著减少中间层数量。
代码实现
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashSet;
import java.util.Set;
class Solution {
// 解法一:单向 BFS
public int openLock(String[] deadends, String target) {
Set<String> dead = new HashSet<>(Arrays.asList(deadends));
if (dead.contains("0000")) {
return -1;
}
if (target.equals("0000")) {
return 0;
}
Set<String> seen = new HashSet<>();
seen.add("0000");
Deque<String> q = new ArrayDeque<>();
q.offer("0000");
int steps = 0;
while (!q.isEmpty()) {
int size = q.size();
++steps;
for (int t = 0; t < size; ++t) {
char[] cur = q.poll().toCharArray();
for (int i = 0; i < 4; ++i) {
for (int delta = -1; delta <= 1; delta += 2) {
char[] next = cur.clone();
next[i] = (char) ('0' + (next[i] - '0' + delta + 10) % 10);
String ns = new String(next);
if (ns.equals(target)) {
return steps;
}
if (!dead.contains(ns) && seen.add(ns)) {
q.offer(ns);
}
}
}
}
}
return -1;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
String[] d1 = {"0201", "0101", "0102", "1212", "2002"};
++total; if (sol.openLock(d1, "0202") == 6) ++pass; else System.out.println("FAIL test1 got " + sol.openLock(d1, "0202"));
String[] d2 = {"8888"};
++total; if (sol.openLock(d2, "0009") == 1) ++pass; else System.out.println("FAIL test2 got " + sol.openLock(d2, "0009"));
String[] d3 = {"8887", "8889", "8878", "8898", "8788", "8988", "7888", "9888"};
++total; if (sol.openLock(d3, "8888") == -1) ++pass; else System.out.println("FAIL test3 got " + sol.openLock(d3, "8888"));
String[] d4 = {"0000"};
++total; if (sol.openLock(d4, "8888") == -1) ++pass; else System.out.println("FAIL test4");
++total; if (sol.openLock(new String[] {}, "0000") == 0) ++pass; else System.out.println("FAIL test5 got " + sol.openLock(new String[] {}, "0000"));
if (pass == total) {
System.out.println("== 0752 PASS ==");
} else {
System.out.println("== 0752 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(10^4) | O(10^4) |
| 解法二 | O(10^4) | O(10^4) |
753. Cracking the Safe(破解保险箱)
题目描述
有一个需要密码才能打开的保险箱。密码是 n 位数,密码的每一位都是范围 [0, k - 1] 中的
一个数字。保险箱有一种特殊的密码校验方法,你可以随意输入密码序列,保险箱会自动记住最后
n 位输入,如果匹配,则能够打开保险箱。
例如,正确的密码是 "345",并且你输入的是 "012345"。
在只知道密码位数 n 和范围边界 k 的前提下,找出并返回确保在输入的某个时刻能够打开保险箱
的任一最短密码序列。
示例 1
- 输入:n = 1, k = 2
- 输出:"10"
- 解释:"01" 也能够确保打开保险箱。
示例 2
- 输入:n = 2, k = 2
- 输出:"01100"
提示: 1 <= n <= 41 <= k <= 101 <= k^n <= 4096
解题思路
解法一(Hierholzer 欧拉回路,主提交):把长度为 n-1 的字符串看作节点,每个节点有 k
条出边(接上一位数字组成 n 位密码)。所有 k^(n-1) 个节点构成有向图,每个节点出度 = 入度
= k,必存在欧拉回路。DFS 遍历回路时「入栈记录出边数字」,最后把初始节点补上即可得到最短
序列 k^n + n - 1 长。为保证确定的输出,DFS 从全 '0' 节点开始、按字典序尝试数字。
时间复杂度 O(k^n),空间复杂度 O(k^n)。
解法二(贪心构造反例理解):每次在前缀后尝试追加最大可用数字,失败则回退。等价思路,但
不易保证欧拉回路正确性,一般以法一为准。
代码实现
import java.util.HashSet;
import java.util.Set;
class Solution {
// 解法一:Hierholzer 欧拉回路
private final Set<String> seen = new HashSet<>();
private StringBuilder sb;
public String crackSafe(int n, int k) {
if (n == 1 && k == 1) {
return "0";
}
sb = new StringBuilder();
seen.clear();
String start = repeat('0', n - 1);
dfs(start, k);
sb.append(start); // 补上起点(回路闭环)
return sb.toString();
}
private void dfs(String node, int k) {
for (int i = 0; i < k; ++i) {
String next = node + i;
if (seen.add(next)) {
String sub = next.substring(1);
dfs(sub, k);
sb.append(i); // 入栈记录出边数字(后序)
}
}
}
private static String repeat(char c, int cnt) {
StringBuilder r = new StringBuilder();
for (int i = 0; i < cnt; ++i) {
r.append(c);
}
return r.toString();
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
String r1 = sol.crackSafe(1, 2);
++total; if (r1.equals("10") || r1.equals("01")) ++pass; else System.out.println("FAIL test1 got " + r1);
String r2 = sol.crackSafe(2, 2);
++total; if (r2.length() == 5 && containsAll(r2, 2, 2)) ++pass; else System.out.println("FAIL test2 got " + r2);
String r3 = sol.crackSafe(2, 3);
++total; if (r3.length() == 10 && containsAll(r3, 2, 3)) ++pass; else System.out.println("FAIL test3 got " + r3);
String r4 = sol.crackSafe(3, 2);
++total; if (r4.length() == 10 && containsAll(r4, 3, 2)) ++pass; else System.out.println("FAIL test4 got " + r4);
if (pass == total) {
System.out.println("== 0753 PASS ==");
} else {
System.out.println("== 0753 FAIL ==");
System.exit(1);
}
}
private static boolean containsAll(String s, int n, int k) {
Set<String> all = new HashSet<>();
for (int i = 0; i + n <= s.length(); ++i) {
all.add(s.substring(i, i + n));
}
int total = 1;
for (int i = 0; i < n; ++i) {
total *= k;
}
return all.size() == total;
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(k^n) | O(k^n) |
754. Reach a Number(到达终点数字)
题目描述
在一根无限长的数轴上,你站在 0 的位置,终点在 target 的位置。你可以做 numMoves 次移动:
每次你可以选择向左或向右移动,第 i 次移动(从 i == 1 开始)在选择的方向上走 i 步。
给定整数 target,返回到达目标所需的最小移动次数(即最小 numMoves)。
示例 1
- 输入:target = 2
- 输出:3
- 解释:第一次从 0 到 1,第二次从 1 到 -1,第三次从 -1 到 2。
示例 2
- 输入:target = 3
- 输出:2
- 解释:第一次从 0 到 1,第二次从 1 到 3。
提示: -10^9 <= target <= 10^9target != 0
解题思路
解法一(数学累加 + 奇偶判断,主提交):对称性使 target 取绝对值不影响答案,令
t = abs(target)。前 k 步可以走出的最大位移是 S = k(k+1)/2。若 S < t 继续加大 k;
若 S == t 则答案为 k。若 S > t,需要把某些「向右」的步翻转成「向左」:翻转一步 i 会把
净位移减少 2i(偶数),所以只要 S - t 是偶数,就可以把某一步(或几步之和为 (S-t)/2)
翻转向来实现精确抵达;若 S - t 是奇数,再多走一步使差值变偶。
因此循环直到 S >= t && (S - t) % 2 == 0,返回步数 k。
时间复杂度 O(√t),空间复杂度 O(1)。
解法二(BFS 小范围对照):步数小时用 BFS 验证数学结论,仅作自测。
代码实现
class Solution {
// 解法一:数学
public int reachNumber(int target) {
long t = Math.abs((long) target);
long k = 0, sum = 0;
while (sum < t || ((sum - t) & 1) == 1) {
++k;
sum += k;
}
return (int) k;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.reachNumber(2) == 3) ++pass; else System.out.println("FAIL test1 got " + sol.reachNumber(2));
++total; if (sol.reachNumber(3) == 2) ++pass; else System.out.println("FAIL test2 got " + sol.reachNumber(3));
++total; if (sol.reachNumber(-2) == 3) ++pass; else System.out.println("FAIL test3");
++total; if (sol.reachNumber(1) == 1) ++pass; else System.out.println("FAIL test4 got " + sol.reachNumber(1));
++total; if (sol.reachNumber(4) == 3) ++pass; else System.out.println("FAIL test5 got " + sol.reachNumber(4));
++total; if (sol.reachNumber(6) == 3) ++pass; else System.out.println("FAIL test6 got " + sol.reachNumber(6));
++total; if (sol.reachNumber(1000000000 - 1) > 0) ++pass; else System.out.println("FAIL test7");
if (pass == total) {
System.out.println("== 0754 PASS ==");
} else {
System.out.println("== 0754 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | ||
|---|---|---|---|---|
| 解法一 | O(√ | target | ) | O(1) |
755. Pour Water(倒水)
题目描述
给定一个地形高度图,用一个整数数组 heights 表示,其中 heights[i] 表示索引 i 处的地形
高度。每个位置的宽度为 1。同时给定两个整数 volume 和 k,表示有 volume 单位的水将落
在索引 k 处。水首先落在索引 k 处,并停留在该位置当前的最高高度(地形或已有水)之上。
然后,它会按照以下规则流动:
- 如果水滴向左移动最终会下降,则向左移动;
- 否则,如果水滴向右移动最终会下降,则向右移动;
- 否则,停留在当前位置并堆积。
这里「最终会下降」指的是水滴如果沿该方向移动,最终会到达一个更低的高度位置。「高度」是指
地形高度加上该位置已有的水量。
可以假设数组两侧边界之外是无限高的地形。每一单位水必须完全位于某一个格子中,不会被均匀
分散到多个格子。返回注水完成后的高度数组。
示例 1
- 输入:heights = [2,1,1,2,1,2,2], V = 4, K = 3
- 输出:[2,2,2,3,2,2,2]
提示: 1 <= heights.length <= 1000 <= heights[i] <= 990 <= volume <= 20000 <= k < heights.length
解题思路
解法一(逐滴模拟,主提交):对每一滴水:
- 先向左走:从
k出发,只要heights[i] <= heights[i+1]且heights[i] <= 出发点水高 + 地形,就可以继续向左试探,记录「可以落下的最低点」:向左找位置best,优先选低于当前
起点高度(真正下降)的最左边位置;若没有下降位置,选最低且最左的持平位置。
实现技巧:向左遍历要「尽量走到终点」,条件为「下一个不高于当前」。 - 若向左找到下降点,则落在下降点;否则尝试向右;
- 若都未下降,则落在原处
k(在k处堆高)。
时间复杂度 O(volume · n),空间复杂度 O(1)。
解法二(池水填充视角):把每个局部低洼看作蓄水池,整体水位演进。实现较复杂,不展开。
代码实现
class Solution {
// 解法一:逐滴模拟(两边界视为无限高,所以水滴不会越过边界)
public int[] pourWater(int[] heights, int volume, int k) {
int n = heights.length;
for (int drop = 0; drop < volume; ++drop) {
// 1. 尝试向左
int leftBest = k;
int i = k;
while (i > 0 && heights[i - 1] <= heights[i]) {
--i;
if (heights[i] < heights[k]) {
leftBest = i;
} else if (heights[i] == heights[k] && leftBest == k
&& !(i > 0 && heights[i - 1] < heights[i])) {
leftBest = i; // 持平最低处取最左
}
}
// 简化:直接从左边界确认最低点
leftBest = k;
i = k;
while (i > 0 && heights[i - 1] <= heights[i]) {
--i;
}
// 找到最左可行位置后,再找其中最低(优先下降)的位置
int target = i; // 最左可到达
int lowest = i;
for (int j = i; j <= k; ++j) {
if (heights[j] < heights[lowest]
|| (heights[j] == heights[lowest] && j < lowest)) {
lowest = j;
}
}
if (heights[lowest] < heights[k]) {
++heights[lowest];
continue;
}
// 2. 尝试向右
int r = k;
while (r + 1 < n && heights[r + 1] <= heights[r]) {
++r;
}
int rightLowest = r;
for (int j = r; j >= k; --j) {
if (heights[j] < heights[rightLowest]
|| (heights[j] == heights[rightLowest] && j < rightLowest)) {
rightLowest = j;
}
}
if (heights[rightLowest] < heights[k]) {
++heights[rightLowest];
continue;
}
// 3. 都不能下降:落在初始降水量最少的位置(优先 k,其次左/右最凹处)
++heights[Math.min(leftBest == k ? k : leftBest, k)];
}
return heights;
}
// 修正版:经典逐滴模拟(向左/右探到凹陷盆地,落在平台最右处)
public int[] pourWater2(int[] heights, int volume, int k) {
int n = heights.length;
for (int drop = 0; drop < volume; ++drop) {
int pos = k;
// 尝试向左:走到非递增路径的最左端
while (pos > 0 && heights[pos - 1] <= heights[pos]) {
--pos;
}
// 若形成平台,取平台最右(紧贴 k 一侧的最低平台右端)
while (pos < k && heights[pos + 1] == heights[pos]) {
++pos;
}
if (heights[pos] < heights[k]) {
++heights[pos];
continue;
}
// 尝试向右
pos = k;
while (pos + 1 < n && heights[pos + 1] <= heights[pos]) {
++pos;
}
while (pos > k && heights[pos - 1] == heights[pos]) {
--pos;
}
if (heights[pos] < heights[k]) {
++heights[pos];
continue;
}
// 都无法下降:堆在 k
++heights[k];
}
return heights;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[] r1 = sol.pourWater2(new int[] {2, 1, 1, 2, 1, 2, 2}, 4, 3);
++total; if (java.util.Arrays.equals(r1, new int[] {2, 2, 2, 3, 2, 2, 2})) ++pass; else System.out.println("FAIL test1 got " + java.util.Arrays.toString(r1));
int[] r2 = sol.pourWater2(new int[] {1, 2, 3, 4}, 2, 2);
++total; if (java.util.Arrays.equals(r2, new int[] {2, 3, 3, 4})) ++pass; else System.out.println("FAIL test2 got " + java.util.Arrays.toString(r2));
int[] r3 = sol.pourWater2(new int[] {3, 1, 3}, 5, 1);
++total; if (java.util.Arrays.equals(r3, new int[] {4, 4, 4})) ++pass; else System.out.println("FAIL test3 got " + java.util.Arrays.toString(r3));
if (pass == total) {
System.out.println("== 0755 PASS ==");
} else {
System.out.println("== 0755 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(volume · n) | O(1) |
756. Pyramid Transition Matrix(金字塔转换矩阵)
题目描述
你正在把积木堆成金字塔。每个块都有一个颜色,用一个字母表示。每一行的块比它下面的行少一个块,
并且居中。为了使金字塔美观,只有特定的三角形图案是允许的。一个三角形的图案由两个块和叠在
上面的单个块组成。模式是以三个字母字符串的列表形式 allowed 给出的,其中模式的前两个字符
分别表示左右底部块,第三个字符表示顶部块。
例如,"ABC" 表示一个三角形图案,其中一个 'C' 块堆叠在一个 'A' 块(左)和一个 'B'
块(右)之上。
你从作为单个字符串给出的底部的一排积木 bottom 开始,必须将其作为金字塔的底部。在给定
bottom 和 allowed 的情况下,如果你能一直构建到金字塔顶部,使金字塔中的每个三角形图案
都是在 allowed 中的,则返回 true,否则返回 false。
示例 1
- 输入:bottom = "BCD", allowed = ["BCC","CDE","CEA","FFF"]
- 输出:true
示例 2
- 输入:bottom = "AAAA", allowed = ["AAB","AAC","BCD","BBE","DEF"]
- 输出:false
提示: 2 <= bottom.length <= 80 <= allowed.length <= 200allowed中每个字符串的长度为 3- 所有字母均为大写英文字母
allowed中不存在重复的字符串
解题思路
解法一(回溯 DFS + 记忆化,主提交):用 Map<String, Set<Character>> 记录每个左右块对
可生成的所有顶部块列表。dfs(row) 处理当前层 row:若长度为 1 说明到顶,返回 true;
否则枚举下一层 next(每对相邻块 row[i][i+1] 选一个允许的顶部块组合),若下一层能继续
则返回 true。用 Set<String> 记忆化已失败的状态。
状态数 ≤ 8 层 × 7 个组合,指数级但在 8 长度内完全可接受。
时间复杂度最坏 O(7^len),空间复杂度 O(状态数)。
解法二(迭代构建集合):自底向上维护「当前层所有可能字符串」的集合,逐层收缩。逻辑等价,
用集合代替回溯。
代码实现
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
class Solution {
private Map<String, Set<Character>> rules = new HashMap<>();
private Set<String> memoFail = new HashSet<>();
// 解法一:回溯 DFS + 记忆化
public boolean pyramidTransition(String bottom, List<String> allowed) {
rules.clear();
memoFail.clear();
for (String s : allowed) {
rules.computeIfAbsent(s.substring(0, 2), k -> new HashSet<>()).add(s.charAt(2));
}
return dfs(bottom);
}
private boolean dfs(String row) {
if (row.length() == 1) {
return true;
}
if (memoFail.contains(row)) {
return false;
}
// 检查当前层是否所有相邻对都有规则
for (int i = 0; i + 1 < row.length(); ++i) {
if (!rules.containsKey(row.substring(i, i + 2))) {
memoFail.add(row);
return false;
}
}
StringBuilder sb = new StringBuilder();
if (!buildNext(row, 0, sb)) {
memoFail.add(row);
return false;
}
return true;
}
// 枚举下一层所有可能,遇到可行则提前返回 true
private boolean buildNext(String row, int idx, StringBuilder sb) {
if (idx + 1 == row.length()) {
return dfs(sb.toString());
}
String key = row.substring(idx, idx + 2);
Set<Character> tops = rules.get(key);
if (tops == null) {
return false;
}
for (char c : tops) {
sb.append(c);
if (buildNext(row, idx + 1, sb)) {
return true;
}
sb.deleteCharAt(sb.length() - 1);
}
return false;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.pyramidTransition("BCD", java.util.Arrays.asList("BCC", "CDE", "CEA", "FFF"))) ++pass; else System.out.println("FAIL test1");
++total; if (!sol.pyramidTransition("AAAA", java.util.Arrays.asList("AAB", "AAC", "BCD", "BBE", "DEF"))) ++pass; else System.out.println("FAIL test2");
++total; if (sol.pyramidTransition("A", java.util.Arrays.asList())) ++pass; else System.out.println("FAIL test3");
++total; if (!sol.pyramidTransition("ABC", java.util.Arrays.asList("ABD", "BCE"))) ++pass; else System.out.println("FAIL test4");
if (pass == total) {
System.out.println("== 0756 PASS ==");
} else {
System.out.println("== 0756 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(指数) | O(指数) |
| 解法二 | O(指数) | O(指数) |
757. Set Intersection Size At Least Two(设置交集大小至少为2)
题目描述
给你一个二维整数数组 intervals,其中 intervals[i] = [starti, endi] 表示从 starti
到 endi 的所有整数,包括 starti 和 endi。包含集合是一个名为 nums 的数组,并满足
intervals 中的每个区间都至少有两个整数在 nums 中。返回包含集合可能的最小大小。
示例 1
- 输入:intervals = [[1,3],[3,7],[8,9]]
- 输出:5
- 解释:nums = [2, 3, 4, 8, 9]
示例 2
- 输入:intervals = [[1,3],[1,4],[2,5],[3,5]]
- 输出:3
- 解释:nums = [2, 3, 4]
示例 3
- 输入:intervals = [[1,2],[2,3],[2,4],[4,5]]
- 输出:5
提示: 1 <= intervals.length <= 3000intervals[i].length == 20 <= starti < endi <= 10^8
解题思路
解法一(贪心 + 双计数,主提交):先把区间按右端点升序(右端点相同按左端点降序)排序,然后
维护已选集合 nums 中「最后两个元素」a < b(初始无)。对每个区间 [l, r]:
- 若
b在区间内且a也在区间内,已满足 2 个,跳过; - 若只有
b在区间内(或 ab 都已在区间内但不足 2 个),只需补 1 个,贪心取r - 1或新
的b; - 若一个都不在,需要补 2 个,取
r-1, r。
用「计数当前集合与区间的交集个数」判断即可:统计cnt = (a >= l && a <= r) + (b >= l && b <= r)。
按 cnt 分三种情况更新 a/b。
时间复杂度 O(m·log m),空间复杂度 O(m)。
解法二(动态规划对照):区间端点离散化后求最小点数,能保证正确但繁琐,作为参考思路。
代码实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
// 解法一:贪心 + 维护最后两个已选元素
public int intersectionSizeTwo(int[][] intervals) {
// 按右端点升序;相等时左端点降序(尽量让后面的区间覆盖更多已选点)
Arrays.sort(intervals, (x, y) -> x[1] != y[1] ? x[1] - y[1] : y[0] - x[0]);
int ans = 0;
int a = -1, b = -1; // 已选集合中最后两个元素,a < b
for (int[] iv : intervals) {
int l = iv[0], r = iv[1];
int cnt = 0;
if (a >= l && a <= r) ++cnt;
if (b >= l && b <= r) ++cnt;
if (cnt >= 2) {
continue;
}
if (cnt == 1) {
// 还需补 1 个:优先补 r(r 能覆盖后续尽可能多的区间)
// 新集合末尾两个为 b 和 r(若 b 原本不在区间内,则 a?b 都更新)
if (b >= l && b <= r) {
// b 在区间内,a 不在:补一个 r
a = b;
b = r;
++ans;
} else {
// a 在区间内,b 不在:说明 b 是上一次补在 r'(> 当前 r?不可能,因为排序右端点非降)
// 此时 b > r,按理不会发生(按右端点排序);为健壮,直接补 r
a = b;
b = r;
++ans;
}
} else {
// 一个都不在:补 r-1, r
a = r - 1;
b = r;
ans += 2;
}
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[][] i1 = {{1, 3}, {3, 7}, {8, 9}};
++total; if (sol.intersectionSizeTwo(i1) == 5) ++pass; else System.out.println("FAIL test1 got " + sol.intersectionSizeTwo(i1));
int[][] i2 = {{1, 3}, {1, 4}, {2, 5}, {3, 5}};
++total; if (sol.intersectionSizeTwo(i2) == 3) ++pass; else System.out.println("FAIL test2 got " + sol.intersectionSizeTwo(i2));
int[][] i3 = {{1, 2}, {2, 3}, {2, 4}, {4, 5}};
++total; if (sol.intersectionSizeTwo(i3) == 5) ++pass; else System.out.println("FAIL test3 got " + sol.intersectionSizeTwo(i3));
int[][] i4 = {{0, 1}};
++total; if (sol.intersectionSizeTwo(i4) == 2) ++pass; else System.out.println("FAIL test4 got " + sol.intersectionSizeTwo(i4));
if (pass == total) {
System.out.println("== 0757 PASS ==");
} else {
System.out.println("== 0757 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(m·log m) | O(m) |
758. Bold Words in String(字符串中的加粗单词)
题目描述
给定一个关键词集合 words 和一个字符串 s,将所有 s 中出现的关键词 words[i] 加粗。
所有在标签 <b> 和 </b> 中的字母都会加粗。加粗后返回 s。返回的字符串需要使用尽可能少
的标签,当然标签应形成有效的组合。
示例 1
- 输入:words = ["ab","bc"], s = "aabcd"
- 输出:"aabcd"
- 解释:注意返回 "aabcd" 会使用更多的标签,因此是错误的。
示例 2
- 输入:words = ["ab","cb"], s = "aabcd"
- 输出:"aabcd"
提示: 1 <= s.length <= 5000 <= words.length <= 501 <= words[i].length <= 10s和words[i]由小写英文字母组成
解题思路
解法一(差分标记 + 区间合并输出,主提交):先算出每个位置是否应加粗。把每个关键词在 s 中
出现的区间 [l, r) 用差分数组 +1/-1 标记,再求前缀和,bold[i] = 前缀和 > 0。最后按
bold 的连续段拼出带标签的字符串:段起点前加 <b>,段终点后加 </b>。
时间复杂度 O(|words| · |s| · L),空间复杂度 O(|s|)。
解法二(朴素区间合并):用布尔数组记录加粗位置后,扫描合并。逻辑与法一同构,实现直接。
代码实现
import java.util.HashSet;
import java.util.Set;
class Solution {
// 解法一:差分标记 + 前缀和
public String boldWords(String[] words, String s) {
int n = s.length();
int[] diff = new int[n + 1];
for (String w : words) {
if (w.isEmpty()) {
continue;
}
int idx = s.indexOf(w);
while (idx != -1) {
++diff[idx];
--diff[idx + w.length()];
idx = s.indexOf(w, idx + 1);
}
}
boolean[] bold = new boolean[n];
int run = 0;
for (int i = 0; i < n; ++i) {
run += diff[i];
bold[i] = run > 0;
}
StringBuilder sb = new StringBuilder();
int i = 0;
while (i < n) {
if (bold[i]) {
int j = i;
while (j < n && bold[j]) {
++j;
}
sb.append("<b>").append(s, i, j).append("</b>");
i = j;
} else {
sb.append(s.charAt(i));
++i;
}
}
return sb.toString();
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
String r1 = sol.boldWords(new String[] {"ab", "bc"}, "aabcd");
++total; if (r1.equals("a<b>abc</b>d")) ++pass; else System.out.println("FAIL test1 got " + r1);
String r2 = sol.boldWords(new String[] {"ab", "cb"}, "aabcd");
++total; if (r2.equals("a<b>ab</b>cd")) ++pass; else System.out.println("FAIL test2 got " + r2);
String r3 = sol.boldWords(new String[] {"a", "b"}, "abc");
++total; if (r3.equals("<b>ab</b>c")) ++pass; else System.out.println("FAIL test3 got " + r3);
String r4 = sol.boldWords(new String[] {}, "abc");
++total; if (r4.equals("abc")) ++pass; else System.out.println("FAIL test4 got " + r4);
String r5 = sol.boldWords(new String[] {"aaa"}, "aaab");
++total; if (r5.equals("<b>aaa</b>b")) ++pass; else System.out.println("FAIL test5 got " + r5);
if (pass == total) {
System.out.println("== 0758 PASS ==");
} else {
System.out.println("== 0758 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | ||||
|---|---|---|---|---|---|---|
| 解法一 | O(50· | s | ·L) | O( | s | ) |
759. Employee Free Time(员工空闲时间)
题目描述
给定员工的 schedule 列表,表示每个员工的工作时间。每个员工都有一个非重叠的时间段
Intervals 列表,这些时间段已经排好序。返回表示所有员工的共同、正数长度的空闲时间的有限
时间段列表,同样需要排好序。
例如,schedule = [[[1,2],[5,6]],[[1,3]],[[4,10]]] 的共同空闲时间为 [[3,4]]。
示例 1
- 输入:schedule = [[[1,2],[5,6]],[[1,3]],[[4,10]]]
- 输出:[[3,4]]
示例 2
- 输入:schedule = [[[1,3],[6,7]],[[2,4]],[[2,5],[9,12]]]
- 输出:[[5,6],[7,9]]
提示: 1 <= schedule.length, schedule[i].length <= 500 <= interval.start < interval.end <= 10^8- 每个员工的工作时间段互不重叠且已排序
解题思路
解法一(合并所有工作时间段 + 找空隙,主提交):把所有员工的工作区间收集到一个列表,按
start 升序(相同按 end 升序)排序后线性合并,得到所有员工「至少一人在工作」的合并区间
列表 merged。相邻两个合并区间 [A.end, B.start] 之间若 start < end 即为共同空闲区间。
时间复杂度 O(T·log T),空间复杂度 O(T),T 为总区间数。
解法二(最小堆多路归并):模拟时间线推进,用最小堆按 start 取最早结束的工作区间,维护
当前最晚结束时间,遇到空隙即空闲段。实现略繁,逻辑等价。
代码实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
class Solution {
// 力扣该题提供的 Interval 结构
private static class Interval {
int start;
int end;
Interval() { start = 0; end = 0; }
Interval(int s, int e) { start = s; end = e; }
}
// 解法一:合并所有工作区间 + 找空隙
public List<Interval> employeeFreeTime(List<List<Interval>> schedule) {
List<Interval> all = new ArrayList<>();
for (List<Interval> emp : schedule) {
all.addAll(emp);
}
all.sort((a, b) -> a.start != b.start ? a.start - b.start : a.end - b.end);
List<Interval> merged = new ArrayList<>();
for (Interval iv : all) {
if (merged.isEmpty() || iv.start > merged.get(merged.size() - 1).end) {
merged.add(iv);
} else {
Interval last = merged.get(merged.size() - 1);
last.end = Math.max(last.end, iv.end);
}
}
List<Interval> ans = new ArrayList<>();
for (int i = 1; i < merged.size(); ++i) {
int s = merged.get(i - 1).end;
int e = merged.get(i).start;
if (s < e) {
ans.add(new Interval(s, e));
}
}
return ans;
}
// ===== 本地测试 =====
private static List<Interval> mk(int[][] arr) {
List<Interval> list = new ArrayList<>();
for (int[] a : arr) {
list.add(new Interval(a[0], a[1]));
}
return list;
}
private static String fmt(List<Interval> list) {
StringBuilder sb = new StringBuilder("[");
for (Interval iv : list) {
sb.append("[").append(iv.start).append(",").append(iv.end).append("],");
}
if (sb.length() > 1) sb.setLength(sb.length() - 1);
sb.append("]");
return sb.toString();
}
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
List<List<Interval>> s1 = new ArrayList<>();
s1.add(mk(new int[][] {{1, 2}, {5, 6}}));
s1.add(mk(new int[][] {{1, 3}}));
s1.add(mk(new int[][] {{4, 10}}));
List<Interval> r1 = sol.employeeFreeTime(s1);
++total; if (fmt(r1).equals("[[3,4]]")) ++pass; else System.out.println("FAIL test1 got " + fmt(r1));
List<List<Interval>> s2 = new ArrayList<>();
s2.add(mk(new int[][] {{1, 3}, {6, 7}}));
s2.add(mk(new int[][] {{2, 4}}));
s2.add(mk(new int[][] {{2, 5}, {9, 12}}));
List<Interval> r2 = sol.employeeFreeTime(s2);
++total; if (fmt(r2).equals("[[5,6],[7,9]]")) ++pass; else System.out.println("FAIL test2 got " + fmt(r2));
if (pass == total) {
System.out.println("== 0759 PASS ==");
} else {
System.out.println("== 0759 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(T·log T) | O(T) |
| 解法二 | O(T·log K) | O(K) |
760. Find Anagram Mappings(找出变位映射)
题目描述
给你两个整数数组 nums1 和 nums2,其中 nums2 是 nums1 的一个变位词。两个数组都
可能包含重复元素。返回一个下标映射数组 mapping,它将 nums1 映射到 nums2,其中
mapping[i] = j 表示 nums1 中的第 i 个元素出现在 nums2 的第 j 个下标上。如果有
多个答案,返回任意一个。
数组 a 是数组 b 的一个变位词意味着 b 是通过将 a 中元素的顺序随机打乱生成的。
示例 1
- 输入:nums1 = [12,28,46,32,50], nums2 = [50,12,32,46,28]
- 输出:[1,4,3,2,0]
示例 2
- 输入:nums1 = [84,46], nums2 = [84,46]
- 输出:[0,1]
提示: 1 <= nums1.length <= 100nums2.length == nums1.length0 <= nums1[i], nums2[i] <= 10^5nums2是nums1的一个变位词
解题思路
解法一(值 -> 下标列表 + 队列消费,主提交):由于存在重复元素,用 Map<Integer, Deque>
记录 nums2 中每个值出现的所有下标(从前到后)。遍历 nums1,对每个值弹出其下一个未使用
的下标作为映射。
时间复杂度 O(n),空间复杂度 O(n)。
解法二(值 -> 下标计数游标):给 nums2 每个值记录「当前消费到第几个下标」,用
Map<Integer, int[]> 或两个独立计数器配合列表实现。逻辑等价。
代码实现
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
class Solution {
// 解法一:Map<值, 下标队列>
public int[] anagramMappings(int[] nums1, int[] nums2) {
Map<Integer, java.util.Deque<Integer>> pos = new HashMap<>();
for (int j = 0; j < nums2.length; ++j) {
pos.computeIfAbsent(nums2[j], k -> new java.util.ArrayDeque<>()).offer(j);
}
int[] ans = new int[nums1.length];
for (int i = 0; i < nums1.length; ++i) {
ans[i] = pos.get(nums1[i]).poll();
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[] n1 = {12, 28, 46, 32, 50};
int[] n2 = {50, 12, 32, 46, 28};
int[] r1 = sol.anagramMappings(n1, n2);
++total; if (Arrays.equals(r1, new int[] {1, 4, 3, 2, 0})) ++pass; else System.out.println("FAIL test1 got " + Arrays.toString(r1));
int[] r2 = sol.anagramMappings(new int[] {84, 46}, new int[] {84, 46});
++total; if (Arrays.equals(r2, new int[] {0, 1})) ++pass; else System.out.println("FAIL test2 got " + Arrays.toString(r2));
int[] r3 = sol.anagramMappings(new int[] {1, 1, 2}, new int[] {2, 1, 1});
// 校验映射合法性:nums2[r3[i]] == nums1[i]
boolean ok = r3.length == 3;
for (int i = 0; i < 3 && ok; ++i) {
if (n2.length == 3) ok = false; // 用独立数组校验
}
int[] nn1 = {1, 1, 2}, nn2 = {2, 1, 1};
ok = r3.length == 3;
for (int i = 0; i < 3 && ok; ++i) {
if (nn2[r3[i]] != nn1[i]) {
ok = false;
}
}
++total; if (ok) ++pass; else System.out.println("FAIL test3 got " + Arrays.toString(r3));
if (pass == total) {
System.out.println("== 0760 PASS ==");
} else {
System.out.println("== 0760 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(n) |
| 解法二 | O(n) | O(n) |
761. Special Binary String(特殊的二进制字符串)
题目描述
特殊的二进制字符串是具有以下两个性质的二进制序列:
0的数量与1的数量相等;- 二进制序列的每一个前缀中
1的数量要大于等于0的数量。
给定一个特殊的二进制字符串s。一次移动操作包括选择字符串s中的两个连续的、非空的、
特殊子串,并交换它们。返回在字符串上应用任意次操作后可能得到的字典序最大的字符串。
示例 1
- 输入:S = "11011000"
- 输出:"11100100"
示例 2
- 输入:s = "10"
- 输出:"10"
提示: 1 <= s.length <= 50
解题思路
解法一(递归分治 + 排序拼接,主提交):任何特殊二进制串都形如 "1" + A + "0",其中 A
是若干特殊子串的拼接。一次移动可以交换两个相邻的特殊子串,因此同一层级内可以通过冒泡交换
把子串按字典序降序排列。递归处理每一层,最后返回 "1" + (排序后的内层) + "0"。
实现:用计数器扫描,遇到 '1' 加 1、遇到 '0' 减 1,当归零时就把这一整段递归处理。
时间复杂度 O(n²)(每层排序),空间复杂度 O(n)。
解法二(暴力交换最大):直接模拟所有相邻特殊子串交换找最大值,状态较多,仅作理论参考。
代码实现
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
class Solution {
// 解法一:递归分治
public String makeLargestSpecial(String s) {
if (s.length() <= 2) {
return s;
}
int n = s.length();
List<String> parts = new ArrayList<>();
int cnt = 0, start = 0;
for (int i = 0; i < n; ++i) {
cnt += s.charAt(i) == '1' ? 1 : -1;
if (cnt == 0) {
// 完整特殊子串 [start, i]
parts.add("1" + makeLargestSpecial(s.substring(start + 1, i)) + "0");
start = i + 1;
}
}
parts.sort(Collections.reverseOrder());
StringBuilder sb = new StringBuilder();
for (String p : parts) {
sb.append(p);
}
return sb.toString();
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.makeLargestSpecial("11011000").equals("11100100")) ++pass; else System.out.println("FAIL test1 got " + sol.makeLargestSpecial("11011000"));
++total; if (sol.makeLargestSpecial("10").equals("10")) ++pass; else System.out.println("FAIL test2 got " + sol.makeLargestSpecial("10"));
++total; if (sol.makeLargestSpecial("101100").equals("110010")) ++pass; else System.out.println("FAIL test3 got " + sol.makeLargestSpecial("101100"));
++total; if (sol.makeLargestSpecial("1").equals("1")) ++pass; else System.out.println("FAIL test4");
if (pass == total) {
System.out.println("== 0761 PASS ==");
} else {
System.out.println("== 0761 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n²) | O(n) |
762. Prime Number of Set Bits in Binary Representation
题目描述
给你两个整数 left 和 right,在闭区间 [left, right] 范围内,统计并返回「计算置位位数
为质数」的整数个数。计算置位位数就是二进制表示中 1 的个数。
例如,21 的二进制表示 10101 有 3 个计算置位。
示例 1
- 输入:left = 6, right = 10
- 输出:4
- 解释:6(110, 2 个)、7(111, 3 个)、9(1001, 2 个)、10(1010, 2 个),共 4 个。
示例 2
- 输入:left = 10, right = 15
- 输出:5
提示: 1 <= left <= right <= 10^60 <= right - left <= 10^4
解题思路
解法一(位计数 + 质数表,主提交):对区间内每个数用 Integer.bitCount 求置位个数。因为
right <= 10^6 < 2^20,置位个数最多 20,用布尔数组预生成 [0, 20] 的质数标记,查表判断。
时间复杂度 O((right - left) · 1),空间复杂度 O(1)。
解法二(手写 lowbit 计数):不用 Integer.bitCount,用 n &= n - 1 循环数 1,逻辑等价,
便于教学。
代码实现
class Solution {
// 解法一:bitCount + 质数表
public int countPrimeSetBits(int left, int right) {
boolean[] prime = new boolean[21];
for (int x : new int[] {2, 3, 5, 7, 11, 13, 17, 19}) {
prime[x] = true;
}
int ans = 0;
for (int v = left; v <= right; ++v) {
if (prime[Integer.bitCount(v)]) {
++ans;
}
}
return ans;
}
// 解法二:手工 lowbit 计数
public int countPrimeSetBits2(int left, int right) {
boolean[] prime = new boolean[21];
for (int x : new int[] {2, 3, 5, 7, 11, 13, 17, 19}) {
prime[x] = true;
}
int ans = 0;
for (int v = left; v <= right; ++v) {
int cnt = 0, x = v;
while (x != 0) {
x &= x - 1;
++cnt;
}
if (prime[cnt]) {
++ans;
}
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.countPrimeSetBits(6, 10) == 4) ++pass; else System.out.println("FAIL test1 got " + sol.countPrimeSetBits(6, 10));
++total; if (sol.countPrimeSetBits(10, 15) == 5) ++pass; else System.out.println("FAIL test2 got " + sol.countPrimeSetBits(10, 15));
++total; if (sol.countPrimeSetBits2(6, 10) == 4) ++pass; else System.out.println("FAIL test3");
++total; if (sol.countPrimeSetBits2(10, 15) == 5) ++pass; else System.out.println("FAIL test4");
++total; if (sol.countPrimeSetBits(1, 1) == 0) ++pass; else System.out.println("FAIL test5");
if (pass == total) {
System.out.println("== 0762 PASS ==");
} else {
System.out.println("== 0762 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(区间长度) | O(1) |
| 解法二 | O(区间长度 · log val) | O(1) |
763. Partition Labels(划分字母区间)
题目描述
给你一个字符串 s。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
例如,字符串 "ababcc" 能够被分为 ["abab", "cc"],但类似 ["aba", "bcc"] 的划分是
非法的。注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s。
返回一个表示每个字符串片段的长度的列表。
示例 1
- 输入:s = "ababcbacadefegdehijhklij"
- 输出:[9,7,8]
- 解释:划分结果为 "ababcbaca"、"defegde"、"hijhklij",每个字母最多出现在一个片段中。
示例 2
- 输入:s = "eccbbbbdec"
- 输出:[10]
提示: 1 <= s.length <= 500s仅由小写英文字母组成
解题思路
解法一(贪心 + 最右位置,主提交):先扫描一遍记录每个字母最后一次出现的下标 last[26]。
再从左往右扫描,维护当前片段的起始 start 和「当前片段内所有字母的最右出现位置」end。
当 i == end 时说明当前片段可以结束,长度 end - start + 1 加入答案,start = i + 1。
贪心正确性:每个片段必须延伸至其中任一字母的最后出现位置,因此在 end 处切分是最早的合法
切分点,保证片段数最多。
时间复杂度 O(n),空间复杂度 O(1)。
解法二(区间合并视角):把每个字母首次/末次出现视为区间,等价于合并重叠区间后取连通块。
代码实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
// 解法一:贪心 + 最右位置
public List<Integer> partitionLabels(String s) {
int[] last = new int[26];
int n = s.length();
for (int i = 0; i < n; ++i) {
last[s.charAt(i) - 'a'] = i;
}
List<Integer> ans = new ArrayList<>();
int start = 0, end = 0;
for (int i = 0; i < n; ++i) {
end = Math.max(end, last[s.charAt(i) - 'a']);
if (i == end) {
ans.add(end - start + 1);
start = i + 1;
}
}
return ans;
}
// 解法二:区间合并视角
public List<Integer> partitionLabels2(String s) {
int[][] seg = new int[26][2];
for (int[] r : seg) {
r[0] = Integer.MAX_VALUE;
r[1] = -1;
}
for (int i = 0; i < s.length(); ++i) {
int c = s.charAt(i) - 'a';
seg[c][0] = Math.min(seg[c][0], i);
seg[c][1] = Math.max(seg[c][1], i);
}
List<int[]> intervals = new ArrayList<>();
for (int[] r : seg) {
if (r[0] <= r[1]) {
intervals.add(r);
}
}
intervals.sort((a, b) -> a[0] - b[0]);
List<Integer> ans = new ArrayList<>();
int curL = intervals.get(0)[0], curR = intervals.get(0)[1];
for (int i = 1; i < intervals.size(); ++i) {
int[] r = intervals.get(i);
if (r[0] < curR) {
curR = Math.max(curR, r[1]);
} else {
ans.add(curR - curL + 1);
curL = r[0];
curR = r[1];
}
}
ans.add(curR - curL + 1);
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
List<Integer> r1 = sol.partitionLabels("ababcbacadefegdehijhklij");
++total; if (r1.equals(Arrays.asList(9, 7, 8))) ++pass; else System.out.println("FAIL test1 got " + r1);
List<Integer> r2 = sol.partitionLabels("eccbbbbdec");
++total; if (r2.equals(Arrays.asList(10))) ++pass; else System.out.println("FAIL test2 got " + r2);
List<Integer> r3 = sol.partitionLabels2("ababcbacadefegdehijhklij");
++total; if (r3.equals(Arrays.asList(9, 7, 8))) ++pass; else System.out.println("FAIL test3 got " + r3);
List<Integer> r4 = sol.partitionLabels("caedbdedda");
++total; if (r4.equals(Arrays.asList(1, 9))) ++pass; else System.out.println("FAIL test4 got " + r4);
if (pass == total) {
System.out.println("== 0763 PASS ==");
} else {
System.out.println("== 0763 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(1) |
| 解法二 | O(n + 26·log26) | O(26) |
764. Largest Plus Sign(最大加号标志)
题目描述
在一个 n x n 的矩阵 grid 中,除了在数组 mines 中给出的元素为 0,其他每个元素都为 1。
mines[i] = [xi, yi] 表示 grid[xi][yi] == 0。返回 grid 中包含 1 的最大的轴对齐加号
标志的阶数。如果未找到加号标志,则返回 0。
一个 k 阶由 1 组成的「轴对称」加号标志具有中心网格 grid[r][c] == 1,以及 4 个从中心
向上、向下、向左、向右延伸、长度为 k-1、由 1 组成的臂。注意,只有加号标志的所有网格要求
为 1,别的网格可能为 0 也可能为 1。
示例 1
- 输入:n = 5, mines = [[4, 2]]
- 输出:2
示例 2
- 输入:n = 1, mines = [[0, 0]]
- 输出:0 提示:
1 <= n <= 5001 <= mines.length <= 50000 <= xi, yi < n- 每一对
(xi, yi)都不重复
解题思路
解法一(四方向 DP,主提交):用四个矩阵分别记录每个格子向上、下、左、右连续 1 的最长长度
(不含自身或含自身视写法而定,统一处理边界)。up[i][j] 等可以由邻居递推:
up[i][j] = (grid[i][j] == 1) ? up[i-1][j] + 1 : 0;- 同理 down/left/right。
某格子能作为中心的最大阶数 =
min(up, down, left, right),取全局最大。 时间复杂度 O(n²),空间复杂度 O(n²)。 解法二(每个中心暴力扩展):对每个非雷格子向四个方向数 1 的长度,最坏 O(n³),作对照。
代码实现
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
class Solution {
// 解法一:四方向 DP
public int orderOfLargestPlusSign(int n, int[][] mines) {
int[][] grid = new int[n][n];
for (int[] row : grid) {
Arrays.fill(row, 1);
}
for (int[] m : mines) {
grid[m[0]][m[1]] = 0;
}
int[][] up = new int[n][n];
int[][] down = new int[n][n];
int[][] left = new int[n][n];
int[][] right = new int[n][n];
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == 1) {
up[i][j] = (i > 0 ? up[i - 1][j] : 0) + 1;
left[i][j] = (j > 0 ? left[i][j - 1] : 0) + 1;
}
}
}
for (int i = n - 1; i >= 0; --i) {
for (int j = n - 1; j >= 0; --j) {
if (grid[i][j] == 1) {
down[i][j] = (i + 1 < n ? down[i + 1][j] : 0) + 1;
right[i][j] = (j + 1 < n ? right[i][j + 1] : 0) + 1;
}
}
}
int ans = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
int k = Math.min(Math.min(up[i][j], down[i][j]),
Math.min(left[i][j], right[i][j]));
ans = Math.max(ans, k);
}
}
return ans;
}
// 解法二:中心扩展暴力对照
public int orderOfLargestPlusSign2(int n, int[][] mines) {
int[][] g = new int[n][n];
for (int[] row : g) {
Arrays.fill(row, 1);
}
for (int[] m : mines) {
g[m[0]][m[1]] = 0;
}
int ans = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (g[i][j] == 0) {
continue;
}
int k = 1;
while (i - k >= 0 && i + k < n && j - k >= 0 && j + k < n
&& g[i - k][j] == 1 && g[i + k][j] == 1
&& g[i][j - k] == 1 && g[i][j + k] == 1) {
++k;
}
ans = Math.max(ans, k);
}
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[][] m1 = {{4, 2}};
++total; if (sol.orderOfLargestPlusSign(5, m1) == 2) ++pass; else System.out.println("FAIL test1 got " + sol.orderOfLargestPlusSign(5, m1));
int[][] m2 = {{0, 0}};
++total; if (sol.orderOfLargestPlusSign(1, m2) == 0) ++pass; else System.out.println("FAIL test2");
int[][] m3 = {};
++total; if (sol.orderOfLargestPlusSign(2, m3) == 1) ++pass; else System.out.println("FAIL test3 got " + sol.orderOfLargestPlusSign(2, m3));
++total; if (sol.orderOfLargestPlusSign2(5, m1) == 2) ++pass; else System.out.println("FAIL test4");
if (pass == total) {
System.out.println("== 0764 PASS ==");
} else {
System.out.println("== 0764 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n²) | O(n²) |
| 解法二 | O(n³) | O(n²) |
765. Couples Holding Hands(情侣牵手)
题目描述
n 对情侣坐在连续排列的 2n 个座位上,想要牵到对方的手。人和座位由一个整数数组 row
表示,其中 row[i] 是坐在第 i 个座位上的人的 ID。情侣们按顺序编号,第一对是 (0, 1),
第二对是 (2, 3),以此类推,最后一对是 (2n - 2, 2n - 1)。
返回最少交换座位的次数,以便每对情侣可以并肩坐在一起。每次交换可选择任意两人,让他们站起来
交换座位。
示例 1
- 输入:row = [0,2,1,3]
- 输出:1
示例 2
- 输入:row = [3,2,0,1]
- 输出:0 提示:
2n == row.length2 <= n <= 30n是偶数0 <= row[i] < 2nrow中所有元素均无重复
解题思路
解法一(座位对位置映射 + 贪心交换,主提交):先记录每个人的座位下标 pos[person]。从左到右
每两个座位 (2*i, 2*i+1) 检查是否为情侣:
- 若
row[2i]的伴侣已在旁边则跳过; - 否则找到伴侣所在下标
j = pos[partner],把row[2i+1]与row[j]交换,同时更新pos。每对错位贡献 1 次交换。 时间复杂度 O(n),空间复杂度 O(n)。 解法二(并查集环计数):把「占位错误」建模成置换环,每个大小为 c 的环需要 c-1 次交换, 答案 = Σ(c - 1)。两两凑对视角等价,实现略繁。
代码实现
import java.util.HashMap;
import java.util.Map;
class Solution {
// 解法一:位置映射 + 贪心交换
public int minSwapsCouples(int[] row) {
int n = row.length;
int[] pos = new int[n];
for (int i = 0; i < n; ++i) {
pos[row[i]] = i;
}
int ans = 0;
for (int i = 0; i < n; i += 2) {
int p = row[i];
int partner = (p % 2 == 0) ? p + 1 : p - 1;
if (row[i + 1] == partner) {
continue;
}
// 找到 partner 的位置并交换到 i+1
int j = pos[partner];
int tmp = row[i + 1];
row[i + 1] = partner;
row[j] = tmp;
pos[partner] = i + 1;
pos[tmp] = j;
++ans;
}
return ans;
}
// 解法二:并查集(环)
private int[] parent;
private int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
public int minSwapsCouples2(int[] row) {
int n = row.length / 2;
parent = new int[n];
for (int i = 0; i < n; ++i) {
parent[i] = i;
}
Map<Integer, Integer> seatToCouple = new HashMap<>();
for (int i = 0; i < row.length; ++i) {
seatToCouple.put(row[i], row[i] / 2);
}
for (int i = 0; i < row.length; i += 2) {
int a = seatToCouple.get(row[i]);
int b = seatToCouple.get(row[i + 1]);
int ra = find(a), rb = find(b);
if (ra != rb) {
parent[ra] = rb;
}
}
// 每个连通块需要 size-1 次交换
int[] size = new int[n];
int blocks = 0;
boolean[] vis = new boolean[n];
for (int i = 0; i < n; ++i) {
int r = find(i);
if (size[r] == 0) {
++blocks;
}
++size[r];
}
int ans = 0;
for (int i = 0; i < n; ++i) {
if (size[i] > 0) {
ans += size[i] - 1;
}
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[] r1 = {0, 2, 1, 3};
++total; if (sol.minSwapsCouples(r1) == 1) ++pass; else System.out.println("FAIL test1 got " + sol.minSwapsCouples(r1));
int[] r2 = {3, 2, 0, 1};
++total; if (sol.minSwapsCouples(r2) == 0) ++pass; else System.out.println("FAIL test2");
int[] r3 = {0, 2, 1, 3};
++total; if (sol.minSwapsCouples2(r3) == 1) ++pass; else System.out.println("FAIL test3");
int[] r4 = {5, 4, 2, 6, 3, 1, 0, 7};
++total; if (sol.minSwapsCouples(r4) == 2) ++pass; else System.out.println("FAIL test4 got " + sol.minSwapsCouples(r4));
int[] r5 = {5, 4, 2, 6, 3, 1, 0, 7};
++total; if (sol.minSwapsCouples2(r5) == 2) ++pass; else System.out.println("FAIL test5 got " + sol.minSwapsCouples2(r5));
if (pass == total) {
System.out.println("== 0765 PASS ==");
} else {
System.out.println("== 0765 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(n) |
| 解法二 | O(n·α) | O(n) |
766. Toeplitz Matrix(托普利茨矩阵)
题目描述
给你一个 m x n 的矩阵 matrix。如果这个矩阵是托普利茨矩阵,返回 true;否则返回 false。
如果矩阵上每一条由左上到右下的对角线上的元素都相同,那么这个矩阵是托普利茨矩阵。
示例 1
- 输入:matrix = [[1,2,3,4],[5,1,2,3],[9,5,1,2]]
- 输出:true
- 解释:各条对角线均为 [9]、[5,5]、[1,1,1]、[2,2,2]、[3,3]、[4]。
示例 2
- 输入:matrix = [[1,2],[2,2]]
- 输出:false 提示:
m == matrix.lengthn == matrix[i].length1 <= m, n <= 200 <= matrix[i][j] <= 99
解题思路
解法一(逐元素比较左上方邻居,主提交):托普利茨矩阵等价于每个元素与它左上方的元素相等,
即 matrix[i][j] == matrix[i-1][j-1](对 i >= 1, j >= 1)。一次双重循环检查即可,
每条对角线自然被该条件覆盖。
时间复杂度 O(m·n),空间复杂度 O(1)。
解法二(逐对角线比较):显式枚举每条从首行或首列出发的斜线,逐条比较。逻辑直观,实现略长。
代码实现
class Solution {
// 解法一:比较左上邻居
public boolean isToeplitzMatrix(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
if (matrix[i][j] != matrix[i - 1][j - 1]) {
return false;
}
}
}
return true;
}
// 解法二:逐对角线比较
public boolean isToeplitzMatrix2(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
// 首行出发的对角线
for (int j = 0; j < n; ++j) {
int val = matrix[0][j];
for (int i = 1, c = j + 1; i < m && c < n; ++i, ++c) {
if (matrix[i][c] != val) {
return false;
}
}
}
// 首列出发的对角线
for (int i = 1; i < m; ++i) {
int val = matrix[i][0];
for (int r = i + 1, c = 1; r < m && c < n; ++r, ++c) {
if (matrix[r][c] != val) {
return false;
}
}
}
return true;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[][] m1 = {{1, 2, 3, 4}, {5, 1, 2, 3}, {9, 5, 1, 2}};
++total; if (sol.isToeplitzMatrix(m1)) ++pass; else System.out.println("FAIL test1");
int[][] m2 = {{1, 2}, {2, 2}};
++total; if (!sol.isToeplitzMatrix(m2)) ++pass; else System.out.println("FAIL test2");
++total; if (sol.isToeplitzMatrix2(m1)) ++pass; else System.out.println("FAIL test3");
++total; if (!sol.isToeplitzMatrix2(m2)) ++pass; else System.out.println("FAIL test4");
int[][] m3 = {{1}};
++total; if (sol.isToeplitzMatrix(m3)) ++pass; else System.out.println("FAIL test5");
if (pass == total) {
System.out.println("== 0766 PASS ==");
} else {
System.out.println("== 0766 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(m·n) | O(1) |
| 解法二 | O(m·n) | O(1) |
767. Reorganize String(重构字符串)
题目描述
给定一个字符串 s,检查是否能重新排布其中的字母,使得任意两个相邻字符不相同。返回 s 的
任意可能的重新排列。若不可行,返回空字符串 ""。
示例 1
- 输入:s = "aab"
- 输出:"aba"
示例 2
- 输入:s = "aaab"
- 输出:"" 提示:
1 <= s.length <= 500s只包含小写字母
解题思路
解法一(贪心 + 优先队列,主提交):若某字母出现次数超过 (n + 1) / 2 则必不可能(鸽巢
原理)。否则用大根堆按「出现次数」维护字母,每次取两个最多的字母交替放置,避免相邻相同。
若最后只剩一个字母则补到末尾(其剩余次数必为 1)。
时间复杂度 O(n·log 26) = O(n),空间复杂度 O(26)。
解法二(排序重排):把字符按「先频次、后字母」排序后重新交错摆放;若某字母频次过半也返回空。
思路直观,实现略绕。
代码实现
import java.util.PriorityQueue;
class Solution {
// 解法一:贪心 + 优先队列
public String reorganizeString(String s) {
int[] cnt = new int[26];
for (char c : s.toCharArray()) {
++cnt[c - 'a'];
}
int n = s.length();
for (int c : cnt) {
if (c > (n + 1) / 2) {
return ""; // 无法重排
}
}
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> b[1] - a[1]); // {字母, 次数}
for (int i = 0; i < 26; ++i) {
if (cnt[i] > 0) {
pq.offer(new int[] {i, cnt[i]});
}
}
StringBuilder sb = new StringBuilder();
while (pq.size() >= 2) {
int[] a = pq.poll();
int[] b = pq.poll();
sb.append((char) ('a' + a[0]));
sb.append((char) ('a' + b[0]));
if (--a[1] > 0) {
pq.offer(a);
}
if (--b[1] > 0) {
pq.offer(b);
}
}
if (!pq.isEmpty()) {
sb.append((char) ('a' + pq.poll()[0]));
}
// 二次校验(防御)
String res = sb.toString();
for (int i = 1; i < res.length(); ++i) {
if (res.charAt(i) == res.charAt(i - 1)) {
return "";
}
}
return res;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
String res1 = sol.reorganizeString("aab");
++total; if (res1.equals("aba")) ++pass; else System.out.println("FAIL test1 got " + res1);
++total; if (sol.reorganizeString("aaab").equals("")) ++pass; else System.out.println("FAIL test2 got " + sol.reorganizeString("aaab"));
String res3 = sol.reorganizeString("vvvlo");
++total; if (res3.equals("vlvov") || res3.equals("vovlv") || res3.equals("volvv") == false) ++pass; else System.out.println("FAIL test3 got " + res3);
String res4 = sol.reorganizeString("aaabc");
++total; if (!res4.equals("")) ++pass; else System.out.println("FAIL test4 got " + res4);
if (pass == total) {
System.out.println("== 0767 PASS ==");
} else {
System.out.println("== 0767 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(26) |
| 解法二 | O(n·log n) | O(n) |
768. Max Chunks To Make Sorted II(最多能完成排序的块 II)
题目描述
给你一个整数数组 arr。将 arr 分割成若干块,并将这些块分别进行排序。之后再连接起来,
使得连接的结果和按升序排序后的原数组相同。返回能将数组分成的最多块数。
示例 1
- 输入:arr = [5,4,3,2,1]
- 输出:1
示例 2
- 输入:arr = [2,1,3,4,4]
- 输出:4
- 解释:分成 [2,1]、[3]、[4]、[4] 可以得到最多的块数。 提示:
1 <= arr.length <= 20000 <= arr[i] <= 10^8
解题思路
解法一(前缀最大 ≤ 后缀最小,主提交):位置 i 与 i+1 之间可以作为切分点,当且仅当
arr[0..i] 中的最大值 <= arr[i+1..n-1] 中的最小值。预处理后缀最小值数组 suffixMin,
一遍扫描计数。
时间复杂度 O(n),空间复杂度 O(n)。
解法二(单调栈):维护一个单调非减栈,栈内元素代表每个块排序后的最大值;当新元素小于栈顶
时,需要把前面的块合并。代码简练,但理解门槛略高。
代码实现
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
class Solution {
// 解法一:前缀最大 <= 后缀最小
public int maxChunksToSorted(int[] arr) {
int n = arr.length;
int[] suffixMin = new int[n + 1];
suffixMin[n] = Integer.MAX_VALUE;
for (int i = n - 1; i >= 0; --i) {
suffixMin[i] = Math.min(arr[i], suffixMin[i + 1]);
}
int ans = 0, curMax = -1;
for (int i = 0; i < n; ++i) {
curMax = Math.max(curMax, arr[i]);
if (curMax <= suffixMin[i + 1]) {
++ans; // 可以在此处切分
}
}
return ans;
}
// 解法二:单调栈(栈底为块的最大值)
public int maxChunksToSorted2(int[] arr) {
Deque<Integer> stk = new ArrayDeque<>();
for (int v : arr) {
if (stk.isEmpty() || v >= stk.peek()) {
stk.push(v);
} else {
int mx = stk.pop();
while (!stk.isEmpty() && v < stk.peek()) {
stk.pop();
}
stk.push(mx); // 合并后的块最大值保持为原先的 mx
}
}
return stk.size();
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[] a1 = {5, 4, 3, 2, 1};
++total; if (sol.maxChunksToSorted(a1) == 1) ++pass; else System.out.println("FAIL test1 got " + sol.maxChunksToSorted(a1));
int[] a2 = {2, 1, 3, 4, 4};
++total; if (sol.maxChunksToSorted(a2) == 4) ++pass; else System.out.println("FAIL test2 got " + sol.maxChunksToSorted(a2));
++total; if (sol.maxChunksToSorted2(a1) == 1) ++pass; else System.out.println("FAIL test3 got " + sol.maxChunksToSorted2(a1));
++total; if (sol.maxChunksToSorted2(a2) == 4) ++pass; else System.out.println("FAIL test4");
int[] a5 = {1, 1, 0, 0, 1};
++total; if (sol.maxChunksToSorted(a5) == 2) ++pass; else System.out.println("FAIL test5 got " + sol.maxChunksToSorted(a5));
if (pass == total) {
System.out.println("== 0768 PASS ==");
} else {
System.out.println("== 0768 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(n) |
| 解法二 | O(n) | O(n) |
769. Max Chunks To Make Sorted(最多能完成排序的块)
题目描述
给定一个长度为 n 的整数数组 arr,它表示在 [0, n - 1] 范围内的整数的排列(每个整数
恰好出现一次)。我们将 arr 分割成若干块(即分区),并对每个块单独排序。将它们连接起来后,
使得连接的结果和按升序排序后的原数组相同。返回数组能分成的最多块数量。
示例 1
- 输入:arr = [4,3,2,1,0]
- 输出:1
示例 2
- 输入:arr = [1,0,2,3,4]
- 输出:4
- 解释:分成 [1, 0], [2], [3], [4] 可以得到最多的块数,排序后连接为 [0, 1, 2, 3, 4]。 提示:
n == arr.length1 <= n <= 100 <= arr[i] < narr中每个元素都不同
解题思路
解法一(前缀最大值,主提交):因为 arr 是 [0, n-1] 的排列,若位置 i 处能成为块的右
边界,则该块内元素的最大值必须恰好等于 i(这样块排序后恰好是 [left..i] 的连续整数)。
从左往右维护前缀最大值 curMax,每当 curMax == i 时 ans++。
时间复杂度 O(n),空间复杂度 O(1)。
解法二(分治交换验证):模拟「每组块排序后连接」的过程,枚举切分点并验证,最坏 O(n²),
作对照参考。
代码实现
import java.util.ArrayList;
import java.util.List;
class Solution {
// 解法一:前缀最大值
public int maxChunksToSorted(int[] arr) {
int ans = 0, curMax = 0;
for (int i = 0; i < arr.length; ++i) {
curMax = Math.max(curMax, arr[i]);
if (curMax == i) {
++ans; // 前 i+1 个元素的集合恰为 [0..i]
}
}
return ans;
}
// 解法二:朴素枚举验证(对照组)
public int maxChunksToSorted2(int[] arr) {
int n = arr.length, ans = 1;
for (int mask = 0; mask < (1 << (n - 1)); ++mask) {
// 用位掩码枚举切分点,验证每个块最小值/最大值排序拼接后整体有序
int prevMax = -1, ok = 1;
for (int i = 0; i < n; ) {
int j = i, mx = -1, mn = n + 1;
int m = mask;
while (true) {
mx = Math.max(mx, arr[j]);
mn = Math.min(mn, arr[j]);
boolean cut = (j == n - 1) || ((m >> j) & 1) == 1;
++j;
if (cut) {
break;
}
}
if (mn < prevMax) {
ok = 0;
break;
}
// 块内元素应构成连续区间 [mn, mx] 且长度 = mx - mn + 1 = j - i
if (mx - mn + 1 != j - i) {
ok = 0;
break;
}
prevMax = mx;
i = j;
}
if (ok == 1) {
ans = Math.max(ans, Integer.bitCount(mask) + 1);
}
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[] a1 = {4, 3, 2, 1, 0};
++total; if (sol.maxChunksToSorted(a1) == 1) ++pass; else System.out.println("FAIL test1 got " + sol.maxChunksToSorted(a1));
int[] a2 = {1, 0, 2, 3, 4};
++total; if (sol.maxChunksToSorted(a2) == 4) ++pass; else System.out.println("FAIL test2 got " + sol.maxChunksToSorted(a2));
++total; if (sol.maxChunksToSorted2(a1) == 1) ++pass; else System.out.println("FAIL test3");
++total; if (sol.maxChunksToSorted2(a2) == 4) ++pass; else System.out.println("FAIL test4");
int[] a3 = {0};
++total; if (sol.maxChunksToSorted(a3) == 1) ++pass; else System.out.println("FAIL test5");
int[] a4 = {1, 2, 0, 3};
++total; if (sol.maxChunksToSorted(a4) == 2) ++pass; else System.out.println("FAIL test6 got " + sol.maxChunksToSorted(a4));
if (pass == total) {
System.out.println("== 0769 PASS ==");
} else {
System.out.println("== 0769 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(1) |
770. Basic Calculator IV(基本计算器 IV)
题目描述
给定一个表达式如 expression = "e + 8 - a + 5" 和一个求值映射,如 {"e": 1}(给定的形式
为 evalvars = ["e"] 和 evalints = [1]),返回表示简化表达式的标记列表,例如
["-1*a","14"]。
- 表达式交替使用块和符号,每个块和符号之间有一个空格;
- 块要么是括号中的表达式,要么是变量,要么是非负整数;
- 变量是一个由小写字母组成的字符串(不包括数字)。变量可以有多个字母,且从不具有前导系数 或一元运算符;
- 表达式按通常顺序求值:先算括号,然后乘法,再算加法和减法;
- 输出格式:对于系数非零的每个自变量项,按字典序将自变量写在一个项中(如
a*b*c),项中 变量写在一起;项的次数等于变量个数;按次数降序、字典序升序排序输出;前三系数直接放在 左边,用星号分隔(前导系数 1 仍要打印);常数项输出为纯数字。
示例 1
- 输入:expression = "e + 8 - a + 5", evalvars = ["e"], evalints = [1]
- 输出:["-1*a","14"]
示例 2
- 输入:expression = "e - 8 + temperature - pressure", evalvars = ["e","temperature"], evalints = [1,12]
- 输出:["-1*pressure","5"]
示例 3
- 输入:expression = "(e + 8) * (e - 8)", evalvars = [], evalints = []
- 输出:["1ee","-64"] 提示:
- 表达式长度不超过 200
- 表达式只包含小写字母、数字、
+、-、*、()和空格 - 表达式中变量/数字个数不超过 100
- 单独变量的长度不超过 20
解题思路
解法一(多项式表示 + 递归下降,主提交):把每个表达式看作「多项式」——各项记为
Map<List<String>, Integer>,键为按字典序排好的变量列表(如 {"a","a","b"}),值为系数。
用递归下降或栈解析:
expr()处理加减:带符号合并多项式;term()处理乘除(此处只有乘):多项式乘法 = 键列表合并 + 系数相乘;factor()处理数字 / 变量(用 eval 映射替换为数字)/(expr)。 加减时变量列表为空的项就是常数项。最后把所有系数 != 0的项按「次数降序、字典序升序」 排序输出:常数项排最后,k*a*a、14等格式按题意拼接。 时间复杂度 O(项数²) 量级(多项式膨胀),表达式中元素 <= 100,可接受。 解法二(迭代双栈 + 多项式栈):用双栈模拟,逻辑等价,代码更工程化。
代码实现
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
class Solution {
private final Map<String, Integer> varmap = new HashMap<>();
private List<String> tokens;
private int pos;
public List<String> basicCalculatorIV(String expression, String[] evalvars, int[] evalints) {
varmap.clear();
for (int i = 0; i < evalvars.length; ++i) {
varmap.put(evalvars[i], evalints[i]);
}
// 分词:把 "(", ")", "+", "-", "*", 数字, 变量 拆为 token
tokens = new ArrayList<>();
String e = expression.replace(" ", "");
int n = e.length();
for (int i = 0; i < n; ) {
char c = e.charAt(i);
if (c == '(' || c == ')' || c == '+' || c == '-' || c == '*') {
tokens.add(String.valueOf(c));
++i;
} else {
int j = i;
while (j < n && Character.isLetterOrDigit(e.charAt(j))) {
++j;
}
tokens.add(e.substring(i, j));
i = j;
}
}
pos = 0;
Map<List<String>, Integer> poly = expr();
// 收集结果
List<String> ans = new ArrayList<>();
List<Map.Entry<List<String>, Integer>> items = new ArrayList<>();
for (Map.Entry<List<String>, Integer> en : poly.entrySet()) {
if (en.getValue() != 0) {
items.add(en);
}
}
// 常量项先摘出
List<Map.Entry<List<String>, Integer>> constItems = new ArrayList<>();
List<Map.Entry<List<String>, Integer>> varItems = new ArrayList<>();
for (Map.Entry<List<String>, Integer> en : items) {
if (en.getKey().isEmpty()) {
constItems.add(en);
} else {
varItems.add(en);
}
}
varItems.sort((a, b) -> {
int d = b.getKey().size() - a.getKey().size(); // 次数降序
if (d != 0) return d;
for (int i = 0; i < a.getKey().size(); ++i) {
int cmp = a.getKey().get(i).compareTo(b.getKey().get(i));
if (cmp != 0) return cmp;
}
return 0;
});
for (Map.Entry<List<String>, Integer> en : varItems) {
int coeff = en.getValue();
StringBuilder sb = new StringBuilder();
sb.append(coeff);
for (String v : en.getKey()) {
sb.append('*').append(v);
}
ans.add(sb.toString());
}
for (Map.Entry<List<String>, Integer> en : constItems) {
ans.add(String.valueOf(en.getValue()));
}
if (ans.isEmpty()) {
ans.add("0");
}
return ans;
}
private Map<List<String>, Integer> expr() {
Map<List<String>, Integer> res = term();
while (pos < tokens.size() && (tokens.get(pos).equals("+") || tokens.get(pos).equals("-"))) {
String op = tokens.get(pos++);
Map<List<String>, Integer> next = term();
for (Map.Entry<List<String>, Integer> en : next.entrySet()) {
int sign = op.equals("-") ? -1 : 1;
res.merge(en.getKey(), sign * en.getValue(), (a, b) -> a + b);
}
}
return res;
}
private Map<List<String>, Integer> term() {
Map<List<String>, Integer> res = factor();
while (pos < tokens.size() && tokens.get(pos).equals("*")) {
++pos;
Map<List<String>, Integer> r2 = factor();
res = multiply(res, r2);
}
return res;
}
private Map<List<String>, Integer> factor() {
String tok = tokens.get(pos++);
if (tok.equals("(")) {
Map<List<String>, Integer> inner = expr();
// 跳过右括号
while (pos < tokens.size() && !tokens.get(pos).equals(")")) {
++pos;
}
if (pos < tokens.size() && tokens.get(pos).equals(")")) {
++pos;
}
return inner;
}
if (Character.isDigit(tok.charAt(0))) {
int v = Integer.parseInt(tok);
Map<List<String>, Integer> m = new HashMap<>();
m.put(new ArrayList<>(), v);
return m;
}
// 变量:替换为数值或保留为单项式
Integer val = varmap.get(tok);
if (val != null) {
Map<List<String>, Integer> m = new HashMap<>();
m.put(new ArrayList<>(), val);
return m;
}
List<String> vars = new ArrayList<>();
vars.add(tok);
Map<List<String>, Integer> m = new HashMap<>();
m.put(vars, 1);
return m;
}
private Map<List<String>, Integer> multiply(Map<List<String>, Integer> a, Map<List<String>, Integer> b) {
Map<List<String>, Integer> res = new HashMap<>();
for (Map.Entry<List<String>, Integer> ea : a.entrySet()) {
for (Map.Entry<List<String>, Integer> eb : b.entrySet()) {
List<String> vars = new ArrayList<>(ea.getKey());
vars.addAll(eb.getKey());
vars.sort(String::compareTo);
res.merge(vars, ea.getValue() * eb.getValue(), (x, y) -> x + y);
}
}
return res;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
List<String> r1 = sol.basicCalculatorIV("e + 8 - a + 5", new String[] {"e"}, new int[] {1});
++total; if (r1.equals(java.util.Arrays.asList("-1*a", "14"))) ++pass; else System.out.println("FAIL test1 got " + r1);
List<String> r2 = sol.basicCalculatorIV("e - 8 + temperature - pressure", new String[] {"e", "temperature"}, new int[] {1, 12});
++total; if (r2.equals(java.util.Arrays.asList("-1*pressure", "5"))) ++pass; else System.out.println("FAIL test2 got " + r2);
List<String> r3 = sol.basicCalculatorIV("(e + 8) * (e - 8)", new String[] {}, new int[] {});
++total; if (r3.equals(java.util.Arrays.asList("1*e*e", "-64"))) ++pass; else System.out.println("FAIL test3 got " + r3);
List<String> r4 = sol.basicCalculatorIV("a * b * c + b * a * c * 4", new String[] {}, new int[] {});
++total; if (r4.equals(java.util.Arrays.asList("5*a*b*c"))) ++pass; else System.out.println("FAIL test4 got " + r4);
List<String> r5 = sol.basicCalculatorIV("7 - 7", new String[] {}, new int[] {});
++total; if (r5.equals(java.util.Arrays.asList("0"))) ++pass; else System.out.println("FAIL test5 got " + r5);
if (pass == total) {
System.out.println("== 0770 PASS ==");
} else {
System.out.println("== 0770 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(P²) | O(P) |
771. Jewels and Stones(宝石与石头)
题目描述
给你一个字符串 jewels 代表石头中宝石的类型,另有一个字符串 stones 代表你拥有的石头。
stones 中每个字符代表了一种你拥有的石头的类型,你想知道你拥有的石头中有多少是宝石。
字母区分大小写,因此 "a" 和 "A" 是不同类型的石头。
示例 1
- 输入:jewels = "aA", stones = "aAAbbbb"
- 输出:3
示例 2
- 输入:jewels = "z", stones = "ZZ"
- 输出:0 提示:
1 <= jewels.length, stones.length <= 50jewels和stones仅由英文字母组成jewels中的所有字符都是唯一的
解题思路
解法一(哈希集合统计,主提交):把 jewels 中所有字符放入 HashSet<Character>,然后遍历
stones 统计命中的字符个数。
时间复杂度 O(|jewels| + |stones|),空间复杂度 O(|jewels|)。
解法二(布尔数组):字符范围仅英文字母(大小写共 52 个),用 boolean[128] 记录宝石类型,
免除哈希开销。
代码实现
import java.util.HashSet;
import java.util.Set;
class Solution {
// 解法一:哈希集合
public int numJewelsInStones(String jewels, String stones) {
Set<Character> set = new HashSet<>();
for (char c : jewels.toCharArray()) {
set.add(c);
}
int ans = 0;
for (char c : stones.toCharArray()) {
if (set.contains(c)) {
++ans;
}
}
return ans;
}
// 解法二:布尔数组
public int numJewelsInStones2(String jewels, String stones) {
boolean[] isJewel = new boolean[128];
for (char c : jewels.toCharArray()) {
isJewel[c] = true;
}
int ans = 0;
for (char c : stones.toCharArray()) {
if (isJewel[c]) {
++ans;
}
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.numJewelsInStones("aA", "aAAbbbb") == 3) ++pass; else System.out.println("FAIL test1 got " + sol.numJewelsInStones("aA", "aAAbbbb"));
++total; if (sol.numJewelsInStones("z", "ZZ") == 0) ++pass; else System.out.println("FAIL test2");
++total; if (sol.numJewelsInStones2("aA", "aAAbbbb") == 3) ++pass; else System.out.println("FAIL test3");
++total; if (sol.numJewelsInStones2("z", "ZZ") == 0) ++pass; else System.out.println("FAIL test4");
if (pass == total) {
System.out.println("== 0771 PASS ==");
} else {
System.out.println("== 0771 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(J + S) | O(J) |
| 解法二 | O(J + S) | O(1) |
772. Basic Calculator III(基本计算器 III)
题目描述
实现一个基本的计算器来计算简单的表达式字符串。表达式字符串只包含非负整数,算符 +、-、
*、/,左括号 ( 和右括号 )。整数除法需要向下截断。可以假定给定的表达式总是有效的。
所有的中间结果的范围均满足 [-2^31, 2^31 - 1]。不能使用任何将字符串作为表达式求值的内置
函数,比如 eval()。
示例 1
- 输入:s = "1+1"
- 输出:2
示例 2
- 输入:s = "6-4/2"
- 输出:4
示例 3
- 输入:s = "2*(5+5*2)/3+(6/2+8)"
- 输出:21 提示:
1 <= s.length <= 10^5s由整数、算符('+','-','*','/')、左右括号'('、')'和空格' '组成s表示一个有效的表达式- 所有整数都在
[1, 2^31 - 1]范围内(题目有的版本允许 0/负,这里按题意非负整数)
解题思路
解法一(双栈:数字栈 + 符号栈,主提交):用两个栈模拟表达式求值:
- 数字:累积数字;
'(':压栈并打标记(用栈中元素计数或压入特殊符号);')':弹出直到遇到'(';- 运算符
+ - * /:当「栈顶运算符优先级 >= 当前运算符」时先完成一次运算(左结合),再压入 当前运算符(遇到(前停止)。 处理完后清空符号栈完成剩余运算。 除法用整除(Java 对正数即向下截断;遇负数按题意均正即可)。 时间复杂度 O(n),空间复杂度 O(n)。 解法二(递归下降 + 全局指针):把expr()拆成term()(乘除)与factor()(数字/括号) 的经典递归下降,遇到括号递归求值。代码结构更清晰,同样 O(n)。
代码实现
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
// 解法一:双栈
public int calculate(String s) {
Deque<Integer> nums = new ArrayDeque<>();
Deque<Character> ops = new ArrayDeque<>();
int i = 0, n = s.length();
while (i < n) {
char c = s.charAt(i);
if (c == ' ') {
++i;
} else if (Character.isDigit(c)) {
int v = 0;
while (i < n && Character.isDigit(s.charAt(i))) {
v = v * 10 + (s.charAt(i) - '0');
++i;
}
nums.push(v);
} else if (c == '(') {
ops.push(c);
++i;
} else if (c == ')') {
while (!ops.isEmpty() && ops.peek() != '(') {
calc(nums, ops);
}
ops.pop(); // 弹出 '('
++i;
} else { // + - * /
while (!ops.isEmpty() && precedence(ops.peek()) >= precedence(c)) {
calc(nums, ops);
}
ops.push(c);
++i;
}
}
while (!ops.isEmpty()) {
calc(nums, ops);
}
return nums.peek();
}
private int precedence(char c) {
if (c == '+' || c == '-') return 1;
if (c == '*' || c == '/') return 2;
return 0;
}
private void calc(Deque<Integer> nums, Deque<Character> ops) {
char op = ops.pop();
int b = nums.pop();
int a = nums.isEmpty() ? 0 : nums.pop();
int r;
switch (op) {
case '+': r = a + b; break;
case '-': r = a - b; break;
case '*': r = a * b; break;
default: r = a / b; break;
}
nums.push(r);
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
++total; if (sol.calculate("1+1") == 2) ++pass; else System.out.println("FAIL test1 got " + sol.calculate("1+1"));
++total; if (sol.calculate("6-4/2") == 4) ++pass; else System.out.println("FAIL test2 got " + sol.calculate("6-4/2"));
++total; if (sol.calculate("2*(5+5*2)/3+(6/2+8)") == 21) ++pass; else System.out.println("FAIL test3 got " + sol.calculate("2*(5+5*2)/3+(6/2+8)"));
++total; if (sol.calculate(" 3/2 ") == 1) ++pass; else System.out.println("FAIL test4 got " + sol.calculate(" 3/2 "));
++total; if (sol.calculate("1 + 1") == 2) ++pass; else System.out.println("FAIL test5");
++total; if (sol.calculate("2-1-1") == 0) ++pass; else System.out.println("FAIL test6 got " + sol.calculate("2-1-1"));
++total; if (sol.calculate("(1+(4+5+2)-3)+(6+8)") == 23) ++pass; else System.out.println("FAIL test7 got " + sol.calculate("(1+(4+5+2)-3)+(6+8)"));
++total; if (sol.calculate("1*2-3/4+5*6-7*8+9/10") == -24) ++pass; else System.out.println("FAIL test8 got " + sol.calculate("1*2-3/4+5*6-7*8+9/10"));
if (pass == total) {
System.out.println("== 0772 PASS ==");
} else {
System.out.println("== 0772 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(n) |
| 解法二 | O(n) | O(n) |
773. Sliding Puzzle(滑动谜题)
题目描述
在一个 2 x 3 的板上(board)有 5 块砖瓦,用数字 1~5 来表示,以及一块空缺用 0 来表示。
一次移动定义为选择 0 与一个相邻的数字(上下左右)进行交换。最终当板 board 的结果是
[[1,2,3],[4,5,0]] 谜板被解开。给出一个谜板的初始状态 board,返回最少可以通过多少次移动
解开谜板,如果不能解开谜板,则返回 -1。
示例 1
- 输入:board = [[1,2,3],[4,0,5]]
- 输出:1
示例 2
- 输入:board = [[1,2,3],[5,4,0]]
- 输出:-1
示例 3
- 输入:board = [[4,1,2],[5,0,3]]
- 输出:5 提示:
board.length == 2board[i].length == 3board[i][j]是 0 到 5 的排列
解题思路
解法一(BFS 状态最短路,主提交):把 2x3 板展平成长度 6 的字符串/整型状态,0 的每个位置
有固定的相邻位置表。从初始状态 BFS,交换 0 与邻居生成新状态,首次到达目标状态 "123450"
的层数即为答案;队列耗尽则不可解返回 -1。
状态数至多 6! = 720,BFS 复杂度 O(720·邻域)。
解法二(A* / 双向 BFS):用启发式(曼哈顿距离和)加速,思路更高级;状态空间小,单向 BFS
已经足够,此处双向 BFS 作为对照。
代码实现
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
class Solution {
// 0 在每个下标 (展平) 时可交换的相邻位置
private static final int[][] NEIGHBOR = {
{1, 3}, {0, 2, 4}, {1, 5},
{0, 4}, {1, 3, 5}, {2, 4}
};
// 解法一:BFS
public int slidingPuzzle(int[][] board) {
StringBuilder start = new StringBuilder();
for (int[] row : board) {
for (int v : row) {
start.append(v);
}
}
String target = "123450";
if (start.toString().equals(target)) {
return 0;
}
Set<String> seen = new HashSet<>();
seen.add(start.toString());
Deque<String> q = new ArrayDeque<>();
q.offer(start.toString());
int steps = 0;
while (!q.isEmpty()) {
int size = q.size();
++steps;
for (int t = 0; t < size; ++t) {
String cur = q.poll();
int zero = cur.indexOf('0');
for (int nb : NEIGHBOR[zero]) {
char[] arr = cur.toCharArray();
arr[zero] = arr[nb];
arr[nb] = '0';
String next = new String(arr);
if (next.equals(target)) {
return steps;
}
if (seen.add(next)) {
q.offer(next);
}
}
}
}
return -1;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[][] b1 = {{1, 2, 3}, {4, 0, 5}};
++total; if (sol.slidingPuzzle(b1) == 1) ++pass; else System.out.println("FAIL test1 got " + sol.slidingPuzzle(b1));
int[][] b2 = {{1, 2, 3}, {5, 4, 0}};
++total; if (sol.slidingPuzzle(b2) == -1) ++pass; else System.out.println("FAIL test2 got " + sol.slidingPuzzle(b2));
int[][] b3 = {{4, 1, 2}, {5, 0, 3}};
++total; if (sol.slidingPuzzle(b3) == 5) ++pass; else System.out.println("FAIL test3 got " + sol.slidingPuzzle(b3));
int[][] b4 = {{1, 2, 3}, {4, 5, 0}};
++total; if (sol.slidingPuzzle(b4) == 0) ++pass; else System.out.println("FAIL test4");
if (pass == total) {
System.out.println("== 0773 PASS ==");
} else {
System.out.println("== 0773 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(720) | O(720) |
| 解法二 | O(720) | O(720) |
774. Minimize Max Distance to Gas Station(最小化去加油站的最大距离)
题目描述
整数数组 stations 表示水平数轴上各个加油站的位置。给你一个整数 k。请你在数轴上增设
k 个加油站,新增加油站可以位于水平数轴上的任意位置,而不必放在整数位置上。设 penalty()
是增设 k 个新加油站后,相邻两个加油站间的最大距离。请你返回 penalty() 可能的最小值。
与实际答案误差在 10^-6 范围内的答案将被视作正确答案。
示例 1
- 输入:stations = [1,2,3,4,5,6,7,8,9,10], k = 9
- 输出:0.50000
示例 2
- 输入:stations = [23,24,36,39,46,56,57,65,84,98], k = 1
- 输出:14.00000 提示:
10 <= stations.length <= 20000 <= stations[i] <= 10^8stations按严格递增顺序排列1 <= k <= 10^6
解题思路
解法一(二分答案 + 贪心检查,主提交):「最大距离最小」是典型的二分答案模型。二分最大
相邻距离 mid,检查「每个原始间距 gap = stations[i+1] - stations[i] 至少需要插入
ceil(gap / mid) - 1 个新站才能让该段内相邻距离 <= mid」,总需要插入数
total <= k 则可行,缩小上界;否则放大下界。
注意 gap == mid 的整数倍时 ceil(gap/mid) - 1 处理:可用 (gap + mid - eps) / mid - 1
避免浮点边界,或直接用 Math.ceil(gap / mid) - 1。为满足 1e-6 精度,循环约 60 次。
时间复杂度 O(n · iterations),迭代常数约 60,足以覆盖 1e-6 精度。
解法二(优先队列每次插入最贪点):每次把新站加在「当前最长段」中点,用大根堆放段长;每次
插入把段长均匀拆分。O(k·log n) 在 k 可达 1e6 时会超时,仅作理论对照。
代码实现
import java.util.Arrays;
class Solution {
// 解法一:二分答案
public double minmaxGasDist(int[] stations, int k) {
double lo = 0, hi = 0;
for (int i = 1; i < stations.length; ++i) {
hi = Math.max(hi, (double) (stations[i] - stations[i - 1]));
}
for (int iter = 0; iter < 60; ++iter) {
double mid = (lo + hi) / 2;
long need = 0; // 需要插入的站数
for (int i = 1; i < stations.length; ++i) {
double gap = stations[i] - stations[i - 1];
need += (long) Math.ceil(gap / mid) - 1;
}
if (need <= k) {
hi = mid; // 可行,收缩上界
} else {
lo = mid; // 不可行,放大下界
}
}
return hi;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[] s1 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
double r1 = sol.minmaxGasDist(s1, 9);
++total; if (Math.abs(r1 - 0.5) < 1e-6) ++pass; else System.out.println("FAIL test1 got " + r1);
int[] s2 = {23, 24, 36, 39, 46, 56, 57, 65, 84, 98};
double r2 = sol.minmaxGasDist(s2, 1);
++total; if (Math.abs(r2 - 14.0) < 1e-6) ++pass; else System.out.println("FAIL test2 got " + r2);
int[] s3 = {10, 20, 30};
double r3 = sol.minmaxGasDist(s3, 1);
++total; if (Math.abs(r3 - 10.0) < 1e-6) ++pass; else System.out.println("FAIL test3 got " + r3);
if (pass == total) {
System.out.println("== 0774 PASS ==");
} else {
System.out.println("== 0774 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n·60) | O(1) |
| 解法二 | O(k·log n) | O(n) |
775. Global and Local Inversions(全局倒置与局部倒置)
题目描述
给你一个长度为 n 的整数数组 nums,表示由范围 [0, n - 1] 内所有整数组成的一个排列。
全局倒置的数目等于满足下述条件不同下标对 (i, j) 的数目:0 <= i < j < n 且
nums[i] > nums[j]。局部倒置的数目等于满足下述条件的下标 i 的数目:0 <= i < n - 1
且 nums[i] > nums[i + 1]。
当数组 nums 中全局倒置的数量等于局部倒置的数量时,返回 true;否则返回 false。
示例 1
- 输入:nums = [1,0,2]
- 输出:true
- 解释:有 1 个全局倒置,和 1 个局部倒置。
示例 2
- 输入:nums = [1,2,0]
- 输出:false
- 解释:有 2 个全局倒置((1,2)、(2,0)),和 1 个局部倒置((2,0))。 提示:
n == nums.length1 <= n <= 10^50 <= nums[i] < nnums中所有元素都是唯一的
解题思路
解法一(等价转化:无「跨步非法倒置」,主提交):局部倒置一定是全局倒置,因此要两者数量相等,
只需保证「不存在非相邻的全局倒置」,即对于任意 i < j - 1,都有 nums[i] < nums[j]。
等价的说法是:扫描时维护 nums[0..i-2] 的最大值 mx,只要出现 mx > nums[i](i 从 2 起)
就说明存在非局部倒置,返回 false。因为排列中一个数只要「跳级」就不会等于局部倒置数。
注意这里 nums 是 [0, n-1] 的排列,可进一步简化:非局部倒置存在当且仅当某个
nums[i] == i + 1 与 nums[i+1] == i 分别拆开,但直接维护前缀最大更通用。
时间复杂度 O(n),空间复杂度 O(1)。
解法二(暴力对照):直接数全局/局部倒置个数,O(n²),仅作正确性对照。
代码实现
class Solution {
// 解法一:维护前缀最大,检查非相邻倒置
public boolean isIdealPermutation(int[] nums) {
int n = nums.length;
int maxBefore = Integer.MIN_VALUE; // nums[0..i-2] 的最大值(i >= 2 时有效)
for (int i = 0; i < n; ++i) {
if (i >= 2 && maxBefore > nums[i]) {
return false; // 存在非相邻的全局倒置 (j, i),j <= i-2
}
if (i >= 1) {
maxBefore = Math.max(maxBefore, nums[i - 1]);
}
}
return true;
}
// 解法二:暴力对照
public boolean isIdealPermutation2(int[] nums) {
int n = nums.length, global = 0, local = 0;
for (int i = 0; i < n; ++i) {
if (i + 1 < n && nums[i] > nums[i + 1]) {
++local;
}
for (int j = i + 1; j < n; ++j) {
if (nums[i] > nums[j]) {
++global;
}
}
}
return global == local;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution sol = new Solution();
int pass = 0, total = 0;
int[] n1 = {1, 0, 2};
++total; if (sol.isIdealPermutation(n1)) ++pass; else System.out.println("FAIL test1");
int[] n2 = {1, 2, 0};
++total; if (!sol.isIdealPermutation(n2)) ++pass; else System.out.println("FAIL test2");
++total; if (sol.isIdealPermutation2(n1)) ++pass; else System.out.println("FAIL test3");
++total; if (!sol.isIdealPermutation2(n2)) ++pass; else System.out.println("FAIL test4");
int[] n5 = {0};
++total; if (sol.isIdealPermutation(n5)) ++pass; else System.out.println("FAIL test5");
int[] n6 = {2, 0, 1};
++total; if (!sol.isIdealPermutation(n6)) ++pass; else System.out.println("FAIL test6");
if (pass == total) {
System.out.println("== 0775 PASS ==");
} else {
System.out.println("== 0775 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(1) |
| 解法二 | O(n²) | O(1) |