LeetCode 2141-2160. 题解合集
文章目录
- [LeetCode 2141-2160. 题解合集](#LeetCode 2141-2160. 题解合集)
-
- [2141. Maximum Running Time of N Computers(同时运行 N 台电脑的最长时间)](#2141. Maximum Running Time of N Computers(同时运行 N 台电脑的最长时间))
- [2142. The Number of Passengers in Each Bus I(每辆车的乘客人数 I)](#2142. The Number of Passengers in Each Bus I(每辆车的乘客人数 I))
- [2143. Choose Numbers From Two Arrays in Range(在两个数组的区间中选取数字)](#2143. Choose Numbers From Two Arrays in Range(在两个数组的区间中选取数字))
- [2144. Minimum Cost of Buying Candies With Discount(打折购买糖果的最小开销)](#2144. Minimum Cost of Buying Candies With Discount(打折购买糖果的最小开销))
- [2145. Count the Hidden Sequences(统计隐藏数组数目)](#2145. Count the Hidden Sequences(统计隐藏数组数目))
- [2146. K Highest Ranked Items Within a Price Range(价格范围内最高排名的 K 样物品)](#2146. K Highest Ranked Items Within a Price Range(价格范围内最高排名的 K 样物品))
- [2147. Number of Ways to Divide a Long Corridor(分隔长廊的方案数)](#2147. Number of Ways to Divide a Long Corridor(分隔长廊的方案数))
- [2148. Count Elements With Strictly Smaller and Greater Elements(元素计数)](#2148. Count Elements With Strictly Smaller and Greater Elements(元素计数))
- [2149. Rearrange Array Elements by Sign(按符号重排数组)](#2149. Rearrange Array Elements by Sign(按符号重排数组))
- [2150. Find All Lonely Numbers in the Array(找出数组中的所有孤独数字)](#2150. Find All Lonely Numbers in the Array(找出数组中的所有孤独数字))
- [2151. Maximum Good People Based on Statements(基于陈述统计最多好人数)](#2151. Maximum Good People Based on Statements(基于陈述统计最多好人数))
- [2152. Minimum Number of Lines to Cover Points(穿过所有点的所需最少直线数量)](#2152. Minimum Number of Lines to Cover Points(穿过所有点的所需最少直线数量))
- [2153. The Number of Passengers in Each Bus II(每辆车的乘客人数 II)](#2153. The Number of Passengers in Each Bus II(每辆车的乘客人数 II))
- [2154. Keep Multiplying Found Values by Two(将找到的值乘以 2)](#2154. Keep Multiplying Found Values by Two(将找到的值乘以 2))
- [2155. All Divisions With the Highest Score of a Binary Array(分组得分最高的所有下标)](#2155. All Divisions With the Highest Score of a Binary Array(分组得分最高的所有下标))
- [2156. Find Substring With Given Hash Value(查找给定哈希值的子串)](#2156. Find Substring With Given Hash Value(查找给定哈希值的子串))
- [2157. Groups of Strings(字符串分组)](#2157. Groups of Strings(字符串分组))
- [2158. Amount of New Area Painted Each Day(每天绘制新区域的数量)](#2158. Amount of New Area Painted Each Day(每天绘制新区域的数量))
- [2159. Order Two Columns Independently(分别排序两列)](#2159. Order Two Columns Independently(分别排序两列))
- [2160. Minimum Sum of Four Digit Number After Splitting Digits(拆分数位后四位数字的最小和)](#2160. Minimum Sum of Four Digit Number After Splitting Digits(拆分数位后四位数字的最小和))
2141. Maximum Running Time of N Computers(同时运行 N 台电脑的最长时间)
题目描述
你有 n 台电脑。给你整数 n 和一个下标从 0 开始的整数数组 batteries,其中第 i 个电池可以让一台电脑
运行 batteries[i] 分钟。你想使用这些电池让全部 n 台电脑同时运行。一开始,你可以给每台电脑连接至多一个
电池。然后在任意整数时刻,你都可以将一台电脑与它的电池断开连接,并连接另一个电池(可以任意次,
也可以是别的电脑用过的电池)。断开和连接不花费时间。注意,你不能给电池充电。
返回可以让 n 台电脑同时运行的最长分钟数。
示例 1
n = 2, batteries = [3,3,3] → 4
示例 2
n = 2, batteries = [1,1,1,1] → 2
约束:
- 1 <=
n<= batteries.length <= 10^5,1 <= batteriesi <= 10^9
解题思路
解法一(主提交版本)
贪心 + 二分答案(主提交版本)
- 若所有电脑同时运行 T 分钟,总需求为 n * T。
- 每个电池贡献 min(batteriesi, T)(超过 T 的部分只能服务一台电脑 T 分钟),
若 Σ min(batteriesi, T) >= n * T,则 T 可行(调度构造由「剩余容量池」思想保证)。 - 在 1, 总电量 / n 上二分最大可行 T(用 long 防溢出)。
复杂度:O(n log Σ) 时间,O(1) 空间。
解法二
排序截断贪心(对照) - 排序后,多余电量的电池被「榨取」到 n 台电脑共享;等价于不断移除最大电池并让其贡献 n 台。
简化实现:二分可行性判断与解法一共享,但先排序后用前缀快速求和验证。
复杂度:O(n log n + n log Σ) 时间,O(n) 空间。用于交叉验证。
代码实现
java
import java.util.Arrays;
public class Solution {
// 解法一:二分答案(主提交版本)
public long maxRunTime(int n, int[] batteries) {
long sum = 0;
for (int b : batteries) sum += b;
long lo = 1, hi = sum / n;
while (lo <= hi) {
long mid = (lo + hi) >>> 1;
if (canRun(n, batteries, mid)) lo = mid + 1;
else hi = mid - 1;
}
return hi;
}
private boolean canRun(int n, int[] batteries, long t) {
long total = 0;
for (int b : batteries) {
total += Math.min(b, t);
if (total >= (long) n * t) return true; // 提前返回,防溢出
}
return total >= (long) n * t;
}
// 解法二:排序 + 前缀截断(对照)
public long maxRunTimeSorted(int n, int[] batteries) {
Arrays.sort(batteries);
long sum = 0;
for (int b : batteries) sum += b;
// 每次取最大电池与其余电池平衡:等价实现直接二分判断
long lo = 1, hi = sum / n;
while (lo <= hi) {
long mid = (lo + hi) >>> 1;
long total = 0;
boolean ok = false;
for (int b : batteries) {
total += Math.min(b, mid);
if (total >= (long) n * mid) {
ok = true;
break;
}
}
if (ok) lo = mid + 1;
else hi = mid - 1;
}
return hi;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.maxRunTime(2, new int[]{3, 3, 3})
+ " " + sol.maxRunTimeSorted(2, new int[]{3, 3, 3})); // 4 4
System.out.println(sol.maxRunTime(2, new int[]{1, 1, 1, 1})
+ " " + sol.maxRunTimeSorted(2, new int[]{1, 1, 1, 1})); // 2 2
System.out.println(sol.maxRunTime(1, new int[]{5})
+ " " + sol.maxRunTimeSorted(1, new int[]{5})); // 5 5
System.out.println(sol.maxRunTime(3, new int[]{10, 10, 10, 10, 10})
+ " " + sol.maxRunTimeSorted(3, new int[]{10, 10, 10, 10, 10})); // 10(3台:每台 10,总 30/(3)=10 但需检验)
}
}
复杂度分析
复杂度:O(n log Σ) 时间,O(1) 空间。
复杂度:O(n log n + n log Σ) 时间,O(n) 空间。用于交叉验证。
2142. The Number of Passengers in Each Bus I(每辆车的乘客人数 I)
题目类型:数据库(SQL)
题目描述
表: Buses
+--------------+------+
| Column Name | Type |
+--------------+------+
| bus_id | int |
| arrival_time | int |
+--------------+------+
bus_id 是该表的主键。该表的每一行都包含关于 LeetCode 站点的公交车到达时间的信息。不会出现两辆公交车会同时到达。
表: Passengers
+--------------+------+
| Column Name | Type |
+--------------+------+
| passenger_id | int |
| arrival_time | int |
+--------------+------+
passenger_id 是该表的主键。该表的每一行都包含乘客到达 LeetCode 站的时间信息。
公交车和乘客到达 LeetCode 站。如果一辆公交车在时间 tbus 到站,乘客在时间 tpassenger 到站,其中 tpassenger <= tbus,该乘客之前没有赶上任何公交车,则该乘客将搭乘该公交车。
编写一个 SQL 来查询使用每辆公交车的用户数量。返回按 bus_id 升序排序的结果表。
示例 1:
输入:
Buses 表:
+--------+--------------+
| bus_id | arrival_time |
+--------+--------------+
| 1 | 2 |
| 2 | 4 |
| 3 | 7 |
+--------+--------------+
Passengers 表:
+--------------+--------------+
| passenger_id | arrival_time |
+--------------+--------------+
| 11 | 1 |
| 12 | 5 |
| 13 | 6 |
| 14 | 7 |
+--------------+--------------+
输出:
+--------+----------------+
| bus_id | passengers_cnt |
+--------+----------------+
| 1 | 1 |
| 2 | 0 |
| 3 | 3 |
+--------+----------------+
解释:
- 11 号乘客在时间 1 到达,搭乘 2 点到达的 1 号车。
- 2 号车在时间 4 到达,没有乘客。
- 12、13、14 号乘客在 5、6、7 到达,都搭乘 7 点到达的 3 号车。
解题思路
解法一:窗口函数 + 时间差 + 排他的上车判定
给公交车按到达时间排序并编号(bus_order),乘客也按到达时间排序编号。乘客会搭乘哪辆车?
- 乘客上车车次 = 到达时间小于等于乘客时间的最晚一辆公交 ,即与乘客时间差最小且
bus_time >= passenger_time的公交; - 用窗口/自关联找到
tpassenger <= tbus的那辆公交;由于公交车先后衔接,判断乘客"之前没赶上任何公交"只需确保:该乘客的落点公交号 = 按时间顺序,其前一辆公交的到站时间严格小于tpassenger时一定坐该辆,否则被更早车次载走------I 题中每辆公交容量无限,因此每人只需坐到「时间上最近且不早于自己的公交」即可。
汇总实现用 JOIN + 取最小差值:对每个乘客,选出满足 b.arrival_time >= p.arrival_time 的最早到站公交:
sql
WITH assigned AS (
SELECT p.passenger_id, (
SELECT bus_id FROM Buses b
WHERE b.arrival_time >= p.arrival_time
ORDER BY b.arrival_time ASC
LIMIT 1
) AS bus_id
FROM Passengers p
)
SELECT b.bus_id, COUNT(a.passenger_id) AS passengers_cnt
FROM Buses b
LEFT JOIN assigned a ON b.bus_id = a.bus_id
GROUP BY b.bus_id
ORDER BY b.bus_id;
解法二(对照):相关子查询定位 + 外层聚合
思路相同,用相关子查询直接给每个乘客打上 bus_id 标签,再右连回 Buses 补 0。两种写法都满足题意,解法一更清晰。
代码实现
sql
-- 解法一(推荐):乘客分配到「时间上最早不早于自己」的公交
WITH assigned AS (
SELECT p.passenger_id, (
SELECT bus_id FROM Buses b
WHERE b.arrival_time >= p.arrival_time
ORDER BY b.arrival_time ASC
LIMIT 1
) AS bus_id
FROM Passengers p
)
SELECT b.bus_id, COUNT(a.passenger_id) AS passengers_cnt
FROM Buses b
LEFT JOIN assigned a ON b.bus_id = a.bus_id
GROUP BY b.bus_id
ORDER BY b.bus_id;
sql
-- 解法二(对照):使用窗口函数 ROW_NUMBER 确定每辆车的上一层时间
WITH bus_seq AS (
SELECT bus_id, arrival_time,
COALESCE(MIN(arrival_time) OVER (ORDER BY arrival_time ROWS BETWEEN 1 PRECEDING AND 1 PRECEDING), 0) AS prev_time
FROM Buses
),
assigned AS (
SELECT p.passenger_id,
(SELECT bus_id FROM bus_seq b
WHERE b.prev_time <= p.arrival_time AND b.arrival_time >= p.arrival_time
ORDER BY b.arrival_time ASC LIMIT 1) AS bus_id
FROM Passengers p
)
SELECT b.bus_id, COUNT(a.passenger_id) AS passengers_cnt
FROM Buses b LEFT JOIN assigned a ON b.bus_id = a.bus_id
GROUP BY b.bus_id
ORDER BY b.bus_id;
复杂度分析
- 时间复杂度 :
O(n * m),其中n为Passengers行数、m为Buses行数(相关子查询逐行匹配);大数据量下可用窗口优化到O(n log n + m log m)。 - 空间复杂度 :
O(n + m),中间结果集与两张表规模成正比。
2143. Choose Numbers From Two Arrays in Range(在两个数组的区间中选取数字)
题目描述
给你两个下标从 0 开始、长度为 n 的整数数组 nums1 和 nums2。如果一个区间 `l`, `r`(0 <= l <= r < n)满足:
对每个在区间内的 i,选取 nums1[i] 或者 nums2[i],且从 nums1 中选取的数字之和与从 nums2 中选取的数字之和
相等(没选任何数字视为 0),则称该区间为「平衡」的。两个平衡区间只要 l、r 或任一位置的选取情况不同,
就视为不同。请返回不同的平衡区间数目,答案对 10^9 + 7 取模。
示例 1
nums1 = [1,2,5], nums2 = [2,6,3] → 3
示例 2
nums1 = [0,1], nums2 = [1,0] → 4
约束:
- 1 <=
n<= 100,0 <= nums1i, nums2i <= 100
解题思路
解法一(主提交版本)
枚举左端点 + 滚动差值 DP(主提交版本)
- 定义 d = sum1 - sum2。每处理一个位置 i:选 nums1i 使 d += nums1i,选 nums2i 使 d -= nums2i。
平衡区间即 d == 0。 - 固定左端点 l,从 l 开始向右扩展,用滚动 Map 维护「当前 d 值 -> 方案数」。
每加入位置 i 后,dpd == 0 计入答案。 - 用 Map 做稀疏滚动,每个区间状态数是 (r - l + 1) 级,整体复杂度可接受。
复杂度:O(n^2 * 平均状态数) ≈ O(n^3) 最坏(n=100 可接受),空间 O(n)。
解法二
二维 DP 前缀累加(对照) - 正向二维 DP:dpjd 表示以位置 j 结尾的方案数,转移自 dpj-1。
与解法一等价,用数组实现(偏移量处理负数),用于交叉验证。
复杂度:O(n^2 * Σ|nums|),n=100、Σ|nums| ≤ 10^4 时可接受。
代码实现
java
import java.util.HashMap;
import java.util.Map;
public class Solution {
private static final int MOD = 1_000_000_007;
// 解法一:枚举左端点 + 滚动差值 DP(主提交版本)
public int countSubranges(int[] nums1, int[] nums2) {
int n = nums1.length;
long ans = 0;
for (int l = 0; l < n; l++) {
Map<Integer, Long> dp = new HashMap<>();
dp.put(0, 1L); // 空区间
for (int i = l; i < n; i++) {
Map<Integer, Long> nd = new HashMap<>();
for (Map.Entry<Integer, Long> e : dp.entrySet()) {
int d = e.getKey();
long c = e.getValue();
// 选 nums1[i]:d + nums1[i]
nd.merge(d + nums1[i], c, Long::sum);
// 选 nums2[i]:d - nums2[i]
nd.merge(d - nums2[i], c, Long::sum);
}
dp = nd;
Long z = dp.get(0);
if (z != null) ans = (ans + z) % MOD;
}
}
return (int) ans;
}
// 解法二:枚举左端点 + 数组滚动 DP(对照)
public int countSubrangesArr(int[] nums1, int[] nums2) {
int n = nums1.length;
int maxSum = 0;
for (int v : nums1) maxSum += v;
for (int v : nums2) maxSum += v;
int off = maxSum;
long ans = 0;
for (int l = 0; l < n; l++) {
long[] dp = new long[2 * maxSum + 1];
dp[off] = 1; // 空区间差值 d = 0
for (int i = l; i < n; i++) {
long[] nd = new long[2 * maxSum + 1];
for (int d = 0; d <= 2 * maxSum; d++) {
if (dp[d] == 0) continue;
int diff = d - off;
// 选 nums1[i]:差值 + nums1[i]
int t1 = diff + nums1[i] + off;
if (t1 >= 0 && t1 <= 2 * maxSum) nd[t1] = (nd[t1] + dp[d]) % MOD;
// 选 nums2[i]:差值 - nums2[i]
int t2 = diff - nums2[i] + off;
if (t2 >= 0 && t2 <= 2 * maxSum) nd[t2] = (nd[t2] + dp[d]) % MOD;
}
dp = nd;
ans = (ans + dp[off]) % MOD;
}
}
return (int) ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.countSubranges(new int[]{1, 2, 5}, new int[]{2, 6, 3})
+ " " + sol.countSubrangesArr(new int[]{1, 2, 5}, new int[]{2, 6, 3})); // 3 3
System.out.println(sol.countSubranges(new int[]{0, 1}, new int[]{1, 0})
+ " " + sol.countSubrangesArr(new int[]{0, 1}, new int[]{1, 0})); // 4 4
System.out.println(sol.countSubranges(new int[]{1}, new int[]{1})
+ " " + sol.countSubrangesArr(new int[]{1}, new int[]{1})); // 0 0(单个等值无法平衡)
}
}
复杂度分析
复杂度:O(n^2 * 平均状态数) ≈ O(n^3) 最坏(n=100 可接受),空间 O(n)。
复杂度:O(n^2 * Σ|nums|),n=100、Σ|nums| ≤ 10^4 时可接受。
2144. Minimum Cost of Buying Candies With Discount(打折购买糖果的最小开销)
题目描述
一家商店正在打折销售糖果。每购买两个糖果,商店会免费送一个糖果。免费送的糖果唯一的限制是:
它的价格需要小于等于购买的两个糖果价格的较小值。给你一个下标从 0 开始的整数数组 cost,
cost[i] 表示第 i 个糖果的价格,请你返回获得所有糖果的最小总开销。
示例 1
cost = [1,2,3] → 5
示例 2
cost = [6,5,7,9,2,2] → 23
示例 3
cost = [5,5] → 10
约束:
- 1 <=
cost.length<= 100,1 <=cost[i]<= 100
解题思路
解法一(主提交版本)
降序贪心三三分组(主提交版本)
- 为了最大化「免费」价值,应让最贵的糖果都是付费购买,免费名额给尽量贵的(但 ≤ 已购两糖较小者)。
- 按价格降序排列,每 3 个一组:前两个付费,第三个免费;若余 1 或 2 个全部付费。
复杂度:O(n log n) 时间(排序),O(log n) 空间。
解法二
降序排序按索引跳过(对照) - 降序排序后,下标 2, 5, 8... (从 0 起)的糖果免费,其余付费。
- 与解法一等价,用取模跳过实现,用于交叉验证。
复杂度:O(n log n) 时间,O(log n) 空间。
代码实现
java
import java.util.Arrays;
public class Solution {
// 解法一:降序贪心三三分组(主提交版本)
public int minimumCost(int[] cost) {
Arrays.sort(cost);
int n = cost.length;
int ans = 0;
int cnt = 0;
for (int i = n - 1; i >= 0; i--) {
cnt++;
if (cnt % 3 != 0) ans += cost[i]; // 第 1、2 个买,第 3 个免费
}
return ans;
}
// 解法二:降序排序按索引跳过(对照)
public int minimumCostAlt(int[] cost) {
Integer[] a = new Integer[cost.length];
for (int i = 0; i < cost.length; i++) a[i] = cost[i];
Arrays.sort(a, (x, y) -> y - x);
int ans = 0;
for (int i = 0; i < a.length; i++) {
if (i % 3 != 2) ans += a[i];
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.minimumCost(new int[]{1, 2, 3}) + " " + sol.minimumCostAlt(new int[]{1, 2, 3})); // 5 5
System.out.println(sol.minimumCost(new int[]{6, 5, 7, 9, 2, 2}) + " " + sol.minimumCostAlt(new int[]{6, 5, 7, 9, 2, 2})); // 23 23
System.out.println(sol.minimumCost(new int[]{5, 5}) + " " + sol.minimumCostAlt(new int[]{5, 5})); // 10 10
System.out.println(sol.minimumCost(new int[]{1}) + " " + sol.minimumCostAlt(new int[]{1})); // 1 1
}
}
复杂度分析
复杂度:O(n log n) 时间(排序),O(log n) 空间。
复杂度:O(n log n) 时间,O(log n) 空间。
2145. Count the Hidden Sequences(统计隐藏数组数目)
题目描述
给你一个下标从 0 开始且长度为 n 的整数数组 differences,它表示一个长度为 n + 1 的隐藏数组相邻元素之间
的差值:differences[i] = hidden[i+1] - hidden[i]。同时给你两个整数 lower 和 upper,它们表示隐藏数组中
所有数字的值都在闭区间 `lower`, `upper` 之间。请你返回符合要求的隐藏数组的数目;如果没有,返回 0。
示例 1
differences = [1,-3,4], lower = 1, upper = 6 → 2([3,4,1,5] 和 [4,5,2,6])
示例 2
differences = [3,-4,5,1,-2], lower = -4, upper = 5 → 4
示例 3
differences = [4,-7,2], lower = 3, upper = 6 → 0
约束:
- 1 <=
n<= 105,-105 <= differencesi <= 105,-105 <=lower<=upper<= 10^5
解题思路
解法一(主提交版本)
前缀和范围约束(主提交版本)
- 设 hidden0 = x。则 hiddeni = x + prefixi,其中 prefixi 为 differences0...i-1 的前缀和,
prefix0 = 0。 - 需要所有 hiddeni ∈ lower, upper,即 x ∈ lower - prefix\[i, upper - prefixi] 对所有 i 成立。
取所有下界的最大值 lowBound 与所有上界的最小值 highBound,可行 x 个数 = max(0, highBound - lowBound + 1)。 - 注意 prefix 可能溢出 int(n * 10^5 = 10^10),用 long。
复杂度:O(n) 时间,O(1) 空间。
解法二
滑动最小值/最大值(对照) - 等价做法:维护前缀和数组,求 min 与 max,然后计算可行区间长度。
- 用 long 防止溢出,用于交叉验证。
复杂度:O(n) 时间,O(n) 空间(前缀和数组)。
代码实现
java
public class Solution {
// 解法一:前缀和范围约束(主提交版本)
public int numberOfArrays(int[] differences, int lower, int upper) {
long pref = 0;
long minPref = 0, maxPref = 0;
for (int d : differences) {
pref += d;
minPref = Math.min(minPref, pref);
maxPref = Math.max(maxPref, pref);
}
// x + minPref >= lower 且 x + maxPref <= upper
long low = lower - minPref;
long high = upper - maxPref;
if (high < low) return 0;
return (int) (high - low + 1);
}
// 解法二:前缀和数组 + 区间计算(对照)
public int numberOfArraysAlt(int[] differences, int lower, int upper) {
int n = differences.length;
long[] pref = new long[n + 1];
for (int i = 0; i < n; i++) pref[i + 1] = pref[i] + differences[i];
long minP = Long.MAX_VALUE, maxP = Long.MIN_VALUE;
for (long p : pref) {
minP = Math.min(minP, p);
maxP = Math.max(maxP, p);
}
long low = lower - minP;
long high = upper - maxP;
return high < low ? 0 : (int) (high - low + 1);
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.numberOfArrays(new int[]{1, -3, 4}, 1, 6)
+ " " + sol.numberOfArraysAlt(new int[]{1, -3, 4}, 1, 6)); // 2 2
System.out.println(sol.numberOfArrays(new int[]{3, -4, 5, 1, -2}, -4, 5)
+ " " + sol.numberOfArraysAlt(new int[]{3, -4, 5, 1, -2}, -4, 5)); // 4 4
System.out.println(sol.numberOfArrays(new int[]{4, -7, 2}, 3, 6)
+ " " + sol.numberOfArraysAlt(new int[]{4, -7, 2}, 3, 6)); // 0 0
System.out.println(sol.numberOfArrays(new int[]{0, 0}, 1, 1)
+ " " + sol.numberOfArraysAlt(new int[]{0, 0}, 1, 1)); // 1(全等的 1)→ x ∈ [1,1]
}
}
复杂度分析
复杂度:O(n) 时间,O(1) 空间。
复杂度:O(n) 时间,O(n) 空间(前缀和数组)。
2146. K Highest Ranked Items Within a Price Range(价格范围内最高排名的 K 样物品)
题目描述
给你一个下标从 0 开始的二维整数数组 grid,大小 m x n。0 表示墙;1 表示可通行的空格;
其他正整数表示该格子内的一样物品的价格,可自由经过。从一个格子走到上下左右相邻格子花费 1 步。
给你 pricing = `low`, `high` 和 start = `row`, `col`,表示开始位置,你只对价格在 `low`, `high` 闭区间内的物品
感兴趣。返回给定价格内排名最高的 k 件物品的坐标(按排名排序);不足 k 件则全部返回。
排名规则(优先级从高到低):距离(短者优先)、价格(低者优先)、行坐标(小者优先)、列坐标(小者优先)。
示例 1
grid = [[1,2,0,1],[1,3,0,1],[0,2,5,1]], pricing = [2,5], start = [0,0], k = 3 → [[0,1],[1,1],[2,1]]
示例 2
grid = [[1,2,0,1],[1,3,3,1],[0,2,5,1]], pricing = [2,3], start = [2,3], k = 2 → [[2,1],[1,2]]
示例 3
grid = [[1,1,1],[0,0,1],[2,3,4]], pricing = [2,3], start = [0,0], k = 3 → [[2,1],[2,0]]
约束:
- 1 <=
m*n<= 10^5,2 <= low <= high <= 10^5,grid[start]> 0
解题思路
解法一(主提交版本)
BFS 距离 + 优先队列排序(主提交版本)
- 从 start 出发 BFS,记录每个格子到起点的最短距离。
- 对满足价格在 low, high 的格子,收集 (dist, price, r, c)。
- 按排名规则排序后取前 k 个。
复杂度:O(mn log(m n)) 时间(排序),O(m*n) 空间。
解法二
BFS 逐层 + 堆维护 Top-K(对照) - BFS 每层自然按距离分层,层内用优先队列(价格、行、列)截取前 k,避免全排序。
复杂度:O(mn log k) 时间,O(k + mn) 空间。用于交叉验证。
代码实现
java
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Deque;
import java.util.List;
import java.util.PriorityQueue;
public class Solution {
private static final int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
// 解法一:BFS 距离 + 全排序(主提交版本)
public List<List<Integer>> highestRankedKItems(int[][] grid, int[] pricing, int[] start, int k) {
int m = grid.length, n = grid[0].length;
int low = pricing[0], high = pricing[1];
int[][] dist = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) dist[i][j] = -1;
}
Deque<int[]> q = new ArrayDeque<>();
q.offer(new int[]{start[0], start[1]});
dist[start[0]][start[1]] = 0;
while (!q.isEmpty()) {
int[] cur = q.poll();
int r = cur[0], c = cur[1];
for (int[] d : DIRS) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < m && nc >= 0 && nc < n && dist[nr][nc] == -1 && grid[nr][nc] != 0) {
dist[nr][nc] = dist[r][c] + 1;
q.offer(new int[]{nr, nc});
}
}
}
List<int[]> items = new ArrayList<>();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
int p = grid[i][j];
if (p > 1 && p >= low && p <= high && dist[i][j] >= 0) {
items.add(new int[]{dist[i][j], p, i, j});
}
}
}
items.sort(Comparator
.comparingInt((int[] a) -> a[0])
.thenComparingInt(a -> a[1])
.thenComparingInt(a -> a[2])
.thenComparingInt(a -> a[3]));
List<List<Integer>> ans = new ArrayList<>();
for (int i = 0; i < Math.min(k, items.size()); i++) {
int[] it = items.get(i);
ans.add(java.util.Arrays.asList(it[2], it[3]));
}
return ans;
}
// 解法二:BFS 逐层 + 堆截取(对照)
public List<List<Integer>> highestRankedKItemsHeap(int[][] grid, int[] pricing, int[] start, int k) {
int m = grid.length, n = grid[0].length;
int low = pricing[0], high = pricing[1];
boolean[][] vis = new boolean[m][n];
Deque<int[]> q = new ArrayDeque<>();
q.offer(new int[]{start[0], start[1]});
vis[start[0]][start[1]] = true;
// 堆:按 (距离, 价格, 行, 列) 升序,用 long 打包比较或自定义比较器
PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> {
for (int t = 0; t < 4; t++) {
if (a[t] != b[t]) return a[t] - b[t];
}
return 0;
});
int dist = 0;
while (!q.isEmpty()) {
int size = q.size();
for (int s = 0; s < size; s++) {
int[] cur = q.poll();
int r = cur[0], c = cur[1];
int p = grid[r][c];
if (p > 1 && p >= low && p <= high) {
heap.offer(new int[]{dist, p, r, c});
}
for (int[] d : DIRS) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < m && nc >= 0 && nc < n && !vis[nr][nc] && grid[nr][nc] != 0) {
vis[nr][nc] = true;
q.offer(new int[]{nr, nc});
}
}
}
dist++;
}
List<List<Integer>> ans = new ArrayList<>();
for (int i = 0; i < k && !heap.isEmpty(); i++) {
int[] it = heap.poll();
ans.add(java.util.Arrays.asList(it[2], it[3]));
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.highestRankedKItems(new int[][]{{1, 2, 0, 1}, {1, 3, 0, 1}, {0, 2, 5, 1}},
new int[]{2, 5}, new int[]{0, 0}, 3)); // [[0,1],[1,1],[2,1]]
System.out.println(sol.highestRankedKItemsHeap(new int[][]{{1, 2, 0, 1}, {1, 3, 0, 1}, {0, 2, 5, 1}},
new int[]{2, 5}, new int[]{0, 0}, 3));
System.out.println(sol.highestRankedKItems(new int[][]{{1, 2, 0, 1}, {1, 3, 3, 1}, {0, 2, 5, 1}},
new int[]{2, 3}, new int[]{2, 3}, 2)); // [[2,1],[1,2]]
System.out.println(sol.highestRankedKItems(new int[][]{{1, 1, 1}, {0, 0, 1}, {2, 3, 4}},
new int[]{2, 3}, new int[]{0, 0}, 3)); // [[2,1],[2,0]]
}
}
复杂度分析
复杂度:O(mn log(m n)) 时间(排序),O(mn) 空间。
复杂度:O(mn log k) 时间,O(k + m*n) 空间。用于交叉验证。
2147. Number of Ways to Divide a Long Corridor(分隔长廊的方案数)
题目描述
在一个图书馆的走廊里,有一些座位('S')和装饰植物('P')排成一列。给你一个长度为 n 的字符串 corridor。
在下标 0 的左边和下标 n-1 的右边已经分别各放了一个屏风。你还需要额外放置一些屏风,
每一个位置 i-1 和 i 之间(1 <= i <= n-1)至多能放一个屏风。
请将走廊用屏风划分为若干段,且每一段内都恰好有两个座位。返回划分走廊的方案数,对 10^9 + 7 取模。
示例 1
corridor = "SSPPSPS" → 3
示例 2
corridor = "PPSPSP" → 1
示例 3
corridor = "S" → 0
约束:
- 1 <=
n<= 10^5,corridor[i]为 'S' 或 'P'
解题思路
解法一(主提交版本)
座位分组乘法原理(主提交版本)
- 若总座位数不是偶数,直接返回 0。
- 按「每 2 个座位一组」划分:第 k 组内两个座位之间的植物不影响分组;
组间的分隔处(第 2 个座位的下一个位置 到 下一组第 1 个座位之前)可选任意位置放屏风,
方案数为 gap(中间植物数 + 1 种切分,因为还可以紧贴)。 - 更精确:第 t 组(1 起)的最后一个座位位置 p2,第 t+1 组第一个座位位置 p1,
之间的屏风可放在 p2+1 ... p1 共 (p1 - p2) 个位置,贡献乘 (p1 - p2)。
复杂度:O(n) 时间,O(1) 空间。
解法二
座位位置数组乘法(对照) - 收集所有座位的下标到数组 seats。
- 若座位总数不是偶数(含 0),返回 0。
- 每两个座位构成一组:第 t 组(1 起)的第 2 个座位与第 t+1 组的第 1 个座位之间,
屏风可放在任意空隙处,方案数为 seats2t - seats2t-1(第 3、5、7... 个座位相对前一个的间距)。 - 各 gap 相乘取模即答案。
复杂度:O(n) 时间,O(n) 空间。用于交叉验证。
代码实现
java
public class Solution {
private static final int MOD = 1_000_000_007;
// 解法一:座位分组乘法原理(主提交版本)
public int numberOfWays(String corridor) {
int n = corridor.length();
long ans = 1;
int seatCount = 0;
int lastSeatOfGroup = -1; // 当前组第二个座位的下标
for (int i = 0; i < n; i++) {
if (corridor.charAt(i) == 'S') {
seatCount++;
if (seatCount % 2 == 0) {
lastSeatOfGroup = i;
} else if (seatCount > 1) {
// 新一组开始:上一组最后座位与本组第一座位之间的空隙数为可选屏风位置
long gap = i - lastSeatOfGroup;
ans = ans * gap % MOD;
}
}
}
if (seatCount == 0 || (seatCount & 1) != 0) return 0;
return (int) ans;
}
// 解法二:座位位置数组乘法(对照)
public int numberOfWaysSeats(String corridor) {
java.util.List<Integer> seats = new java.util.ArrayList<>();
for (int i = 0; i < corridor.length(); i++) {
if (corridor.charAt(i) == 'S') seats.add(i);
}
int c = seats.size();
if (c == 0 || (c & 1) != 0) return 0;
long ans = 1;
for (int t = 2; t < c; t += 2) {
int gap = seats.get(t) - seats.get(t - 1);
ans = ans * gap % MOD;
}
return (int) ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.numberOfWays("SSPPSPS") + " " + sol.numberOfWaysSeats("SSPPSPS")); // 3 3
System.out.println(sol.numberOfWays("PPSPSP") + " " + sol.numberOfWaysSeats("PPSPSP")); // 1 1
System.out.println(sol.numberOfWays("S") + " " + sol.numberOfWaysSeats("S")); // 0 0
System.out.println(sol.numberOfWays("SS") + " " + sol.numberOfWaysSeats("SS")); // 1 1
System.out.println(sol.numberOfWays("SSS") + " " + sol.numberOfWaysSeats("SSS")); // 0 0
System.out.println(sol.numberOfWays("PPPP") + " " + sol.numberOfWaysSeats("PPPP")); // 0 0
// 座位位置 [1,2,4,5,7,8]:SS PP SS PP SS → gap: 4-2=2, 7-5=2 → 4 种
System.out.println(sol.numberOfWays("SSPPSSPPSS") + " " + sol.numberOfWaysSeats("SSPPSSPPSS")); // 4 4
}
}
复杂度分析
复杂度:O(n) 时间,O(1) 空间。
复杂度:O(n) 时间,O(n) 空间。用于交叉验证。
2148. Count Elements With Strictly Smaller and Greater Elements(元素计数)
题目描述
给你一个整数数组 nums,统计并返回在 nums 中同时至少具有一个严格较小元素和一个严格较大元素的元素数目。
示例 1
nums = [11,7,2,15] → 2(7 和 11)
示例 2
nums = [-3,3,3,90] → 2(两个 3)
约束:
- 1 <=
nums.length<= 100,-10^5 <=nums[i]<= 10^5
解题思路
解法一(主提交版本)
最大最小值夹逼(主提交版本)
- 元素若「既有严格较小又有严格较大」,等价于它不是全局最小值、也不是全局最大值。
- 先求 min、max,统计 numsi 在 (min, max) 之间的个数。
复杂度:O(n) 时间,O(1) 空间。
解法二
排序后中间段(对照) - 排序后,跳过头部的最小值段和尾部的最大值段,剩余元素个数即为答案。
复杂度:O(n log n) 时间,O(log n) 空间。用于交叉验证。
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
// 解法一:最大最小值夹逼(主提交版本)
public int countElements(int[] nums) {
int min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;
for (int v : nums) {
min = Math.min(min, v);
max = Math.max(max, v);
}
int cnt = 0;
for (int v : nums) {
if (v > min && v < max) cnt++;
}
return cnt;
}
// 解法二:排序后中间段(对照)
public int countElementsSort(int[] nums) {
java.util.Arrays.sort(nums);
int n = nums.length;
int i = 0;
while (i < n && nums[i] == nums[0]) i++; // 跳过最小值段
if (i == n) return 0;
int j = n - 1;
while (j >= 0 && nums[j] == nums[n - 1]) j--; // 跳过最大值段
return Math.max(0, j - i + 1);
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.countElements(new int[]{11, 7, 2, 15}) + " " + sol.countElementsSort(new int[]{11, 7, 2, 15})); // 2 2
System.out.println(sol.countElements(new int[]{-3, 3, 3, 90}) + " " + sol.countElementsSort(new int[]{-3, 3, 3, 90})); // 2 2
System.out.println(sol.countElements(new int[]{1, 1, 1}) + " " + sol.countElementsSort(new int[]{1, 1, 1})); // 0 0
System.out.println(sol.countElements(new int[]{1}) + " " + sol.countElementsSort(new int[]{1})); // 0 0
}
}
复杂度分析
复杂度:O(n) 时间,O(1) 空间。
复杂度:O(n log n) 时间,O(log n) 空间。用于交叉验证。
2149. Rearrange Array Elements by Sign(按符号重排数组)
题目描述
给你一个下标从 0 开始的整数数组 nums,数组长度为偶数,由数目相等的正整数和负整数组成。
返回满足下述条件的数组:
- 任意连续的两个整数符号相反;
- 对于符号相同的所有整数,保留它们在
nums中的顺序; - 重排后数组以正整数开头。
示例 1
nums = [3,1,-2,-5,2,-4] → [3,-2,1,-5,2,-4]
示例 2
nums = [-1,1] → [1,-1]
约束:
- 2 <=
nums.length<= 2*10^5,nums由相等数量的正负整数组成
解题思路
解法一(主提交版本)
双索引交替填充(主提交版本)
- 正数索引 pi = 0,负数索引 ni = 1,分别从 0 和 1 开始递增 2 填充结果数组。
- 扫描原数组,正数放入 anspi、pi += 2;负数放入 ansni、ni += 2。
复杂度:O(n) 时间(无需排序),O(n) 空间(结果数组)。
解法二
分组填充(对照) - 先把正数、负数分别收集到 list,再按 正、负、正、负... 的顺序拼接。
复杂度:O(n) 时间,O(n) 空间。用于交叉验证。
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
// 解法一:双索引交替填充(主提交版本)
public int[] rearrangeArray(int[] nums) {
int n = nums.length;
int[] ans = new int[n];
int pi = 0, ni = 1;
for (int v : nums) {
if (v > 0) {
ans[pi] = v;
pi += 2;
} else {
ans[ni] = v;
ni += 2;
}
}
return ans;
}
// 解法二:分组填充(对照)
public int[] rearrangeArrayList(int[] nums) {
List<Integer> pos = new ArrayList<>();
List<Integer> neg = new ArrayList<>();
for (int v : nums) {
if (v > 0) pos.add(v);
else neg.add(v);
}
int[] ans = new int[nums.length];
for (int i = 0; i < pos.size(); i++) {
ans[2 * i] = pos.get(i);
ans[2 * i + 1] = neg.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());
}
public static void main(String[] args) {
Solution sol = new Solution();
print(sol.rearrangeArray(new int[]{3, 1, -2, -5, 2, -4})); // [3,-2,1,-5,2,-4]
print(sol.rearrangeArrayList(new int[]{3, 1, -2, -5, 2, -4}));
print(sol.rearrangeArray(new int[]{-1, 1})); // [1,-1]
print(sol.rearrangeArrayList(new int[]{-1, 1}));
}
}
复杂度分析
复杂度:O(n) 时间(无需排序),O(n) 空间(结果数组)。
复杂度:O(n) 时间,O(n) 空间。用于交叉验证。
2150. Find All Lonely Numbers in the Array(找出数组中的所有孤独数字)
题目描述
给你一个整数数组 nums。如果数字 x 在数组中仅出现一次,且没有相邻数字(即 x + 1 和 x - 1)出现在数组中,
则认为数字 x 是孤独数字。返回 nums 中的所有孤独数字,可以按任何顺序返回。
示例 1
nums = [10,6,5,8] → [10,8]
示例 2
nums = [1,3,5,3] → [1,5]
约束:
- 1 <=
nums.length<= 10^5,0 <=nums[i]<= 10^6
解题思路
解法一(主提交版本)
计数 + 邻域检查(主提交版本)
- 先统计每个数字出现次数。
- 对每个出现次数恰为 1 的数字 x,检查 x-1 与 x+1 是否都不在数组中。
复杂度:O(n) 时间,O(n) 空间。
解法二
排序后扫描(对照) - 排序后,孤独数字满足:与前一个元素不等且差值 > 1,与后一个元素不等且差值 > 1(考虑重复)。
按「该值只出现一次且前后无相邻值」判断。
复杂度:O(n log n) 时间,O(1) 空间(结果除外)。用于交叉验证。
代码实现
java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Solution {
// 解法一:计数 + 邻域检查(主提交版本)
public List<Integer> findLonely(int[] nums) {
Map<Integer, Integer> cnt = new HashMap<>();
for (int v : nums) cnt.put(v, cnt.getOrDefault(v, 0) + 1);
List<Integer> ans = new ArrayList<>();
for (Map.Entry<Integer, Integer> e : cnt.entrySet()) {
int x = e.getKey();
if (e.getValue() != 1) continue;
if (cnt.containsKey(x - 1) || cnt.containsKey(x + 1)) continue;
ans.add(x);
}
return ans;
}
// 解法二:排序后扫描(对照)
public List<Integer> findLonelySort(int[] nums) {
java.util.Arrays.sort(nums);
int n = nums.length;
List<Integer> ans = new ArrayList<>();
for (int i = 0; i < n; i++) {
if (i > 0 && nums[i - 1] == nums[i]) continue; // 重复值
if (i + 1 < n && nums[i + 1] == nums[i]) continue;
if (i > 0 && nums[i - 1] == nums[i] - 1) continue; // 前邻
if (i + 1 < n && nums[i + 1] == nums[i] + 1) continue; // 后邻
ans.add(nums[i]);
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.findLonely(new int[]{10, 6, 5, 8})); // [10,8]
System.out.println(sol.findLonelySort(new int[]{10, 6, 5, 8}));
System.out.println(sol.findLonely(new int[]{1, 3, 5, 3})); // [1,5]
System.out.println(sol.findLonelySort(new int[]{1, 3, 5, 3}));
System.out.println(sol.findLonely(new int[]{0, 0, 2})); // [2]
System.out.println(sol.findLonelySort(new int[]{0, 0, 2}));
}
}
复杂度分析
复杂度:O(n) 时间,O(n) 空间。
复杂度:O(n log n) 时间,O(1) 空间(结果除外)。用于交叉验证。
2151. Maximum Good People Based on Statements(基于陈述统计最多好人数)
题目描述
游戏中存在两种角色:好人(只说真话)与坏人(可能说真话也可能说假话)。
给你一个 n x n 的二维整数数组 statements:statements[i][j] = 1 表示 i 认为 j 是好人;
0 表示 i 认为 j 是坏人;2 表示 i 没有对 j 作出陈述。玩家不会对自己作出陈述(对角线全为 2)。
根据这 n 个玩家的陈述,返回可以认为是好人的最大数目。
示例 1
statements = [[2,1,2],[1,2,2],[2,0,2]] → 2
示例 2
statements = [[2,0],[0,2]] → 1
约束:
- 2 <=
n<= 15,statements[i][j]为 0/1/2,statements[i][i]== 2
解题思路
解法一(主提交版本)
枚举好人掩码 + 一致性校验(主提交版本)
- n <= 15,可枚举所有 2^n 种「好人集合」。
- 对每个掩码 mask(第 i 位为 1 表示 i 是好人):校验每个好人 i 的每条非 2 陈述
statementsij 与「j 是否为好人」一致;若所有好人陈述都一致,则该掩码合法,
统计其中好人个数,取最大。
复杂度:O(2^n * n^2),n <= 15 可行;空间 O(1)。
解法二
回溯剪枝(对照) - 用 DFS 尝试把每个人设为好人或坏人,边设置边检查已定好人的陈述约束;回溯剪枝。
复杂度:最坏 O(2^n * n^2),空间 O(n)。用于交叉验证。
代码实现
java
public class Solution {
// 解法一:枚举好人掩码 + 一致性校验(主提交版本)
public int maximumGood(int[][] statements) {
int n = statements.length;
int best = 0;
for (int mask = 0; mask < (1 << n); mask++) {
if (!valid(mask, statements, n)) continue;
best = Math.max(best, Integer.bitCount(mask));
}
return best;
}
private boolean valid(int mask, int[][] statements, int n) {
for (int i = 0; i < n; i++) {
if ((mask >> i & 1) == 0) continue; // 坏人说的话可真可假,不约束
for (int j = 0; j < n; j++) {
int s = statements[i][j];
if (s == 2) continue;
boolean jGood = (mask >> j & 1) == 1;
if ((s == 1) != jGood) return false; // 好人说的必须全真
}
}
return true;
}
// 解法二:DFS 回溯(对照)
private int n;
private int best;
private int[][] st;
public int maximumGoodDfs(int[][] statements) {
this.st = statements;
this.n = statements.length;
this.best = 0;
int[] status = new int[n]; // 0=未定, 1=好人, -1=坏人
dfs(0, 0, status);
return best;
}
private void dfs(int idx, int goodCnt, int[] status) {
if (idx == n) {
best = Math.max(best, goodCnt);
return;
}
if (goodCnt + (n - idx) <= best) return; // 剪枝
// 尝试 idx 是坏人
int[] save = status.clone();
status[idx] = -1;
if (consistent(idx, status)) dfs(idx + 1, goodCnt, status);
// 尝试 idx 是好人
System.arraycopy(save, 0, status, 0, n);
status[idx] = 1;
if (consistent(idx, status)) dfs(idx + 1, goodCnt + 1, status);
System.arraycopy(save, 0, status, 0, n);
}
// 检查已定为好人的前面所有人的陈述与当前 status 是否冲突
private boolean consistent(int idx, int[] status) {
for (int i = 0; i < n; i++) {
if (status[i] != 1) continue;
for (int j = 0; j < n; j++) {
int s = st[i][j];
if (s == 2) continue;
if (status[j] != 0 && (s == 1) != (status[j] == 1)) return false;
}
}
return true;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.maximumGood(new int[][]{{2, 1, 2}, {1, 2, 2}, {2, 0, 2}})
+ " " + sol.maximumGoodDfs(new int[][]{{2, 1, 2}, {1, 2, 2}, {2, 0, 2}})); // 2 2
System.out.println(sol.maximumGood(new int[][]{{2, 0}, {0, 2}})
+ " " + sol.maximumGoodDfs(new int[][]{{2, 0}, {0, 2}})); // 1 1
System.out.println(sol.maximumGood(new int[][]{{2, 1}, {1, 2}})
+ " " + sol.maximumGoodDfs(new int[][]{{2, 1}, {1, 2}})); // 2(互称好人)
}
}
复杂度分析
复杂度:O(2^n * n^2),n <= 15 可行;空间 O(1)。
复杂度:最坏 O(2^n * n^2),空间 O(n)。用于交叉验证。
2152. Minimum Number of Lines to Cover Points(穿过所有点的所需最少直线数量)
题目描述
给定一个 points 数组,points[i] = `xi`, `yi` 表示直角坐标系 X-Y 的一个点。现在考虑向 X-Y 坐标系中添加直线,
使得每个点至少在一条直线上。返回能够穿过所有点的所需最少直线数量。
示例 1
points = [[0,1],[2,3],[4,5],[4,3]] → 2
示例 2
points = [[0,2],[-2,-2],[1,4]] → 1
约束:
- 1 <=
points.length<= 10,坐标范围 -100, 100,点互不相同
解题思路
解法一(主提交版本)
掩码记忆化搜索(主提交版本)
- n <= 10,可用位掩码表示点集。先把「过两点 i、j 的直线覆盖的所有点」预计算为 coverij。
- DFS(mask):mask 表示未覆盖的点集。任选其中一点 i,枚举它与 mask 中任意一点 j 组成的直线,
一次覆盖 coverij,递归 mask & ~coverij;也可以选 i 单独成一直线(覆盖 {i})。 - 用备忘录避免重复计算。答案即 dfs(全 1 掩码)。
复杂度:O(2^n * n^2),n <= 10 可行。
解法二
贪心 + 状态压缩 BFS(对照) - 等价视角:从全未覆盖状态开始,每一步选一条直线覆盖剩余点,求最小步数------用 BFS 层数即最少直线数,
用 Map<mask, depth> 记录。用于交叉验证。
复杂度:O(2^n * n^2) 时间,O(2^n) 空间。
代码实现
java
import java.util.HashMap;
import java.util.Map;
public class Solution {
// 解法一:掩码记忆化搜索(主提交版本)
private int[][] cover; // cover[i][j]:过 i、j 两点的直线覆盖的点集掩码
private int fullMask;
private int[] memo;
private int n;
public int minimumLines(int[][] points) {
this.n = points.length;
fullMask = (1 << n) - 1;
cover = new int[n][n];
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int m = 0;
for (int k = 0; k < n; k++) {
if (collinear(points[i], points[j], points[k])) m |= (1 << k);
}
cover[i][j] = cover[j][i] = m;
}
cover[i][i] = 1 << i;
}
memo = new int[1 << n];
java.util.Arrays.fill(memo, -1);
return dfs(fullMask);
}
private boolean collinear(int[] a, int[] b, int[] c) {
// 叉积为零
long cross = (long) (b[0] - a[0]) * (c[1] - a[1]) - (long) (b[1] - a[1]) * (c[0] - a[0]);
return cross == 0;
}
private int dfs(int mask) {
if (mask == 0) return 0;
if (memo[mask] != -1) return memo[mask];
// 选 mask 中最低位点 i
int i = Integer.numberOfTrailingZeros(mask);
int rest = mask & ~(1 << i);
if (rest == 0) return memo[mask] = 1;
int best = n; // 上界放一个较大的数
int m = rest;
while (m != 0) {
int j = Integer.numberOfTrailingZeros(m);
int cov = cover[i][j] & mask;
best = Math.min(best, 1 + dfs(mask & ~cov));
m &= (m - 1);
}
// 也可以只覆盖 i 一个点
best = Math.min(best, 1 + dfs(rest));
return memo[mask] = best;
}
// 解法二:状态压缩 BFS(对照)
public int minimumLinesBfs(int[][] points) {
int n = points.length;
int full = (1 << n) - 1;
int[][] cov = new int[n][n];
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int m = 0;
for (int k = 0; k < n; k++) {
if (collinear(points[i], points[j], points[k])) m |= (1 << k);
}
cov[i][j] = cov[j][i] = m;
}
cov[i][i] = 1 << i;
}
java.util.ArrayDeque<Integer> q = new java.util.ArrayDeque<>();
Map<Integer, Integer> dist = new HashMap<>();
q.offer(full);
dist.put(full, 0);
while (!q.isEmpty()) {
int mask = q.poll();
int d = dist.get(mask);
if (mask == 0) return d;
int i = Integer.numberOfTrailingZeros(mask);
int rest = mask & ~(1 << i);
int m = rest;
while (m != 0) {
int j = Integer.numberOfTrailingZeros(m);
int nxt = mask & ~(cov[i][j] & mask);
if (!dist.containsKey(nxt)) {
dist.put(nxt, d + 1);
q.offer(nxt);
}
m &= (m - 1);
}
int nxtSingle = rest;
if (!dist.containsKey(nxtSingle)) {
dist.put(nxtSingle, d + 1);
q.offer(nxtSingle);
}
}
return n;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.minimumLines(new int[][]{{0, 1}, {2, 3}, {4, 5}, {4, 3}})
+ " " + sol.minimumLinesBfs(new int[][]{{0, 1}, {2, 3}, {4, 5}, {4, 3}})); // 2 2
System.out.println(sol.minimumLines(new int[][]{{0, 2}, {-2, -2}, {1, 4}})
+ " " + sol.minimumLinesBfs(new int[][]{{0, 2}, {-2, -2}, {1, 4}})); // 1 1
System.out.println(sol.minimumLines(new int[][]{{0, 0}, {1, 1}, {2, 2}, {3, 3}})
+ " " + sol.minimumLinesBfs(new int[][]{{0, 0}, {1, 1}, {2, 2}, {3, 3}})); // 1 1
}
}
复杂度分析
复杂度:O(2^n * n^2),n <= 10 可行。
复杂度:O(2^n * n^2) 时间,O(2^n) 空间。
2153. The Number of Passengers in Each Bus II(每辆车的乘客人数 II)
题目类型:数据库(SQL)
题目描述
表: Buses
+--------------+------+
| Column Name | Type |
+--------------+------+
| bus_id | int |
| arrival_time | int |
| capacity | int |
+--------------+------+
bus_id 包含唯一的值。该表的每一行都包含关于公交车到达 LeetCode 站点的时间和它的容量(空座位的数量)的信息。不会出现两辆公交车同时到达,所有公交车的容量都是正整数。
表: Passengers
+--------------+------+
| Column Name | Type |
+--------------+------+
| passenger_id | int |
| arrival_time | int |
+--------------+------+
passenger_id 包含唯一的值。该表的每一行都包含乘客到达 LeetCode 站的时间信息。
公交车和乘客到达 LeetCode 站。如果一辆公交车在 tbus 时间点到达车站,乘客在 tpassenger 到达车站,其中 tpassenger <= tbus,而该乘客没有赶上任何公交车,则该乘客将搭乘该公交车。此外,每辆公交车都有一个容量。如果在公交车到站的那一刻,等待的乘客超过了它的载客量 capacity,只有 capacity 个乘客才会搭乘该公交车。
编写解决方案,报告使用每条总线的用户数量。返回按 bus_id 升序排序的结果表。
示例 1:
输入:
Buses 表:
+--------+--------------+----------+
| bus_id | arrival_time | capacity |
+--------+--------------+----------+
| 1 | 2 | 1 |
| 2 | 4 | 10 |
| 3 | 7 | 2 |
+--------+--------------+----------+
Passengers 表:
+--------------+--------------+
| passenger_id | arrival_time |
+--------------+--------------+
| 11 | 1 |
| 12 | 1 |
| 13 | 5 |
| 14 | 6 |
| 15 | 7 |
+--------------+--------------+
输出:
+--------+----------------+
| bus_id | passengers_cnt |
+--------+----------------+
| 1 | 1 |
| 2 | 1 |
| 3 | 2 |
+--------+----------------+
解释:
- 1 号车(容量 1)在时间 2 到达,搭载 11 号乘客;
- 2 号车(容量 10)在时间 4 到达,搭载剩下的 12 号乘客;
- 3 号车(容量 2)在时间 7 到达,搭载 13、14 号乘客(先到先上,超过容量的 15 号上不了车)。
解题思路
解法一:时间窗口统计 + 容量截断(累计客流模型)
第 i 辆公交车(按到达时间排序)能载的乘客数 = 在它之前已到站且未上车的人数;而"未上车"的人数是累计到达人数 减去此前各车已载人数之和,再被本车容量截断:
- 记
cum_pass[i]= 到达时间<= bus_i.arrival_time的乘客总数; - 记
prev_loaded[i]= 前i-1辆车累计载客数; - 则本车可接待人数 =
LEAST(capacity_i, cum_pass[i] - prev_loaded[i])(取非负)。
在 SQL 中用窗口函数 SUM() OVER (ORDER BY arrival_time) 计算每个公交时点的累计到达、并用递归/自连接累加已载客数即可。由于前车的载客又依赖更早的载客,属于递推 关系,SQL 里可用窗口+逐行累积的等价写法(或利用 SUM(...) OVER 配合 CASE 近似两遍递推)。
解法二(对照):LEAD/LAG 时间窗口 + 每窗容量
把相邻两辆公交 (t_prev, t_cur] 视为一个窗口,窗口内到达的乘客是候选;但窗口内乘客可能因为上一窗口装不下的"积压"而超过容量,因此仍需累计扣减------即解法一是 II 题标准解法,解法二提供另一种实现视角。
代码实现
sql
-- 解法一(推荐):累计到达 - 累计已载,容量截断(MySQL 语法)
-- 借助递归/推导:对每辆车,waiting = 累计到站 - 此前已载;loaded = LEAST(capacity, waiting)
WITH bus_rank AS (
SELECT bus_id, arrival_time, capacity,
ROW_NUMBER() OVER (ORDER BY arrival_time) AS rk
FROM Buses
),
pass_cum AS (
SELECT b.rk, b.bus_id, b.arrival_time, b.capacity,
(SELECT COUNT(*) FROM Passengers p WHERE p.arrival_time <= b.arrival_time) AS cum
FROM bus_rank b
)
-- 递推:第 rk 辆车的已载客数依赖前一辆的已载客数,用递归 CTE 表达
WITH RECURSIVE loaded(rk, bus_id, prev_loaded, loaded_this) AS (
SELECT rk, bus_id, 0,
LEAST(capacity, cum)
FROM pass_cum WHERE rk = 1
UNION ALL
SELECT pc.rk, pc.bus_id,
l.prev_loaded + l.loaded_this,
LEAST(pc.capacity, pc.cum - (l.prev_loaded + l.loaded_this))
FROM pass_cum pc
JOIN loaded l ON pc.rk = l.rk + 1
)
SELECT bus_id, loaded_this AS passengers_cnt
FROM loaded
ORDER BY bus_id;
sql
-- 解法二(对照):标记每辆车的上一辆到站时间,窗口内统计候选人数后做容量截断
WITH bus_gap AS (
SELECT bus_id, arrival_time, capacity,
COALESCE(LAG(arrival_time) OVER (ORDER BY arrival_time), 0) AS prev_time
FROM Buses
),
cand AS (
SELECT b.bus_id, b.capacity,
(SELECT COUNT(*) FROM Passengers p
WHERE p.arrival_time > b.prev_time AND p.arrival_time <= b.arrival_time) AS in_window
FROM bus_gap b
)
SELECT bus_id, LEAST(capacity, in_window) AS passengers_cnt
FROM cand
ORDER BY bus_id;
复杂度分析
- 时间复杂度 :
O((m + n) log n)主流程(排序 + 窗口计数),其中m为公交数、n为乘客数;递归 CTE 每层扫描一次。 - 空间复杂度 :
O(m + n),中间 CTE 结果集与两表规模成正比。
2154. Keep Multiplying Found Values by Two(将找到的值乘以 2)
题目描述
给你一个整数数组 nums,另给你一个整数 original,这是需要在 nums 中搜索的第一个数字。
接下来,你需要按下述步骤操作:如果在 nums 中找到 original,将 original 乘以 2,得到新的 original;
否则停止这一过程。只要能在数组中找到新的 original,就继续重复这一过程。返回 original 的最终值。
示例 1
nums = [5,3,6,1,12], original = 3 → 24
示例 2
nums = [2,7,9], original = 4 → 4
约束:
- 1 <=
nums.length<= 1000,1 <=nums[i], original <= 1000
解题思路
解法一(主提交版本)
哈希集合循环(主提交版本)
- 将所有值放入 HashSet,循环:只要 original 在集合中,就 original *= 2。
复杂度:O(n) 时间,O(n) 空间。
解法二
排序后顺序查找(对照) - 排序后,每次二分或逐步递增判断是否存在;用循环 while + 是否存在即可。
复杂度:O(n log n) 时间(排序),O(log n) 额外空间。用于交叉验证。
代码实现
java
import java.util.HashSet;
import java.util.Set;
public class Solution {
// 解法一:哈希集合循环(主提交版本)
public int findFinalValue(int[] nums, int original) {
Set<Integer> set = new HashSet<>();
for (int v : nums) set.add(v);
while (set.contains(original)) {
original *= 2;
}
return original;
}
// 解法二:排序后存在性判断(对照)
public int findFinalValueSort(int[] nums, int original) {
java.util.Arrays.sort(nums);
int cur = original;
while (java.util.Arrays.binarySearch(nums, cur) >= 0) {
cur *= 2;
}
return cur;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.findFinalValue(new int[]{5, 3, 6, 1, 12}, 3)
+ " " + sol.findFinalValueSort(new int[]{5, 3, 6, 1, 12}, 3)); // 24 24
System.out.println(sol.findFinalValue(new int[]{2, 7, 9}, 4)
+ " " + sol.findFinalValueSort(new int[]{2, 7, 9}, 4)); // 4 4
System.out.println(sol.findFinalValue(new int[]{2, 4, 8, 16}, 2)
+ " " + sol.findFinalValueSort(new int[]{2, 4, 8, 16}, 2)); // 32 32
}
}
复杂度分析
复杂度:O(n) 时间,O(n) 空间。
复杂度:O(n log n) 时间(排序),O(log n) 额外空间。用于交叉验证。
2155. All Divisions With the Highest Score of a Binary Array(分组得分最高的所有下标)
题目描述
给你一个下标从 0 开始的二进制数组 nums,长度为 n。nums 可以按下标 i(0 <= i <= n)拆分成两个数组
(可能为空):numsleft(下标 0 到 i-1)和 numsright(下标 i 到 n-1)。
下标 i 的分组得分为 numsleft 中 0 的个数和 numsright 中 1 的个数之和。
返回分组得分最高的所有不同下标,可以按任意顺序返回。
示例 1
nums = [0,0,1,0] → [2,4]
示例 2
nums = [0,0,0] → [3]
示例 3
nums = [1,1] → [0]
约束:
- 1 <=
n<= 10^5,nums[i]为 0 或 1
解题思路
解法一(主提交版本)
前后计数扫描(主提交版本)
- 第一遍统计全部 1 的个数 totalOnes(即 i=0 时 numsright 中 1 的个数)。
- 从左往右移动 i,同步维护 leftZeros 与 rightOnes:
初始 i=0:leftZeros=0, rightOnes=totalOnes;每移动一步,若 numsi-1==0 则 leftZeros++,
若 ==1 则 rightOnes--。得分 = leftZeros + rightOnes。 - 记录最大得分与所有达到该得分的下标。
复杂度:O(n) 时间,O(n) 空间(结果)。
解法二
前缀和数组(对照) - 预处理前缀 1 个数 pref,则 leftZeros = i - prefi,rightOnes = prefn - prefi,
逐个计算得分。用于交叉验证。
复杂度:O(n) 时间,O(n) 空间。
代码实现
java
import java.util.ArrayList;
import java.util.List;
public class Solution {
// 解法一:前后计数扫描(主提交版本)
public List<Integer> maxScoreIndices(int[] nums) {
int n = nums.length;
int totalOnes = 0;
for (int v : nums) totalOnes += v;
int leftZeros = 0, rightOnes = totalOnes;
int best = -1;
List<Integer> ans = new ArrayList<>();
for (int i = 0; i <= n; i++) {
int score = leftZeros + rightOnes;
if (score > best) {
best = score;
ans.clear();
ans.add(i);
} else if (score == best) {
ans.add(i);
}
if (i < n) {
if (nums[i] == 0) leftZeros++;
else rightOnes--;
}
}
return ans;
}
// 解法二:前缀和数组(对照)
public List<Integer> maxScoreIndicesPref(int[] nums) {
int n = nums.length;
int[] pref = new int[n + 1];
for (int i = 0; i < n; i++) pref[i + 1] = pref[i] + nums[i];
int best = -1;
List<Integer> ans = new ArrayList<>();
for (int i = 0; i <= n; i++) {
int leftZeros = i - pref[i];
int rightOnes = pref[n] - pref[i];
int score = leftZeros + rightOnes;
if (score > best) {
best = score;
ans.clear();
ans.add(i);
} else if (score == best) {
ans.add(i);
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.maxScoreIndices(new int[]{0, 0, 1, 0})); // [2,4]
System.out.println(sol.maxScoreIndicesPref(new int[]{0, 0, 1, 0}));
System.out.println(sol.maxScoreIndices(new int[]{0, 0, 0})); // [3]
System.out.println(sol.maxScoreIndicesPref(new int[]{0, 0, 0}));
System.out.println(sol.maxScoreIndices(new int[]{1, 1})); // [0]
System.out.println(sol.maxScoreIndicesPref(new int[]{1, 1}));
}
}
复杂度分析
复杂度:O(n) 时间,O(n) 空间(结果)。
复杂度:O(n) 时间,O(n) 空间。
2156. Find Substring With Given Hash Value(查找给定哈希值的子串)
题目描述
给定整数 p 和 m,一个长度为 k 且下标从 0 开始的字符串 s 的哈希值按如下函数计算:
hash(s, p, m) = (val(s[0]) * p^0 + val(s[1]) * p^1 + ... + val(s[k-1]) * p^(k-1)) mod m,
其中 val(s[i]) 表示 s[i] 在字母表中的下标(val('a')=1 到 val('z')=26)。
给你字符串 s 和整数 power、modulo、k、hashValue,返回 s 中第一个长度为 k 的子串 sub,
满足 hash(sub, power, modulo) == hashValue。测试数据保证至少存在一个这样的子串。
示例 1
s = "leetcode", power = 7, modulo = 20, k = 2, hashValue = 0 → "ee"
示例 2
s = "fbxzaad", power = 31, modulo = 100, k = 3, hashValue = 32 → "fbx"
约束:
- 1 <=
k<=s.length<= 2*10^4,1 <= power, modulo <= 10^9,0 <=hashValue< modulo
解题思路
解法一(主提交版本)
从右往左的无除法滚动哈希(主提交版本)
- 哈希定义中删除窗口最左字符需要除以 p,而 modulo 不保证为质数、不能求逆,
因此选择「从右往左」滚动:已知 H(w_l)(窗口 sl...l+k))时, H(w_{l-1}) = (p \* H(w_l) + val(s\[l-1) - val(sl+k-1) * p^k) mod m,
全部只需乘、加、减,无需除法模逆。 - 初始化最右窗口 [n-k, n) 的哈希(预计算 p 的幂),向左滑动,记录最左(第一个)匹配。
复杂度:O(n) 时间(预计算 p^k 为 O(k)),O(1) 额外空间(结果 O(k))。
解法二
预计算幂暴力(对照,供交叉验证) - 预计算 p0...p(k-1) mod m,枚举所有窗口直接求和比对。
复杂度:O(n*k) 时间,O(k) 空间。仅适合小规模交叉验证。
代码实现
java
public class Solution {
// 解法一:无除法滚动哈希(主提交版本)
public String subStrHash(String s, int power, int modulo, int k, int hashValue) {
int n = s.length();
long pmod = power % modulo;
// 预计算 p^k mod m
long pk = 1;
for (int i = 0; i < k; i++) pk = pk * pmod % modulo;
// 初始化最右窗口 [n-k, n) 的哈希 H = Σ val(s[n-k+t]) * p^t
long h = 0, pw = 1;
for (int t = 0; t < k; t++) {
h = (h + val(s.charAt(n - k + t)) * pw) % modulo;
pw = pw * pmod % modulo;
}
int ansPos = -1;
for (int l = n - k; l >= 0; l--) {
if (h == hashValue) ansPos = l; // 越左更新,最终为最左匹配
if (l == 0) break;
// 左移窗口:H(w_{l-1}) = (p * H(w_l) + val(s[l-1]) - val(s[l+k-1]) * p^k) mod m
long nxt = h * pmod % modulo;
nxt = (nxt + val(s.charAt(l - 1))) % modulo;
nxt = ((nxt - val(s.charAt(l + k - 1)) * pk) % modulo + modulo) % modulo;
h = nxt;
}
return s.substring(ansPos, ansPos + k);
}
private long val(char c) {
return c - 'a' + 1;
}
// 解法二:暴力窗口(对照,小规模)
public String subStrHashBrute(String s, int power, int modulo, int k, int hashValue) {
int n = s.length();
long[] pw = new long[k];
pw[0] = 1;
long pmod = power % modulo;
for (int i = 1; i < k; i++) pw[i] = pw[i - 1] * pmod % modulo;
for (int i = 0; i + k <= n; i++) {
long h = 0;
for (int j = 0; j < k; j++) {
h = (h + val(s.charAt(i + j)) * pw[j]) % modulo;
}
if (h == hashValue) return s.substring(i, i + k);
}
return "";
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.subStrHash("leetcode", 7, 20, 2, 0)
+ " | " + sol.subStrHashBrute("leetcode", 7, 20, 2, 0)); // ee | ee
System.out.println(sol.subStrHash("fbxzaad", 31, 100, 3, 32)
+ " | " + sol.subStrHashBrute("fbxzaad", 31, 100, 3, 32)); // fbx | fbx
System.out.println(sol.subStrHash("abcdef", 2, 10, 1, 5)
+ " | " + sol.subStrHashBrute("abcdef", 2, 10, 1, 5)); // e | e(val('e')=5)
System.out.println(sol.subStrHash("abcde", 2, 10, 1, 3)
+ " | " + sol.subStrHashBrute("abcde", 2, 10, 1, 3)); // c | c(val('c')=3)
System.out.println(sol.subStrHash("zzzzz", 3, 101, 2, 3)
+ " | " + sol.subStrHashBrute("zzzzz", 3, 101, 2, 3)); // zz | zz(26+26*3=104 ≡ 3 mod 101)
}
}
复杂度分析
复杂度:O(n) 时间(预计算 p^k 为 O(k)),O(1) 额外空间(结果 O(k))。
复杂度:O(n*k) 时间,O(k) 空间。仅适合小规模交叉验证。
2157. Groups of Strings(字符串分组)
题目描述
给你一个下标从 0 开始的字符串数组 words。每个字符串都只包含小写英文字母,且每个字母至多只出现一次。
如果通过以下操作之一,可以从 s1 的字母集合得到 s2 的字母集合,则称这两个字符串为「关联的」:
- 添加一个字母;或 删去一个字母;或 将一个字母替换成另外任意一个字母(可以替换为这个字母本身)。
数组words可以分为一个或多个无交集的组:一个组内的任一字符串与其他组的字符串都不关联(方案唯一)。
返回长度为 2 的数组ans:ans[0]是总组数,ans[1]是字符串数目最多的组所包含的字符串数目。
示例 1
words = ["a","b","ab","cde"] → [2,3]
示例 2
words = ["a","ab","abc"] → [1,3]
约束:
- 1 <=
words.length<= 2*10^4,1 <=words[i].length<= 26,字母不重复
解题思路
解法一(主提交版本)
位掩码并查集(主提交版本)
- 每个字符串用 26 位掩码表示。关键观察:「关联」是等价关系,且可通过「掩码经一次操作可达」传递。
对任意掩码 m,邻居为:(1)删除一位(2)添加一位(3)替换一位(等价于删后加)。 - 用并查集:先按掩码聚类所有词(相同掩码必然同组),再枚举「删一位/加一位/替换」检查图中所有可达的
不同掩码是否存在于词集合中,有则 union。 - 由于字母集大小 26 且词长度 ≤ 26,添加/删除/替换操作数为 O(26^2) 级别,
对 2*10^4 个词在掩码空间做此类合并仍可行(实际实现先按掩码去重,词分散)。 - 简化正确且高效的做法:对每个去重后的掩码 m,枚举 26 位删除得到 m1,若能找到 m1 则 union;
以及枚举添加(在第 i 位为 0 时设置)得到 m2 并 union。替换的传递性可由 delete+add 覆盖------
但「替换」允许一步直达,为不遗漏,还需检查「删除一位后再添加任意一位」得到的新掩码是否也在集合中并行 union;
稳妥起见对每个 m 枚举替换:删一位(26)再为每个可能的添加位(26)共 O(26^2) 检查。
复杂度:O(W * (26^2) * α) 最坏,实际远小于(掩码去重);空间 O(去重个数)。
解法二
纯枚举邻居 BFS(对照) - 与并查集同构:对去重掩码建图,BFS 求连通块。用于交叉验证。
复杂度:同上,空间 O(W)。
代码实现
java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Solution {
// 解法一:位掩码 + 并查集(主提交版本)
public int[] groupStrings(String[] words) {
Map<Integer, Integer> idOfMask = new HashMap<>();
for (String w : words) {
int m = mask(w);
idOfMask.putIfAbsent(m, idOfMask.size());
}
int n = idOfMask.size();
List<Integer> masks = new ArrayList<>(idOfMask.keySet());
// 统计每个 mask 的词频
int[] size = new int[n];
for (String w : words) {
size[idOfMask.get(mask(w))]++;
}
int[] parent = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
int[] compSize = size.clone();
for (int i = 0; i < n; i++) {
int m = masks.get(i);
// 1) 删除一位
for (int bit = 0; bit < 26; bit++) {
if ((m >> bit & 1) == 1) {
int m1 = m ^ (1 << bit);
Integer id = idOfMask.get(m1);
if (id != null) union(i, id, parent, compSize);
// 2) 删除 bit 再添加另一位(替换)
for (int b2 = 0; b2 < 26; b2++) {
if ((m1 >> b2 & 1) == 0) {
int m2 = m1 | (1 << b2);
Integer id2 = idOfMask.get(m2);
if (id2 != null) union(i, id2, parent, compSize);
}
}
}
}
// 3) 直接添加一位
for (int bit = 0; bit < 26; bit++) {
if ((m >> bit & 1) == 0) {
int m3 = m | (1 << bit);
Integer id3 = idOfMask.get(m3);
if (id3 != null) union(i, id3, parent, compSize);
}
}
}
int groups = 0, max = 0;
for (int i = 0; i < n; i++) {
if (find(i, parent) == i) {
groups++;
max = Math.max(max, compSize[i]);
}
}
return new int[]{groups, max};
}
private int find(int x, int[] parent) {
while (parent[x] != x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
private void union(int a, int b, int[] parent, int[] compSize) {
int ra = find(a, parent), rb = find(b, parent);
if (ra == rb) return;
if (compSize[ra] < compSize[rb]) {
int t = ra; ra = rb; rb = t;
}
parent[rb] = ra;
compSize[ra] += compSize[rb];
}
private int mask(String w) {
int m = 0;
for (int i = 0; i < w.length(); i++) m |= 1 << (w.charAt(i) - 'a');
return m;
}
// 解法二:BFS 连通块(对照)
public int[] groupStringsBfs(String[] words) {
Map<Integer, Integer> idOfMask = new HashMap<>();
int[] size = new int[words.length];
int n = 0;
for (String w : words) {
int m = mask(w);
if (!idOfMask.containsKey(m)) idOfMask.put(m, n++);
size[idOfMask.get(m)]++;
}
List<Integer> masks = new ArrayList<>(idOfMask.keySet());
boolean[] vis = new boolean[n];
int groups = 0, max = 0;
for (int i = 0; i < n; i++) {
if (vis[i]) continue;
groups++;
int cnt = 0;
java.util.ArrayDeque<Integer> q = new java.util.ArrayDeque<>();
q.offer(i);
vis[i] = true;
while (!q.isEmpty()) {
int u = q.poll();
cnt += size[u];
int m = masks.get(u);
java.util.Set<Integer> nbr = new java.util.HashSet<>();
for (int bit = 0; bit < 26; bit++) {
if ((m >> bit & 1) == 1) {
int m1 = m ^ (1 << bit);
nbr.add(m1);
for (int b2 = 0; b2 < 26; b2++) {
if ((m1 >> b2 & 1) == 0) nbr.add(m1 | (1 << b2));
}
} else {
nbr.add(m | (1 << bit));
}
}
for (int nb : nbr) {
Integer id = idOfMask.get(nb);
if (id != null && !vis[id]) {
vis[id] = true;
q.offer(id);
}
}
}
max = Math.max(max, cnt);
}
return new int[]{groups, max};
}
public static void main(String[] args) {
Solution sol = new Solution();
int[] r0 = sol.groupStrings(new String[]{"a", "b", "ab", "cde"});
System.out.println(r0[0] + "," + r0[1]); // 2,3
int[] r1 = sol.groupStringsBfs(new String[]{"a", "b", "ab", "cde"});
System.out.println(r1[0] + "," + r1[1]);
int[] r2 = sol.groupStrings(new String[]{"a", "ab", "abc"});
System.out.println(r2[0] + "," + r2[1]); // 1,3
int[] r3 = sol.groupStringsBfs(new String[]{"a", "ab", "abc"});
System.out.println(r3[0] + "," + r3[1]);
int[] r4 = sol.groupStrings(new String[]{"az", "b"});
System.out.println(r4[0] + "," + r4[1]); // "az" 替换 z 成 b -> "ab"? 不关联,应为 2,1? 实际: az 和 b 差太多
}
}
复杂度分析
复杂度:O(W * (26^2) * α) 最坏,实际远小于(掩码去重);空间 O(去重个数)。
复杂度:同上,空间 O(W)。
2158. Amount of New Area Painted Each Day(每天绘制新区域的数量)
题目描述
有一幅细长的画,可以用数轴来表示。给你一个长度为 n、下标从 0 开始的二维整数数组 paint,
其中 paint[i] = `starti`, `endi` 表示在第 i 天你需要绘制 starti 和 endi 之间的区域。
多次绘制同一区域会导致不均匀,因此每个区域最多只能绘制一次。返回一个长度为 n 的整数数组 worklog,
其中 worklog[i] 是你在第 i 天绘制的新的区域数量。
示例 1
paint = [[1,4],[4,7],[5,8]] → [3,3,1]
示例 2
paint = [[1,4],[5,8],[4,7]] → [3,3,1]
示例 3
paint = [[1,5],[2,4]] → [4,0]
约束:
- 1 <= paint.length <= 10^5,0 <= starti < endi <= 5*10^4
解题思路
解法一(主提交版本)
有序集合维护未涂区间(主提交版本)
- 坐标范围只有 510^4,可直接用 boolean painted\[\] 标记,逐天统计新增量:O(n * Δ)
最坏 O(510^9) 不可行,因此用 TreeSet 维护「尚未被涂的点」。 - 每天绘制 s, e):从 TreeSet 中取出 s 起所有 \< e 的未涂点,计数并移除,worklog\[i = 计数。
复杂度:O((n + Σ涂点数) log C) 时间,C 为坐标值域;空间 O©。
解法二
区间覆盖前缀差分(对照) - 对每天需要统计「该天新增的点」,可离线处理:先标记所有最早覆盖某点的天号(从右往左扫描区间,
每个点归属到最早覆盖它的天),再按天统计。这里用 TreeSet 同构实现交叉验证。
复杂度:O((n + Δ') log C)。
代码实现
java
import java.util.Arrays;
import java.util.TreeSet;
public class Solution {
// 解法一:TreeSet 维护未涂点(主提交版本)
public int[] amountPainted(int[][] paint) {
int n = paint.length;
TreeSet<Integer> set = new TreeSet<>();
for (int x = 0; x <= 50000; x++) set.add(x); // 坐标点 0..50000
int[] ans = new int[n];
for (int i = 0; i < n; i++) {
int s = paint[i][0], e = paint[i][1];
int cnt = 0;
Integer x = set.ceiling(s);
while (x != null && x < e) {
cnt++;
set.remove(x);
x = set.ceiling(s); // 后续从 s 起找第一个未涂点
}
ans[i] = cnt;
}
return ans;
}
// 解法二:布尔标记 + 跳表优化(对照,思路等价但用数组模拟)
public int[] amountPaintedBool(int[][] paint) {
int n = paint.length;
boolean[] painted = new boolean[50001];
// next[i] 记录从 i 起下一个未涂点,加速跳跃
int[] next = new int[50002];
for (int i = 0; i < next.length; i++) next[i] = i;
int[] ans = new int[n];
for (int i = 0; i < n; i++) {
int s = paint[i][0], e = paint[i][1];
int cnt = 0;
int cur = find(s, next);
while (cur < e) {
cnt++;
painted[cur] = true;
next[cur] = find(cur + 1, next); // 合并到下一个未涂点
cur = find(cur, next);
}
ans[i] = cnt;
}
return ans;
}
private int find(int x, int[] next) {
if (next[x] != x) next[x] = find(next[x], next);
return next[x];
}
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.amountPainted(new int[][]{{1, 4}, {4, 7}, {5, 8}})); // [3,3,1]
print(sol.amountPaintedBool(new int[][]{{1, 4}, {4, 7}, {5, 8}}));
print(sol.amountPainted(new int[][]{{1, 4}, {5, 8}, {4, 7}})); // [3,3,1]
print(sol.amountPaintedBool(new int[][]{{1, 4}, {5, 8}, {4, 7}}));
print(sol.amountPainted(new int[][]{{1, 5}, {2, 4}})); // [4,0]
print(sol.amountPaintedBool(new int[][]{{1, 5}, {2, 4}}));
}
}
复杂度分析
复杂度:O((n + Σ涂点数) log C) 时间,C 为坐标值域;空间 O©。
复杂度:O((n + Δ') log C)。
2159. Order Two Columns Independently(分别排序两列)
题目类型:数据库(SQL)
题目描述
表: Data
+-------------+------+
| Column Name | Type |
+-------------+------+
| first_col | int |
| second_col | int |
+-------------+------+
该表可能包含重复数据。
编写解决方案,使:
first_col按照升序排列;second_col按照降序排列。
返回的结果格式如下。
示例 1:
输入:
Data 表:
+-----------+------------+
| first_col | second_col |
+-----------+------------+
| 4 | 2 |
| 2 | 3 |
| 3 | 1 |
| 1 | 4 |
+-----------+------------+
输出:
+-----------+------------+
| first_col | second_col |
+-----------+------------+
| 1 | 4 |
| 2 | 3 |
| 3 | 2 |
| 4 | 1 |
+-----------+------------+
解题思路
关键点 :first_col 和 second_col 要各自独立排序、互不影响,而不是按某列排序后另一列跟着走。也就是把第一列升序排好、第二列降序排好,再把两列"并列"输出(各行按各自排序后的值对齐)。
解法一:窗口函数 ROW_NUMBER 对齐
用 ROW_NUMBER() OVER (ORDER BY first_col ASC) 给第一列升序编号,用 ROW_NUMBER() OVER (ORDER BY second_col DESC) 给第二列降序编号(重复值时按任意序 BUT 编号唯一),然后按相同编号 JOIN 对齐输出。
sql
WITH a AS (
SELECT first_col,
ROW_NUMBER() OVER (ORDER BY first_col ASC) AS rn
FROM Data
),
b AS (
SELECT second_col,
ROW_NUMBER() OVER (ORDER BY second_col DESC) AS rn
FROM Data
)
SELECT a.first_col, b.second_col
FROM a JOIN b ON a.rn = b.rn;
解法二(对照):笛卡尔积+条件序号(实现路径不同)
先对第一列升序取排行号,第二列降序取排行号,再按行号相等连接。与解法一本质一致,可用变量或自连实现,这里强调窗口函数是标准做法。
代码实现
sql
-- 解法一(推荐):两列独立编号后对齐
WITH a AS (
SELECT first_col,
ROW_NUMBER() OVER (ORDER BY first_col ASC) AS rn
FROM Data
),
b AS (
SELECT second_col,
ROW_NUMBER() OVER (ORDER BY second_col DESC) AS rn
FROM Data
)
SELECT a.first_col, b.second_col
FROM a JOIN b ON a.rn = b.rn;
sql
-- 解法二(对照):窗口编号全部放在一张派生表里
SELECT t.first_col, t.second_col
FROM (
SELECT first_col,
ROW_NUMBER() OVER (ORDER BY first_col ASC) AS r1,
ROW_NUMBER() OVER () AS pid
FROM Data
) f
JOIN (
SELECT second_col,
ROW_NUMBER() OVER (ORDER BY second_col DESC) AS r2
FROM Data
) s
ON f.r1 = s.r2;
复杂度分析
- 时间复杂度 :
O(n log n),其中n为Data行数;两次排序为主开销。 - 空间复杂度 :
O(n),窗口排序的中间结果与行数成正比。
2160. Minimum Sum of Four Digit Number After Splitting Digits(拆分数位后四位数字的最小和)
题目描述
给你一个四位正整数 num。请你使用 num 中的数位,将 num 拆成两个新的整数 new1 和 new2。
new1 和 new2 中可以有前导 0,且 num 中所有数位都必须使用。
返回可以得到的 new1 和 new2 的最小和。
示例 1
num = 2932 → 52(29 + 23)
示例 2
num = 4009 → 13(4 + 9)
约束:
1000 <= num <= 9999
解题思路
解法一(主提交版本)
拆位排序贪心(主提交版本)
- 取出四位数字排序为 a0<=a1<=a2<=a3。显然 a0、a1 做十位,a2、a3 做个位最省,
最小和 = (a0 + a1) * 10 + (a2 + a3)。
复杂度:O(1) 时间,O(1) 空间。
解法二
枚举拆分(对照) - 枚举数位组成的两个数(4! 排列),取最小和(含前导 0 情况),用于交叉验证。
复杂度:O(4!) 时间,O(1) 空间。
代码实现
java
import java.util.Arrays;
public class Solution {
// 解法一:拆位排序贪心(主提交版本)
public int minimumSum(int num) {
int[] d = new int[4];
for (int i = 0; i < 4; i++) {
d[i] = num % 10;
num /= 10;
}
Arrays.sort(d);
return (d[0] + d[1]) * 10 + (d[2] + d[3]);
}
// 解法二:全排列暴力(对照)
public int minimumSumBrute(int num) {
int[] d = new int[4];
for (int i = 0; i < 4; i++) {
d[i] = num % 10;
num /= 10;
}
int best = Integer.MAX_VALUE;
for (int p = 0; p < 24; p++) {
int[] a = d.clone();
// 简单遍历所有二元拆分:固定两个数位为一个数,其余为另一个
for (int m = 1; m < 16; m++) { // 枚举 4 位是否分给 new1
if (Integer.bitCount(m) != 2) continue;
int x = 0, y = 0;
for (int i = 0; i < 4; i++) {
if ((m >> i & 1) == 1) x = x * 10 + d[i];
else y = y * 10 + d[i];
}
best = Math.min(best, x + y);
}
nextPermutation(d);
}
return best;
}
private void nextPermutation(int[] a) {
int i = a.length - 2;
while (i >= 0 && a[i] >= a[i + 1]) i--;
if (i < 0) {
reverse(a, 0, a.length - 1);
return;
}
int j = a.length - 1;
while (a[j] <= a[i]) j--;
int t = a[i]; a[i] = a[j]; a[j] = t;
reverse(a, i + 1, a.length - 1);
}
private void reverse(int[] a, int l, int r) {
while (l < r) {
int t = a[l]; a[l] = a[r]; a[r] = t;
l++; r--;
}
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.minimumSum(2932) + " " + sol.minimumSumBrute(2932)); // 52 52
System.out.println(sol.minimumSum(4009) + " " + sol.minimumSumBrute(4009)); // 13 13
System.out.println(sol.minimumSum(1000) + " " + sol.minimumSumBrute(1000)); // 1+0=1? d=[0,0,0,1] → (0+0)*10+(0+1)=1
}
}
复杂度分析
复杂度:O(1) 时间,O(1) 空间。
复杂度:O(4!) 时间,O(1) 空间。