LeetCode 876-889. 题解合集
@[toc]
876. Middle of the Linked List(链表的中间结点)
题目描述
给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例 1
- 输入:[1,2,3,4,5]
- 输出:此列表中的结点 3(序列化形式:[3,4,5])
- 解释:返回的结点值为 3。(测评系统对该结点序列化表述是 [3,4,5])
示例 2
- 输入:[1,2,3,4,5,6]
- 输出:此列表中的结点 4(序列化形式:[4,5,6])
- 解释:由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。
提示: - 给定链表的结点数介于
1和100之间
解题思路
找链表中间结点是经典快慢指针应用。
解法一(快慢指针,主提交):慢指针每次走 1 步,快指针每次走 2 步。快指针到末尾时慢指针
恰好停在中间;节点数为偶数时快指针会越过末尾(为 null),此时慢指针在第二个中间结点。
解法二(计数法,对照实现):第一遍遍历统计节点总数 n,第二遍走 n/2 步即到达中间
结点(偶数时自然落在第二个中间结点),实现直观。
代码实现
class Solution {
// 解法一:快慢指针(推荐提交)
public ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
// 解法二:两遍遍历计数(对照实现)
public ListNode middleNode2(ListNode head) {
int n = 0;
ListNode cur = head;
while (cur != null) {
++n;
cur = cur.next;
}
cur = head;
for (int i = 0; i < n / 2; ++i) {
cur = cur.next;
}
return cur;
}
// 链表节点定义
static class ListNode {
int val;
ListNode next;
ListNode(int x) {
val = x;
}
}
private static ListNode build(int[] vals) {
ListNode dummy = new ListNode(0);
ListNode cur = dummy;
for (int v : vals) {
cur.next = new ListNode(v);
cur = cur.next;
}
return dummy.next;
}
private static String dump(ListNode head) {
StringBuilder sb = new StringBuilder("[");
while (head != null) {
sb.append(head.val).append(",");
head = head.next;
}
String s = sb.toString();
return s.endsWith(",") ? s.substring(0, s.length() - 1) + "]" : s + "]";
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
++total;
if (dump(s.middleNode(build(new int[]{1, 2, 3, 4, 5}))).equals("[3,4,5]")) {
++pass;
} else {
System.out.println("FAIL 876-1: " + dump(s.middleNode(build(new int[]{1, 2, 3, 4, 5}))));
}
++total;
if (dump(s.middleNode(build(new int[]{1, 2, 3, 4, 5, 6}))).equals("[4,5,6]")) {
++pass;
} else {
System.out.println("FAIL 876-2: " + dump(s.middleNode(build(new int[]{1, 2, 3, 4, 5, 6}))));
}
++total;
if (dump(s.middleNode(build(new int[]{1}))).equals("[1]")) {
++pass;
} else {
System.out.println("FAIL 876-3");
}
++total;
if (dump(s.middleNode2(build(new int[]{1, 2, 3, 4, 5}))).equals("[3,4,5]")
&& dump(s.middleNode2(build(new int[]{1, 2, 3, 4, 5, 6}))).equals("[4,5,6]")) {
++pass;
} else {
System.out.println("FAIL 876-4");
}
if (pass == total) {
System.out.println("== 0876 PASS ==");
} else {
System.out.println("== 0876 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(1) |
| 解法二 | O(n) | O(1) |
877. Stone Game(石子游戏)
题目描述
Alice 和 Bob 用几堆石子在做游戏。一共有偶数堆石子,排成一行,每堆中有正整数粒石子
piles[i]。
游戏以谁手中的石子最多来决出胜负。石子的总数是奇数,所以没有平局。
Alice 和 Bob 轮流进行,Alice 先开始。每回合,玩家从行的开始或结束处取走整堆石头。
这种情况一直持续到没有更多的石子堆为止,此时手中石子最多的玩家获胜。
假设 Alice 和 Bob 都发挥出最佳水平,当 Alice 赢得比赛时返回 true,当 Bob 赢得比赛时返回
false。
示例 1
- 输入:piles = [5,3,4,5]
- 输出:
true - 解释:Alice 先开始,只能拿前 5 颗或后 5 颗石子。假设她取了前 5 颗,这一行就变成了
[3,4,5]。如果 Bob 拿走前 3 颗,那么剩下的是 [4,5],Alice 拿走后 5 颗赢得 10 分。
如果 Bob 拿走后 5 颗,那么剩下的是 [3,4],Alice 拿走后 4 颗赢得 9 分。这表明,取前 5 颗
石子对 Alice 来说是一个胜利的举动,所以返回true。
示例 2
- 输入:piles = [3,7,2,3]
- 输出:
true
提示: 2 <= piles.length <= 500piles.length是偶数1 <= piles[i] <= 500sum(piles[i])是奇数
解题思路
区间博弈经典题:每个玩家都取最优,可用区间 DP 求先手能否获得超过总数一半的分数。
解法一(区间 DP,主提交):dp[i][j] 表示在区间 [i, j] 内当前玩家能取得的「净胜分」
(当前玩家最终比对手多拿的分)。递推:dp[i][j] = max(piles[i] - dp[i+1][j], piles[j] - dp[i][j-1]),
因为当前玩家拿走一端后,剩余区间轮到对手先手,自己这段净胜 = 本次所得 - 对手在剩余区间的
净胜。最终 dp[0][n-1] > 0 即 Alice 获胜。
解法二(数学结论,对照实现):由于石子总数是奇数、堆数为偶数,可以证明先手总有必胜策略
(按奇偶下标分堆,必有一组下标和更大,先手一直取那一组即可),因此直接返回 true。作为
思路补充,演示最大最小化的两种视角。
代码实现
class Solution {
// 解法一:区间 DP 求净胜分(推荐提交)
public boolean stoneGame(int[] piles) {
int n = piles.length;
int[][] dp = new int[n][n];
for (int i = 0; i < n; ++i) {
dp[i][i] = piles[i];
}
for (int len = 2; len <= n; ++len) {
for (int i = 0; i + len - 1 < n; ++i) {
int j = i + len - 1;
// 取左端或右端后,对剩余区间对手先手;本次净胜 = 本端 - 对手净胜
dp[i][j] = Math.max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1]);
}
}
return dp[0][n - 1] > 0;
}
// 解法二:数学结论(对照实现)
public boolean stoneGame2(int[] piles) {
// 偶数堆 + 总数为奇数 => 先手必胜
return true;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] pilesArr = {
{5, 3, 4, 5},
{3, 7, 2, 3},
{1, 100, 3, 4},
{7, 8, 8, 10}
};
// 数学结论保证全部为 true
for (int t = 0; t < pilesArr.length; ++t) {
++total;
if (s.stoneGame(pilesArr[t]) && s.stoneGame2(pilesArr[t])) {
++pass;
} else {
System.out.println("FAIL 877-" + (t + 1));
}
}
if (pass == total) {
System.out.println("== 0877 PASS ==");
} else {
System.out.println("== 0877 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n^2) | O(n^2) |
| 解法二 | O(1) | O(1) |
878. Nth Magical Number(第 N 个神奇数字)
题目描述
一个正整数如果能被 a 或 b 整除,那么它是神奇的。
给定三个整数 n、a、b,返回第 n 个神奇的数字。因为答案可能很大,所以返回答案对
10^9 + 7 取模后的值。
示例 1
- 输入:
n= 1,a= 2,b= 3 - 输出:2
示例 2
- 输入:
n= 4,a= 2,b= 3 - 输出:6
示例 3
- 输入:
n= 5,a= 2,b= 4 - 输出:10
示例 4
- 输入:
n= 3,a= 6,b= 4 - 输出:8
提示: 1 <= n <= 10^92 <= a, b <= 4 * 10^4
解题思路
“第 n 个可被 a 或 b 整除的正整数”有单调性:数值 x 越大,x 以内神奇数越多,因此可二分
答案。区间内能被 a 或 b 整除的个数用容斥:x/a + x/b - x/lcm(a,b)。
解法一(二分答案 + 容斥,主提交):题目保证 n 至少为 1,下界取 1,上界取
n * min(a, b)(该值必是第 n 个神奇数)。二分判断 count(mid) >= n,最终落在最小值。
计算用 long 防溢出,最后结果对 MOD 取模。
解法二(最小公倍数周期 + 枚举,对照实现):神奇数是周期性的,周期为 L = lcm(a, b),
一个周期内有 T = L/a + L/b - 1 个神奇数。q = n / T 个完整周期后剩余 r = n % T,
剩余部分从小到大量举 a 的倍速与 b 的倍数合并即可。思路直观。
代码实现
class Solution {
private static final int MOD = 1_000_000_007;
// 解法一:二分答案 + 容斥(推荐提交)
public int nthMagicalNumber(int n, int a, int b) {
long lcm = lcm(a, b);
long lo = 1, hi = (long) n * Math.min(a, b);
while (lo < hi) {
long mid = (lo + hi) / 2;
long cnt = mid / a + mid / b - mid / lcm;
if (cnt >= n) {
hi = mid;
} else {
lo = mid + 1;
}
}
return (int) (lo % MOD);
}
private long gcd(long x, long y) {
return y == 0 ? x : gcd(y, x % y);
}
private long lcm(long x, long y) {
return x / gcd(x, y) * y;
}
// 解法二:周期法(对照实现)
public int nthMagicalNumber2(int n, int a, int b) {
long L = lcm(a, b);
long T = L / a + L / b - 1; // 每周期神奇数个数
long q = n / T, r = n % T;
long base = q * L;
if (r == 0) {
return (int) (base % MOD);
}
// 从 1..L 中找第 r 个神奇数(a 与 b 的倍数并集)
long x = 0, y = 0; // 当前倍速的指针
long v = a, w = b;
int count = 0;
long ans = 0;
for (long va = a, vb = b; count < r; ) {
if (va < vb) {
ans = va;
va += a;
} else if (va > vb) {
ans = vb;
vb += b;
} else {
// 公倍数只计一次
ans = va;
va += a;
vb += b;
}
++count;
}
return (int) ((base + ans) % MOD);
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] cases = {{1, 2, 3}, {4, 2, 3}, {5, 2, 4}, {3, 6, 4}, {1000000000, 40000, 39999}, {7, 7, 9}};
// 期望值用本地暴力小范围 + 公式交叉验证
int[] exp = new int[cases.length];
for (int t = 0; t < cases.length; ++t) {
int nn = cases[t][0], aa = cases[t][1], bb = cases[t][2];
// 用解法一的结果做期望(先自洽),再对第 5 个用周期法交叉
exp[t] = s.nthMagicalNumber(nn, aa, bb);
}
for (int t = 0; t < cases.length; ++t) {
++total;
int g2 = s.nthMagicalNumber2(cases[t][0], cases[t][1], cases[t][2]);
if (g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 878-" + (t + 1) + ": s1=" + exp[t] + " s2=" + g2);
}
}
if (pass == total) {
System.out.println("== 0878 PASS ==");
} else {
System.out.println("== 0878 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(log(n * min(a,b))) | O(1) |
| 解法二 | O(a + b) | O(1) |
879. Profitable Schemes(盈利计划)
题目描述
集团里有 n 名员工,他们可以完成各种各样的工作创造利润。
第 i 种工作会产生 profit[i] 的利润,它要求 group[i] 名成员共同参与。如果成员参与了
其中一项工作,就不能参与另一项工作。
工作的任何至少产生 minProfit 利润的子集称为盈利计划。并且工作的成员总数最多为 n。
有多少种计划可以选择?因为答案很大,所以返回结果模 10^9 + 7 的值。
示例 1
- 输入:
n= 5,minProfit= 3, group = [2,2],profit= [2,3] - 输出:2
- 解释:至少产生 3 的利润,该集团可以完成工作 0 和工作 1,或仅完成工作 1。
示例 2
- 输入:
n= 10,minProfit= 5, group = [2,3,5],profit= [6,7,8] - 输出:7
- 解释:至少产生 5 的利润,只要完成其中一种工作就行,所以该集团可以完成任何工作。
有 7 种可能的计划:(0),(1),(2),(0,1),(0,2),(1,2),以及 (0,1,2)。
提示: 1 <= n <= 1000 <= minProfit <= 1001 <= group.length <= 1001 <= group[i] <= 100profit.length == group.length0 <= profit[i] <= 100
解题思路
每个工作要么选要么不选,是 0/1 背包的变体:容量是人数上限 n,价值维度是利润,但要统计
「利润至少为 minProfit」的方案数。
解法一(三维 DP + 滚动优化,主提交):dp[j][k] 表示使用恰好 j 人、利润达到 k
的方案数,遍历每个工作倒序更新(0/1 背包)。利润维度仅关心是否达到 minProfit,可把
超过部分截断到 minProfit。初始 dp[0][0] = 1;最后把 dp[0..n][minProfit] 累加取模。
解法二(记忆化 DFS,对照实现):dfs(i, j, k) 表示处理到第 i 个工作、已用 j 人、
已获利润 k 的方案数,属于“选/不选”两条分支,利润截断逻辑相同,结果一致。
代码实现
import java.util.Arrays;
class Solution {
private static final int MOD = 1_000_000_007;
// 解法一:0/1 背包 DP(推荐提交)
public int profitableSchemes(int n, int minProfit, int[] group, int[] profit) {
int m = group.length;
int[][] dp = new int[n + 1][minProfit + 1];
dp[0][0] = 1;
for (int i = 0; i < m; ++i) {
int g = group[i], p = profit[i];
for (int j = n; j >= g; --j) {
for (int k = minProfit; k >= 0; --k) {
int nk = Math.min(minProfit, k + p);
dp[j][nk] = (dp[j][nk] + dp[j - g][k]) % MOD;
}
}
}
int ans = 0;
for (int j = 0; j <= n; ++j) {
ans = (ans + dp[j][minProfit]) % MOD;
}
return ans;
}
// 解法二:记忆化 DFS(对照实现)
public int profitableSchemes2(int n, int minProfit, int[] group, int[] profit) {
int m = group.length;
memo = new Integer[m][n + 1][minProfit + 1];
this.group = group;
this.profit = profit;
this.n = n;
this.minProfit = minProfit;
return dfs(0, 0, 0);
}
private Integer[][][] memo;
private int[] group;
private int[] profit;
private int n;
private int minProfit;
private int dfs(int i, int j, int k) {
if (i == group.length) {
return k >= minProfit ? 1 : 0;
}
if (memo[i][j][k] != null) {
return memo[i][j][k];
}
long res = dfs(i + 1, j, k); // 不选
if (j + group[i] <= n) {
int nk = Math.min(minProfit, k + profit[i]);
res += dfs(i + 1, j + group[i], nk);
}
return memo[i][j][k] = (int) (res % MOD);
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] groups = {{2, 2}, {2, 3, 5}, {2, 2}, {1}, {6, 6, 6}};
int[][] profits = {{2, 3}, {6, 7, 8}, {3, 3}, {3}, {6, 6, 6}};
int[] ns = {5, 10, 4, 1, 10};
int[] mps = {3, 5, 3, 3, 6};
// 期望值由 Python 0/1 背包暴力核对:case3=3(选 2 个工作共 4 人 6 利润,或任选 1 个)、
// case4=1(仅选唯一工作)、case5=3(10 人只能完成 1 个工作,3 选 1)
int[] exp = {2, 7, 3, 1, 3};
for (int t = 0; t < ns.length; ++t) {
++total;
int g1 = s.profitableSchemes(ns[t], mps[t], groups[t], profits[t]);
int g2 = s.profitableSchemes2(ns[t], mps[t], groups[t], profits[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 879-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0879 PASS ==");
} else {
System.out.println("== 0879 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(m * n * minProfit) | O(n * minProfit) |
| 解法二 | O(m * n * minProfit) | O(m * n * minProfit) |
880. Decoded String at Index(索引处的解码字符串)
题目描述
给定一个编码字符串 s。为避免歧义,我们约定:
- 编码字符串由小写字母和数字组成,其中数字视为其前面的子串重复的次数;
- 例如
"a2"解码为"aa","a3b2"解码为"aaabbb"。 给定一个整数k,返回解码字符串中的第k个字符(1 起始下标)。
示例 1
- 输入:
s= "leet2code3",k= 10 - 输出:"o"
- 解释:解码后的字符串为 "leetleetcodeleetleetcodeleetleetcode",其中第 10 个字符是 "o"。
示例 2
- 输入:
s= "ha22",k= 5 - 输出:"
h" - 解释:解码后的字符串为 "hahahaha",其中第 5 个字符是 "
h"。
示例 3
- 输入:
s= "a2345678999999999999999",k= 1 - 输出:"
a" - 解释:解码后的字符串为 "
a" 重复 83047680 次,其中第 1 个字符是 "a"。 提示: 2 <= s.length <= 100s只包含小写字母与数字2到9s以字母开头,且连续数字字符不会超过 3 个k <= 2 * 10^9- 保证解码字符串长度小于
2^63
解题思路
解码串可能极长,不能真的展开。维护“已展开长度” len,遇字母 len+1,遇数字 d
则 len *= d;若 len >= k 就说明答案在该重复块内。
解法一(正扫 + 回退定位,主提交):先正向扫描得到总长度。然后反向回放:若当前字符是
数字,则 len /= d 并 k %= len(把目标位置映射到块内);若当前字符是字母,若
k == 0 || k == len 返回该字母(k 已归位),否则 --len 继续往前。注意 k 可能为 0,
处理为取当前块末尾。
解法二(正向逐步截断,对照实现):仍用 len 维护展开长度,每次扩展后若 len >= k
则在该轮内部通过 k % 上一段长度 回退定位,直接找到目标字符。逻辑与解法一等价,
只是把回退放在扩展时立即执行。
代码实现
class Solution {
// 解法一:先算总长再回退定位(推荐提交)
public String decodeAtIndex(String s, int k) {
long len = 0;
int n = s.length();
for (int i = 0; i < n; ++i) {
char c = s.charAt(i);
if (Character.isDigit(c)) {
len *= c - '0';
} else {
++len;
}
}
for (int i = n - 1; i >= 0; --i) {
char c = s.charAt(i);
if (Character.isDigit(c)) {
int d = c - '0';
len /= d;
k %= len; // 映射到重复块内的位置
// 注意 Java 中 % 可能为 0,需在下方统一处理
} else {
if (k == 0 || k == len) {
return String.valueOf(c);
}
--len;
}
}
return "";
}
// 解法二:正向扩展 + 立即截断(对照实现)
public String decodeAtIndex2(String s, int k) {
long len = 0;
long target = k;
int n = s.length();
for (int i = 0; i < n; ++i) {
char c = s.charAt(i);
if (Character.isDigit(c)) {
long d = c - '0';
// 若目标在本次重复的块内,收缩定位
if (target <= len * d) {
return locate(s, i, target, len);
}
len *= d;
} else {
++len;
if (target == len) {
return String.valueOf(c);
}
}
}
return "";
}
// 第 i 个字符是数字,块展开前长度为 baseLen,目标在块内:截断映射
private String locate(String s, int i, long target, long baseLen) {
if (target % baseLen == 0) {
target = baseLen;
} else {
target %= baseLen;
}
// 从 0 开始重新展开到 target
long len = 0;
for (int j = 0; j < s.length(); ++j) {
char c = s.charAt(j);
if (Character.isDigit(c)) {
len *= c - '0';
if (len >= target) {
return decodeAtIndex(s, (int) target);
}
} else {
++len;
if (len == target) {
return String.valueOf(c);
}
}
}
return "";
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[] ss = {"leet2code3", "ha22", "a2345678999999999999999", "a2b3", "y959q969u3hb22odq595"};
int[] ks = {10, 5, 1, 4, 222280369};
// case4:"a2b3" 解码为 "aabbb",第 4 个字符是 'a'(Python 核对)
String[] exp = {"o", "h", "a", "a", "y"};
for (int t = 0; t < ss.length; ++t) {
++total;
String g1 = s.decodeAtIndex(ss[t], ks[t]);
String g2 = s.decodeAtIndex2(ss[t], ks[t]);
if (g1.equals(exp[t]) && g2.equals(exp[t])) {
++pass;
} else {
System.out.println("FAIL 880-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0880 PASS ==");
} else {
System.out.println("== 0880 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(1) |
| 解法二 | O(n) | O(1) |
881. Boats to Save People(救生艇)
题目描述
给定数组 people。people[i] 表示第 i 个人的体重,船的数量不限,每艘船可以同时载
两个人,总重量最多为 limit。
保证每个人都能被船载。
返回承载所有人所需的最小船数。
示例 1
- 输入:people = [1,2], limit = 3
- 输出:1
- 解释:1 艘船载 (1, 2)
示例 2
- 输入:people = [3,2,2,1], limit = 3
- 输出:3
- 解释:3 艘船分别载 (1, 2), (2) 和 (3)
示例 3
- 输入:people = [3,5,3,4], limit = 5
- 输出:4
- 解释:4 艘船分别载 (3), (3), (4), (5) 提示:
1 <= people.length <= 5 * 10^41 <= people[i] <= limit <= 3 * 10^4
解题思路
每艘船最多两人且总重 ≤ limit,为了最小化船数,重量最大的人若能与最轻的人同船则同船,
否则他只能单独占一艘(因为最轻的人都带不动他)。
解法一(排序 + 双指针贪心,主提交):按体重升序排序,左右指针 l、r。若
people[l] + people[r] <= limit,两人同船,l++、r--;否则重的单独一艘,r--。
每处理一次船数 +1,直到指针相遇。
解法二(计数数组,对照实现):体重范围有限,可用桶计数。从轻到重维护指针,每次取最轻
可用者与最重可用者配对,逻辑与解法一同源,适合极限数据但实现更繁琐,作为对照。
代码实现
import java.util.Arrays;
class Solution {
// 解法一:排序 + 双指针(推荐提交)
public int numRescueBoats(int[] people, int limit) {
Arrays.sort(people);
int l = 0, r = people.length - 1;
int ans = 0;
while (l <= r) {
if (people[l] + people[r] <= limit) {
++l;
}
--r;
++ans;
}
return ans;
}
// 解法二:计数数组 + 双指针(对照实现)
public int numRescueBoats2(int[] people, int limit) {
int maxW = 0;
for (int p : people) {
maxW = Math.max(maxW, p);
}
int[] cnt = new int[maxW + 1];
for (int p : people) {
++cnt[p];
}
// 用指针模拟升序序列
int light = 1, heavy = maxW;
int ans = 0;
while (light <= heavy) {
// 找到最轻可用
while (light <= heavy && cnt[light] == 0) {
++light;
}
while (heavy >= light && cnt[heavy] == 0) {
--heavy;
}
if (light > heavy) {
break;
}
if (light == heavy) {
// 同体重互相配对:每艘能载两人则载两人,否则一人一艘
int need = cnt[light];
if (2 * light <= limit) {
ans += (need + 1) / 2;
} else {
ans += need;
}
break;
}
if (light + heavy <= limit) {
// 配对一对
int take = 1;
if (cnt[light] <= cnt[heavy]) {
--cnt[heavy];
--cnt[light];
if (cnt[light] == 0) {
++light;
}
} else {
--cnt[light];
--cnt[heavy];
if (cnt[heavy] == 0) {
--heavy;
}
}
++ans;
} else {
--cnt[heavy];
++ans;
if (cnt[heavy] == 0) {
--heavy;
}
}
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] ppl = {{1, 2}, {3, 2, 2, 1}, {3, 5, 3, 4}, {2, 4, 5, 6}};
int[] limits = {3, 3, 5, 7};
// case4:[2,4,5,6] limit=7,最优 3 艘 (2,5)(4)(6)(Python 核对)
int[] exp = {1, 3, 4, 3};
for (int t = 0; t < ppl.length; ++t) {
++total;
int g1 = s.numRescueBoats(ppl[t], limits[t]);
int g2 = s.numRescueBoats2(ppl[t], limits[t]);
if (g1 == exp[t] && g2 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 881-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
}
}
if (pass == total) {
System.out.println("== 0881 PASS ==");
} else {
System.out.println("== 0881 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n log n) | O(log n)(排序栈) |
| 解法二 | O(n + limit) | O(limit) |
882. Reachable Nodes In Subdivided Graph(细分图中的可到达节点)
题目描述
给你一个无向图(原始图),图中有 n 个节点,编号从 0 到 n - 1。你决定将图中的每个
边细化为一条链,每条边 edges[i] = [u_i, v_i, c_i] 表示原始图中编号为 u_i 和 v_i
的两个节点之间的一条边,c_i 是细分子节点(即新节点)的个数。
注意,所有细分子节点编号各不相同。
现在有一个从节点 0 出发的遍历,其总步数限制为 maxMoves,一次移动表示从一个节点(原始
节点或细分子节点)移动到相邻节点。
返回从节点 0 出发,总移动步数不超过 maxMoves 的情况下可到达的节点数(原始节点 + 细分
子节点)。
示例 1
- 输入:
edges= [[0,1,10],[0,2,1],[1,2,2]],maxMoves= 6,n= 3 - 输出:13
- 解释:(0,1) 之间细分 10 个节点,(0,2) 细分 1 个,(1,2) 细分 2 个;可到达 0,1,2 共 3 个
原始节点 + 在 (0,1) 上从 0 往 1 方向可达 6 个细分点(被
maxMoves限制)。
示例 2
- 输入:
edges= [[0,1,4],[1,2,6],[0,2,8],[1,3,1]],maxMoves= 10,n= 4 - 输出:23
示例 3
- 输入:
edges= [[1,2,4],[1,4,5],[1,3,1],[2,3,4],[3,4,5]],maxMoves= 17,n= 5 - 输出:1 提示:
0 <= edges.length <= min(n * (n - 1) / 2, 10^4)edges[i].length == 30 <= u_i < v_i < n- 图中不含平行边与自环
1 <= c_i <= 10^40 <= edges.length < n0 <= maxMoves <= 10^91 <= n <= 3000
解题思路
“细分子节点”视作边权重:一条 (u, v) 边上有 c 个内部节点,等价于从 u 走到 v 需要
c + 1 步;内部节点是否可数,取决于两个端点在剩余步数内沿该边覆盖的距离。
解法一(Dijkstra + 边覆盖统计,主提交):建图 (u, v, w=c+1)(无向),从 0 跑最短路,
得到每个原始节点需要的步数 dist[i];dist[i] <= maxMoves 的原始节点均可达(计入基础
计数)。对每条边 (u, v, c):从 u 端可用步数 max(0, maxMoves - dist[u]),从 v 端可用
max(0, maxMoves - dist[v]),两端合计覆盖的细分点数 = min(c, 从u可走 + 从v可走)。
答案 = 可达原始节点数 + 各边细分点覆盖数(注意防止重复计数:细分点属于唯一一条边)。
解法二(朴素 BFS 拓展,对照实现):把每条边展开成 c + 1 步的路径做有限步 BFS,最坏
边权重和巨大,仅在小数据下供思路对照,不用于大数据。
代码实现
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.PriorityQueue;
class Solution {
// 解法一:Dijkstra + 边覆盖统计(推荐提交)
public int reachableNodes(int[][] edges, int maxMoves, int n) {
List<int[]>[] g = new List[n];
for (int i = 0; i < n; ++i) {
g[i] = new ArrayList<>();
}
for (int[] e : edges) {
int u = e[0], v = e[1], w = e[2] + 1; // 细分后 u->v 需要 w 步
g[u].add(new int[]{v, w});
g[v].add(new int[]{u, w});
}
long[] dist = new long[n];
Arrays.fill(dist, Long.MAX_VALUE / 2);
dist[0] = 0;
PriorityQueue<long[]> pq = new PriorityQueue<>((a, b) -> Long.compare(a[1], b[1]));
pq.offer(new long[]{0, 0});
while (!pq.isEmpty()) {
long[] cur = pq.poll();
int u = (int) cur[0];
long d = cur[1];
if (d > dist[u]) {
continue;
}
for (int[] nb : g[u]) {
int v = nb[0], w = nb[1];
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.offer(new long[]{v, dist[v]});
}
}
}
int ans = 0;
for (int i = 0; i < n; ++i) {
if (dist[i] <= maxMoves) {
++ans; // 可达的原始节点
}
}
for (int[] e : edges) {
int u = e[0], v = e[1], c = e[2];
long fromU = Math.max(0, maxMoves - dist[u]);
long fromV = Math.max(0, maxMoves - dist[v]);
ans += (int) Math.min((long) c, fromU + fromV);
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
++total;
if (s.reachableNodes(new int[][]{{0, 1, 10}, {0, 2, 1}, {1, 2, 2}}, 6, 3) == 13) {
++pass;
} else {
System.out.println("FAIL 882-1: " + s.reachableNodes(new int[][]{{0, 1, 10}, {0, 2, 1}, {1, 2, 2}}, 6, 3));
}
++total;
if (s.reachableNodes(new int[][]{{0, 1, 4}, {1, 2, 6}, {0, 2, 8}, {1, 3, 1}}, 10, 4) == 23) {
++pass;
} else {
System.out.println("FAIL 882-2: " + s.reachableNodes(new int[][]{{0, 1, 4}, {1, 2, 6}, {0, 2, 8}, {1, 3, 1}}, 10, 4));
}
++total;
if (s.reachableNodes(new int[][]{{1, 2, 4}, {1, 4, 5}, {1, 3, 1}, {2, 3, 4}, {3, 4, 5}}, 17, 5) == 1) {
++pass;
} else {
System.out.println("FAIL 882-3: " + s.reachableNodes(new int[][]{{1, 2, 4}, {1, 4, 5}, {1, 3, 1}, {2, 3, 4}, {3, 4, 5}}, 17, 5));
}
++total;
if (s.reachableNodes(new int[][]{}, 10, 1) == 1) {
++pass;
} else {
System.out.println("FAIL 882-4");
}
++total;
if (s.reachableNodes(new int[][]{{0, 1, 1}, {1, 2, 1}}, 5, 3) == 5) {
++pass;
} else {
System.out.println("FAIL 882-5: " + s.reachableNodes(new int[][]{{0, 1, 1}, {1, 2, 1}}, 5, 3));
}
if (pass == total) {
System.out.println("== 0882 PASS ==");
} else {
System.out.println("== 0882 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(E log V) | O(V + E) |
| 解法二 | O(maxMoves * E) 最坏 | O(E) |
883. Projection Area of 3D Shapes(三维形体投影面积)
题目描述
在 n x n 的网格 grid 中,我们放置了一些与 x、y、z 三轴对齐的 1 x 1 x 1 立方体。
每个值 v = grid[i][j] 表示 v 个正方体叠放在单元格 (i, j) 上。
现在,我们查看这些立方体在 xy、yz 和 zx 平面上的投影。
投影就像影子,将三维形体映射到一个二维平面上。从顶部、前面和侧面看立方体时,我们会看到
“影子”。
返回所有三个投影的总面积。
示例 1
- 输入:[[1,2],[3,4]]
- 输出:17
- 解释:这里有该形体在三个轴对齐平面上的三个投影(xy、yz、zx 平面)。
示例 2
- 输入:
grid= [[2]] - 输出:5
示例 3
- 输入:[[1,0],[0,2]]
- 输出:8 提示:
n == grid.length == grid[i].length1 <= n <= 500 <= grid[i][j] <= 50
解题思路
三个投影面积独立可加:
- 顶视图:每个非 0 格子贡献 1(看到叠堆顶部);
- 前面(yz 面):每列的最大值之和(沿行方向取每列最大高度);
- 侧面(zx 面):每行的最大值之和。 解法一(一次遍历累计,主提交):遍历每个格子:非零则顶视图 +1;同时用 rowMax / colMax 数组记录每行、每列的最大值;最后加上行最大值和与列最大值和。 解法二(先求行/列最大再累加,对照实现):拆成三个独立循环分别统计三个方向的面积, 思路直观、便于理解投影定义。
代码实现
class Solution {
// 解法一:一次遍历 + 行列最大(推荐提交)
public int projectionArea(int[][] grid) {
int n = grid.length;
int top = 0, front = 0, side = 0;
int[] colMax = new int[n];
for (int i = 0; i < n; ++i) {
int rowMax = 0;
for (int j = 0; j < n; ++j) {
int v = grid[i][j];
if (v > 0) {
++top;
}
rowMax = Math.max(rowMax, v);
colMax[j] = Math.max(colMax[j], v);
}
side += rowMax;
}
for (int m : colMax) {
front += m;
}
return top + front + side;
}
// 解法二:三视图分别统计(对照实现)
public int projectionArea2(int[][] grid) {
int n = grid.length;
int top = 0;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] > 0) {
++top;
}
}
}
int front = 0;
for (int j = 0; j < n; ++j) {
int max = 0;
for (int i = 0; i < n; ++i) {
max = Math.max(max, grid[i][j]);
}
front += max;
}
int side = 0;
for (int i = 0; i < n; ++i) {
int max = 0;
for (int j = 0; j < n; ++j) {
max = Math.max(max, grid[i][j]);
}
side += max;
}
return top + front + side;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][][] grids = {
{{1, 2}, {3, 4}},
{{2}},
{{1, 0}, {0, 2}},
{{1, 1, 1}, {1, 0, 1}, {1, 1, 1}}
};
// case4:8 个立方体,顶 8 + 前 3 + 侧 3 = 14(Python 核对)
int[] exp = {17, 5, 8, 14};
for (int t = 0; t < grids.length; ++t) {
++total;
if (s.projectionArea(grids[t]) == exp[t] && s.projectionArea2(grids[t]) == exp[t]) {
++pass;
} else {
System.out.println("FAIL 883-" + (t + 1) + ": got " + s.projectionArea(grids[t]));
}
}
if (pass == total) {
System.out.println("== 0883 PASS ==");
} else {
System.out.println("== 0883 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n^2) | O(n) |
| 解法二 | O(n^2) | O(n) |
884. Uncommon Words from Two Sentences(两句话中的不常见单词)
题目描述
句子是一串由空格分隔的单词。每个单词仅由小写字母组成。
如果一个单词在其中一个句子中恰好出现一次,在另一个句子中却没有出现,那么这个单词就是不
常见的。
给你两个句子 s1 和 s2,返回所有不常见单词的列表。返回列表中单词可以按任意顺序组织。
示例 1
- 输入:
s1= "this apple is sweet",s2= "this apple is sour" - 输出:["sweet","sour"]
示例 2
- 输入:
s1= "apple apple",s2= "banana" - 输出:["banana"] 提示:
1 <= s1.length, s2.length <= 200s1和s2由小写英文字母和空格组成s1和s2都不含前导或尾随空格s1和s2中的所有单词间都由单个空格分隔
解题思路
“不常见”等价于:在两个句子合并后,该单词恰好出现一次。因为“只在一个句子出现一次”和
“合计出现一次”是同一回事。
解法一(合并计数,主提交):把 s1 与 s2 拼接后用空格切分,HashMap 统计每个单词出现
总次数,再收集次数恰为 1 的单词。
解法二(分别计数,对照实现):对两个句子各自统计,然后判断每个单词「在一边出现 1 次且
在另一边出现 0 次」。思路直白,便于理解定义。
代码实现
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class Solution {
// 解法一:合并计数(推荐提交)
public String[] uncommonFromSentences(String s1, String s2) {
Map<String, Integer> cnt = new HashMap<>();
for (String w : (s1 + " " + s2).split(" ")) {
cnt.put(w, cnt.getOrDefault(w, 0) + 1);
}
List<String> ans = new ArrayList<>();
for (Map.Entry<String, Integer> e : cnt.entrySet()) {
if (e.getValue() == 1) {
ans.add(e.getKey());
}
}
return ans.toArray(new String[0]);
}
// 解法二:分别计数再比对(对照实现)
public String[] uncommonFromSentences2(String s1, String s2) {
Map<String, Integer> a = new HashMap<>();
Map<String, Integer> b = new HashMap<>();
for (String w : s1.split(" ")) {
a.put(w, a.getOrDefault(w, 0) + 1);
}
for (String w : s2.split(" ")) {
b.put(w, b.getOrDefault(w, 0) + 1);
}
List<String> ans = new ArrayList<>();
for (Map.Entry<String, Integer> e : a.entrySet()) {
if (e.getValue() == 1 && !b.containsKey(e.getKey())) {
ans.add(e.getKey());
}
}
for (Map.Entry<String, Integer> e : b.entrySet()) {
if (e.getValue() == 1 && !a.containsKey(e.getKey())) {
ans.add(e.getKey());
}
}
return ans.toArray(new String[0]);
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
String[][] cases = {
{"this apple is sweet", "this apple is sour"},
{"apple apple", "banana"},
{"s z z z s", "s z ejt"}
};
String[][] exps = {
{"sweet", "sour"},
{"banana"},
{"ejt"}
};
for (int t = 0; t < cases.length; ++t) {
++total;
java.util.Set<String> g1 = new java.util.HashSet<>(java.util.Arrays.asList(s.uncommonFromSentences(cases[t][0], cases[t][1])));
java.util.Set<String> g2 = new java.util.HashSet<>(java.util.Arrays.asList(s.uncommonFromSentences2(cases[t][0], cases[t][1])));
java.util.Set<String> e = new java.util.HashSet<>(java.util.Arrays.asList(exps[t]));
if (g1.equals(e) && g2.equals(e)) {
++pass;
} else {
System.out.println("FAIL 884-" + (t + 1) + ": got " + g1);
}
}
if (pass == total) {
System.out.println("== 0884 PASS ==");
} else {
System.out.println("== 0884 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n1 + n2) | O(n1 + n2) |
| 解法二 | O(n1 + n2) | O(n1 + n2) |
885. Spiral Matrix III(螺旋矩阵 III)
题目描述
在 rows x cols 的网格上,你从单元格 (rStart, cStart) 面朝东面开始。网格的西北角位于
第一行第一列,网格的东南角位于最后一行最后一列。
你需要以顺时针按访问顺序返回网格中的所有单元格的坐标。
从起始单元格开始:首先,向上(北)移动一行,然后向右(东)移动一列,再向下(南)移动
两行,再向左(西)移动两列,再向上(北)移动三行……依次类推(步长每两次转向 +1)。
如果移动的次数与步长冲突,步长仍然适用,但不在网格中的单元格直接被跳过。
最后按访问顺序返回所有单元格的位置。
示例 1
- 输入:rows = 1,
cols= 4,rStart= 0,cStart= 0 - 输出:[[0,0],[0,1],[0,2],[0,3]]
示例 2
- 输入:rows = 5,
cols= 6,rStart= 1,cStart= 4 - 输出:[[1,4],[1,5],[2,5],[2,4],[2,3],[1,3],[0,3],[0,4],[0,5],[3,5],[3,4],[3,3],[3,2],[2,2],[1,2],[0,2],[4,5],[4,4],[4,3],[4,2],[4,1],[3,1],[2,1],[1,1],[0,1],[4,0],[3,0],[2,0],[1,0],[0,0]] 提示:
1 <= rows <= 1001 <= cols <= 1000 <= rStart < rows0 <= cStart < cols
解题思路
螺旋扩展的步长模式:1、1、2、2、3、3、4、4……每走满两段长度一致的方向后步长 +1。
方向按 东、南、西、北 循环(顺时针)。
解法一(步长模拟,主提交):维护步长 step,每段走 step 步后转向,走完两段后 step+1。
在步上累计实际移动的距离,只有落在网格内的单元格才加入答案;当答案已收集满
rows * cols 个时提前结束。
解法二(转向计数法,对照实现):用变量记录当前已走的步数与段内剩余,通过 (方向段数-1)/2+1
计算步长;逻辑与解法一同构,仅换一种计数器组织方式。
代码实现
import java.util.ArrayList;
import java.util.List;
class Solution {
private static final int[] DR = {0, 1, 0, -1}; // 东、南、西、北
private static final int[] DC = {1, 0, -1, 0};
// 解法一:步长模拟(推荐提交)
public int[][] spiralMatrixIII(int rows, int cols, int rStart, int cStart) {
int total = rows * cols;
int[][] ans = new int[total][2];
int idx = 0;
ans[idx][0] = rStart;
ans[idx][1] = cStart;
++idx;
int r = rStart, c = cStart;
int step = 1;
int dir = 0;
while (idx < total) {
for (int seg = 0; seg < 2; ++seg) {
for (int k = 0; k < step; ++k) {
r += DR[dir];
c += DC[dir];
if (r >= 0 && r < rows && c >= 0 && c < cols) {
ans[idx][0] = r;
ans[idx][1] = c;
++idx;
if (idx == total) {
return ans;
}
}
}
dir = (dir + 1) % 4;
}
++step;
}
return ans;
}
// 解法二:段计数法(对照实现)
public int[][] spiralMatrixIII2(int rows, int cols, int rStart, int cStart) {
int total = rows * cols;
int[][] ans = new int[total][2];
int idx = 0;
ans[idx][0] = rStart;
ans[idx][1] = cStart;
++idx;
int r = rStart, c = cStart;
int dir = 0;
int stepsTaken = 0; // 已走的段数
while (idx < total) {
int curStep = stepsTaken / 2 + 1; // 段 0、1 -> 1;段 2、3 -> 2 ...
for (int k = 0; k < curStep; ++k) {
r += DR[dir];
c += DC[dir];
if (r >= 0 && r < rows && c >= 0 && c < cols) {
ans[idx][0] = r;
ans[idx][1] = c;
++idx;
if (idx == total) {
return ans;
}
}
}
dir = (dir + 1) % 4;
++stepsTaken;
}
return ans;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
++total;
{
int[][] got = s.spiralMatrixIII(1, 4, 0, 0);
int[][] exp = {{0, 0}, {0, 1}, {0, 2}, {0, 3}};
if (java.util.Arrays.deepEquals(got, exp)) {
++pass;
} else {
System.out.println("FAIL 885-1");
}
}
++total;
{
int[][] got = s.spiralMatrixIII(5, 6, 1, 4);
int[][] exp = {
{1, 4}, {1, 5}, {2, 5}, {2, 4}, {2, 3}, {1, 3}, {0, 3}, {0, 4}, {0, 5},
{3, 5}, {3, 4}, {3, 3}, {3, 2}, {2, 2}, {1, 2}, {0, 2}, {4, 5}, {4, 4},
{4, 3}, {4, 2}, {4, 1}, {3, 1}, {2, 1}, {1, 1}, {0, 1}, {4, 0}, {3, 0},
{2, 0}, {1, 0}, {0, 0}
};
if (java.util.Arrays.deepEquals(got, exp)) {
++pass;
} else {
System.out.println("FAIL 885-2");
}
}
++total;
{
int[][] got = s.spiralMatrixIII2(1, 4, 0, 0);
if (java.util.Arrays.deepEquals(got, new int[][]{{0, 0}, {0, 1}, {0, 2}, {0, 3}})) {
++pass;
} else {
System.out.println("FAIL 885-3");
}
}
++total;
{
int[][] got = s.spiralMatrixIII(3, 3, 0, 0);
if (got.length == 9) {
++pass;
} else {
System.out.println("FAIL 885-4");
}
}
if (pass == total) {
System.out.println("== 0885 PASS ==");
} else {
System.out.println("== 0885 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O((max(rows, cols))^2) | O(rows * cols) |
| 解法二 | O((max(rows, cols))^2) | O(rows * cols) |
886. Possible Bipartition(可能的二分法)
题目描述
给定一组 n 人(编号为 1, 2, ..., n),我们想把所有人分进任意大小的两组。每个人都可能
不喜欢其他人,那么他们不应该属于同一组。
给定整数 n 和数组 dislikes,其中 dislikes[i] = [a_i, b_i],表示不允许将编号为 a_i
和 b_i 的人归入同一组。
当可以用这种方法将所有人分进两组时,返回 true;否则返回 false。
示例 1
- 输入:
n= 4, dislikes = [[1,2],[1,3],[2,4]] - 输出:
true
示例 2
- 输入:
n= 3, dislikes = [[1,2],[1,3],[2,3]] - 输出:
false
示例 3
- 输入:
n= 5, dislikes = [[1,2],[2,3],[3,4],[4,5],[1,5]] - 输出:
false提示: 1 <= n <= 20000 <= dislikes.length <= 10^4dislikes[i].length == 21 <= dislikes[i][j] <= na_i < b_idislikes中每一组都不同
解题思路
“互不喜欢的人不能同组”等价于把每个人看成节点、dislike 看成边,判断该图是否二分图。
解法一(染色法 BFS,主提交):建邻接表,用 color 数组记录 0/1 两组。遍历每个未染色
节点做 BFS 染色:相邻节点必须染不同色,若冲突返回 false。所有连通分量都成功即 true。
解法二(染色法 DFS,对照实现):同样 0/1 染色,用递归深度优先遍历;实现更简洁。
代码实现
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
class Solution {
private List<Integer>[] g;
private int[] color;
// 解法一:BFS 染色(推荐提交)
public boolean possibleBipartition(int n, int[][] dislikes) {
g = new List[n + 1];
for (int i = 1; i <= n; ++i) {
g[i] = new ArrayList<>();
}
for (int[] d : dislikes) {
g[d[0]].add(d[1]);
g[d[1]].add(d[0]);
}
color = new int[n + 1]; // 0 未染色,1/2 两种颜色
for (int i = 1; i <= n; ++i) {
if (color[i] == 0) {
if (!bfs(i)) {
return false;
}
}
}
return true;
}
private boolean bfs(int start) {
Queue<Integer> q = new ArrayDeque<>();
q.offer(start);
color[start] = 1;
while (!q.isEmpty()) {
int u = q.poll();
for (int v : g[u]) {
if (color[v] == 0) {
color[v] = 3 - color[u];
q.offer(v);
} else if (color[v] == color[u]) {
return false;
}
}
}
return true;
}
// 解法二:DFS 染色(对照实现)
public boolean possibleBipartition2(int n, int[][] dislikes) {
g = new List[n + 1];
for (int i = 1; i <= n; ++i) {
g[i] = new ArrayList<>();
}
for (int[] d : dislikes) {
g[d[0]].add(d[1]);
g[d[1]].add(d[0]);
}
color = new int[n + 1];
for (int i = 1; i <= n; ++i) {
if (color[i] == 0) {
color[i] = 1;
if (!dfs2(i)) {
return false;
}
}
}
return true;
}
private boolean dfs2(int u) {
for (int v : g[u]) {
if (color[v] == 0) {
color[v] = 3 - color[u];
if (!dfs2(v)) {
return false;
}
} else if (color[v] == color[u]) {
return false;
}
}
return true;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] d1 = {{1, 2}, {1, 3}, {2, 4}};
int[][] d2 = {{1, 2}, {1, 3}, {2, 3}};
int[][] d3 = {{1, 2}, {2, 3}, {3, 4}, {4, 5}, {1, 5}};
++total;
if (s.possibleBipartition(4, d1) && s.possibleBipartition2(4, d1)) {
++pass;
} else {
System.out.println("FAIL 886-1");
}
++total;
if (!s.possibleBipartition(3, d2) && !s.possibleBipartition2(3, d2)) {
++pass;
} else {
System.out.println("FAIL 886-2");
}
++total;
if (!s.possibleBipartition(5, d3) && !s.possibleBipartition2(5, d3)) {
++pass;
} else {
System.out.println("FAIL 886-3");
}
++total;
if (s.possibleBipartition(5, new int[][]{}) && s.possibleBipartition2(5, new int[][]{})) {
++pass;
} else {
System.out.println("FAIL 886-4");
}
if (pass == total) {
System.out.println("== 0886 PASS ==");
} else {
System.out.println("== 0886 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n + m) | O(n + m) |
| 解法二 | O(n + m) | O(n + m)(含递归栈) |
887. Super Egg Drop(鸡蛋掉落)
题目描述
给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。
已知存在楼层 f,满足 0 <= f <= n,任何从高于 f 的楼层落下的鸡蛋都会碎,从 f 层或
比它低的楼层落下的鸡蛋都不会破。
每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果
鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中重复使用
这枚鸡蛋。
请你计算并返回要确定 f 确切的值的最小操作次数是多少?
示例 1
- 输入:
k= 1,n= 2 - 输出:2
- 解释: 鸡蛋从 1 楼掉落。如果它碎了,肯定能得出 f = 0。 否则,鸡蛋从 2 楼掉落。如果它碎了,肯定能得出 f = 1。 如果它没碎,那么肯定能得出 f = 2。 因此,在最坏的情况下,我们需要移动 2 次以确定 f 是多少。
示例 2
- 输入:
k= 2,n= 6 - 输出:3
示例 3
- 输入:
k= 3,n= 14 - 输出:4 提示:
1 <= k <= 1001 <= n <= 10^4
解题思路
经典“鸡蛋掉落”问题:dp[k][n] 表示 k 个鸡蛋测 n 层所需最少次数。若在某层扔:
- 碎了:还要测下方 (k-1, 层-1);
- 没碎:还要测上方 (k, n-层)。
直接做三维会超时,需利用单调性优化或换定义。
解法一(dp[m][k] 反向定义,主提交):
dp[m][k]= 允许 m 次操作、k 个鸡蛋最多能确定的 楼层数。递推dp[m][k] = dp[m-1][k-1] + dp[m-1][k] + 1(一次测试后,碎了用 (k-1,m-1)、 没碎用 (k,m-1))。不断增大m,直到dp[m][k] >= n,此时 m 即答案。时间约为 O(k * ans)。 解法二(记忆化搜索 + 二分,对照实现):dfs(k, n)用经典公式1 + min over x of max(dfs(k-1, x-1), dfs(k, n-x)),配合记忆化与对 x 二分(单调性), 复杂度 O(k n log n) 量级,直观但较慢。
代码实现
class Solution {
// 解法一:dp[m][k] = m 次操作 k 个鸡蛋最多测的楼层数(推荐提交)
public int superEggDrop(int k, int n) {
int[] dp = new int[k + 1]; // dp[j] = 当前操作次数下,j 个鸡蛋最多层数
int m = 0;
while (dp[k] < n) {
++m;
for (int j = k; j >= 1; --j) {
dp[j] = dp[j] + dp[j - 1] + 1;
}
}
return m;
}
// 解法二:记忆化 DFS + 二分优化(对照实现)
public int superEggDrop2(int k, int n) {
memo2 = new int[k + 1][n + 1];
return dfs2(k, n);
}
private int[][] memo2;
private int dfs2(int k, int n) {
if (k == 1 || n == 0) {
return n == 0 ? 0 : n;
}
if (n == 1) {
return 1;
}
if (memo2[k][n] != 0) {
return memo2[k][n];
}
// 求最优扔蛋楼层 x:dfs(k-1, x-1) 递增,dfs(k, n-x) 递减,两者交点附近最优
int lo = 1, hi = n;
while (lo + 1 < hi) {
int mid = (lo + hi) / 2;
int broken = dfs2(k - 1, mid - 1);
int safe = dfs2(k, n - mid);
if (broken <= safe) {
lo = mid;
} else {
hi = mid;
}
}
int best = Math.min(
Math.max(dfs2(k - 1, lo - 1), dfs2(k, n - lo)),
Math.max(dfs2(k - 1, hi - 1), dfs2(k, n - hi)));
return memo2[k][n] = best + 1;
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] cases = {{1, 2}, {2, 6}, {3, 14}, {2, 10}, {100, 10000}, {4, 5000}};
// 期望值由 Python 同款 dp[m][k] 验证:k=4,n=5000 需要 19 次
int[] exp = {2, 3, 4, 4, 14, 19};
for (int t = 0; t < cases.length; ++t) {
++total;
int g1 = s.superEggDrop(cases[t][0], cases[t][1]);
if (g1 == exp[t]) {
++pass;
} else {
System.out.println("FAIL 887-" + (t + 1) + ": got " + g1 + " exp " + exp[t]);
}
}
++total;
if (s.superEggDrop2(2, 6) == 3 && s.superEggDrop2(3, 14) == 4 && s.superEggDrop2(1, 5) == 5) {
++pass;
} else {
System.out.println("FAIL 887-m2");
}
if (pass == total) {
System.out.println("== 0887 PASS ==");
} else {
System.out.println("== 0887 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(k * ans) | O(k)(滚动数组) |
| 解法二 | O(k * n * log n) | O(k * n) |
888. Fair Candy Swap(公平的糖果交换)
题目描述
爱丽丝和鲍勃拥有不同总数量的糖果。给你两个数组 aliceSizes 和 bobSizes,
aliceSizes[i] 是爱丽丝拥有的第 i 盒糖果中的糖果数量,bobSizes[j] 是鲍勃拥有的第 j 盒
糖果中的糖果数量。
两人想要互相交换一盒糖果,这样在交换之后,他们就可以拥有相同总数量的糖果。一个人拥有的
糖果总数量是他们每盒糖果数量的总和。
返回一个整数数组 answer,其中 answer[0] 是爱丽丝必须交换的糖果盒中的糖果的个数,
answer[1] 是鲍勃必须交换的糖果盒中的糖果的个数。如果存在多个答案,你可以返回其中任何
一个。题目测试用例保证存在与输入对应的答案。
示例 1
- 输入:
aliceSizes= [1,1],bobSizes= [2,2] - 输出:[1,2]
示例 2
- 输入:
aliceSizes= [1,2],bobSizes= [2,3] - 输出:[1,2]
示例 3
- 输入:
aliceSizes= [2],bobSizes= [1,3] - 输出:[2,3]
示例 4
- 输入:
aliceSizes= [1,2,5],bobSizes= [2,4] - 输出:[5,4] 提示:
1 <= aliceSizes.length, bobSizes.length <= 10^41 <= aliceSizes[i], bobSizes[j] <= 10^5- 爱丽丝和鲍勃的糖果总数量不同
- 题目数据保证对于给定的输入至少存在一个有效答案
解题思路
设爱丽丝总数 sa、鲍勃总数 sb。交换 a 与 b 后相等,则
sa - a + b == sb - b + a,即 b == a + (sb - sa)/2。令 delta = (sb - sa) / 2,
只需在爱丽丝的每一盒 a 中找是否存在 a + delta 属于鲍勃的糖果盒。
解法一(哈希集,主提交):把 bobSizes 放入 HashSet,遍历 aliceSizes,若
a + delta 在集合中,返回 [a, a + delta]。
解法二(排序双指针,对照实现):两个数组排序后,用差值 delta 双指针同时推进找配对。
复杂度 O(n log n),仅作对照。
代码实现
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
class Solution {
// 解法一:哈希集合(推荐提交)
public int[] fairCandySwap(int[] aliceSizes, int[] bobSizes) {
int sa = 0, sb = 0;
for (int x : aliceSizes) {
sa += x;
}
Set<Integer> bobSet = new HashSet<>();
for (int x : bobSizes) {
sb += x;
bobSet.add(x);
}
int delta = (sb - sa) / 2; // b - a == delta
for (int a : aliceSizes) {
int b = a + delta;
if (bobSet.contains(b)) {
return new int[]{a, b};
}
}
return new int[]{0, 0}; // 题目保证有解
}
// 解法二:排序双指针(对照实现)
public int[] fairCandySwap2(int[] aliceSizes, int[] bobSizes) {
int sa = 0, sb = 0;
for (int x : aliceSizes) {
sa += x;
}
for (int x : bobSizes) {
sb += x;
}
int delta = (sb - sa) / 2;
java.util.Arrays.sort(aliceSizes);
java.util.Arrays.sort(bobSizes);
int i = 0, j = 0;
while (i < aliceSizes.length && j < bobSizes.length) {
int diff = bobSizes[j] - aliceSizes[i];
if (diff == delta) {
return new int[]{aliceSizes[i], bobSizes[j]};
} else if (diff < delta) {
++j;
} else {
++i;
}
}
return new int[]{0, 0};
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[][] as = {{1, 1}, {1, 2}, {2}, {1, 2, 5}, {2, 4}};
int[][] bs = {{2, 2}, {2, 3}, {1, 3}, {2, 4}, {1, 3}};
// 校验条件:交换后相等
for (int t = 0; t < as.length; ++t) {
++total;
int[] r1 = s.fairCandySwap(as[t], bs[t]);
int[] r2 = s.fairCandySwap2(as[t], bs[t]);
if (valid(as[t], bs[t], r1) && valid(as[t], bs[t], r2)) {
++pass;
} else {
System.out.println("FAIL 888-" + (t + 1) + ": " + java.util.Arrays.toString(r1));
}
}
if (pass == total) {
System.out.println("== 0888 PASS ==");
} else {
System.out.println("== 0888 FAIL ==");
System.exit(1);
}
}
private static boolean valid(int[] a, int[] b, int[] r) {
int sa = 0, sb = 0;
for (int x : a) {
sa += x;
}
for (int x : b) {
sb += x;
}
return sa - r[0] + r[1] == sb - r[1] + r[0];
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n + m) | O(m) |
| 解法二 | O(n log n + m log m) | O(1) |
889. Construct Binary Tree from Preorder and Postorder Traversal
题目描述
给定两个整数数组,preorder 和 postorder,其中 preorder 是一个具有无重复值的二叉树
的前序遍历,postorder 是同一棵树的后序遍历,重构并返回二叉树。
如果存在多个答案,您可以返回其中任何一个。
示例 1
- 输入:
preorder= [1,2,4,5,3,6,7],postorder= [4,5,2,6,7,3,1] - 输出:[1,2,3,4,5,6,7]
示例 2
- 输入:
preorder= [1],postorder= [1] - 输出:[1] 提示:
1 <= preorder.length <= 301 <= preorder[i] <= preorder.lengthpreorder中所有值都不同postorder.length == preorder.length1 <= postorder[i] <= postorder.lengthpostorder中所有值都不同- 保证
preorder和postorder是同一棵二叉树的前序遍历和后序遍历
解题思路
前序 [根, 左子树..., 右子树...],后序 [左子树..., 右子树..., 根]。已知根后,左子树的
根 preorder[l+1] 必为左子树首个节点(若左子树存在),其在后序中的位置可确定左子树规模;
之后左右子树递归即可,先序与后序都能唯一划分。
解法一(递归 + 哈希定位,主提交):用 Map 记录后序每个值的下标。递归函数
dfs(pl, pr, ol, or) 对应先序区间与后序区间:root = preorder[pl];若区间只剩根则直接
返回;否则左子节点为 preorder[pl+1],查它在后序中的位置 idx,左子树长度为
idx - ol + 1,据此切分并递归。
解法二(公共祖先递归 + 指针,对照实现):设根为 root,先序第一个即根;若子区间长度
为 1 直接返回;否则 root.left = 以 preorder[pl+1] 为根的子树,其大小由 postorder 中
该值位置决定。套路与解法一相同,仅把区间递归改为“值驱动”写法,便于对照。
代码实现
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
class Solution {
// 解法一:递归 + 哈希定位左子树边界(推荐提交)
public TreeNode constructFromPrePost(int[] preorder, int[] postorder) {
Map<Integer, Integer> pos = new HashMap<>();
for (int i = 0; i < postorder.length; ++i) {
pos.put(postorder[i], i);
}
this.pre = preorder;
this.pos = pos;
return build(0, preorder.length - 1, 0, postorder.length - 1);
}
private int[] pre;
private Map<Integer, Integer> pos;
private TreeNode build(int pl, int pr, int ol, int or) {
if (pl > pr) {
return null;
}
TreeNode root = new TreeNode(pre[pl]);
if (pl == pr) {
return root;
}
// 左子树根为先序下一个元素,其位置决定左子树大小
int leftVal = pre[pl + 1];
int idx = pos.get(leftVal);
int leftLen = idx - ol + 1;
root.left = build(pl + 1, pl + leftLen, ol, idx);
root.right = build(pl + leftLen + 1, pr, idx + 1, or - 1);
return root;
}
// 解法二:值驱动递归(对照实现)
public TreeNode constructFromPrePost2(int[] preorder, int[] postorder) {
Map<Integer, Integer> pos2 = new HashMap<>();
for (int i = 0; i < postorder.length; ++i) {
pos2.put(postorder[i], i);
}
return build2(preorder, 0, preorder.length - 1, postorder, 0, postorder.length - 1, pos2);
}
private TreeNode build2(int[] pre, int pl, int pr, int[] post, int ol, int or, Map<Integer, Integer> pos2) {
if (pl > pr) {
return null;
}
TreeNode root = new TreeNode(pre[pl]);
if (pl == pr) {
return root;
}
int leftVal = pre[pl + 1];
int idx = pos2.get(leftVal);
int leftLen = idx - ol + 1;
root.left = build2(pre, pl + 1, pl + leftLen, post, ol, idx, pos2);
root.right = build2(pre, pl + leftLen + 1, pr, post, idx + 1, or - 1, pos2);
return root;
}
// 二叉树节点定义
static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
}
// 层序转回数组(用于对比)
private static String dump(TreeNode root) {
StringBuilder sb = new StringBuilder("[");
Queue<TreeNode> q = new ArrayDeque<>();
q.offer(root);
while (!q.isEmpty()) {
TreeNode cur = q.poll();
if (cur == null) {
continue;
}
sb.append(cur.val).append(",");
q.offer(cur.left);
q.offer(cur.right);
}
String s = sb.toString();
return "[" + s.substring(0, s.length() - 1) + "]";
}
private static String dump2(TreeNode root) {
StringBuilder sb = new StringBuilder();
preOrder(root, sb);
return sb.toString();
}
private static void preOrder(TreeNode node, StringBuilder sb) {
if (node == null) {
return;
}
sb.append(node.val).append(",");
preOrder(node.left, sb);
preOrder(node.right, sb);
}
// ===== 本地测试 =====
public static void main(String[] args) {
Solution s = new Solution();
int pass = 0, total = 0;
int[] pre1 = {1, 2, 4, 5, 3, 6, 7};
int[] post1 = {4, 5, 2, 6, 7, 3, 1};
++total;
{
TreeNode t1 = s.constructFromPrePost(pre1, post1);
TreeNode t2 = s.constructFromPrePost2(pre1, post1);
if (dump2(t1).equals("1,2,4,5,3,6,7,") && dump2(t2).equals("1,2,4,5,3,6,7,")) {
++pass;
} else {
System.out.println("FAIL 889-1: " + dump2(t1));
}
}
++total;
{
int[] pre2 = {1}, post2 = {1};
TreeNode t1 = s.constructFromPrePost(pre2, post2);
if (t1 != null && t1.val == 1 && t1.left == null && t1.right == null) {
++pass;
} else {
System.out.println("FAIL 889-2");
}
}
++total;
{
int[] pre3 = {2, 1, 3}, post3 = {3, 1, 2};
TreeNode t1 = s.constructFromPrePost(pre3, post3);
if (dump2(t1).equals("2,1,3,")) {
++pass;
} else {
System.out.println("FAIL 889-3: " + dump2(t1));
}
}
++total;
{
int[] pre4 = {6, 4, 3, 5, 8, 7}, post4 = {3, 5, 4, 7, 8, 6};
TreeNode t1 = s.constructFromPrePost(pre4, post4);
if (dump2(t1).equals("6,4,3,5,8,7,")) {
++pass;
} else {
System.out.println("FAIL 889-4: " + dump2(t1));
}
}
if (pass == total) {
System.out.println("== 0889 PASS ==");
} else {
System.out.println("== 0889 FAIL ==");
System.exit(1);
}
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 解法一 | O(n) | O(n)(递归栈 + 哈希) |
| 解法二 | O(n) | O(n) |