LeetCode 2081-2100. 题解合集
文章目录
- [LeetCode 2081-2100. 题解合集](#LeetCode 2081-2100. 题解合集)
-
- [2081. Sum of k-Mirror Numbers(k 镜像数字的和)](#2081. Sum of k-Mirror Numbers(k 镜像数字的和))
- [2082. The Number of Rich Customers(富有客户的数量)](#2082. The Number of Rich Customers(富有客户的数量))
- [2083. Substrings That Begin and End With the Same Letter(求以相同字母开头和结尾的子串总数)](#2083. Substrings That Begin and End With the Same Letter(求以相同字母开头和结尾的子串总数))
- [2084. Drop Type 1 Orders for Customers With Type 0 Orders(为订单类型为 0 的客户删除类型为 1 的订单)](#2084. Drop Type 1 Orders for Customers With Type 0 Orders(为订单类型为 0 的客户删除类型为 1 的订单))
- [2085. Count Common Words With One Occurrence(统计出现过一次的公共字符串)](#2085. Count Common Words With One Occurrence(统计出现过一次的公共字符串))
- [2086. Minimum Number of Food Buckets to Feed the Hamsters(喂食仓鼠的最小食物桶数)](#2086. Minimum Number of Food Buckets to Feed the Hamsters(喂食仓鼠的最小食物桶数))
- [2087. Minimum Cost Homecoming of a Robot in a Grid(网格图中机器人回家的最小代价)](#2087. Minimum Cost Homecoming of a Robot in a Grid(网格图中机器人回家的最小代价))
- [2088. Count Fertile Pyramids in a Land(统计农场中肥沃金字塔的数目)](#2088. Count Fertile Pyramids in a Land(统计农场中肥沃金字塔的数目))
- [2089. Find Target Indices After Sorting Array(找出数组排序后的目标下标)](#2089. Find Target Indices After Sorting Array(找出数组排序后的目标下标))
- [2090. K Radius Subarray Averages(半径为 k 的子数组平均值)](#2090. K Radius Subarray Averages(半径为 k 的子数组平均值))
- [2091. Removing Minimum and Maximum From Array(从数组中移除最大值和最小值)](#2091. Removing Minimum and Maximum From Array(从数组中移除最大值和最小值))
- [2092. Find All People With Secret(找出知晓秘密的所有专家)](#2092. Find All People With Secret(找出知晓秘密的所有专家))
- [2093. Minimum Cost to Reach City With Discounts(前往目标城市的最小费用)](#2093. Minimum Cost to Reach City With Discounts(前往目标城市的最小费用))
- [2094. Finding 3-Digit Even Numbers(找出 3 位偶数)](#2094. Finding 3-Digit Even Numbers(找出 3 位偶数))
- [2095. Delete the Middle Node of a Linked List(删除链表的中间节点)](#2095. Delete the Middle Node of a Linked List(删除链表的中间节点))
- [2096. Step-By-Step Directions From a Binary Tree Node to Another(从二叉树一个节点到另一个节点每一步的方向)](#2096. Step-By-Step Directions From a Binary Tree Node to Another(从二叉树一个节点到另一个节点每一步的方向))
- [2097. Valid Arrangement of Pairs(合法重新排列数对)](#2097. Valid Arrangement of Pairs(合法重新排列数对))
- [2098. Subsequence of Size K With the Largest Even Sum(长度为 K 的最大偶数和子序列)](#2098. Subsequence of Size K With the Largest Even Sum(长度为 K 的最大偶数和子序列))
- [2099. Find Subsequence of Length K With the Largest Sum(找到和最大的长度为 K 的子序列)](#2099. Find Subsequence of Length K With the Largest Sum(找到和最大的长度为 K 的子序列))
- [2100. Find Good Days to Rob the Bank(适合野炊的日子)](#2100. Find Good Days to Rob the Bank(适合野炊的日子))
2081. Sum of k-Mirror Numbers(k 镜像数字的和)
题目描述
k 镜像数字 指一个在十进制下是一个回文数,且它在 k 进制下也是一个回文数的正整数。
例如,9 在十进制下是回文数,它在二进制下是 1001(1001 也是回文),所以 9 是 2 镜像数字。
给你整数 k 和一个整数 n,请返回 k 镜像数字中最小 的 n 个数之和。
示例 1
k = 2, n = 5,输出 25。解释:最小的 5 个 2 镜像数字是 1, 3, 5, 7, 9,和为 25。
示例 2
k = 3, n = 7,输出 499。解释:最小的 7 个 3 镜像数字是 1, 2, 4, 8, 121, 151, 212,和为 499。
示例 3
k = 7, n = 17,输出 20379000。
解题思路
解法一(主提交版本)
按长度生成十进制回文数 + 检查 k 进制回文(主提交版本)
- 依次按回文数长度
l = 1, 2, 3, ...生成十进制回文数:取前半部分i ∈ [10^((l-1)/2), 10^((l+1)/2)),
镜像到后半(长度奇数时去掉末位再接)构造完整回文;该生成顺序保证回文数递增(同长度内 i 递增)。 - 对每个十进制回文数
v,转成 k 进制数字序列并检查是否回文;是则累加并计数,取到 n 个即返回。
复杂度:生成数量与 n 成正比,每个候选 O(log_k v),n ≤ 30 极小。
解法二
暴力从 1 枚举到 n 个为止(对照) - 从 1 开始逐个数检查"十进制回文且 k 进制回文",取满 n 个退出。
- 复杂度取决于实际数密度,仅用于小规模交叉验证。
代码实现
java
public class Solution {
// 解法一:构造十进制回文(主提交版本)
public long kMirror(int k, int n) {
long ans = 0;
for (int l = 1; ; l++) {
int low = (int) Math.pow(10, (l - 1) / 2);
int high = (int) Math.pow(10, (l + 1) / 2);
for (int i = low; i < high; i++) {
long v = i;
int j = (l % 2 == 0) ? i : i / 10; // 镜像的后半起点
while (j > 0) {
v = v * 10 + j % 10;
j /= 10;
}
if (isKPalindrome(v, k)) {
ans += v;
n--;
if (n == 0) {
return ans;
}
}
}
}
}
private boolean isKPalindrome(long x, int k) {
java.util.List<Integer> digits = new java.util.ArrayList<>();
while (x > 0) {
digits.add((int) (x % k));
x /= k;
}
for (int i = 0, j = digits.size() - 1; i < j; i++, j--) {
if (!digits.get(i).equals(digits.get(j))) {
return false;
}
}
return true;
}
// 解法二:暴力枚举(对照)
public long kMirrorBrute(int k, int n) {
long ans = 0;
long x = 1;
while (n > 0) {
if (isDecPalindrome(x) && isKPalindrome(x, k)) {
ans += x;
n--;
}
x++;
}
return ans;
}
private boolean isDecPalindrome(long x) {
String s = Long.toString(x);
for (int i = 0, j = s.length() - 1; i < j; i++, j--) {
if (s.charAt(i) != s.charAt(j)) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.kMirror(2, 5)); // 25
System.out.println(sol.kMirrorBrute(2, 5)); // 25
System.out.println(sol.kMirror(3, 7)); // 499
System.out.println(sol.kMirrorBrute(3, 7)); // 499
System.out.println(sol.kMirror(7, 5)); // 自测
System.out.println(sol.kMirrorBrute(7, 5));
}
}
复杂度分析
复杂度:生成数量与 n 成正比,每个候选 O(log_k v),n ≤ 30 极小。
2082. The Number of Rich Customers(富有客户的数量)
题目类型:数据库(SQL)
题目描述
表:Store
+-------------+------+
| Column Name | Type |
+-------------+------+
| bill_id | int |
| customer_id | int |
| amount | int |
+-------------+------+
bill_id 是这个表的主键(具有唯一值的列)。每一行包含一个订单的金额及相关客户的信息。
编写解决方案,找出至少有一个 订单的金额严格大于 500 的客户的数量。
示例 1:
输入:
Store 表:
+---------+-------------+--------+
| bill_id | customer_id | amount |
+---------+-------------+--------+
| 6 | 1 | 549 |
| 8 | 1 | 834 |
| 4 | 2 | 394 |
| 11 | 3 | 657 |
| 13 | 3 | 257 |
+---------+-------------+--------+
输出:
+------------+
| rich_count |
+------------+
| 2 |
+------------+
解释:
客户 1 有 2 个订单金额严格大于 500。
客户 2 没有任何订单金额严格大于 500。
客户 3 有 1 个订单金额严格大于 500。
解题思路
解法一:先过滤去重再计数(推荐)
先用 WHERE amount > 500 过滤出所有满足条件的订单,再对 customer_id 做 DISTINCT 去重,最后统计去重后的客户数。过滤缩小数据量后再去重,DISTINCT 的开销更小。
解法二(对照):子查询 EXISTS 判定 ------对每个客户用 EXISTS 检查其是否存在金额大于 500 的订单,存在即计入。更贴近「至少有一个」的语义,且不依赖过滤顺序,便于理解。
代码实现
sql
-- 解法一(推荐):先过滤再 DISTINCT 去重
SELECT COUNT(DISTINCT customer_id) AS rich_count
FROM Store
WHERE amount > 500;
sql
-- 解法二(对照):EXISTS 子查询逐客户判定
SELECT COUNT(*) AS rich_count
FROM (
SELECT DISTINCT customer_id
FROM Store s1
WHERE EXISTS (
SELECT 1 FROM Store s2
WHERE s2.customer_id = s1.customer_id
AND s2.amount > 500
)
) t;
复杂度分析
- 时间复杂度 :
O(n),其中n为Store表行数;一次扫描过滤 + 去重(或 EXISTS 逐行判定)。 - 空间复杂度 :
O(k),k为满足条件的客户数;DISTINCT/去重需要存储去重集合。
2083. Substrings That Begin and End With the Same Letter(求以相同字母开头和结尾的子串总数)
题目描述
给你一个仅由小写英文字母组成的字符串 s,返回以相同字母 开头和结尾的子字符串的总数 。
子字符串是字符串中连续的非空字符序列。
例:s = "abca" 中以 'a' 开头和结尾的子串有 "a"、"abca",以 'b' 的有 "b",以 'c' 的有 "c",共 4 个。
示例 1
s = "abcba",输出 7。解释:a:a(2个)、b:b、c:c......实际 "a"(0), "abcba"(0..4), "b"(1), "bcb"(1..3),
"c"(2), "b"(3), "a"(4) 共 7 个。
示例 2
s = "abacad",输出 9。
示例 3
s = "a",输出 1。
解题思路
解法一(主提交版本)
计数累加(主提交版本)
- 若某个字母
c在字符串中出现f次,则"以 c 开头且以 c 结尾"的子串 = 任意选两个出现位置
(含同一位置)组成区间:C(f,1) + C(f,2) = f*(f+1)/2,即遍历后对每个 c 累加。 - 更简洁:从左到右扫描,
cnt[c]为已出现的次数,当前字符作为结尾 ,可以与之前任意一个
c(含自身重复)组成以 c 开头的子串,故每个字符贡献++cnt[c]。 - 时间复杂度:O(n),单次扫描累加。
- 空间复杂度 :O(1),仅 26 个字母的计数数组。
解法二
暴力枚举子串(对照) - 枚举每个子串检查首尾字符是否相同。O(n^2),仅用于交叉验证。
代码实现
java
public class Solution {
// 解法一:计数累加(主提交版本)
public long numberOfSubstrings(String s) {
int[] cnt = new int[26];
long ans = 0;
for (int i = 0; i < s.length(); i++) {
int c = s.charAt(i) - 'a';
cnt[c]++;
ans += cnt[c]; // 以当前字符结尾,与前面各次出现(含此次)组成子串
}
return ans;
}
// 解法二:暴力枚举(对照)
public long numberOfSubstringsBrute(String s) {
int n = s.length();
long ans = 0;
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
if (s.charAt(i) == s.charAt(j)) {
ans++;
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.numberOfSubstrings("abcba")); // 7
System.out.println(sol.numberOfSubstringsBrute("abcba")); // 7
System.out.println(sol.numberOfSubstrings("abacad")); // 9
System.out.println(sol.numberOfSubstringsBrute("abacad")); // 9
System.out.println(sol.numberOfSubstrings("a")); // 1
System.out.println(sol.numberOfSubstringsBrute("a")); // 1
}
}
复杂度分析
- 时间复杂度:O(n),单次扫描累加。
- 空间复杂度:O(1),仅 26 个字母的计数数组。
2084. Drop Type 1 Orders for Customers With Type 0 Orders(为订单类型为 0 的客户删除类型为 1 的订单)
题目类型:数据库(SQL)
题目描述
活动表:Orders
+-------------+------+
| Column Name | Type |
+-------------+------+
| order_id | int |
| customer_id | int |
| order_type | int |
+-------------+------+
order_id 是此表的主键列。此表的每一行都表示订单的 ID、订购该订单的客户的 ID 以及订单类型。订单可以是类型 0 或类型 1。
编写 SQL 查询,根据以下条件报告所有订单:
- 如果客户至少有一个类型为
0的订单,则不要报告该客户的任何类型为1的订单。 - 否则,报告客户的所有订单。
按任意顺序返回结果表。
示例 1:
输入:
Orders table:
+----------+-------------+------------+
| order_id | customer_id | order_type |
+----------+-------------+------------+
| 1 | 1 | 0 |
| 2 | 1 | 0 |
| 11 | 2 | 0 |
| 12 | 2 | 1 |
| 21 | 3 | 1 |
| 22 | 3 | 0 |
| 31 | 4 | 1 |
| 32 | 4 | 1 |
+----------+-------------+------------+
输出:
+----------+-------------+------------+
| order_id | customer_id | order_type |
+----------+-------------+------------+
| 31 | 4 | 1 |
| 32 | 4 | 1 |
| 1 | 1 | 0 |
| 2 | 1 | 0 |
| 11 | 2 | 0 |
| 22 | 3 | 0 |
+----------+-------------+------------+
解释:
客户 1 有两个类型为 0 的订单,我们两个都返回。
客户 2 有类型为 0 和类型为 1 的订单,我们只返回类型为 0 的订单。
客户 3 有类型为 0 和类型为 1 的订单,我们只返回类型为 0 的订单。
客户 4 有两个类型为 1 的订单,我们两个都返回。
解题思路
解法一:NOT EXISTS 排除(推荐)
对每个订单 o,只有当其所属客户不存在 任何类型为 0 的订单时才报告(此时该订单自身必为类型 1);若客户有类型 0 订单,则类型 1 订单被滤除、类型 0 订单自然保留(NOT EXISTS 内部查不到「别人的类型 0 订单」这一说法不成立,正确写法见下):
正确语义是二选一:
- 客户没有 类型
0订单 → 报告该客户全部订单(全部是类型1); - 客户有 类型
0订单 → 报告该客户全部订单中类型为0的。
因此可直接用 NOT EXISTS (同客户存在 order_type = 0 的订单) 排除:对类型 1 订单,若客户有 0 订单则被排除;对类型 0 订单,同客户存在 0 订单(即它自己)?------不对,需用条件区分,标准写法见代码块(用 o.order_type = 0 OR NOT EXISTS(...))。
解法二(对照):窗口函数 MARK 标记 ------用 MAX(order_type = 0) OVER (PARTITION BY customer_id) 得到每个客户是否拥有类型 0 订单的标记,再过滤保留「标记为 0 的全部订单」或「标记为 1 且 order_type = 0 的订单」。
代码实现
sql
-- 解法一(推荐):类型0订单全保留 + 无类型0订单客户的类型1订单保留
SELECT order_id, customer_id, order_type
FROM Orders o
WHERE o.order_type = 0
OR NOT EXISTS (
SELECT 1 FROM Orders x
WHERE x.customer_id = o.customer_id
AND x.order_type = 0
);
sql
-- 解法二(对照):窗口函数标记客户是否含类型0订单
WITH marked AS (
SELECT order_id, customer_id, order_type,
MAX(CASE WHEN order_type = 0 THEN 1 ELSE 0 END)
OVER (PARTITION BY customer_id) AS has_zero
FROM Orders
)
SELECT order_id, customer_id, order_type
FROM marked
WHERE has_zero = 0 -- 客户无类型0订单:全部订单保留(都是类型1)
OR order_type = 0; -- 客户有类型0订单:只保留类型0订单
复杂度分析
- 时间复杂度 :
O(n²)(最坏)或O(n log n)(有索引时),其中n为Orders行数;NOT EXISTS逐行子查询依赖customer_id索引,窗口函数版本为O(n log n)(分区排序)。 - 空间复杂度 :
O(n),窗口函数需要按customer_id分区排序的中间结果。
2085. Count Common Words With One Occurrence(统计出现过一次的公共字符串)
题目描述
给你两个字符串数组 words1 和 words2,请你返回在两个字符串数组中都恰好出现一次的字符串的数目。
示例 1
words1 = ["leetcode","is","amazing","as","is"], words2 = ["amazing","leetcode","is"],输出 2。
- 解释 :在两个数组中都恰好出现一次的字符串是 "
leetcode" 与 "amazing",共 2 个。
示例 2
words1 = ["b","bb","bbb"], words2 = ["a","aa","aaa"],输出 0。
示例 3
words1 = ["a","ab"], words2 = ["a","a","a","ab"],输出 1("ab" 在 words2 出现 1 次,"a" 出现 3 次)。
解题思路
解法一(主提交版本)
双哈希表计数(主提交版本)
- 分别统计
words1、words2中各字符串出现次数; - 遍历
words1的计数表:若该字符串在两个表中都计数为 1,答案 +1。
复杂度:O(|words1| + |words2|)。
解法二
双层扫描纯计数(对照) - 不需要哈希表:对每个在 words1 中出现次数为 1 的词,统计它在 words2 中只出现 1 次才计入。
- O(n^2) 仅用于交叉验证。
代码实现
java
public class Solution {
// 解法一:双哈希表(主提交版本)
public int countWords(String[] words1, String[] words2) {
java.util.Map<String, Integer> m1 = new java.util.HashMap<>();
java.util.Map<String, Integer> m2 = new java.util.HashMap<>();
for (String w : words1) {
m1.put(w, m1.getOrDefault(w, 0) + 1);
}
for (String w : words2) {
m2.put(w, m2.getOrDefault(w, 0) + 1);
}
int ans = 0;
for (java.util.Map.Entry<String, Integer> e : m1.entrySet()) {
if (e.getValue() == 1 && m2.getOrDefault(e.getKey(), 0) == 1) {
ans++;
}
}
return ans;
}
// 解法二:双层扫描(对照)
public int countWordsBrute(String[] words1, String[] words2) {
int ans = 0;
for (String w : words1) {
int c1 = 0, c2 = 0;
for (String x : words1) {
if (x.equals(w)) {
c1++;
}
}
if (c1 != 1) {
continue;
}
for (String x : words2) {
if (x.equals(w)) {
c2++;
}
}
if (c2 == 1) {
ans++;
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.countWords(new String[]{"leetcode", "is", "amazing", "as", "is"},
new String[]{"amazing", "leetcode", "is"})); // 2
System.out.println(sol.countWordsBrute(new String[]{"leetcode", "is", "amazing", "as", "is"},
new String[]{"amazing", "leetcode", "is"})); // 2
System.out.println(sol.countWords(new String[]{"b", "bb", "bbb"}, new String[]{"a", "aa", "aaa"})); // 0
System.out.println(sol.countWordsBrute(new String[]{"b", "bb", "bbb"}, new String[]{"a", "aa", "aaa"})); // 0
System.out.println(sol.countWords(new String[]{"a", "ab"}, new String[]{"a", "a", "a", "ab"})); // 1
System.out.println(sol.countWordsBrute(new String[]{"a", "ab"}, new String[]{"a", "a", "a", "ab"})); // 1
}
}
复杂度分析
复杂度:O(|words1| + |words2|)。
2086. Minimum Number of Food Buckets to Feed the Hamsters(喂食仓鼠的最小食物桶数)
题目描述
给你一个下标从 0 开始的字符串 street。street 中每个字符要么是表示房间的字符 'H',要么是表示
空位的字符 '.'。你可以在空位放置食物桶,每个桶可以喂食相邻 的房间:如果桶放在 i 处,则房间 i - 1
和 i + 1(存在的话)的仓鼠都可以吃到食物。
你需要喂养所有 的仓鼠,每个房间至多 放置一个桶,每个桶只能喂养它自己相邻的房间。
返回能喂养所有仓鼠所需的最少食物桶数量;如果不可能喂养所有仓鼠,返回 -1。
示例 1
street = "H..H",输出 2。解释:在位置 1 放桶喂 H(0),位置 2 放桶喂 H(3)。
示例 2
street = ".H.H.",输出 1。解释:在位置 2 放一个桶即可同时喂 H(1) 和 H(3)。
示例 3
street = ".HHH.",输出 -1。解释:中间 H(2) 两侧都是 H,无法放桶喂它。
解题思路
解法一(主提交版本)
贪心扫描(主提交版本)
- 从左到右扫描,遇到仓鼠
'H'时优先看它右边的空位能否放桶(i+1是.):放桶喂当前 H
并顺便可能喂下一个相邻 H(贪心:右优先可覆盖更多);否则看左边空位(i-1是.)放桶;
两者都不行则返回 -1。 - 放桶后跳过该桶可能覆盖的仓鼠(
i += 2)避免重复。 - 时间复杂度:O(n),单次贪心扫描。
- 空间复杂度 :O(1)。
解法二
暴力枚举桶放置方案(对照) - 汇总所有空位下标 bit(共
total个),用mask从0到(1<<total)-1枚举"哪些空位放桶",
检查每种方案是否喂饱全部仓鼠,取合法方案中的最小桶数;Integer.bitCount(mask)统计桶数。 - 数据范围小(长度 ≤ 1e5 时不可行,仅用于小规模交叉验证;官方约束 n ≤ 1e5,此处用作对照)。
代码实现
java
public class Solution {
// 解法一:贪心(主提交版本)
public int minimumBuckets(String street) {
int n = street.length();
int ans = 0;
for (int i = 0; i < n; i++) {
if (street.charAt(i) == 'H') {
if (i + 1 < n && street.charAt(i + 1) == '.') {
ans++;
i += 2; // 跳过该桶可能覆盖的右侧仓鼠(若存在)
} else if (i > 0 && street.charAt(i - 1) == '.') {
ans++;
} else {
return -1;
}
}
}
return ans;
}
// 解法二:暴力枚举桶放置方案(对照)
public int minimumBucketsBrute(String street) {
int n = street.length();
int total = 0;
for (int i = 0; i < n; i++) {
if (street.charAt(i) == '.') {
total++;
}
}
int best = -1;
for (int mask = 0; mask < (1 << total); mask++) {
int cnt = Integer.bitCount(mask);
if (best != -1 && cnt >= best) {
continue;
}
boolean[] feed = new boolean[n];
int p = 0;
for (int i = 0; i < n; i++) {
if (street.charAt(i) == '.') {
if ((mask & (1 << p)) != 0) {
if (i > 0) {
feed[i - 1] = true;
}
if (i + 1 < n) {
feed[i + 1] = true;
}
}
p++;
}
}
boolean ok = true;
for (int i = 0; i < n; i++) {
if (street.charAt(i) == 'H' && !feed[i]) {
ok = false;
break;
}
}
if (ok && (best == -1 || cnt < best)) {
best = cnt;
}
}
return best;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.minimumBuckets("H..H")); // 2
System.out.println(sol.minimumBucketsBrute("H..H")); // 2
System.out.println(sol.minimumBuckets(".H.H.")); // 1
System.out.println(sol.minimumBucketsBrute(".H.H.")); // 1
System.out.println(sol.minimumBuckets(".HHH.")); // -1
System.out.println(sol.minimumBucketsBrute(".HHH.")); // -1
System.out.println(sol.minimumBuckets("H.H")); // 自测
System.out.println(sol.minimumBucketsBrute("H.H")); // 自测
}
}
复杂度分析
- 时间复杂度:O(n),单次贪心扫描。
- 空间复杂度:O(1)。
2087. Minimum Cost Homecoming of a Robot in a Grid(网格图中机器人回家的最小代价)
题目描述
给你一个 m x n 的网格图,其中 (0, 0) 是最左上角的格子,(m - 1, n - 1) 是最右下角的格子。
给你整数数组 startPos = [startrow, startcol] 表示机器人初始位置,homePos = [homerow, homecol]
表示机器人的家。机器人每一步可以往上下左右四个方向移动且不能出界,每一步都有代价:
如果向上下移动到第 r 行的格子,代价为 rowCosts[r];如果向左右移动到第 c 列的格子,代价为
colCosts[c]。返回机器人回家需要的最小总代价。
示例 1
startPos = [1,0], homePos = [2,3], rowCosts = [5,4,3], colCosts = [8,2,6,7] → 18
示例 2
startPos = [0,0], homePos = [0,0], rowCosts = [5], colCosts = [26] → 0
解题思路
解法一(主提交版本)
数学求和(主提交版本)
- 关键观察:无论怎么走,只要最终从 startrow 到达 homerow,就必然会进入 (startrow, homerow]
之间的每一行(向下)或 [homerow, startrow) 之间的每一行(向上),行代价与路径形状无关;
列同理。因此最小代价 = 「进入的目标行代价之和(不含起始行)」+「进入的目标列代价之和(不含起始列)」。 - 上下移动:若 startrow < homerow,累加 rowCostsstartrow+1 ... homerow;反之累加
rowCostshomerow ... startrow-1;相同则为 0。左右移动对称处理 colCosts。
复杂度:O(m + n),其中 m 为行数、n 为列数。
解法二
Dijkstra 最短路(对照) - 把网格中的每个格子视为节点、四向移动视为边(边权 = 进入目标格子的行/列代价),跑堆优化
Dijkstra 求 (startrow, startcol) 到 (homerow, homecol) 的最短路(经过点数为 1 时无移动代价 0)。 - 该解法仅适用于小规模网格(节点数 ≤ 10^4 时可行),用于交叉验证解法一的正确性。
复杂度:O(R * C * log(R * C)),R*C 为网格节点数。
代码实现
java
public class Solution {
// 解法一:数学求和(主提交版本)
public int minCost(int[] startPos, int[] homePos, int[] rowCosts, int[] colCosts) {
int ans = 0;
int sr = startPos[0], sc = startPos[1];
int hr = homePos[0], hc = homePos[1];
if (sr < hr) {
for (int r = sr + 1; r <= hr; r++) ans += rowCosts[r];
} else {
for (int r = hr; r < sr; r++) ans += rowCosts[r];
}
if (sc < hc) {
for (int c = sc + 1; c <= hc; c++) ans += colCosts[c];
} else {
for (int c = hc; c < sc; c++) ans += colCosts[c];
}
return ans;
}
// 解法二:Dijkstra 最短路(对照,仅小规模验证)
public int minCostDijkstra(int[] startPos, int[] homePos, int[] rowCosts, int[] colCosts) {
int m = rowCosts.length, n = colCosts.length;
int s = startPos[0] * n + startPos[1];
int t = homePos[0] * n + homePos[1];
int[] dist = new int[m * n];
java.util.Arrays.fill(dist, Integer.MAX_VALUE);
dist[s] = 0;
java.util.PriorityQueue<int[]> pq = new java.util.PriorityQueue<>((a, b) -> a[1] - b[1]);
pq.offer(new int[]{s, 0});
int[] dr = {-1, 1, 0, 0};
int[] dc = {0, 0, -1, 1};
while (!pq.isEmpty()) {
int[] cur = pq.poll();
int idx = cur[0], d = cur[1];
if (d != dist[idx]) continue;
if (idx == t) return d;
int r = idx / n, c = idx % n;
for (int k = 0; k < 4; k++) {
int nr = r + dr[k], nc = c + dc[k];
if (nr < 0 || nr >= m || nc < 0 || nc >= n) continue;
int w = (k < 2) ? rowCosts[nr] : colCosts[nc];
int nid = nr * n + nc;
if (dist[nid] > d + w) {
dist[nid] = d + w;
pq.offer(new int[]{nid, dist[nid]});
}
}
}
return dist[t];
}
public static void main(String[] args) {
Solution sol = new Solution();
// 示例 1
int r1 = sol.minCost(new int[]{1, 0}, new int[]{2, 3}, new int[]{5, 4, 3}, new int[]{8, 2, 6, 7});
int r1b = sol.minCostDijkstra(new int[]{1, 0}, new int[]{2, 3}, new int[]{5, 4, 3}, new int[]{8, 2, 6, 7});
System.out.println(r1 + " " + r1b); // 18 18
// 示例 2
int r2 = sol.minCost(new int[]{0, 0}, new int[]{0, 0}, new int[]{5}, new int[]{26});
int r2b = sol.minCostDijkstra(new int[]{0, 0}, new int[]{0, 0}, new int[]{5}, new int[]{26});
System.out.println(r2 + " " + r2b); // 0 0
// 自测:向下+向左混合
int r3 = sol.minCost(new int[]{2, 3}, new int[]{0, 0}, new int[]{5, 4, 3}, new int[]{8, 2, 6, 7});
int r3b = sol.minCostDijkstra(new int[]{2, 3}, new int[]{0, 0}, new int[]{5, 4, 3}, new int[]{8, 2, 6, 7});
System.out.println(r3 + " " + r3b); // (rowCosts[0]+rowCosts[1]=5+4) + (colCosts[0]+colCosts[1]+colCosts[2]=8+2+6) = 25
}
}
复杂度分析
复杂度:O(m + n),其中 m 为行数、n 为列数。
复杂度:O(R * C * log(R * C)),R*C 为网格节点数。
2088. Count Fertile Pyramids in a Land(统计农场中肥沃金字塔的数目)
题目描述
有一个矩形网格状的农场,划分为 m 行 n 列的单元格,每个格子要么是肥沃的(1)要么是贫瘠的(0)。
金字塔区域:顶端是其最上方的格子,高度 h 覆盖 h 行,区域内任意格子 (i, j) 满足
r <= i <= r + h - 1 且 c - (i - r) <= j <= c + (i - r),区域内格子数大于 1 且全部肥沃。
倒金字塔:顶端是其最下方的格子,高度 h 满足 r - h + 1 <= i <= r 且 c - (r - i) <= j <= c + (r - i),
同样格子数大于 1 且全部肥沃。
返回网格中金字塔和倒金字塔的总数目。
示例 1
grid = [[0,1,1,0],[1,1,1,1]] → 2(2 个正金字塔,无倒金字塔)
示例 2
grid = [[1,1,1],[1,1,1]] → 2(1 正 1 倒)
示例 3
grid = [[1,0,1],[0,0,0],[1,0,1]] → 0
示例 4
grid = [[1,1,1,1,0],[1,1,1,1,1],[1,1,1,1,1],[0,1,0,0,1]] → 13(7 正 6 倒)
解题思路
解法一(主提交版本)
动态规划(主提交版本)
- 定义
f[i][j]= 以(i, j)为顶端(最上方格子)能形成的最大金字塔高度(行数,至少 1)。 - 逆向递推(从最后一行往上):若
grid[i][j] == 0则f[i][j] = 0;否则
f[i][j] = 1 + min(f[i+1][j-1], f[i+1][j], f[i+1][j+1])(边界视为 0)。 - 以
(i, j)为顶端的金字塔高度可取 2...fij,共贡献f[i][j] - 1个,累加全部格子即正金字塔数。 - 倒金字塔等价于把网格上下翻转后按同样规则统计(此时"顶端"仍取最上方,即翻转后的最下)。
翻转后 count + 原 count即为答案。
复杂度:O(m * n),共扫描两遍矩阵。
解法二
暴力逐高度校验(对照) - 枚举每个格子作为顶端、枚举高度 h ≥ 2,逐格检查金字塔覆盖区间内的所有格子是否都肥沃;
正、倒两个方向各做一遍。仅适用于小规模矩阵(m*n ≤ 10^4),用于交叉验证。
复杂度:O(m * n * min(m,n) * h^2) 最坏,仅作对照。
代码实现
java
public class Solution {
// 解法一:动态规划(主提交版本)
public int countPyramids(int[][] grid) {
int m = grid.length, n = grid[0].length;
int ans = countOne(grid); // 正金字塔
int[][] rev = new int[m][n];
for (int i = 0; i < m; i++) rev[i] = grid[m - 1 - i]; // 上下翻转
ans += countOne(rev); // 倒金字塔(翻转后按正金字塔统计)
return ans;
}
// 统计以"顶端最上"方向的金字塔数目
private int countOne(int[][] grid) {
int m = grid.length, n = grid[0].length;
int[][] f = new int[m][n];
int sum = 0;
for (int i = m - 1; i >= 0; i--) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
if (i == m - 1 || j == 0 || j == n - 1) {
f[i][j] = 1;
} else {
f[i][j] = 1 + Math.min(f[i + 1][j - 1],
Math.min(f[i + 1][j], f[i + 1][j + 1]));
}
sum += f[i][j] - 1;
}
}
}
return sum;
}
// 解法二:暴力逐高度校验(对照,仅小规模验证)
public int countPyramidsBrute(int[][] grid) {
int m = grid.length, n = grid[0].length;
return countBrute(grid, true) + countBrute(grid, false);
}
// up=true 正金字塔(顶端在上),up=false 倒金字塔(顶端在下)
private int countBrute(int[][] grid, boolean up) {
int m = grid.length, n = grid[0].length;
int cnt = 0;
for (int r = 0; r < m; r++) {
for (int c = 0; c < n; c++) {
int top = up ? r : r, bottom = up ? r : r;
// 逐高度扩展
for (int h = 2; ; h++) {
int r0 = up ? r : r - h + 1; // 覆盖区首行
int r1 = up ? r + h - 1 : r; // 覆盖区末行
if (r0 < 0 || r1 >= m) break;
boolean ok = true;
for (int i = r0; i <= r1 && ok; i++) {
int d = up ? (i - r) : (r - i); // 与顶端的行距
for (int j = c - d; j <= c + d; j++) {
if (j < 0 || j >= n || grid[i][j] != 1) { ok = false; break; }
}
}
if (!ok) break;
cnt++;
}
}
}
return cnt;
}
public static void main(String[] args) {
Solution sol = new Solution();
int[][] g1 = {{0, 1, 1, 0}, {1, 1, 1, 1}};
System.out.println(sol.countPyramids(g1) + " " + sol.countPyramidsBrute(g1)); // 2 2
int[][] g2 = {{1, 1, 1}, {1, 1, 1}};
System.out.println(sol.countPyramids(g2) + " " + sol.countPyramidsBrute(g2)); // 2 2
int[][] g3 = {{1, 0, 1}, {0, 0, 0}, {1, 0, 1}};
System.out.println(sol.countPyramids(g3) + " " + sol.countPyramidsBrute(g3)); // 0 0
int[][] g4 = {{1, 1, 1, 1, 0}, {1, 1, 1, 1, 1}, {1, 1, 1, 1, 1}, {0, 1, 0, 0, 1}};
System.out.println(sol.countPyramids(g4) + " " + sol.countPyramidsBrute(g4)); // 13 13
int[][] g5 = {{1}};
System.out.println(sol.countPyramids(g5) + " " + sol.countPyramidsBrute(g5)); // 0 0
}
}
复杂度分析
复杂度:O(m * n),共扫描两遍矩阵。
复杂度:O(m * n * min(m,n) * h^2) 最坏,仅作对照。
2089. Find Target Indices After Sorting Array(找出数组排序后的目标下标)
题目描述
给你一个下标从 0 开始的整数数组 nums 以及一个目标元素 target。
目标下标是满足 nums[i] == target 的下标 i。将 nums 按非递减顺序排序后,
返回由目标下标组成的列表(递增顺序);若不存在返回空列表。
示例 1
nums = [1,2,5,2,3], target = 2 → [1,2]
示例 2
nums = [1,2,5,2,3], target = 3 → [3]
示例 3
nums = [1,2,5,2,3], target = 4 → []
解题思路
解法一(主提交版本)
计数 + 计算(主提交版本)
- 统计
less = 小于 target 的元素个数、cnt = 等于 target 的元素个数。 - 排序后 target 的下标恰为
[less, less + cnt - 1],依次填入列表即可,无需真正排序。
复杂度:O(n) 时间,O(1) 额外空间(不计输出)。
解法二
排序 + 遍历(对照) - 复制数组升序排序后,扫描等于 target 的下标依次加入列表。
复杂度:O(n log n),仅作对照。
代码实现
java
public class Solution {
// 解法一:计数 + 计算(主提交版本)
public java.util.List<Integer> targetIndices(int[] nums, int target) {
int less = 0, cnt = 0;
for (int x : nums) {
if (x < target) less++;
else if (x == target) cnt++;
}
java.util.List<Integer> ans = new java.util.ArrayList<>();
for (int i = less; i < less + cnt; i++) ans.add(i);
return ans;
}
// 解法二:排序 + 遍历(对照)
public java.util.List<Integer> targetIndicesSort(int[] nums, int target) {
int[] sorted = nums.clone();
java.util.Arrays.sort(sorted);
java.util.List<Integer> ans = new java.util.ArrayList<>();
for (int i = 0; i < sorted.length; i++) {
if (sorted[i] == target) ans.add(i);
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.targetIndices(new int[]{1, 2, 5, 2, 3}, 2)); // [1, 2]
System.out.println(sol.targetIndicesSort(new int[]{1, 2, 5, 2, 3}, 2)); // [1, 2]
System.out.println(sol.targetIndices(new int[]{1, 2, 5, 2, 3}, 3)); // [3]
System.out.println(sol.targetIndices(new int[]{1, 2, 5, 2, 3}, 5)); // [4]
System.out.println(sol.targetIndices(new int[]{1, 2, 5, 2, 3}, 4)); // []
System.out.println(sol.targetIndices(new int[]{1}, 1)); // [0]
}
}
复杂度分析
复杂度:O(n) 时间,O(1) 额外空间(不计输出)。
复杂度:O(n log n),仅作对照。
2090. K Radius Subarray Averages(半径为 k 的子数组平均值)
题目描述
给你一个下标从 0 开始的数组 nums(长度 n)和一个整数 k。
半径为 k 的子数组平均值:以下标 i 为中心、半径 k,即下标 [i - k, i + k] 内所有元素的平均值,
使用截断式整数除法(向下取整)。若 i 前或后不足 k 个元素,则平均值为 -1。
构建并返回长度为 n 的数组 avgs。
示例 1
nums = [7,4,3,9,1,8,5,2,6], k = 3 → [-1,-1,-1,5,4,4,-1,-1,-1]
示例 2
nums = [100000], k = 0 → [100000]
示例 3
nums = [8], k = 100000 → [-1]
解题思路
解法一(主提交版本)
前缀和(主提交版本)
- 构建前缀和
prefix,prefix[i+1] = nums[0] + ... + nums[i]。 - 对每个下标
i:若i - k < 0或i + k >= n则置 -1;否则窗口和为
prefix[i+k+1] - prefix[i-k],平均值为(窗口和) / (2k+1)(整数除法)。
复杂度:O(n) 时间,O(n) 额外空间(前缀和数组)。
解法二
滑动窗口(对照) - 维护一个长度为
2k+1的窗口和,从中心i = k开始向右滑:每步减去左端、加上右端得到新窗口和。
与解法一思路等价(同为 O(n)),但无前缀和数组,仅用于交叉验证。
复杂度:O(n) 时间,O(n) 空间存结果。
代码实现
java
public class Solution {
// 解法一:前缀和(主提交版本)
public int[] getAverages(int[] nums, int k) {
int n = nums.length;
long[] prefix = new long[n + 1];
for (int i = 0; i < n; i++) prefix[i + 1] = prefix[i] + nums[i];
int[] ans = new int[n];
int len = 2 * k + 1;
for (int i = 0; i < n; i++) {
if (i - k < 0 || i + k >= n) {
ans[i] = -1;
} else {
ans[i] = (int) ((prefix[i + k + 1] - prefix[i - k]) / len);
}
}
return ans;
}
// 解法二:滑动窗口(对照)
public int[] getAveragesSlide(int[] nums, int k) {
int n = nums.length;
int[] ans = new int[n];
java.util.Arrays.fill(ans, -1);
if (n < 2 * k + 1) return ans;
long sum = 0;
for (int i = 0; i < 2 * k + 1; i++) sum += nums[i];
int len = 2 * k + 1;
for (int i = k; i + k < n; i++) {
ans[i] = (int) (sum / len);
if (i + k + 1 < n) {
sum -= nums[i - k];
sum += nums[i + k + 1];
}
}
return ans;
}
private static void print(int[] a) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < a.length; i++) {
if (i > 0) sb.append(",");
sb.append(a[i]);
}
sb.append("]");
System.out.println(sb.toString());
}
public static void main(String[] args) {
Solution sol = new Solution();
print(sol.getAverages(new int[]{7, 4, 3, 9, 1, 8, 5, 2, 6}, 3)); // [-1,-1,-1,5,4,4,-1,-1,-1]
print(sol.getAveragesSlide(new int[]{7, 4, 3, 9, 1, 8, 5, 2, 6}, 3));
print(sol.getAverages(new int[]{100000}, 0)); // [100000]
print(sol.getAveragesSlide(new int[]{100000}, 0));
print(sol.getAverages(new int[]{8}, 100000)); // [-1]
print(sol.getAveragesSlide(new int[]{8}, 100000));
}
}
复杂度分析
复杂度:O(n) 时间,O(n) 额外空间(前缀和数组)。
复杂度:O(n) 时间,O(n) 空间存结果。
2091. Removing Minimum and Maximum From Array(从数组中移除最大值和最小值)
题目描述
给你一个下标从 0 开始的数组 nums,由若干互不相同的整数组成。数组中有唯一的最小值和最大值,
你的目标是从数组中移除这两个元素。一次删除操作定义为从数组前面移除一个元素或从数组后面移除一个元素。
返回将最小值和最大值都移除需要的最小删除次数。
示例 1
nums = [2,10,7,5,4,1,8,6] → 5(前面删 2 个 + 后面删 3 个)
示例 2
nums = [0,-4,19,1,8,-2,-3,5] → 3(全部从前面删)
示例 3
nums = [101] → 1(最小=最大,只需删 1 次)
解题思路
解法一(主提交版本)
枚举删除方向组合(主提交版本)
- 先找出最小值的下标
minIdx和最大值的下标maxIdx,令l = min(minIdx, maxIdx)、r = max(...)
(指两个目标元素中靠左/靠右的下标)。 - 三种候选方案取最小:
- 全部从左边删:
r + 1次(从左边删掉覆盖到靠右目标的所有元素); - 全部从右边删:
n - l次(删掉靠左目标及其右侧全部); - 一个从左边删、一个从右边删:
(l + 1) + (n - r)次。
- 三者取最小值即为答案。
复杂度:O(n) 时间(找最值下标),O(1) 空间。
解法二
扫描前后删(对照) - 直接按"从前面删几个、再从后面删几个"枚举
front从 0 到 n,并利用下标检查
两个目标是否都被覆盖,取合法方案中最小的front + back。仅用于交叉验证。
复杂度:O(n^2) 最坏,仅作对照。
代码实现
java
public class Solution {
// 解法一:枚举删除方向组合(主提交版本)
public int minimumDeletions(int[] nums) {
int n = nums.length;
int minIdx = 0, maxIdx = 0;
for (int i = 1; i < n; i++) {
if (nums[i] < nums[minIdx]) minIdx = i;
if (nums[i] > nums[maxIdx]) maxIdx = i;
}
int l = Math.min(minIdx, maxIdx);
int r = Math.max(minIdx, maxIdx);
int ans = Math.min(r + 1, n - l); // 全左删 / 全右删
ans = Math.min(ans, (l + 1) + (n - r)); // 左一个右一个
return ans;
}
// 解法二:暴力枚举 front/back(对照)
public int minimumDeletionsBrute(int[] nums) {
int n = nums.length;
int minIdx = 0, maxIdx = 0;
for (int i = 1; i < n; i++) {
if (nums[i] < nums[minIdx]) minIdx = i;
if (nums[i] > nums[maxIdx]) maxIdx = i;
}
int best = n + 1;
for (int front = 0; front <= n; front++) {
for (int back = 0; back <= n - front; back++) {
// 前面删 [0, front-1],后面删 [n-back, n-1]
boolean hasMin = minIdx < front || minIdx >= n - back;
boolean hasMax = maxIdx < front || maxIdx >= n - back;
if (hasMin && hasMax) best = Math.min(best, front + back);
}
}
return best;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.minimumDeletions(new int[]{2, 10, 7, 5, 4, 1, 8, 6}) + " "
+ sol.minimumDeletionsBrute(new int[]{2, 10, 7, 5, 4, 1, 8, 6})); // 5 5
System.out.println(sol.minimumDeletions(new int[]{0, -4, 19, 1, 8, -2, -3, 5}) + " "
+ sol.minimumDeletionsBrute(new int[]{0, -4, 19, 1, 8, -2, -3, 5})); // 3 3
System.out.println(sol.minimumDeletions(new int[]{101}) + " "
+ sol.minimumDeletionsBrute(new int[]{101})); // 1 1
System.out.println(sol.minimumDeletions(new int[]{1, 2, 3, 4, 5}) + " "
+ sol.minimumDeletionsBrute(new int[]{1, 2, 3, 4, 5})); // 3 3
}
}
复杂度分析
复杂度:O(n) 时间(找最值下标),O(1) 空间。
复杂度:O(n^2) 最坏,仅作对照。
2092. Find All People With Secret(找出知晓秘密的所有专家)
题目描述
有 n 个专家从 0 到 n - 1 编号。meetings[i] = [xi, yi, timei] 表示专家 xi 和 yi 在时间 timei
开会。专家 0 最初在时间 0 把秘密分享给专家 firstPerson,之后每次会议中只要有一方知晓秘密,
另一方即刻知晓(同一时间可跨多场会议瞬时传播)。返回所有会议结束后知晓秘密的专家列表,顺序任意。
示例 1
n=6, meetings=[[1,2,5],[2,3,8],[1,5,10]], firstPerson=1 → [0,1,2,3,5]
示例 2
n=4, meetings=[[3,1,3],[1,2,2],[0,3,3]], firstPerson=3 → [0,1,3]
示例 3
n=5, meetings=[[3,4,2],[1,2,1],[2,3,1]], firstPerson=1 → [0,1,2,3,4]
解题思路
解法一(主提交版本)
按时间分组的并查集 + BFS(主提交版本)
- 将会议按时间从小到大排序,同一时间的会议视为一个"时间片"。
- 每个时间片内:把该片所有会议涉及的专家用并查集合并(含知情者),然后把知情者的集合根节点
加入队列做 BFS,把该连通块内所有专家标记为知情。 - 注意:每个时间片结束后需要重置并查集(该片内不连通、但共享知情者的两个块在下一时刻
不应继续被"误连")------采用的做法是:在当前片内先合并、再基于队列扩散标记;处理下一片时重新初始化。
由于n最大 1e5,逐片 new 并查集 O(n * 时间片数) 会超时,因此用"版本戳"实现:known[]记录
专家是否知情;parent[]每片开始前对本片涉及节点 重置为自身,其余节点保持(但连通性只在片内查询)。
复杂度:O((n + m) * α(n)),m 为会议数。
解法二
时间分片 + 图 BFS(对照) - 同一时间片建立临时邻接表,把所有知情节点作为起点同时 BFS,染色整片可到达的节点为知情。
- 与解法一思路等价:片内连通、片间隔离。仅实现方式不同(图 BFS vs 并查集),用于交叉验证。
复杂度:O(n + m) 时间、O(n + m) 空间。
代码实现
java
public class Solution {
// 解法一:并查集 + 时间分片(主提交版本)
public java.util.List<Integer> findAllPeople(int n, int[][] meetings, int firstPerson) {
// 按时间排序
java.util.Arrays.sort(meetings, (a, b) -> a[2] - b[2]);
boolean[] known = new boolean[n];
known[0] = true;
known[firstPerson] = true;
int m = meetings.length;
int i = 0;
while (i < m) {
int t = meetings[i][2];
// 收集本时间片所有会议
java.util.List<int[]> group = new java.util.ArrayList<>();
while (i < m && meetings[i][2] == t) {
group.add(meetings[i]);
i++;
}
// 本片涉及的节点集合
java.util.Map<Integer, Integer> idMap = new java.util.HashMap<>();
for (int[] e : group) {
idMap.putIfAbsent(e[0], 0);
idMap.putIfAbsent(e[1], 0);
}
int sz = idMap.size();
int idx = 0;
int[] map = new int[sz];
for (int v : idMap.keySet()) map[idx++] = v;
UnionFind uf = new UnionFind(sz);
for (int[] e : group) {
int a = getIndex(map, e[0], sz);
int b = getIndex(map, e[1], sz);
uf.union(a, b);
}
// 已知情者在片内做"并查集根"标记,再扩散
boolean[] rootKnown = new boolean[sz];
for (int k = 0; k < sz; k++) {
int v = map[k];
if (known[v]) rootKnown[uf.find(k)] = true;
}
// BFS/索引扩散:需要把同一根下所有节点标记
// 简单做法:对每个知情根,遍历本片所有节点把同根者标记
boolean changed = true;
while (changed) {
changed = false;
for (int k = 0; k < sz; k++) {
if (rootKnown[uf.find(k)] && !known[map[k]]) {
known[map[k]] = true;
changed = true;
}
}
}
}
java.util.List<Integer> ans = new java.util.ArrayList<>();
for (int v = 0; v < n; v++) if (known[v]) ans.add(v);
return ans;
}
private int getIndex(int[] map, int v, int sz) {
for (int k = 0; k < sz; k++) if (map[k] == v) return k;
return -1;
}
static class UnionFind {
int[] parent;
UnionFind(int n) {
parent = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
}
int find(int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
void union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra != rb) parent[ra] = rb;
}
}
// 解法二:时间分片 + 图 BFS(对照)
public java.util.List<Integer> findAllPeopleBfs(int n, int[][] meetings, int firstPerson) {
java.util.Arrays.sort(meetings, (a, b) -> a[2] - b[2]);
boolean[] known = new boolean[n];
known[0] = true;
known[firstPerson] = true;
int m = meetings.length;
int i = 0;
while (i < m) {
int t = meetings[i][2];
java.util.List<int[]> group = new java.util.ArrayList<>();
while (i < m && meetings[i][2] == t) {
group.add(meetings[i]);
i++;
}
// 建临时图
int sz = group.size();
// 节点压缩
java.util.TreeMap<Integer, Integer> idx = new java.util.TreeMap<>();
for (int[] e : group) {
idx.putIfAbsent(e[0], 0);
idx.putIfAbsent(e[1], 0);
}
int cnt = 0;
int[] nodeList = new int[idx.size()];
for (int v : idx.keySet()) nodeList[cnt++] = v;
java.util.List<java.util.List<Integer>> g = new java.util.ArrayList<>();
for (int k = 0; k < cnt; k++) g.add(new java.util.ArrayList<>());
for (int[] e : group) {
int a = getIndex(nodeList, e[0], cnt);
int b = getIndex(nodeList, e[1], cnt);
g.get(a).add(b);
g.get(b).add(a);
}
// 从所有知情节点出发 BFS
boolean[] vis = new boolean[cnt];
java.util.ArrayDeque<Integer> q = new java.util.ArrayDeque<>();
for (int k = 0; k < cnt; k++) {
if (known[nodeList[k]] && !vis[k]) {
vis[k] = true;
q.add(k);
}
}
while (!q.isEmpty()) {
int u = q.poll();
known[nodeList[u]] = true;
for (int v : g.get(u)) {
if (!vis[v]) {
vis[v] = true;
q.add(v);
}
}
}
}
java.util.List<Integer> ans = new java.util.ArrayList<>();
for (int v = 0; v < n; v++) if (known[v]) ans.add(v);
return ans;
}
private static void print(java.util.List<Integer> l) {
java.util.Collections.sort(l);
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < l.size(); i++) {
if (i > 0) sb.append(",");
sb.append(l.get(i));
}
sb.append("]");
System.out.println(sb.toString());
}
public static void main(String[] args) {
Solution sol = new Solution();
print(sol.findAllPeople(6, new int[][]{{1, 2, 5}, {2, 3, 8}, {1, 5, 10}}, 1)); // [0,1,2,3,5]
print(sol.findAllPeopleBfs(6, new int[][]{{1, 2, 5}, {2, 3, 8}, {1, 5, 10}}, 1));
print(sol.findAllPeople(4, new int[][]{{3, 1, 3}, {1, 2, 2}, {0, 3, 3}}, 3)); // [0,1,3]
print(sol.findAllPeopleBfs(4, new int[][]{{3, 1, 3}, {1, 2, 2}, {0, 3, 3}}, 3));
print(sol.findAllPeople(5, new int[][]{{3, 4, 2}, {1, 2, 1}, {2, 3, 1}}, 1)); // [0,1,2,3,4]
print(sol.findAllPeopleBfs(5, new int[][]{{3, 4, 2}, {1, 2, 1}, {2, 3, 1}}, 1));
}
}
复杂度分析
复杂度:O((n + m) * α(n)),m 为会议数。
复杂度:O(n + m) 时间、O(n + m) 空间。
2093. Minimum Cost to Reach City With Discounts(前往目标城市的最小费用)
题目描述
有 n 个城市(编号 0 到 n-1),highways[i] = [city1, city2, toll] 表示城市间一条双向公路,
通行费用 toll。你有 discounts 次折扣机会:使用一次折扣可使某条公路通行费降为 toll / 2(向下取整),
每条公路最多使用一次折扣。返回从城市 0 到城市 n-1 的最小费用;若不可达返回 -1。
示例 1
n=5, highways=[[0,1,4],[2,1,3],[1,4,11],[3,2,3],[3,4,2]], discounts=1 → 9
示例 2
n=4, highways=[[1,3,17],[1,2,7],[3,2,5],[0,1,6],[3,0,20]], discounts=20 → 8
示例 3
n=4, highways=[[0,1,3],[2,3,2]], discounts=0 → -1
解题思路
解法一(主提交版本)
分层 Dijkstra(主提交版本)
- 状态为
(城市, 已用折扣数),dist[c][d]表示到达城市 c 且已用 d 次折扣的最小费用。 - 堆中存
{费用, 城市, 已用折扣数},松弛时两类转移:
- 不打折通过边 (u, v, w):
dist[v][d] = min(dist[v][d], dist[u][d] + w); - 若 d < discounts,打折通过:
dist[v][d+1] = min(dist[v][d+1], dist[u][d] + w/2)。
- 答案 =
min(dist[n-1][0..discounts])。
复杂度:O((n + m) * discounts * log(n * discounts)),n≤1000、m≤1000、discounts≤500 可接受。
解法二
SPFA + 分层(对照) - 同样的分层状态,改用队列 SPFA 松弛;实现更简单但平摊复杂度更差。用于交叉验证。
代码实现
java
public class Solution {
// 解法一:分层 Dijkstra(主提交版本)
public int minimumCost(int n, int[][] highways, int discounts) {
java.util.List<java.util.List<int[]>> g = new java.util.ArrayList<>();
for (int i = 0; i < n; i++) g.add(new java.util.ArrayList<>());
for (int[] h : highways) {
g.get(h[0]).add(new int[]{h[1], h[2]});
g.get(h[1]).add(new int[]{h[0], h[2]});
}
long[][] dist = new long[n][discounts + 1];
for (int i = 0; i < n; i++) java.util.Arrays.fill(dist[i], Long.MAX_VALUE);
dist[0][0] = 0;
// 堆元素 {费用, 城市, 已用折扣}
java.util.PriorityQueue<long[]> pq = new java.util.PriorityQueue<>((a, b) -> Long.compare(a[0], b[0]));
pq.offer(new long[]{0, 0, 0});
while (!pq.isEmpty()) {
long[] cur = pq.poll();
long cost = cur[0];
int u = (int) cur[1], d = (int) cur[2];
if (cost != dist[u][d]) continue;
for (int[] e : g.get(u)) {
int v = e[0], w = e[1];
// 不打折
if (cost + w < dist[v][d]) {
dist[v][d] = cost + w;
pq.offer(new long[]{dist[v][d], v, d});
}
// 打折
if (d < discounts && cost + w / 2 < dist[v][d + 1]) {
dist[v][d + 1] = cost + w / 2;
pq.offer(new long[]{dist[v][d + 1], v, d + 1});
}
}
}
long ans = Long.MAX_VALUE;
for (int d = 0; d <= discounts; d++) ans = Math.min(ans, dist[n - 1][d]);
return ans == Long.MAX_VALUE ? -1 : (int) ans;
}
// 解法二:分层 SPFA(对照)
public int minimumCostSpfa(int n, int[][] highways, int discounts) {
java.util.List<java.util.List<int[]>> g = new java.util.ArrayList<>();
for (int i = 0; i < n; i++) g.add(new java.util.ArrayList<>());
for (int[] h : highways) {
g.get(h[0]).add(new int[]{h[1], h[2]});
g.get(h[1]).add(new int[]{h[0], h[2]});
}
long[][] dist = new long[n][discounts + 1];
for (int i = 0; i < n; i++) java.util.Arrays.fill(dist[i], Long.MAX_VALUE);
boolean[][] inQueue = new boolean[n][discounts + 1];
dist[0][0] = 0;
java.util.ArrayDeque<int[]> q = new java.util.ArrayDeque<>();
q.add(new int[]{0, 0});
inQueue[0][0] = true;
while (!q.isEmpty()) {
int[] cur = q.poll();
int u = cur[0], d = cur[1];
inQueue[u][d] = false;
for (int[] e : g.get(u)) {
int v = e[0], w = e[1];
if (dist[u][d] + w < dist[v][d]) {
dist[v][d] = dist[u][d] + w;
if (!inQueue[v][d]) { inQueue[v][d] = true; q.add(new int[]{v, d}); }
}
if (d < discounts && dist[u][d] + w / 2 < dist[v][d + 1]) {
dist[v][d + 1] = dist[u][d] + w / 2;
if (!inQueue[v][d + 1]) { inQueue[v][d + 1] = true; q.add(new int[]{v, d + 1}); }
}
}
}
long ans = Long.MAX_VALUE;
for (int d = 0; d <= discounts; d++) ans = Math.min(ans, dist[n - 1][d]);
return ans == Long.MAX_VALUE ? -1 : (int) ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.minimumCost(5, new int[][]{{0, 1, 4}, {2, 1, 3}, {1, 4, 11}, {3, 2, 3}, {3, 4, 2}}, 1)
+ " " + sol.minimumCostSpfa(5, new int[][]{{0, 1, 4}, {2, 1, 3}, {1, 4, 11}, {3, 2, 3}, {3, 4, 2}}, 1)); // 9 9
System.out.println(sol.minimumCost(4, new int[][]{{1, 3, 17}, {1, 2, 7}, {3, 2, 5}, {0, 1, 6}, {3, 0, 20}}, 20)
+ " " + sol.minimumCostSpfa(4, new int[][]{{1, 3, 17}, {1, 2, 7}, {3, 2, 5}, {0, 1, 6}, {3, 0, 20}}, 20)); // 8 8
System.out.println(sol.minimumCost(4, new int[][]{{0, 1, 3}, {2, 3, 2}}, 0)
+ " " + sol.minimumCostSpfa(4, new int[][]{{0, 1, 3}, {2, 3, 2}}, 0)); // -1 -1
System.out.println(sol.minimumCost(3, new int[][]{{0, 1, 10}, {1, 2, 10}}, 1)
+ " " + sol.minimumCostSpfa(3, new int[][]{{0, 1, 10}, {1, 2, 10}}, 1)); // 15 15 (5+10)
}
}
复杂度分析
复杂度:O((n + m) * discounts * log(n * discounts)),n≤1000、m≤1000、discounts≤500 可接受。
2094. Finding 3-Digit Even Numbers(找出 3 位偶数)
题目描述
给你一个整数数组 digits,每个元素是 0-9 的数字,可能有重复。找出所有互不相同且满足以下条件的整数:
- 由
digits中的三个元素按任意顺序依次连接组成;2) 不含前导零;3) 是偶数。
将结果按递增顺序排序后以数组形式返回。
示例 1
digits = [2,1,3,0] → [102,120,130,132,210,230,302,310,312,320]
示例 2
digits = [2,2,8,8,2] → [222,228,282,288,822,828,882](同一数字可重复使用,但不超过其出现次数)
示例 3
digits = [3,7,5] → []
解题思路
解法一(主提交版本)
计数 + 三层枚举(主提交版本)
- 用
cnt[10]统计每个数字在原数组中的出现次数。 - 枚举百位
a(1~9,需 cnta > 0)、十位b(0~9,注意不能与 a 重复使用同一位置的数字:
若 b == a 需要 cntb >= 2;一般化:按"需要的数量"扣减判断)、个位c(偶数 0/2/4/6/8)。 - 组装
a*100 + b*10 + c,用visited[1000]布尔数组去重,最后把命中的结果排序输出。
复杂度:O(9 * 10 * 5) 常数时间,O(1000) 空间。
解法二
DFS 全排列 + Set 去重(对照) - 生成 digits 中所有 3 元有序排列,过滤"无前导零且为偶数",用 TreeSet 去重排序。
复杂度:O(P(n,3)),n 最大 100 时仅作小规模交叉验证。
代码实现
java
public class Solution {
// 解法一:计数 + 三层枚举(主提交版本)
public int[] findEvenNumbers(int[] digits) {
int[] cnt = new int[10];
for (int d : digits) cnt[d]++;
boolean[] visited = new boolean[1000];
for (int a = 1; a <= 9; a++) {
if (cnt[a] == 0) continue;
cnt[a]--;
for (int b = 0; b <= 9; b++) {
if (cnt[b] == 0) continue;
cnt[b]--;
for (int c = 0; c <= 8; c += 2) {
if (cnt[c] == 0) continue;
int num = a * 100 + b * 10 + c;
visited[num] = true;
}
cnt[b]++;
}
cnt[a]++;
}
java.util.List<Integer> list = new java.util.ArrayList<>();
for (int i = 100; i < 1000; i++) if (visited[i]) list.add(i);
int[] ans = new int[list.size()];
for (int i = 0; i < ans.length; i++) ans[i] = list.get(i);
return ans;
}
// 解法二:DFS 全排列 + TreeSet(对照,仅小规模验证)
public int[] findEvenNumbersDfs(int[] digits) {
java.util.TreeSet<Integer> set = new java.util.TreeSet<>();
permute(digits, new boolean[digits.length], 0, 0, set);
int[] ans = new int[set.size()];
int i = 0;
for (int x : set) ans[i++] = x;
return ans;
}
private void permute(int[] digits, boolean[] used, int depth, int val, java.util.TreeSet<Integer> set) {
if (depth == 3) {
if (val >= 100 && val % 2 == 0) set.add(val);
return;
}
for (int i = 0; i < digits.length; i++) {
if (used[i]) continue;
if (depth == 0 && digits[i] == 0) continue;
used[i] = true;
permute(digits, used, depth + 1, val * 10 + digits[i], set);
used[i] = false;
}
}
private static void print(int[] a) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < a.length; i++) {
if (i > 0) sb.append(",");
sb.append(a[i]);
}
sb.append("]");
System.out.println(sb.toString());
}
public static void main(String[] args) {
Solution sol = new Solution();
print(sol.findEvenNumbers(new int[]{2, 1, 3, 0}));
print(sol.findEvenNumbersDfs(new int[]{2, 1, 3, 0})); // [102,120,130,132,210,230,302,310,312,320]
print(sol.findEvenNumbers(new int[]{2, 2, 8, 8, 2}));
print(sol.findEvenNumbersDfs(new int[]{2, 2, 8, 8, 2})); // [222,228,282,288,822,828,882]
print(sol.findEvenNumbers(new int[]{3, 7, 5})); // []
print(sol.findEvenNumbersDfs(new int[]{3, 7, 5})); // []
print(sol.findEvenNumbers(new int[]{0, 2, 4})); // [204,240,402,420]
print(sol.findEvenNumbersDfs(new int[]{0, 2, 4}));
}
}
复杂度分析
复杂度:O(9 * 10 * 5) 常数时间,O(1000) 空间。
复杂度:O(P(n,3)),n 最大 100 时仅作小规模交叉验证。
2095. Delete the Middle Node of a Linked List(删除链表的中间节点)
题目描述
给你一个链表的头节点 head,删除链表的中间节点,并返回修改后的链表头节点。
长度为 n 的链表中间节点是从头数起第 floor(n / 2) 个节点(下标从 0 开始)。
例如 n=1,2,3,4,5 时中间节点下标分别为 0,1,1,2,2。
示例 1
head = [1,3,4,7,1,2,6] → [1,3,4,1,2,6](n=7,下标 3 的节点 7 被删)
示例 2
head = [1,2,3,4] → [1,2,4]
示例 3
head = [2,1] → [2]
解题思路
解法一(主提交版本)
快慢双指针(主提交版本)
- 虚拟头节点
dummy指向 head;slow = dummy,fast = head。 - fast 每次前进 2 步、slow 前进 1 步;当 fast 走到末尾时,slow 恰好停在中间节点的前驱。
- 令
slow.next = slow.next.next即删除中间节点(n=1 时删 head,dummy 仍能兜底)。
复杂度:O(n) 时间一趟,O(1) 空间。
解法二
计数两趟(对照) - 第一趟统计长度 n;第二趟走到下标 floor(n/2) 的前驱删除之(n=1 特判返回 null)。
复杂度:O(n) 时间两趟,O(1) 空间。
代码实现
java
public class Solution {
// Definition for singly-linked list.
public static class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}
// 解法一:快慢双指针(主提交版本)
public ListNode deleteMiddle(ListNode head) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode slow = dummy, fast = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
slow.next = slow.next.next; // 删除中间节点
return dummy.next;
}
// 解法二:计数两趟(对照)
public ListNode deleteMiddleTwoPass(ListNode head) {
int n = 0;
for (ListNode p = head; p != null; p = p.next) n++;
if (n == 1) return null;
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode slow = dummy;
int target = n / 2; // 中间节点下标
for (int i = 0; i < target; i++) slow = slow.next;
slow.next = slow.next.next;
return dummy.next;
}
private static ListNode build(int[] arr) {
ListNode dummy = new ListNode(0);
ListNode p = dummy;
for (int x : arr) {
p.next = new ListNode(x);
p = p.next;
}
return dummy.next;
}
private static void print(ListNode head) {
StringBuilder sb = new StringBuilder("[");
while (head != null) {
if (sb.length() > 1) sb.append(",");
sb.append(head.val);
head = head.next;
}
sb.append("]");
System.out.println(sb.toString());
}
public static void main(String[] args) {
Solution sol = new Solution();
print(sol.deleteMiddle(build(new int[]{1, 3, 4, 7, 1, 2, 6}))); // [1,3,4,1,2,6]
print(sol.deleteMiddleTwoPass(build(new int[]{1, 3, 4, 7, 1, 2, 6})));
print(sol.deleteMiddle(build(new int[]{1, 2, 3, 4}))); // [1,2,4]
print(sol.deleteMiddleTwoPass(build(new int[]{1, 2, 3, 4})));
print(sol.deleteMiddle(build(new int[]{2, 1}))); // [2]
print(sol.deleteMiddleTwoPass(build(new int[]{2, 1})));
print(sol.deleteMiddle(build(new int[]{1}))); // []
print(sol.deleteMiddleTwoPass(build(new int[]{1})));
}
}
复杂度分析
复杂度:O(n) 时间一趟,O(1) 空间。
复杂度:O(n) 时间两趟,O(1) 空间。
2096. Step-By-Step Directions From a Binary Tree Node to Another(从二叉树一个节点到另一个节点每一步的方向)
题目描述
给你一棵二叉树根节点 root,共 n 个节点,节点值为 1...n 且互不相同。给定起点值 startValue 与
终点值 destValue,找到从 s 到 t 的最短路径并以字符串返回每一步方向:
'L' 去左孩子、'R' 去右孩子、'U' 去父节点。
示例 1
root = [5,1,2,3,null,6,4], startValue=3, destValue=6 → "UURL"
示例 2
root = [2,1], startValue=2, destValue=1 → "L"
解题思路
解法一(主提交版本)
根到节点路径 + 公共前缀(主提交版本)
- DFS 记录从根到每个目标节点的方向字符串(根->目标,路径中 'L'/'R')。
- 两条路径
ps(根→start)与pd(根→dest)共享根到 LCA 的前缀;起点到终点 =
start 从 LCA 向上走('U' × start 路径剩余长度)+ dest 从 LCA 沿 pd 剩余部分走(保持 L/R)。 - 实现:找到两路径第一个不同字符的位置即 LCA 后第一个分支,结果 = "U".repeat(ps 长度 - 公共前缀长度)
- pd 的公共前缀之后部分。若一条路径是另一条的前缀,公共前缀 = 较短者整条。
复杂度:O(n) 时间(DFS 两次 + 找前缀),O(n) 空间(递归栈与路径)。
解法二
最近公共祖先 + 两侧拼接(对照)
- 先递归找 LCA;再在 LCA 子树内分别找 start 与 dest 的路径(与解法一等价,只是显式先求 LCA)。
复杂度:O(n) 时间、O(h) 空间(h 为树高),用于交叉验证。
代码实现
java
public class Solution {
// Definition for a binary tree node.
public static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
// 解法一:根到节点路径 + 公共前缀(主提交版本)
public String getDirections(TreeNode root, int startValue, int destValue) {
StringBuilder sb = new StringBuilder();
find(root, startValue, sb);
String ps = sb.toString();
sb.setLength(0);
find(root, destValue, sb);
String pd = sb.toString();
// 找到公共前缀长度
int i = 0;
while (i < ps.length() && i < pd.length() && ps.charAt(i) == pd.charAt(i)) i++;
StringBuilder ans = new StringBuilder();
for (int k = i; k < ps.length(); k++) ans.append('U'); // start 向上走到 LCA
ans.append(pd.substring(i)); // 从 LCA 向下走到 dest
return ans.toString();
}
// 从根出发找 target,方向串写入 sb(成功返回 true)
private boolean find(TreeNode node, int target, StringBuilder sb) {
if (node == null) return false;
if (node.val == target) return true;
sb.append('L');
if (find(node.left, target, sb)) return true;
sb.setLength(sb.length() - 1);
sb.append('R');
if (find(node.right, target, sb)) return true;
sb.setLength(sb.length() - 1);
return false;
}
// 解法二:显式 LCA + 两侧拼接(对照)
public String getDirectionsLca(TreeNode root, int startValue, int destValue) {
TreeNode lca = lca(root, startValue, destValue);
StringBuilder toStart = new StringBuilder();
find(lca, startValue, toStart); // lca -> start 方向串
StringBuilder toDest = new StringBuilder();
find(lca, destValue, toDest); // lca -> dest 方向串
StringBuilder ans = new StringBuilder();
for (int k = 0; k < toStart.length(); k++) ans.append('U');
ans.append(toDest);
return ans.toString();
}
private TreeNode lca(TreeNode node, int a, int b) {
if (node == null || node.val == a || node.val == b) return node;
TreeNode l = lca(node.left, a, b);
TreeNode r = lca(node.right, a, b);
if (l != null && r != null) return node;
return l != null ? l : r;
}
private static TreeNode build(Integer[] arr) {
if (arr == null || arr.length == 0) return null;
TreeNode root = new TreeNode(arr[0]);
java.util.ArrayDeque<TreeNode> q = new java.util.ArrayDeque<>();
q.add(root);
int i = 1;
while (!q.isEmpty() && i < arr.length) {
TreeNode cur = q.poll();
if (arr[i] != null) {
cur.left = new TreeNode(arr[i]);
q.add(cur.left);
}
i++;
if (i < arr.length && arr[i] != null) {
cur.right = new TreeNode(arr[i]);
q.add(cur.right);
}
i++;
}
return root;
}
public static void main(String[] args) {
Solution sol = new Solution();
TreeNode t1 = build(new Integer[]{5, 1, 2, 3, null, 6, 4});
System.out.println(sol.getDirections(t1, 3, 6)); // UURL
System.out.println(sol.getDirectionsLca(t1, 3, 6)); // UURL
TreeNode t2 = build(new Integer[]{2, 1});
System.out.println(sol.getDirections(t2, 2, 1)); // L
System.out.println(sol.getDirectionsLca(t2, 2, 1)); // L
TreeNode t3 = build(new Integer[]{5, 1, 2, 3, null, 6, 4});
System.out.println(sol.getDirections(t3, 6, 3)); // UUR? 6->2->5->1->3 => "UUL"
System.out.println(sol.getDirectionsLca(t3, 6, 3)); // UUL
}
}
复杂度分析
复杂度:O(n) 时间(DFS 两次 + 找前缀),O(n) 空间(递归栈与路径)。
复杂度:O(n) 时间、O(h) 空间(h 为树高),用于交叉验证。
2097. Valid Arrangement of Pairs(合法重新排列数对)
题目描述
给你一个二维整数数组 pairs,pairs[i] = [starti, endi]。若 pairs 的一个重排在每个下标
i (1 <= i < pairs.length) 都满足 endi-1 == starti,则称之为合法重排。
返回任意一个合法重排。数据保证至少存在一个合法重排。
示例 1
pairs = [[5,1],[4,5],[11,9],[9,4]] → [[11,9],[9,4],[4,5],[5,1]]
示例 2
pairs = [[1,3],[3,2],[2,1]] → [[1,3],[3,2],[2,1]]
示例 3
pairs = [[1,2],[1,3],[2,1]] → [[1,2],[2,1],[1,3]]
解题思路
解法一(主提交版本)
Hierholzer 欧拉路径(主提交版本,自行推导)
- 把每个 starti 与 endi 抽象为有向边 starti -> endi,则「合法重排」= 遍历所有边恰好一次的
欧拉路径。题设保证图有欧拉路径(半欧拉图)。 - 找起点:若存在出度 - 入度 == 1 的节点(欧拉路径起点),取它;否则所有节点出入度相等,
任取一个有出边的节点(欧拉回路)。 - 用迭代式 Hierholzer:栈内维护当前路径,每次取栈顶节点,若其还有未走边则走到一端点入栈;
否则把栈顶弹出并记入path(此时是逆序)。最后把 path 反转,相邻节点成对构成结果。 - 注意节点编号可达 1e9,用「边数组存出边终点」+ 每个节点的边指针下标实现 O(E) 摊还。
复杂度:O(V + E)。
解法二
回溯搜索(对照,仅小规模验证) - 枚举所有边的使用顺序(DFS 回溯),找到第一条满足"首尾相接"的完整排列即返回;
受全排列复杂度限制,仅用于与解法一在小数据上交叉验证。
代码实现
java
public class Solution {
// 解法一:Hierholzer 欧拉路径(主提交版本)
public int[][] validArrangement(int[][] pairs) {
java.util.Map<Integer, java.util.List<Integer>> adj = new java.util.HashMap<>();
java.util.Map<Integer, Integer> in = new java.util.HashMap<>(); // 入度
java.util.Map<Integer, Integer> out = new java.util.HashMap<>(); // 出度
// 用 Map 记录每个节点的出边下标,便于"边游标"式弹出
for (int[] p : pairs) {
int a = p[0], b = p[1];
adj.computeIfAbsent(a, k -> new java.util.ArrayList<>()).add(b);
out.put(a, out.getOrDefault(a, 0) + 1);
in.put(b, in.getOrDefault(b, 0) + 1);
}
// 找欧拉路径起点
int start = pairs[0][0];
for (int v : out.keySet()) {
if (out.getOrDefault(v, 0) - in.getOrDefault(v, 0) == 1) {
start = v;
break;
}
}
// 各节点出边游标
java.util.Map<Integer, Integer> cursor = new java.util.HashMap<>();
// 迭代式 Hierholzer
java.util.List<Integer> path = new java.util.ArrayList<>();
java.util.ArrayDeque<Integer> stack = new java.util.ArrayDeque<>();
stack.push(start);
while (!stack.isEmpty()) {
int u = stack.peek();
java.util.List<Integer> edges = adj.getOrDefault(u, java.util.Collections.emptyList());
int idx = cursor.getOrDefault(u, 0);
if (idx < edges.size()) {
stack.push(edges.get(idx));
cursor.put(u, idx + 1);
} else {
path.add(u);
stack.pop();
}
}
// path 逆序后相邻成对
java.util.Collections.reverse(path);
int[][] ans = new int[path.size() - 1][2];
for (int i = 0; i + 1 < path.size(); i++) {
ans[i][0] = path.get(i);
ans[i][1] = path.get(i + 1);
}
return ans;
}
// 解法二:回溯搜索(对照,仅小规模验证)
public int[][] validArrangementBacktrack(int[][] pairs) {
int n = pairs.length;
int[][] order = new int[n][];
boolean[] used = new boolean[n];
dfs(pairs, used, 0, -1, order);
return order;
}
// 已排前 depth 对,lastEnd 为上一对终点;order 填充后返回 true
private boolean dfs(int[][] pairs, boolean[] used, int depth, int lastEnd, int[][] order) {
if (depth == pairs.length) return true;
for (int i = 0; i < pairs.length; i++) {
if (used[i]) continue;
if (depth > 0 && pairs[i][0] != lastEnd) continue;
used[i] = true;
order[depth] = pairs[i];
if (dfs(pairs, used, depth + 1, pairs[i][1], order)) return true;
used[i] = false;
}
return false;
}
private static void print(int[][] a) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < a.length; i++) {
if (i > 0) sb.append(",");
sb.append("[").append(a[i][0]).append(",").append(a[i][1]).append("]");
}
sb.append("]");
System.out.println(sb.toString());
}
private static boolean valid(int[][] order) {
for (int i = 1; i < order.length; i++) {
if (order[i][0] != order[i - 1][1]) return false;
}
return true;
}
public static void main(String[] args) {
Solution sol = new Solution();
int[][] p1 = {{5, 1}, {4, 5}, {11, 9}, {9, 4}};
int[][] r1 = sol.validArrangement(p1);
print(r1); // [[11,9],[9,4],[4,5],[5,1]] 任一合法
System.out.println(valid(r1));
int[][] r1b = sol.validArrangementBacktrack(p1);
print(r1b);
System.out.println(valid(r1b));
int[][] p2 = {{1, 3}, {3, 2}, {2, 1}};
int[][] r2 = sol.validArrangement(p2);
print(r2); // 任一合法
System.out.println(valid(r2));
System.out.println(valid(sol.validArrangementBacktrack(p2)));
int[][] p3 = {{1, 2}, {1, 3}, {2, 1}};
int[][] r3 = sol.validArrangement(p3);
print(r3);
System.out.println(valid(r3));
System.out.println(valid(sol.validArrangementBacktrack(p3)));
}
}
复杂度分析
复杂度:O(V + E)。
2098. Subsequence of Size K With the Largest Even Sum(长度为 K 的最大偶数和子序列)
题目描述
给你一个整数数组 nums 和一个整数 k,找出 nums 中所有长度为 k 的子序列中的最大偶数和。
返回这个总和;若不存在返回 -1。子序列保持原顺序(允许删元素)。
示例 1
nums = [4,1,5,3,1], k = 3 → 12([4,5,3])
示例 2
nums = [4,6,2], k = 3 → 12([4,6,2])
示例 3
nums = [1,3,5], k = 1 → -1(长度 1 的子序列没有偶数和)
解题思路
解法一(主提交版本)
排序 + 奇偶替换(主提交版本)
- 把所有数按降序排序,先取最大 k 个数得到和
sum。 - 若
sum为偶数直接返回;否则需要"换一个数"把奇偶性翻过来,有两种可选替换:
- 用未被选中的最大偶数换掉已选中最小奇数;2) 用未被选中的最大奇数换掉已选中最小偶数。
两种情况取金额最大的合法替换,二者都不可行则返回 -1。
- 维护「已选中最小的奇/偶」与「未选中最大的偶/奇」四个变量(排序后顺序扫描即可得到)。
复杂度:O(n log n)。
解法二
枚举奇偶个数(对照) - 把数组分成奇、偶两半都降序排序;枚举选中奇数个数
x(x 必须为偶数------偶数个奇数之和才为偶数,
0 <= x <= min(k, 奇数个数)),则偶数个数y = k - x,用前缀和快速求两半各自和,取最大值。
用于交叉验证。
复杂度:O(n log n)。
代码实现
java
public class Solution {
// 解法一:排序 + 奇偶替换(主提交版本)
public long largestEvenSum(int[] nums, int k) {
int n = nums.length;
Integer[] arr = new Integer[n];
for (int i = 0; i < n; i++) arr[i] = nums[i];
java.util.Arrays.sort(arr, (a, b) -> b - a); // 降序
long sum = 0;
int minOddIn = Integer.MAX_VALUE, minEvenIn = Integer.MAX_VALUE;
for (int i = 0; i < k; i++) {
sum += arr[i];
if ((arr[i] & 1) == 1) minOddIn = Math.min(minOddIn, arr[i]);
else minEvenIn = Math.min(minEvenIn, arr[i]);
}
if ((sum & 1) == 0) return sum;
// 未选中部分:最大偶 / 最大奇
int maxOddOut = Integer.MIN_VALUE, maxEvenOut = Integer.MIN_VALUE;
for (int i = k; i < n; i++) {
if ((arr[i] & 1) == 1) maxOddOut = Math.max(maxOddOut, arr[i]);
else maxEvenOut = Math.max(maxEvenOut, arr[i]);
}
long best = -1;
// 方案1:换掉已选最小奇数,换入未选最大偶数
if (minOddIn != Integer.MAX_VALUE && maxEvenOut != Integer.MIN_VALUE) {
best = Math.max(best, sum - minOddIn + maxEvenOut);
}
// 方案2:换掉已选最小偶数,换入未选最大奇数
if (minEvenIn != Integer.MAX_VALUE && maxOddOut != Integer.MIN_VALUE) {
best = Math.max(best, sum - minEvenIn + maxOddOut);
}
return best;
}
// 解法二:枚举奇偶个数(对照)
public long largestEvenSumEnum(int[] nums, int k) {
java.util.List<Integer> odd = new java.util.ArrayList<>();
java.util.List<Integer> even = new java.util.ArrayList<>();
for (int x : nums) {
if ((x & 1) == 1) odd.add(x);
else even.add(x);
}
odd.sort((a, b) -> b - a);
even.sort((a, b) -> b - a);
long[] po = new long[odd.size() + 1];
for (int i = 0; i < odd.size(); i++) po[i + 1] = po[i] + odd.get(i);
long[] pe = new long[even.size() + 1];
for (int i = 0; i < even.size(); i++) pe[i + 1] = pe[i] + even.get(i);
long ans = -1;
// x 个奇数,k-x 个偶数;偶数和要求选中奇数个数 x 为偶数
for (int x = 0; x <= Math.min(k, odd.size()); x++) {
int y = k - x;
if (y < 0 || y > even.size()) continue;
if ((x & 1) != 0) continue; // 奇数个数必须为偶数,和才为偶数
ans = Math.max(ans, po[x] + pe[y]);
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.largestEvenSum(new int[]{4, 1, 5, 3, 1}, 3) + " "
+ sol.largestEvenSumEnum(new int[]{4, 1, 5, 3, 1}, 3)); // 12 12
System.out.println(sol.largestEvenSum(new int[]{4, 6, 2}, 3) + " "
+ sol.largestEvenSumEnum(new int[]{4, 6, 2}, 3)); // 12 12
System.out.println(sol.largestEvenSum(new int[]{1, 3, 5}, 1) + " "
+ sol.largestEvenSumEnum(new int[]{1, 3, 5}, 1)); // -1 -1
System.out.println(sol.largestEvenSum(new int[]{2, 4, 6}, 1) + " "
+ sol.largestEvenSumEnum(new int[]{2, 4, 6}, 1)); // 6 6
System.out.println(sol.largestEvenSum(new int[]{3, 3, 3}, 2) + " "
+ sol.largestEvenSumEnum(new int[]{3, 3, 3}, 2)); // 6 6
System.out.println(sol.largestEvenSum(new int[]{7, 7, 7}, 2) + " "
+ sol.largestEvenSumEnum(new int[]{7, 7, 7}, 2)); // 14 14(7+7=14 为偶数和)
}
}
复杂度分析
复杂度:O(n log n)。
2099. Find Subsequence of Length K With the Largest Sum(找到和最大的长度为 K 的子序列)
题目描述
给你一个整数数组 nums 和一个整数 k,找到 nums 中长度为 k、和最大的一个子序列,
返回任意一个即可。子序列由删除部分元素(或不删)后保序得到。
示例 1
nums = [2,1,3,3], k = 2 → [3,3]
示例 2
nums = [-1,-2,3,4], k = 3 → [-1,3,4]
示例 3
nums = [3,4,3,3], k = 2 → [3,4](或 [4,3])
解题思路
解法一(主提交版本)
排序取 Top-K 再按原序输出(主提交版本)
- 复制数组为
sorted降序排序,取前 k 个值放入哈希表计数cnt(同值出现多次计数)。 - 再扫描原数组:对每个值,若
cnt中还有额度则输出该位置元素并减少额度------这样既保证取值集合
是最大 k 个,又保持原数组相对顺序。
复杂度:O(n log n) 时间,O(n) 空间。
解法二
堆保留 Top-K 下标(对照) - 用容量为 k 的最小堆按「数值」保留最大 k 个元素的下标;随后把这些下标排序后按序取原数组值。
复杂度:O(n log k) 时间,O(k) 空间。
代码实现
java
public class Solution {
// 解法一:排序取 Top-K + 原序回填(主提交版本)
public int[] maxSubsequence(int[] nums, int k) {
int n = nums.length;
Integer[] sorted = new Integer[n];
for (int i = 0; i < n; i++) sorted[i] = nums[i];
java.util.Arrays.sort(sorted, (a, b) -> b - a); // 降序
java.util.Map<Integer, Integer> cnt = new java.util.HashMap<>();
for (int i = 0; i < k; i++) cnt.put(sorted[i], cnt.getOrDefault(sorted[i], 0) + 1);
int[] ans = new int[k];
int idx = 0;
for (int i = 0; i < n && idx < k; i++) {
int c = cnt.getOrDefault(nums[i], 0);
if (c > 0) {
ans[idx++] = nums[i];
cnt.put(nums[i], c - 1);
}
}
return ans;
}
// 解法二:最大堆保下标(对照)
public int[] maxSubsequenceHeap(int[] nums, int k) {
// 大堆按值:只保留 k 个最大的下标。用小堆更容易(值最小的先出)。
java.util.PriorityQueue<Integer> minHeap = new java.util.PriorityQueue<>(
(i, j) -> nums[i] == nums[j] ? i - j : nums[i] - nums[j]);
for (int i = 0; i < nums.length; i++) {
minHeap.offer(i);
if (minHeap.size() > k) minHeap.poll();
}
java.util.List<Integer> idxs = new java.util.ArrayList<>(minHeap);
java.util.Collections.sort(idxs);
int[] ans = new int[k];
for (int i = 0; i < k; i++) ans[i] = nums[idxs.get(i)];
return ans;
}
private static void print(int[] a) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < a.length; i++) {
if (i > 0) sb.append(",");
sb.append(a[i]);
}
sb.append("]");
System.out.println(sb.toString());
}
private static long sum(int[] a) {
long s = 0;
for (int x : a) s += x;
return s;
}
public static void main(String[] args) {
Solution sol = new Solution();
int[] r1 = sol.maxSubsequence(new int[]{2, 1, 3, 3}, 2);
print(r1); // [3,3] sum=6
System.out.println(sum(r1));
print(sol.maxSubsequenceHeap(new int[]{2, 1, 3, 3}, 2));
int[] r2 = sol.maxSubsequence(new int[]{-1, -2, 3, 4}, 3);
print(r2); // [-1,3,4] sum=6
System.out.println(sum(r2));
print(sol.maxSubsequenceHeap(new int[]{-1, -2, 3, 4}, 3));
int[] r3 = sol.maxSubsequence(new int[]{3, 4, 3, 3}, 2);
print(r3); // sum=7
System.out.println(sum(r3));
print(sol.maxSubsequenceHeap(new int[]{3, 4, 3, 3}, 2));
}
}
复杂度分析
复杂度:O(n log n) 时间,O(n) 空间。
复杂度:O(n log k) 时间,O(k) 空间。
2100. Find Good Days to Rob the Bank(适合野炊的日子)
题目描述
给你一个下标从 0 开始的整数数组 security 表示第 i 天的建议出行指数,和一个整数 time。
第 i 天是适合野炊的日子,当且仅当:
i前和后都至少有time天;- 前连续
time天指数非递增:security[i-time]>= ... >=security[i]; - 后连续
time天指数非递减:security[i]<= ... <=security[i+time]。
返回所有适合野炊的日子下标(顺序任意)。
示例 1
security = [5,3,3,3,5,6,2], time = 2 → [2,3]
示例 2
security = [1,1,1,1,1], time = 0 → [0,1,2,3,4]
示例 3
security = [1,2,3,4,5,6], time = 2 → []
解题思路
解法一(主提交版本)
左/右连续非增/非减长度 DP(主提交版本)
left[i]= 以 i 结尾的最长连续非递增段长度:left[i] = left[i-1] + 1当 securityi-1 >= securityi,
否则为 1。right[i]= 以 i 开头的最长连续非递减段长度:right[i] = right[i+1] + 1当 securityi <= securityi+1,
否则为 1。- i 满足条件 <=>
left[i] > time(即含 i 在内连续非增长度至少 time+1)且right[i] > time。
复杂度:O(n) 时间,O(n) 空间。
解法二
暴力逐日校验(对照) - 对每个下标
i,先检查左右两侧是否各有time天,再直接按题意逐项校验:
前time天security[i-time] >= ... >= security[i],后time天security[i] <= ... <= security[i+time]。 - 仅适用于小规模输入(复杂度 O(n * time)),用于与解法一交叉验证。
复杂度:O(n * time) 时间,O(1) 额外空间。
代码实现
java
public class Solution {
// 解法一:左右 DP(主提交版本)
public java.util.List<Integer> goodDaysToRobBank(int[] security, int time) {
int n = security.length;
int[] left = new int[n]; // 以 i 结尾的最长非递增长度
int[] right = new int[n]; // 以 i 开头的最长非递减长度
left[0] = 1;
for (int i = 1; i < n; i++) {
if (security[i - 1] >= security[i]) left[i] = left[i - 1] + 1;
else left[i] = 1;
}
right[n - 1] = 1;
for (int i = n - 2; i >= 0; i--) {
if (security[i] <= security[i + 1]) right[i] = right[i + 1] + 1;
else right[i] = 1;
}
java.util.List<Integer> ans = new java.util.ArrayList<>();
for (int i = 0; i < n; i++) {
if (left[i] >= time + 1 && right[i] >= time + 1) ans.add(i);
}
return ans;
}
// 解法二:暴力逐日校验(对照)
public java.util.List<Integer> goodDaysToRobBankBrute(int[] security, int time) {
int n = security.length;
java.util.List<Integer> ans = new java.util.ArrayList<>();
for (int i = 0; i < n; i++) {
if (i - time < 0 || i + time >= n) continue;
boolean ok = true;
for (int j = i - time; j < i; j++) {
if (security[j] < security[j + 1]) { ok = false; break; }
}
if (!ok) continue;
for (int j = i; j < i + time; j++) {
if (security[j] > security[j + 1]) { ok = false; break; }
}
if (ok) ans.add(i);
}
return ans;
}
private static void print(java.util.List<Integer> l) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < l.size(); i++) {
if (i > 0) sb.append(",");
sb.append(l.get(i));
}
sb.append("]");
System.out.println(sb.toString());
}
public static void main(String[] args) {
Solution sol = new Solution();
print(sol.goodDaysToRobBank(new int[]{5, 3, 3, 3, 5, 6, 2}, 2)); // [2,3]
print(sol.goodDaysToRobBankBrute(new int[]{5, 3, 3, 3, 5, 6, 2}, 2));
print(sol.goodDaysToRobBank(new int[]{1, 1, 1, 1, 1}, 0)); // [0,1,2,3,4]
print(sol.goodDaysToRobBankBrute(new int[]{1, 1, 1, 1, 1}, 0));
print(sol.goodDaysToRobBank(new int[]{1, 2, 3, 4, 5, 6}, 2)); // []
print(sol.goodDaysToRobBankBrute(new int[]{1, 2, 3, 4, 5, 6}, 2));
}
}
复杂度分析
复杂度:O(n) 时间,O(n) 空间。
复杂度:O(n * time) 时间,O(1) 额外空间。