LeetCode2141~2160题目解析

LeetCode 2141-2160. 题解合集

文章目录


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(m
n 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(5
    10^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) 空间。

相关推荐
LabVIEW开发15 分钟前
LabVIEW 连接板:28 个端子怎么排,优先级怎么定
java·算法·labview
天天被压力1 小时前
【A股分钟级高频数据完全手册 #07】北交所历史分钟K线接口:京市Pro版分时批量
java·人工智能·python
天天被压力1 小时前
【A股分钟级高频数据完全手册 #08】股票日、分时数据接口对比:普通分时与Pro版差异实测
java·人工智能·python
特立独行的猫A1 小时前
AI 时代,我为什么推荐用 cj-tauri 开发桌面应用
java·后端·python
金銀銅鐵1 小时前
从一个类名到一张类图:用反射自动生成 PlantUML
java·后端·uml
nice_lcj5202 小时前
Java 并发容器入门:ConcurrentHashMap、CopyOnWriteArrayList、阻塞队列
java·面试·并发编程·阻塞队列·concurrenthashmap·copyonwritearraylist
微小的鱼2 小时前
Linux的渲染与显示系统链路
android·java·linux
若亦_3 小时前
Java中关于this的三种用法
java·开发语言·intellij-idea
山甫aa3 小时前
锁一篇搞定:乐观锁、悲观锁、CAS、数据库锁与分布式锁【从零开始的 Web 后端学习】
java·数据库·分布式·后端·学习·web