LeetCode2461~2480题目解析

LeetCode 2461-2480. 题解合集

文章目录


2461. Maximum Sum of Distinct Subarrays With Length K(长度为 K 子数组中的最大和)

题目描述

给你一个整数数组 nums 和一个整数 k。从 nums 中找出满足:长度 k 且所有元素各不相同 的

子数组,返回其最大和;若不存在返回 0。

示例 1

复制代码
`nums = [1,5,4,2,9,9,9], k = 3` → `15`。

示例 2

复制代码
`nums = [4,4,4], k = 3` → `0`。

提示:

  • 1 <= k <= nums.length <= 10^5,1 <= nums[i] <= 10^5

解题思路

解法一:滑动窗口 + 计数(主提交版本)

  • 维护窗口 l, r 长为当前扩展、计数表记录窗口内元素出现次数与重复数 dup。
  • 右指针 r 扩展 numsr,若窗口内元素互异(dup==0)且窗口大小 == k,则更新答案并删除窗口左端。
  • 标准写法:r 从 0 到 n-1,加入 numsr,若计数>1 dup++;若 r-l+1 > k 则收缩 l;
    若 dup0 且长度k 更新答案。
    解法二:双重循环暴力(对照)
  • 枚举窗口起点,逐个检查含重复则停;O(n*k) 小规模对拍。
    复杂度:
  • 时间复杂度:O(n)。
  • 空间复杂度:O(n)。

代码实现

java 复制代码
import java.util.HashMap;
import java.util.Map;

public class Solution {
    // 解法一:滑动窗口(主提交版本)
    public long maximumSubarraySum(int[] nums, int k) {
        Map<Integer, Integer> cnt = new HashMap<>();
        long best = 0, sum = 0;
        int l = 0, dup = 0;
        for (int r = 0; r < nums.length; r++) {
            int v = nums[r];
            int c = cnt.getOrDefault(v, 0);
            if (c == 1) {
                dup++;
            }
            cnt.put(v, c + 1);
            sum += v;
            while (r - l + 1 > k) {
                int out = nums[l++];
                int oc = cnt.get(out);
                if (oc == 2) {
                    dup--;
                }
                cnt.put(out, oc - 1);
                sum -= out;
            }
            if (dup == 0 && r - l + 1 == k) {
                best = Math.max(best, sum);
            }
        }
        return best;
    }

    // 解法二:暴力(对照)
    public long maximumSubarraySumBrute(int[] nums, int k) {
        long best = 0;
        int n = nums.length;
        for (int i = 0; i + k <= n; i++) {
            Map<Integer, Integer> seen = new HashMap<>();
            long sum = 0;
            boolean ok = true;
            for (int j = i; j < i + k; j++) {
                int v = nums[j];
                if (seen.containsKey(v)) {
                    ok = false;
                    break;
                }
                seen.put(v, 1);
                sum += v;
            }
            if (ok) {
                best = Math.max(best, sum);
            }
        }
        return best;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        check(sol.maximumSubarraySum(new int[] {1, 5, 4, 2, 9, 9, 9}, 3) == 15, "2461 ex1");
        check(sol.maximumSubarraySumBrute(new int[] {1, 5, 4, 2, 9, 9, 9}, 3) == 15, "2461 ex1b");
        check(sol.maximumSubarraySum(new int[] {4, 4, 4}, 3) == 0, "2461 ex2");
        check(sol.maximumSubarraySumBrute(new int[] {4, 4, 4}, 3) == 0, "2461 ex2b");
        // 随机对拍
        java.util.Random rnd = new java.util.Random(223);
        for (int t = 0; t < 500; t++) {
            int n = 1 + rnd.nextInt(30);
            int[] a = new int[n];
            for (int i = 0; i < n; i++) {
                a[i] = 1 + rnd.nextInt(10);
            }
            int k = 1 + rnd.nextInt(n);
            long x = sol.maximumSubarraySum(a, k), y = sol.maximumSubarraySumBrute(a, k);
            if (x != y) {
                throw new AssertionError("2461 mismatch t=" + t + " x=" + x + " y=" + y);
            }
        }
        System.out.println("2461 全部 PASS");
    }

    private static void check(boolean ok, String msg) {
        if (!ok) {
            throw new AssertionError(msg);
        }
        System.out.println("PASS " + msg);
    }
}

复杂度分析

  • 时间复杂度:O(n)。
  • 空间复杂度:O(n)。

2462. Total Cost to Hire K Workers(雇佣 K 位工人的总代价)

题目描述

给你一个下标从 0 开始的整数数组 costs(costs[i] 是雇佣第 i 位工人的代价)以及两个整数 k 和

candidates。共进行 k 轮,每轮恰好雇佣一位工人:从最前面 candidates 人和最后面 candidates 人中

选代价最小者(同价取下标更小);若剩余人数不足 candidates,则从剩余所有人中选。返回雇佣恰好 k 位

工人的总代价。

示例 1

复制代码
`costs=[17,12,10,2,7,2,11,20,8], k=3, candidates=4` → `11`。

示例 2

复制代码
`costs=[1,2,4,1], k=3, candidates=3` → `4`。

提示:

  • 1 <= costs.length <= 10^5,1 <= costs[i] <= 10^5,1 <= k, candidates <= costs.length

解题思路

解法一:双堆模拟(主提交版本)

  • 维护两个最小堆:左堆(前 l 个待选)与右堆(后 r 个待选),每轮比较堆顶(值、下标)取较小者,
    从对应端补充新候选人。当左右区间相遇时合并处理,避免重复。
  • 为标准做法:用两个堆分别装「前端 candidates」与「后端 candidates」,双指针维护边界,
    若 l > r 说明两堆已重叠,直接把两边都放进一个堆即可(防止重复计)。
    解法二:朴素模拟(对照,小规模)
  • 每轮扫描一次剩余数组选最小,O(k*n)。
    复杂度:
  • 时间复杂度:O((n+k) log n)。
  • 空间复杂度:O(n)。

代码实现

java 复制代码
import java.util.Arrays;
import java.util.PriorityQueue;

public class Solution {
    // 解法一:双堆 + 双指针(主提交版本)
    public long totalCost(int[] costs, int k, int candidates) {
        int n = costs.length;
        // 堆元素:代价;其次按下标(编码:代价 * 1e6 + idx)
        PriorityQueue<long[]> left = new PriorityQueue<>((a, b) ->
                a[0] != b[0] ? Long.compare(a[0], b[0]) : Long.compare(a[1], b[1]));
        PriorityQueue<long[]> right = new PriorityQueue<>((a, b) ->
                a[0] != b[0] ? Long.compare(a[0], b[0]) : Long.compare(a[1], b[1]));
        int l = 0, r = n - 1;
        // 初始化:左端 candidates 人、右端 candidates 人
        for (int i = 0; i < candidates && l <= r; i++) {
            left.offer(new long[] {costs[l], l});
            l++;
        }
        for (int i = 0; i < candidates && l <= r; i++) {
            right.offer(new long[] {costs[r], r});
            r--;
        }
        long ans = 0;
        for (int round = 0; round < k; round++) {
            long[] p;
            if (left.isEmpty()) {
                p = right.poll();
            } else if (right.isEmpty()) {
                p = left.poll();
            } else {
                long[] a = left.peek(), b = right.peek();
                if (a[0] < b[0] || (a[0] == b[0] && a[1] < b[1])) {
                    p = left.poll();
                } else {
                    p = right.poll();
                }
            }
            ans += p[0];
            // 从弹出端补充新候选人
            if (p[1] < l) {
                if (l <= r) {
                    left.offer(new long[] {costs[l], l});
                    l++;
                }
            } else {
                if (l <= r) {
                    right.offer(new long[] {costs[r], r});
                    r--;
                }
            }
        }
        return ans;
    }

    // 解法二:朴素模拟(对照,小规模)
    public long totalCostBrute(int[] costs, int k, int candidates) {
        int n = costs.length;
        boolean[] hired = new boolean[n];
        int head = 0, tail = n - 1;
        long ans = 0;
        for (int round = 0; round < k; round++) {
            int bestIdx = -1;
            // 收集候选范围
            int l = head, r = tail;
            // 扫描前面 candidates 个未雇佣
            int cnt = 0;
            for (int i = head; i <= tail && cnt < candidates; i++) {
                if (!hired[i]) {
                    cnt++;
                    if (bestIdx == -1 || costs[i] < costs[bestIdx]
                            || (costs[i] == costs[bestIdx] && i < bestIdx)) {
                        bestIdx = i;
                    }
                }
            }
            // 扫描最后 candidates 个未雇佣
            cnt = 0;
            for (int i = tail; i >= head && cnt < candidates; i--) {
                if (!hired[i]) {
                    cnt++;
                    if (bestIdx == -1 || costs[i] < costs[bestIdx]
                            || (costs[i] == costs[bestIdx] && i < bestIdx)) {
                        bestIdx = i;
                    }
                }
            }
            hired[bestIdx] = true;
            ans += costs[bestIdx];
            while (head <= tail && hired[head]) {
                head++;
            }
            while (tail >= head && hired[tail]) {
                tail--;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        check(sol.totalCost(new int[] {17, 12, 10, 2, 7, 2, 11, 20, 8}, 3, 4) == 11, "2462 ex1");
        check(sol.totalCostBrute(new int[] {17, 12, 10, 2, 7, 2, 11, 20, 8}, 3, 4) == 11, "2462 ex1b");
        check(sol.totalCost(new int[] {1, 2, 4, 1}, 3, 3) == 4, "2462 ex2");
        check(sol.totalCostBrute(new int[] {1, 2, 4, 1}, 3, 3) == 4, "2462 ex2b");
        // 随机对拍
        java.util.Random rnd = new java.util.Random(227);
        for (int t = 0; t < 300; t++) {
            int n = 1 + rnd.nextInt(30);
            int[] a = new int[n];
            for (int i = 0; i < n; i++) {
                a[i] = 1 + rnd.nextInt(100);
            }
            int k = 1 + rnd.nextInt(n);
            int c = 1 + rnd.nextInt(n);
            long x = sol.totalCost(a.clone(), k, c), y = sol.totalCostBrute(a.clone(), k, c);
            if (x != y) {
                throw new AssertionError("2462 mismatch t=" + t + " x=" + x + " y=" + y
                        + " a=" + Arrays.toString(a) + " k=" + k + " c=" + c);
            }
        }
        System.out.println("2462 全部 PASS");
    }

    private static void check(boolean ok, String msg) {
        if (!ok) {
            throw new AssertionError(msg);
        }
        System.out.println("PASS " + msg);
    }
}

复杂度分析

  • 时间复杂度:O((n+k) log n)。
  • 空间复杂度:O(n)。

2463. Minimum Total Distance Traveled(最小移动总距离)

题目描述

X 轴上有一些机器人和工厂。robot 是机器人位置数组,factory = [position, limit] 表示工厂位置与

最多可修理的机器人数量。机器人沿设定方向移动,经过未满工厂则被修理停止。任何时刻可设置部分机器人

方向。目标是最小化所有机器人总移动距离(所有机器人都可被维修)。

示例 1

复制代码
`robot=[0,4,6], factory=[[2,2],[6,2]]` → `4`。

示例 2

复制代码
`robot=[1,-1], factory=[[-2,1],[2,1]]` → `2`。

提示:

  • 1 <= robot.length, factory.length <= 100,位置可负可正,0 <= limit <= robot.length

解题思路

解法一:排序 + 三维 DP(主提交版本)

  • 最优解中机器人按位置顺序被工厂按顺序修理(排序后不相交),因此把 robot 与 factory 都按位置排序。
  • dp[i][j] = 前 i 个工厂修理前 j 个机器人、且第 j 个机器人恰由第 i 个工厂修理(或更前)的最小距离。
    简化经典写法:dp[i][j] 前 i 工厂、前 j 机器人的最小总距离。
    转移:第 i 工厂(position p, limit L)修的最后一批:修 0...min(L, j) 个,
    dp[i][j] = min(dp[i-1][j - t] + cost(i, j-t+1..j)),cost = t 个机器人到 p 的距离和。
    O(F * R^2 * L) 可优化:枚举 j 与 t(t ≤ L)。总 O(FR L) 可过(100100100)。
  • 实现:对 t 从 1...min(L,j) 枚举:dp[i][j] = min(dp[i-1][j], min_t(dp[i-1][j-t] + sumDist))。
    解法二:记忆化 DFS(对照)
  • dfs(i, j):处理前 i 个工厂、前 j 个机器人的最小费用,同上转移。
    复杂度:
  • 时间复杂度:O(F * R * L) ≈ 1e6。
  • 空间复杂度:O(F * R)。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class Solution {
    // 解法一:排序 + DP(主提交版本)
    public long minimumTotalDistance(List<Integer> robot, int[][] factory) {
        List<Integer> rb = new ArrayList<>(robot);
        Collections.sort(rb);
        // 工厂按位置排序
        int[][] fac = factory.clone();
        Arrays.sort(fac, (a, b) -> Integer.compare(a[0], b[0]));
        int R = rb.size(), F = fac.length;
        long INF = Long.MAX_VALUE / 4;
        long[][] dp = new long[F + 1][R + 1];
        for (int j = 1; j <= R; j++) {
            dp[0][j] = INF;
        }
        for (int i = 1; i <= F; i++) {
            int pos = fac[i - 1][0], lim = fac[i - 1][1];
            for (int j = 0; j <= R; j++) {
                dp[i][j] = dp[i - 1][j]; // 第 i 工厂一个不修
                // 第 i 工厂修最后 t 个机器人(t from 1..min(lim, j))
                long acc = 0;
                for (int t = 1; t <= lim && t <= j; t++) {
                    acc += Math.abs(rb.get(j - t) - (long) pos);
                    if (dp[i - 1][j - t] != INF) {
                        dp[i][j] = Math.min(dp[i][j], dp[i - 1][j - t] + acc);
                    }
                }
            }
        }
        return dp[F][R];
    }

    // 解法二:记忆化 DFS(对照)
    public long minimumTotalDistanceDfs(List<Integer> robot, int[][] factory) {
        List<Integer> rb = new ArrayList<>(robot);
        Collections.sort(rb);
        int[][] fac = factory.clone();
        Arrays.sort(fac, (a, b) -> Integer.compare(a[0], b[0]));
        int R = rb.size(), F = fac.length;
        memo = new Long[F + 1][R + 1];
        this.rb = rb;
        this.fac = fac;
        return dfs(0, 0);
    }

    private Long[][] memo;
    private List<Integer> rb;
    private int[][] fac;

    private long dfs(int i, int j) {
        if (j == rb.size()) {
            return 0;
        }
        if (i == fac.length) {
            return Long.MAX_VALUE / 4;
        }
        if (memo[i][j] != null) {
            return memo[i][j];
        }
        // 不修任何机器人
        long best = dfs(i + 1, j);
        // 修 t 个
        int pos = fac[i][0];
        long acc = 0;
        for (int t = 1; t <= fac[i][1] && j + t <= rb.size(); t++) {
            acc += Math.abs(rb.get(j + t - 1) - (long) pos);
            best = Math.min(best, acc + dfs(i + 1, j + t));
        }
        memo[i][j] = best;
        return best;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        check(sol.minimumTotalDistance(Arrays.asList(0, 4, 6), new int[][] {{2, 2}, {6, 2}}) == 4, "2463 ex1");
        check(sol.minimumTotalDistanceDfs(Arrays.asList(0, 4, 6), new int[][] {{2, 2}, {6, 2}}) == 4, "2463 ex1d");
        check(sol.minimumTotalDistance(Arrays.asList(1, -1), new int[][] {{-2, 1}, {2, 1}}) == 2, "2463 ex2");
        check(sol.minimumTotalDistanceDfs(Arrays.asList(1, -1), new int[][] {{-2, 1}, {2, 1}}) == 2, "2463 ex2d");
        // 随机对拍
        java.util.Random rnd = new java.util.Random(229);
        for (int t = 0; t < 80; t++) {
            int rn = 1 + rnd.nextInt(7);
            List<Integer> rbt = new ArrayList<>();
            for (int i = 0; i < rn; i++) {
                rbt.add(-5 + rnd.nextInt(11));
            }
            int fn = 1 + rnd.nextInt(4);
            int[][] f = new int[fn][2];
            int remain = rn;
            for (int i = 0; i < fn; i++) {
                f[i][0] = -5 + rnd.nextInt(11);
                int mx = remain;
                f[i][1] = (i == fn - 1) ? remain : rnd.nextInt(mx + 1);
                remain -= f[i][1];
                if (remain < 0) {
                    f[i][1] += remain;
                    remain = 0;
                }
            }
            // 确保总容量足够
            if (remain > 0) {
                f[fn - 1][1] += remain;
            }
            long x = sol.minimumTotalDistance(rbt, f), y = sol.minimumTotalDistanceDfs(rbt, f);
            if (x != y) {
                throw new AssertionError("2463 mismatch t=" + t + " x=" + x + " y=" + y);
            }
        }
        System.out.println("2463 全部 PASS");
    }

    private static void check(boolean ok, String msg) {
        if (!ok) {
            throw new AssertionError(msg);
        }
        System.out.println("PASS " + msg);
    }
}

复杂度分析

  • 时间复杂度:O(F * R * L) ≈ 1e6。
  • 空间复杂度:O(F * R)。

2464. Minimum Subarrays in a Valid Split(有效分割中的最少子数组数目)

题目描述

给定整数数组 nums。将 nums 拆分为一个或多个互不覆盖的子数组,若每个子数组第一个和最后一个元素

的最大公约数大于 1,则分割有效(每个元素只属于一个子数组)。返回有效分割中最少子数组数目;

若无法有效分割返回 -1。

示例 1

复制代码
`nums = [2,6,3,4,3]` → `2`([2,6]|[3,4,3])。

示例 2

复制代码
`nums = [3,5]` → `2`([3]|[5])。

示例 3

复制代码
`nums = [1,2,1]` → `-1`(元素 1 无法单独成段------但注意示例 2 中 [3] 与 [5] 的长度 1
子数组:首尾同一元素 gcd=自身>1 即可。而 1 的 gcd=1 不合法,且 1 与任何数 gcd 都是 1,无解)。

提示:

  • 1 <= nums.length <= 1000,1 <= nums[i] <= 10^5

解题思路

解法一:区间 DP(主提交版本)

  • dp[i] 前 i 个元素的最少子数组数;dp[i] = min(dp[j] + 1),其中 gcd(nums[j-1], nums[i-1]) > 1
    且 j...i-1 这段可以是一个子数组?------ 不,这里子数组只需首尾 gcd>1,中间无约束。
    所以转移:for j in 1..i: if gcd(nums[j-1], nums[i-1]) > 1: dp[i] = min(dp[i], dp[j-1]+1)。
    预处理所有对 gcd 加速到 O(1)。
    解法二:DFS 记忆化(对照)
  • f(i) 从 i 开始的最少段数,递归枚举下一段的结束点。
    复杂度:
  • 时间复杂度:O(n^2 log A)。
  • 空间复杂度:O(n)。

代码实现

java 复制代码
import java.util.Arrays;

public class Solution {
    // 解法一:区间 DP(主提交版本)
    public int validSubarraySplit(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n + 1];
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= i; j++) {
                if (gcd(nums[j - 1], nums[i - 1]) > 1 && dp[j - 1] != Integer.MAX_VALUE) {
                    dp[i] = Math.min(dp[i], dp[j - 1] + 1);
                }
            }
        }
        return dp[n] == Integer.MAX_VALUE ? -1 : dp[n];
    }

    // 解法二:记忆化 DFS(对照)
    public int validSubarraySplitDfs(int[] nums) {
        this.nums = nums;
        memo = new int[nums.length];
        Arrays.fill(memo, -1);
        int r = f(0);
        return r >= Integer.MAX_VALUE / 2 ? -1 : r;
    }

    private int[] nums;
    private int[] memo;

    private int f(int i) {
        if (i == nums.length) {
            return 0;
        }
        if (memo[i] != -1) {
            return memo[i];
        }
        int best = Integer.MAX_VALUE / 2;
        for (int j = i; j < nums.length; j++) {
            if (gcd(nums[i], nums[j]) > 1) {
                best = Math.min(best, 1 + f(j + 1));
            }
        }
        memo[i] = best;
        return best;
    }

    private static int gcd(int a, int b) {
        while (b != 0) {
            int t = a % b;
            a = b;
            b = t;
        }
        return a;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        check(sol.validSubarraySplit(new int[] {2, 6, 3, 4, 3}) == 2, "2464 ex1");
        check(sol.validSubarraySplitDfs(new int[] {2, 6, 3, 4, 3}) == 2, "2464 ex1d");
        check(sol.validSubarraySplit(new int[] {3, 5}) == 2, "2464 ex2");
        check(sol.validSubarraySplitDfs(new int[] {3, 5}) == 2, "2464 ex2d");
        check(sol.validSubarraySplit(new int[] {1, 2, 1}) == -1, "2464 ex3");
        check(sol.validSubarraySplitDfs(new int[] {1, 2, 1}) == -1, "2464 ex3d");
        // 随机对拍
        java.util.Random rnd = new java.util.Random(233);
        for (int t = 0; t < 300; t++) {
            int n = 1 + rnd.nextInt(20);
            int[] a = new int[n];
            for (int i = 0; i < n; i++) {
                a[i] = 1 + rnd.nextInt(40);
            }
            int x = sol.validSubarraySplit(a), y = sol.validSubarraySplitDfs(a);
            if (x != y) {
                throw new AssertionError("2464 mismatch t=" + t + " x=" + x + " y=" + y);
            }
        }
        System.out.println("2464 全部 PASS");
    }

    private static void check(boolean ok, String msg) {
        if (!ok) {
            throw new AssertionError(msg);
        }
        System.out.println("PASS " + msg);
    }
}

复杂度分析

  • 时间复杂度:O(n^2 log A)。
  • 空间复杂度:O(n)。

2465. Number of Distinct Averages(不同的平均值数目)

题目描述

给你一个下标从 0 开始、长度为偶数 的整数数组 nums。重复以下步骤直到 nums 为空:

找到最小值并删除;找到最大值并删除;计算被删两数的平均值 (a + b) / 2。

返回过程中能得到的不同平均值的数目。

示例 1

复制代码
`nums = [4,1,4,0,3,5]` → `2`(2.5、2.5、3.5)。

示例 2

复制代码
`nums = [1,100]` → `1`。

提示:

  • 2 <= nums.length <= 100,长度为偶数,0 <= nums[i] <= 100

解题思路

解法一:排序 + 双指针 + Set(主提交版本)

  • 排序后最小在左、最大在右,两两配对求和存入 Set(用 2*sum 或和值去重,避免浮点),
    返回 Set 大小。平均值去重等价于「和」去重(除以 2 单调)。
    解法二:TreeMap 删除(对照)
  • 用有序 Map 维护 min/max 逐个删除,思路不同。
    复杂度:
  • 时间复杂度:O(n log n)。
  • 空间复杂度:O(n)。

代码实现

java 复制代码
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class Solution {
    // 解法一:排序双指针(主提交版本)
    public int distinctAverages(int[] nums) {
        Arrays.sort(nums);
        Set<Integer> sums = new HashSet<>();
        int n = nums.length;
        for (int i = 0; i < n / 2; i++) {
            sums.add(nums[i] + nums[n - 1 - i]);
        }
        return sums.size();
    }

    // 解法二:TreeMap(对照)
    public int distinctAverages2(int[] nums) {
        java.util.TreeMap<Integer, Integer> map = new java.util.TreeMap<>();
        for (int x : nums) {
            map.put(x, map.getOrDefault(x, 0) + 1);
        }
        Set<Integer> sums = new HashSet<>();
        while (!map.isEmpty()) {
            int mn = map.firstKey(), mx = map.lastKey();
            sums.add(mn + mx);
            dec(map, mn);
            dec(map, mx);
        }
        return sums.size();
    }

    private void dec(java.util.TreeMap<Integer, Integer> map, int k) {
        int c = map.get(k) - 1;
        if (c == 0) {
            map.remove(k);
        } else {
            map.put(k, c);
        }
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        check(sol.distinctAverages(new int[] {4, 1, 4, 0, 3, 5}) == 2, "2465 ex1");
        check(sol.distinctAverages2(new int[] {4, 1, 4, 0, 3, 5}) == 2, "2465 ex12");
        check(sol.distinctAverages(new int[] {1, 100}) == 1, "2465 ex2");
        check(sol.distinctAverages2(new int[] {1, 100}) == 1, "2465 ex22");
        // 随机对拍
        java.util.Random rnd = new java.util.Random(239);
        for (int t = 0; t < 500; t++) {
            int n = 2 + 2 * rnd.nextInt(15);
            int[] a = new int[n];
            for (int i = 0; i < n; i++) {
                a[i] = rnd.nextInt(100);
            }
            int x = sol.distinctAverages(a), y = sol.distinctAverages2(a);
            if (x != y) {
                throw new AssertionError("2465 mismatch t=" + t + " x=" + x + " y=" + y);
            }
        }
        System.out.println("2465 全部 PASS");
    }

    private static void check(boolean ok, String msg) {
        if (!ok) {
            throw new AssertionError(msg);
        }
        System.out.println("PASS " + msg);
    }
}

复杂度分析

  • 时间复杂度:O(n log n)。
  • 空间复杂度:O(n)。

2466. Count Ways To Build Good Strings(统计构造好字符串的方案数)

题目描述

给你整数 zero、one、low、high。从空字符串开始,每步执行:在末尾添加 zero 个 '0',

或在末尾添加 one 个 '1'。若得到长度在 `low`, `high` 之间(含边界)的字符串,称为好 字符串。

返回不同好字符串数目,对 10^9+7 取余。

示例 1

复制代码
`low=3, high=3, zero=1, one=1` → `8`。

示例 2

复制代码
`low=2, high=3, zero=1, one=2` → `5`。

提示:

  • 1 <= low <= high <= 10^5,1 <= zero, one <= low

解题思路

解法一:线性 DP(主提交版本)

  • dp[len] = 构造出长度为 len 的字符串的方案数。dp[len] += dp[len-zero](加 0 串)、 dp[len-one](加 1 串),dp[0]=1。
  • 答案 = sum(dplow...high) % MOD。
    解法二:记忆化 DFS(对照)
  • f(len) 表示从长度 len 继续构造的方案数,递归 + memo。
    复杂度:
  • 时间复杂度:O(high)。
  • 空间复杂度:O(high)。

代码实现

java 复制代码
public class Solution {
    private static final int MOD = 1000000007;

    // 解法一:线性 DP(主提交版本)
    public int countGoodStrings(int low, int high, int zero, int one) {
        long[] dp = new long[high + 1];
        dp[0] = 1;
        long ans = 0;
        for (int len = 1; len <= high; len++) {
            if (len >= zero) {
                dp[len] = (dp[len] + dp[len - zero]) % MOD;
            }
            if (len >= one) {
                dp[len] = (dp[len] + dp[len - one]) % MOD;
            }
            if (len >= low) {
                ans = (ans + dp[len]) % MOD;
            }
        }
        return (int) ans;
    }

    // 解法二:记忆化 DFS(对照)
    public int countGoodStringsDfs(int low, int high, int zero, int one) {
        memo = new long[high + 1];
        java.util.Arrays.fill(memo, -1);
        this.low = low;
        return (int) f(0, high, zero, one);
    }

    private int low;
    private long[] memo;

    private long f(int len, int high, int zero, int one) {
        if (len > high) {
            return 0;
        }
        if (memo[len] != -1) {
            return memo[len];
        }
        long res = (len >= low) ? 1 : 0;
        res = (res + f(len + zero, high, zero, one)) % MOD;
        res = (res + f(len + one, high, zero, one)) % MOD;
        memo[len] = res;
        return res;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        check(sol.countGoodStrings(3, 3, 1, 1) == 8, "2466 ex1");
        check(sol.countGoodStringsDfs(3, 3, 1, 1) == 8, "2466 ex1d");
        check(sol.countGoodStrings(2, 3, 1, 2) == 5, "2466 ex2");
        check(sol.countGoodStringsDfs(2, 3, 1, 2) == 5, "2466 ex2d");
        // 随机对拍
        java.util.Random rnd = new java.util.Random(241);
        for (int t = 0; t < 300; t++) {
            int h = 1 + rnd.nextInt(60);
            int l = 1 + rnd.nextInt(h);
            int z = 1 + rnd.nextInt(l);
            int o = 1 + rnd.nextInt(l);
            int x = sol.countGoodStrings(l, h, z, o), y = sol.countGoodStringsDfs(l, h, z, o);
            if (x != y) {
                throw new AssertionError("2466 mismatch t=" + t + " x=" + x + " y=" + y
                        + " l=" + l + " h=" + h);
            }
        }
        System.out.println("2466 全部 PASS");
    }

    private static void check(boolean ok, String msg) {
        if (!ok) {
            throw new AssertionError(msg);
        }
        System.out.println("PASS " + msg);
    }
}

复杂度分析

  • 时间复杂度:O(high)。
  • 空间复杂度:O(high)。

2467. Most Profitable Path in a Tree(树上最大得分和路径)

题目描述

一个 n 个节点的无向树(节点 0...n-1),根为 0。每条边在 edges 中给出。每个节点处有一扇门,

amount[i](偶数)表示开门获得的分数(负数为扣分)。游戏规则:

  • Alice 一开始在节点 0,Bob 在节点 bob。
  • 每一秒 Alice 和 Bob 分别移动到相邻节点。Alice 朝某个叶子移动,Bob 朝节点 0 移动。
  • 经过的节点都要开门计分;若门已被别人打开则不再计分;若两人同时到达同一节点则评分(各得一半)。
  • Alice 到达叶子后停止;Bob 到达 0 后停止;两者互不影响。
    返回 Alice 朝最优叶子移动的最大净得分。

示例 1

复制代码
`edges=[[0,1],[1,2],[1,3],[3,4]], bob=3, amount=[-2,4,2,-4,6]` → `6`。

示例 2

复制代码
`edges=[[0,1]], bob=1, amount=[-7280,2350]` → `-7280`。

提示:

  • 2 <= n <= 10^5,amount[i] 是范围 -10\^4, 10\^4 的偶数,1 <= bob < n

解题思路

解法一:Bob 到达时间 + Alice DFS(主提交版本)

  • 第一步:从 Bob 出发 DFS/BFS,记录 Bob 到达每个节点的时刻 bobTime[v](根 0 也记录)。
  • 第二步:Alice 从根 DFS 到叶子,维护已花费时间 t 与累计得分:
  • 若 t < bobTime[v]:Alice 先到,全额得分 amountv;
  • 若 t == bobTime[v]:同时到达,各得 amountv/2;
  • 若 t > bobTime[v]:Bob 已开过门,得 0。
  • 叶子(除根外度数为 1 的节点)处更新答案最大值。
    解法二:另起一遍 BFS 暴力模拟 Alice 每条路径(对照,小规模)
  • 枚举 Alice 到每个叶子的路径逐步模拟计分,与解法一对拍。
    复杂度:
  • 时间复杂度:O(n)。
  • 空间复杂度:O(n)。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Solution {
    private List<Integer>[] g;
    private int[] bobTime;
    private int[] amount;
    private int best = Integer.MIN_VALUE;

    // 解法一:Bob 路径时间 + Alice DFS(主提交版本)
    public int mostProfitablePath(int[][] edges, int bob, int[] amount) {
        int n = amount.length;
        g = new List[n];
        for (int i = 0; i < n; i++) {
            g[i] = new ArrayList<>();
        }
        for (int[] e : edges) {
            g[e[0]].add(e[1]);
            g[e[1]].add(e[0]);
        }
        // 只给 Bob 到根 0 的路径上的节点时间;其他节点 Bob 不会到达(视为无穷)
        bobTime = new int[n];
        Arrays.fill(bobTime, Integer.MAX_VALUE);
        List<Integer> path = new ArrayList<>();
        findPath(bob, -1, path); // 路径从 bob 到 0
        for (int i = 0; i < path.size(); i++) {
            bobTime[path.get(i)] = i;
        }
        this.amount = amount;
        best = Integer.MIN_VALUE;
        dfsAlice(0, -1, 0, 0);
        return best;
    }

    private boolean findPath(int u, int fa, List<Integer> path) {
        path.add(u);
        if (u == 0) {
            return true;
        }
        for (int v : g[u]) {
            if (v != fa) {
                if (findPath(v, u, path)) {
                    return true;
                }
            }
        }
        path.remove(path.size() - 1);
        return false;
    }

    private void dfsAlice(int u, int fa, int t, int score) {
        int add;
        if (t < bobTime[u]) {
            add = amount[u];
        } else if (t == bobTime[u]) {
            add = amount[u] / 2;
        } else {
            add = 0;
        }
        score += add;
        boolean leaf = true;
        for (int v : g[u]) {
            if (v != fa) {
                leaf = false;
                dfsAlice(v, u, t + 1, score);
            }
        }
        if (leaf) {
            best = Math.max(best, score);
        }
    }

    // 解法二:枚举叶子路径模拟(对照,小规模)
    public int mostProfitablePathBrute(int[][] edges, int bob, int[] amount) {
        int n = amount.length;
        List<Integer>[] gg = new List[n];
        for (int i = 0; i < n; i++) {
            gg[i] = new ArrayList<>();
        }
        for (int[] e : edges) {
            gg[e[0]].add(e[1]);
            gg[e[1]].add(e[0]);
        }
        // Bob 到尾部的根路径时间
        int[] bt = new int[n];
        Arrays.fill(bt, Integer.MAX_VALUE);
        List<Integer> bpath = new ArrayList<>();
        collectBobPath(gg, bob, -1, bpath);
        for (int i = 0; i < bpath.size(); i++) {
            bt[bpath.get(i)] = i;
        }
        int res = Integer.MIN_VALUE;
        // 找所有叶子路径
        List<List<Integer>> paths = new ArrayList<>();
        collectPath(gg, 0, -1, new ArrayList<>(), paths);
        for (List<Integer> path : paths) {
            int score = 0;
            for (int i = 0; i < path.size(); i++) {
                int v = path.get(i);
                if (i < bt[v]) {
                    score += amount[v];
                } else if (i == bt[v]) {
                    score += amount[v] / 2;
                }
            }
            res = Math.max(res, score);
        }
        return res;
    }

    // 收集 bob -> 0 的路径(返回是否找到)
    private boolean collectBobPath(List<Integer>[] gg, int u, int fa, List<Integer> path) {
        path.add(u);
        if (u == 0) {
            return true;
        }
        for (int v : gg[u]) {
            if (v != fa) {
                if (collectBobPath(gg, v, u, path)) {
                    return true;
                }
            }
        }
        path.remove(path.size() - 1);
        return false;
    }

    private void collectPath(List<Integer>[] gg, int u, int fa, List<Integer> cur, List<List<Integer>> paths) {
        cur.add(u);
        boolean leaf = true;
        for (int v : gg[u]) {
            if (v != fa) {
                leaf = false;
                List<Integer> copy = new ArrayList<>(cur);
                collectPath(gg, v, u, copy, paths);
            }
        }
        if (leaf) {
            paths.add(new ArrayList<>(cur));
        }
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        check(sol.mostProfitablePath(new int[][] {{0, 1}, {1, 2}, {1, 3}, {3, 4}}, 3,
                new int[] {-2, 4, 2, -4, 6}) == 6, "2467 ex1");
        check(sol.mostProfitablePathBrute(new int[][] {{0, 1}, {1, 2}, {1, 3}, {3, 4}}, 3,
                new int[] {-2, 4, 2, -4, 6}) == 6, "2467 ex1b");
        check(sol.mostProfitablePath(new int[][] {{0, 1}}, 1, new int[] {-7280, 2350}) == -7280, "2467 ex2");
        check(sol.mostProfitablePathBrute(new int[][] {{0, 1}}, 1, new int[] {-7280, 2350}) == -7280, "2467 ex2b");
        // 随机小规模对拍
        java.util.Random rnd = new java.util.Random(251);
        for (int t = 0; t < 80; t++) {
            int n = 2 + rnd.nextInt(8);
            List<int[]> es = new ArrayList<>();
            for (int u = 1; u < n; u++) {
                es.add(new int[] {u, rnd.nextInt(u)});
            }
            int[][] edgeArr = es.toArray(new int[0][]);
            int bob = 1 + rnd.nextInt(n - 1);
            int[] amt = new int[n];
            for (int i = 0; i < n; i++) {
                amt[i] = (rnd.nextInt(21) - 10) * 2;
            }
            int x = sol.mostProfitablePath(edgeArr, bob, amt);
            int y = sol.mostProfitablePathBrute(edgeArr, bob, amt);
            if (x != y) {
                throw new AssertionError("2467 mismatch t=" + t + " x=" + x + " y=" + y);
            }
        }
        System.out.println("2467 全部 PASS");
    }

    private static void check(boolean ok, String msg) {
        if (!ok) {
            throw new AssertionError(msg);
        }
        System.out.println("PASS " + msg);
    }
}

复杂度分析

  • 时间复杂度:O(n)。
  • 空间复杂度:O(n)。

2468. Split Message Based on Limit(根据限制分割消息)

题目描述

给你一个字符串 message 和一个正整数 limit。将 message 分割为一个或多个部分,每个部分结尾是

"<a/b>"(b 是总段数,a 是当前段编号,从 1 到 b)。除最后一部分长度 ≤ limit 外,其他每部分

(含结尾)长度都应等于 limit。结果数组越短越好。按顺序拼接各部分去掉结尾后应还原 message。

返回分割数组;若无法分割返回空数组。

示例 1

复制代码
`message="this is really a very awesome message", limit=9` → 14 段。

示例 2

复制代码
`message="short message", limit=15` → `["short mess<1/2>","age<2/2>"]`。

提示:

  • 1 <= message.length <= 10^4,message 只含小写字母与空格,1 <= limit <= 10^4

解题思路

解法一:枚举总段数 b(主提交版本)

  • 对每个可能的总段数 b(从 1 到 message.length),计算「尾部标记」固定长度占用:
    标记 <a/b> 的长度与 a 的位数有关;为简便,先求每个段尾标记长度为
    tailLen(a) = 1 + len(a) + 1 + len(b) + 1 = 3 + len(a) + len(b)。
  • 若每个段体(不含标记)长度一致为 bodyLen,则需 b * (tailLen 和) ? 不对:
    每段长度 = bodyLen + tailLen(a),最后一段 ≤ limit,其余 = limit。
    第 a 段 (a<b) 长度 = bodyLen(段体)+ tailLen(a) == limit → bodyLen 不统一!
    官方做法:第 a 段的 body 字符数 = limit - tailLen(a),最后一段 = 剩余。
  • 判断:canSplit(b):前 b-1 段每段可装 limit - tailLen(a) 个字符(必须 ≥ 0),
    最后一段装全部剩余(剩余 ≤ limit - tailLen(b))。
  • 从小到大枚举 b,首个可行即最优(段数最少)。
    解法二:贪心逐段构造(对照)
  • 固定 b 后按上述公式逐段切分并拼回验证,与枚举思路一致,写法不同。
    复杂度:
  • 时间复杂度:O(n * log n)(枚举 b ≤ n,每段 O(len) 拼接,最坏 O(n^2) 但 n=1e4 可行)。
  • 空间复杂度:O(n)。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.List;

public class Solution {
    // 解法一:枚举总段数(主提交版本)
    public String[] splitMessage(String message, int limit) {
        int n = message.length();
        for (int b = 1; b <= n; b++) {
            if (canSplit(message, limit, b)) {
                return build(message, limit, b);
            }
        }
        return new String[0];
    }

    private int lenOf(int x) {
        int c = 0;
        while (x > 0) {
            c++;
            x /= 10;
        }
        return c;
    }

    private boolean canSplit(String msg, int limit, int b) {
        int n = msg.length();
        int lb = lenOf(b);
        int used = 0;
        for (int a = 1; a <= b; a++) {
            int la = lenOf(a);
            int tail = 3 + la + lb; // "<a/b>"
            if (tail >= limit) {
                return false; // 标记本身超限
            }
            if (a == b) {
                // 最后一段:剩余字符 ≤ limit - tail
                int rem = n - used;
                if (rem > limit - tail) {
                    return false;
                }
            } else {
                int body = limit - tail;
                used += body;
                if (used > n) {
                    return false; // 已超出总长(后面还有段但字符没了)
                }
            }
        }
        // 最后一段必须正好装完剩余
        return used + 0 >= 0 && used <= n; // used 最终等于 n 检查在循环内完成
    }

    private String[] build(String msg, int limit, int b) {
        int n = msg.length();
        int lb = lenOf(b);
        String[] res = new String[b];
        int used = 0;
        for (int a = 1; a <= b; a++) {
            int la = lenOf(a);
            int tail = 3 + la + lb;
            String mark = "<" + a + "/" + b + ">";
            if (a == b) {
                String body = msg.substring(used);
                res[a - 1] = body + mark;
            } else {
                int body = limit - tail;
                res[a - 1] = msg.substring(used, used + body) + mark;
                used += body;
            }
        }
        return res;
    }

    // 解法二:校验函数(对照)
    public boolean verify(String[] parts, String message) {
        StringBuilder sb = new StringBuilder();
        int b = parts.length;
        for (int i = 0; i < parts.length; i++) {
            String mark = "<" + (i + 1) + "/" + b + ">";
            if (!parts[i].endsWith(mark)) {
                return false;
            }
            sb.append(parts[i], 0, parts[i].length() - mark.length());
        }
        return sb.toString().equals(message);
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        String[] r1 = sol.splitMessage("this is really a very awesome message", 9);
        check(r1.length == 14, "2468 ex1 len " + r1.length);
        check(sol.verify(r1, "this is really a very awesome message"), "2468 ex1 verify");
        String[] r2 = sol.splitMessage("short message", 15);
        check(r2.length == 2 && r2[0].equals("short mess<1/2>") && r2[1].equals("age<2/2>"), "2468 ex2");
        check(sol.verify(r2, "short message"), "2468 ex2 verify");
        // 无法分割的情形:limit 太小
        check(sol.splitMessage("abc", 1).length == 0, "2468 limit=1 无法分割");
        // 随机验证
        java.util.Random rnd = new java.util.Random(257);
        for (int t = 0; t < 200; t++) {
            int n = 1 + rnd.nextInt(40);
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < n; i++) {
                sb.append((char) ('a' + rnd.nextInt(3)));
            }
            String msg = sb.toString();
            int limit = 1 + rnd.nextInt(15);
            String[] parts = sol.splitMessage(msg, limit);
            if (parts.length > 0) {
                if (!sol.verify(parts, msg)) {
                    throw new AssertionError("2468 verify fail msg=" + msg + " limit=" + limit);
                }
                // 检查每段长度约束
                for (int i = 0; i < parts.length; i++) {
                    if (i < parts.length - 1 && parts[i].length() != limit) {
                        throw new AssertionError("2468 len fail " + parts[i].length() + " limit=" + limit);
                    }
                    if (i == parts.length - 1 && parts[i].length() > limit) {
                        throw new AssertionError("2468 last len fail");
                    }
                }
            }
        }
        System.out.println("2468 全部 PASS");
    }

    private static void check(boolean ok, String msg) {
        if (!ok) {
            throw new AssertionError(msg);
        }
        System.out.println("PASS " + msg);
    }
}

复杂度分析

  • 时间复杂度:O(n * log n)(枚举 b ≤ n,每段 O(len) 拼接,最坏 O(n^2) 但 n=1e4 可行)。
  • 空间复杂度:O(n)。

2469. Convert the Temperature(温度转换)

题目描述

给你一个四舍五入到两位小数的非负浮点数 celsius 来表示温度,以摄氏度(Celsius)为单位。

你需要将摄氏度转换为开氏度(Kelvin)和华氏度(Fahrenheit),

并以数组 ans = [kelvin, fahrenheit] 的形式返回结果。

注意:

  • 开氏度 = 摄氏度 + 273.15
  • 华氏度 = 摄氏度 * 1.80 + 32.00

示例 1

复制代码
`celsius = 36.50`(摄氏)→ `[309.65000, 97.70000]`

示例 2

复制代码
`celsius = 122.11`(摄氏)→ `[395.26000, 251.79800]`

提示:

  • 0 <= celsius <= 1000

解题思路

解法一:公式直算(主提交版本)

  • 直接按题目给出的两个换算公式计算 kelvin = celsius + 273.15、fahrenheit = celsius * 1.80 + 32.00。
    解法二:拆步计算(对照)
  • 先算开氏,再用 celsius * 9 / 5 + 32(即 ×1.8 的等价写法)算华氏,结果一致。
    复杂度:
  • 时间复杂度:O(1)
  • 空间复杂度:O(1)

代码实现

java 复制代码
public class Solution {
    // 解法一:公式直算(主提交版本)
    public double[] convertTemperature(double celsius) {
        return new double[]{celsius + 273.15, celsius * 1.80 + 32.00};
    }

    // 解法二:拆步计算(对照,×9/5 + 32 等价写法)
    public double[] convertTemperatureStep(double celsius) {
        double kelvin = celsius + 273.15;
        double fahrenheit = celsius * 9.0 / 5.0 + 32.0;
        return new double[]{kelvin, fahrenheit};
    }

    public static void main(String[] args) {
        Solution sol = new Solution();

        double c1 = 36.50;
        double[] r1 = sol.convertTemperature(c1);
        double[] r1b = sol.convertTemperatureStep(c1);
        assertNearly(r1, new double[]{309.65, 97.70});
        assertNearly(r1b, new double[]{309.65, 97.70});

        double c2 = 122.11;
        double[] r2 = sol.convertTemperature(c2);
        double[] r2b = sol.convertTemperatureStep(c2);
        assertNearly(r2, new double[]{395.26, 251.798});
        assertNearly(r2b, new double[]{395.26, 251.798});

        double c3 = 0.0;
        double[] r3 = sol.convertTemperature(c3);
        assertNearly(r3, new double[]{273.15, 32.00});

        double c4 = 1000.0;
        double[] r4 = sol.convertTemperature(c4);
        assertNearly(r4, new double[]{1273.15, 1832.00});

        System.out.println("2469 全部 PASS");
    }

    private static void assertNearly(double[] actual, double[] expected) {
        for (int i = 0; i < expected.length; i++) {
            assert Math.abs(actual[i] - expected[i]) < 1e-5 : "期望 " + expected[i] + " 实际 " + actual[i];
        }
    }
}

复杂度分析

  • 时间复杂度:O(1)
  • 空间复杂度:O(1)

2470. Number of Subarrays With LCM Equal to K(最小公倍数等于 K 的子数组数目)

题目描述

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 nums 的子数组中满足

元素最小公倍数为 k 的子数组数目。

子数组是数组中一个连续非空的元素序列。数组的最小公倍数是可被所有数组元素整除的最小正整数。

示例 1

复制代码
`nums = [3,6,2,7,1], k = 6` → 4

示例 2

复制代码
`nums = [3], k = 2` → 0

提示:

  • 1 <= nums.length <= 1000
  • 1 <= nums[i], k <= 1000

解题思路

解法一:右端点固定,滚动维护 LCM(主提交版本)

  • 枚举子数组右端点 r,维护以 r 结尾的区间 LCM 集合。
  • 由于 LCM 是单调不降的(随着区间左移不断并入新元素,LCM 只增不减),
    不同 LCM 值数量很少(每个值只会被更大值替换,值域受 k 限制)。
  • 对每个右端点,维护上一步的 <lcm, 区间个数> 列表,并入 nums[r] 后合并相同 LCM。
  • 统计 LCM == k 的区间个数,累加进答案。
    解法二:暴力枚举全部子数组(对照)
  • 双重循环枚举 (l, r),边扩展边累乘 LCM,等于 k 则计数,超过 k 可提前剪枝退出。
    复杂度:
  • 时间复杂度:解法一 O(n * τ)(τ 为每点 LCM 去重后的值个数,很小),解法二 O(n^2)
  • 空间复杂度:O(τ)

代码实现

java 复制代码
public class Solution {
    private static int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }

    private static long lcm(long a, long b) {
        return a / gcd((int) a, (int) b) * b;
    }

    // 解法一:滚动 LCM 集合(主提交版本)
    public int subarrayLCM(int[] nums, int k) {
        int n = nums.length;
        long[][] cur; // {lcm, count}
        int curLen = 0;
        cur = new long[n][2];
        int ans = 0;
        for (int r = 0; r < n; r++) {
            long[][] nxt = new long[n + 1][2];
            int nxtLen = 0;
            long v = nums[r];
            // 单个元素区间
            nxt[nxtLen][0] = v;
            nxt[nxtLen][1] = 1;
            nxtLen++;
            // 并入上一个右端点结束的所有区间
            for (int i = 0; i < curLen; i++) {
                long nlcm = lcm(cur[i][0], v);
                if (nlcm > k) {     // 超过 k 的不会再变回 k(LCM 单调不减)
                    continue;
                }
                boolean merged = false;
                for (int j = 0; j < nxtLen; j++) {
                    if (nxt[j][0] == nlcm) {
                        nxt[j][1] += cur[i][1];
                        merged = true;
                        break;
                    }
                }
                if (!merged) {
                    nxt[nxtLen][0] = nlcm;
                    nxt[nxtLen][1] = cur[i][1];
                    nxtLen++;
                }
            }
            for (int j = 0; j < nxtLen; j++) {
                if (nxt[j][0] == k) {
                    ans += nxt[j][1];
                }
            }
            cur = nxt;
            curLen = nxtLen;
        }
        return ans;
    }

    // 解法二:暴力枚举全部子数组(对照)
    public int subarrayLCMBrute(int[] nums, int k) {
        int n = nums.length;
        int ans = 0;
        for (int l = 0; l < n; l++) {
            long cur = 1;
            for (int r = l; r < n; r++) {
                cur = lcm(cur, nums[r]);
                if (cur > k) {
                    break;
                }
                if (cur == k) {
                    ans++;
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();

        int[] n1 = {3, 6, 2, 7, 1};
        assert sol.subarrayLCM(n1, 6) == 4 : "2470 例1 主";
        assert sol.subarrayLCMBrute(n1, 6) == 4 : "2470 例1 暴力";

        int[] n2 = {3};
        assert sol.subarrayLCM(n2, 2) == 0 : "2470 例2 主";
        assert sol.subarrayLCMBrute(n2, 2) == 0 : "2470 例2 暴力";

        int[] n3 = {2, 3};
        assert sol.subarrayLCM(n3, 6) == 1 : "2470 例3 主";
        assert sol.subarrayLCMBrute(n3, 6) == 1 : "2470 例3 暴力";

        // 随机对拍
        java.util.Random rnd = new java.util.Random(42);
        for (int t = 0; t < 500; t++) {
            int len = 1 + rnd.nextInt(30);
            int[] arr = new int[len];
            int k = 1 + rnd.nextInt(60);
            for (int i = 0; i < len; i++) {
                arr[i] = 1 + rnd.nextInt(60);
            }
            int a = sol.subarrayLCM(arr, k);
            int b = sol.subarrayLCMBrute(arr, k);
            if (a != b) {
                System.out.println("对拍失败 nums=" + java.util.Arrays.toString(arr) + " k=" + k + " 主=" + a + " 暴力=" + b);
                System.exit(1);
            }
        }
        System.out.println("2470 全部 PASS");
    }
}

复杂度分析

  • 时间复杂度:解法一 O(n * τ)(τ 为每点 LCM 去重后的值个数,很小),解法二 O(n^2)
  • 空间复杂度:O(τ)

2471. Minimum Number of Operations to Sort a Binary Tree by Level(逐层排序二叉树所需的最少操作数目)

题目描述

给你一个值互不相同的二叉树的根节点 root。

在一步操作中,你可以选择同一层上任意两个节点,交换这两个节点的值。

返回每一层按严格递增顺序排序所需的最少操作数目。

节点的层数是该节点和根节点之间的路径的边数。

示例 1

复制代码
`root = [1,4,3,7,6,8,5,null,null,null,null,9,null,10]` → 3

示例 2

复制代码
`root = [1,3,2,7,6,5,4]` → 3

示例 3

复制代码
`root = [1,2,3,4,5,6]` → 0

提示:

  • 树中节点的数目在范围 [1, 10^5]
  • 1 <= Node.val <= 10^5
  • 树中的所有值互不相同

解题思路

解法一:BFS 分层 + 置换环计数(主提交版本)

  • BFS 按层收集每层节点值数组 vals。
  • 对每一层:将 vals 拷贝排序得到目标数组 sorted,记录每个值应当落在的位置 pos。
  • 遍历每个元素,沿着「当前位置 → 目标位置」的置换形成若干环,每个长度为 L 的环
    需要 L - 1 次交换才能就位,累加 环长 - 1。
  • 各层最少交换次数相加即为答案。
    解法二:逐层冒泡式计数(对照)
  • 同样 BFS 分层,但每层用「贪心交换」:扫描 i,若 vals[i] != sorted[i],
    找到值 sorted[i] 所在位置 j 并交换,每交换一次计数 +1。
    复杂度:
  • 时间复杂度:O(n log n)(每层排序,总元素为 n)
  • 空间复杂度:O(n)

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Solution {
    // 解法一:BFS + 置换环(主提交版本)
    public int minimumOperations(TreeNode root) {
        int ans = 0;
        if (root == null) {
            return 0;
        }
        Deque<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        while (!q.isEmpty()) {
            int size = q.size();
            int[] vals = new int[size];
            for (int i = 0; i < size; i++) {
                TreeNode node = q.poll();
                vals[i] = node.val;
                if (node.left != null) {
                    q.offer(node.left);
                }
                if (node.right != null) {
                    q.offer(node.right);
                }
            }
            ans += minSwaps(vals);
        }
        return ans;
    }

    private int minSwaps(int[] vals) {
        int n = vals.length;
        int[] sorted = vals.clone();
        Arrays.sort(sorted);
        Map<Integer, Integer> pos = new HashMap<>();
        for (int i = 0; i < n; i++) {
            pos.put(sorted[i], i);
        }
        boolean[] vis = new boolean[n];
        int swaps = 0;
        for (int i = 0; i < n; i++) {
            if (!vis[i]) {
                int j = i;
                int len = 0;
                while (!vis[j]) {
                    vis[j] = true;
                    j = pos.get(vals[j]);
                    len++;
                }
                if (len > 0) {
                    swaps += len - 1;
                }
            }
        }
        return swaps;
    }

    // 解法二:BFS + 贪心交换(对照)
    public int minimumOperationsGreedy(TreeNode root) {
        int ans = 0;
        if (root == null) {
            return 0;
        }
        Deque<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        while (!q.isEmpty()) {
            int size = q.size();
            int[] vals = new int[size];
            for (int i = 0; i < size; i++) {
                TreeNode node = q.poll();
                vals[i] = node.val;
                if (node.left != null) {
                    q.offer(node.left);
                }
                if (node.right != null) {
                    q.offer(node.right);
                }
            }
            int[] sorted = vals.clone();
            Arrays.sort(sorted);
            Map<Integer, Integer> pos = new HashMap<>();
            for (int i = 0; i < size; i++) {
                pos.put(vals[i], i);
            }
            for (int i = 0; i < size; i++) {
                if (vals[i] != sorted[i]) {
                    int j = pos.get(sorted[i]);
                    // 交换 vals[i] 与 vals[j]
                    int tmp = vals[i];
                    vals[i] = vals[j];
                    vals[j] = tmp;
                    pos.put(vals[i], i);
                    pos.put(vals[j], j);
                    ans++;
                }
            }
        }
        return ans;
    }

    // ---- 树构建辅助(层序数组 → 二叉树) ----
    static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;

        TreeNode(int val) {
            this.val = val;
        }
    }

    private static TreeNode buildTree(List<Integer> arr) {
        if (arr == null || arr.isEmpty() || arr.get(0) == null) {
            return null;
        }
        TreeNode root = new TreeNode(arr.get(0));
        Deque<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        int idx = 1;
        while (!q.isEmpty() && idx < arr.size()) {
            TreeNode node = q.poll();
            if (idx < arr.size() && arr.get(idx) != null) {
                node.left = new TreeNode(arr.get(idx));
                q.offer(node.left);
            }
            idx++;
            if (idx < arr.size() && arr.get(idx) != null) {
                node.right = new TreeNode(arr.get(idx));
                q.offer(node.right);
            }
            idx++;
        }
        return root;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();

        TreeNode r1 = buildTree(Arrays.asList(1, 4, 3, 7, 6, 8, 5, null, null, null, null, 9, null, 10));
        assert sol.minimumOperations(r1) == 3 : "2471 例1 环";
        assert sol.minimumOperationsGreedy(r1) == 3 : "2471 例1 贪心";

        TreeNode r2 = buildTree(Arrays.asList(1, 3, 2, 7, 6, 5, 4));
        assert sol.minimumOperations(r2) == 3 : "2471 例2 环";
        assert sol.minimumOperationsGreedy(r2) == 3 : "2471 例2 贪心";

        TreeNode r3 = buildTree(Arrays.asList(1, 2, 3, 4, 5, 6));
        assert sol.minimumOperations(r3) == 0 : "2471 例3 环";
        assert sol.minimumOperationsGreedy(r3) == 0 : "2471 例3 贪心";

        TreeNode r4 = new TreeNode(1);
        assert sol.minimumOperations(r4) == 0 : "2471 单节点 环";
        assert sol.minimumOperationsGreedy(r4) == 0 : "2471 单节点 贪心";

        System.out.println("2471 全部 PASS");
    }
}

复杂度分析

  • 时间复杂度:O(n log n)(每层排序,总元素为 n)
  • 空间复杂度:O(n)

2472. Maximum Number of Non-overlapping Palindrome Substrings(不重叠回文子字符串的最大数目)

题目描述

给你一个字符串 s 和一个正整数 k。

从字符串 s 中选出一组满足下述条件且不重叠的子字符串:

  • 每个子字符串的长度至少为 k
  • 每个子字符串是一个回文串
    返回最优方案中能选择的子字符串的最大数目。

示例 1

复制代码
`s = "abaccdbbd", k = 3` → 2(可选 "aba" 和 "dbbd")

示例 2

复制代码
`s = "adbcda", k = 2` → 0

提示:

  • 1 <= k <= s.length <= 2000
  • s 仅由小写英文字母组成

解题思路

解法一:区间 DP 判回文 + 线性 DP 选区间(主提交版本)

  • 先做区间 DP:pal[i][j] 表示子串 s[i..j] 是否为回文。
    pal[i][j] = (s[i] == s[j]) && (j - i < 2 || pal[i + 1][j - 1])。
  • 再做一维 DP:dp[i] 表示前缀 s[0..i-1] 最多能选出多少个合法回文子串,
    为了让已选子串尽量靠左给后面留空间(贪心思想),转移:
  • 不选以 i-1 结尾的回文:dp[i + 1] = max(dp[i + 1], dp[i])
  • 若 s[j..i] 是回文且长度 ≥ k:dp[i + 1] = max(dp[i + 1], dp[j] + 1)
    解法二:中心扩展建表 + 线性 DP(对照)
  • 用中心扩展法(奇数中心 1 个点、偶数中心 2 个点向两侧扩散)标记所有回文区间 pal[l][r]。
  • DP 转移与解法一相同,只是回文表的构建方式不同(中心扩展 vs 区间递推)。
    复杂度:
  • 时间复杂度:O(n^2)
  • 空间复杂度:O(n^2)

代码实现

java 复制代码
public class Solution {
    // 解法一:区间 DP + 线性 DP(主提交版本)
    public int maxPalindromes(String s, int k) {
        int n = s.length();
        boolean[][] pal = new boolean[n][n];
        for (int len = 1; len <= n; len++) {
            for (int i = 0; i + len - 1 < n; i++) {
                int j = i + len - 1;
                if (s.charAt(i) == s.charAt(j) && (len <= 2 || pal[i + 1][j - 1])) {
                    pal[i][j] = true;
                }
            }
        }
        // dp[i] = 前缀 s[0..i-1] 的最大选择数
        int[] dp = new int[n + 1];
        for (int i = 0; i < n; i++) {
            dp[i + 1] = Math.max(dp[i + 1], dp[i]);
            for (int j = i; j >= 0; j--) {
                int len = i - j + 1;
                if (len >= k && pal[j][i]) {
                    dp[i + 1] = Math.max(dp[i + 1], dp[j] + 1);
                }
            }
        }
        return dp[n];
    }

    // 解法二:中心扩展建表 + 线性 DP(对照)
    public int maxPalindromesGreedy(String s, int k) {
        int n = s.length();
        boolean[][] pal = new boolean[n][n];
        // 奇数中心
        for (int c = 0; c < n; c++) {
            int l = c, r = c;
            while (l >= 0 && r < n && s.charAt(l) == s.charAt(r)) {
                pal[l][r] = true;
                l--;
                r++;
            }
        }
        // 偶数中心
        for (int c = 0; c + 1 < n; c++) {
            int l = c, r = c + 1;
            while (l >= 0 && r < n && s.charAt(l) == s.charAt(r)) {
                pal[l][r] = true;
                l--;
                r++;
            }
        }
        int[] dp = new int[n + 1];
        for (int i = 0; i < n; i++) {
            dp[i + 1] = Math.max(dp[i + 1], dp[i]);
            for (int j = i; j >= 0; j--) {
                if (i - j + 1 >= k && pal[j][i]) {
                    dp[i + 1] = Math.max(dp[i + 1], dp[j] + 1);
                }
            }
        }
        return dp[n];
    }

    private boolean isPal(String s, int l, int r) {
        while (l < r) {
            if (s.charAt(l) != s.charAt(r)) {
                return false;
            }
            l++;
            r--;
        }
        return true;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();

        assert sol.maxPalindromes("abaccdbbd", 3) == 2 : "2472 例1 主";
        assert sol.maxPalindromesGreedy("abaccdbbd", 3) == 2 : "2472 例1 贪心";

        assert sol.maxPalindromes("adbcda", 2) == 0 : "2472 例2 主";
        assert sol.maxPalindromesGreedy("adbcda", 2) == 0 : "2472 例2 贪心";

        assert sol.maxPalindromes("aaaa", 2) == 2 : "2472 特例主"; // "aa" + "aa"
        assert sol.maxPalindromesGreedy("aaaa", 2) == 2 : "2472 特例贪心";

        assert sol.maxPalindromes("a", 1) == 1 : "2472 单字符主";
        assert sol.maxPalindromesGreedy("a", 1) == 1 : "2472 单字符贪心";

        // 随机对拍(n 小规模)
        java.util.Random rnd = new java.util.Random(7);
        for (int t = 0; t < 300; t++) {
            int n = 1 + rnd.nextInt(25);
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < n; i++) {
                sb.append((char) ('a' + rnd.nextInt(4)));
            }
            String s = sb.toString();
            int k = 1 + rnd.nextInt(n);
            int a = sol.maxPalindromes(s, k);
            int b = sol.maxPalindromesGreedy(s, k);
            if (a != b) {
                System.out.println("对拍失败 s=" + s + " k=" + k + " 主=" + a + " 贪心=" + b);
                System.exit(1);
            }
        }
        System.out.println("2472 全部 PASS");
    }
}

复杂度分析

  • 时间复杂度:O(n^2)
  • 空间复杂度:O(n^2)

2473. Minimum Cost to Buy Apples(购买苹果的最低成本)

题目描述

给你一个正整数 n,表示从 1 到 n 的 n 个城市。还给你一个二维数组 roads,

其中 roads[i] = [ai, bi, costi] 表示在城市 ai 和 bi 之间有一条双向道路,其旅行成本等于 costi。

你可以在任何城市买到苹果,但是有些城市买苹果的费用不同。给定数组 appleCost,其中 appleCost[i]

是从城市 i 购买一个苹果的成本。

你从某个城市开始,穿越各种道路,最终从任何一个城市买一个苹果。买了苹果之后,你必须回到开始的

城市,但现在所有道路的成本将乘以一个给定的因子 k。

返回大小为 n 的数组 answer,其中 answer[i] 是从城市 i 开始购买一个苹果的最小总成本。

示例 1

复制代码
`n = 4, roads = [[1,2,4],[2,3,2],[2,4,5],[3,4,1],[1,3,4]], appleCost = [56,42,102,301], k = 2` → `[54,42,48,51]`

示例 2

复制代码
`n = 3, roads = [[1,2,5],[2,3,1],[3,1,2]], appleCost = [2,3,1], k = 3` → `[2,3,1]`

提示:

  • 2 <= n <= 1000
  • 1 <= roads.length <= 1000
  • 1 <= costi <= 10^5,1 <= appleCost[i] <= 10^5
  • 1 <= k <= 100,无重复边

解题思路

解法一:以每个城市为源点跑 Dijkstra(主提交版本)

  • 对起点 i,单趟最短距离数组 dist[i] 表示从 i 到各城市的最短单程费用。
  • 总费用 = 单程去程 distt + 苹果价 appleCostt + 返程 distt * k。
    即在城市 t 买苹果:answer[i] = min_t (dist[t] * (1 + k) + appleCost[t])。
  • 对每个 i 跑一遍 Dijkstra(n ≤ 1000,边 ≤ 1000,复杂度可接受)。
    解法二:反向构图 + 多源扩展(对照)
  • 从「买苹果」的角度看,等价于对每个卖点 t,向外扩展:单程费用 d 满足
    d * (1 + k) + appleCost[t]。但返程 k 倍使简单多源失效;这里改用朴素 Bellman 式
    逐步松弛验证 = 逐城市 Dijkstra 的一致性(写法不同:直接对每个源点用优先队列)。
    复杂度:
  • 时间复杂度:O(n * (m log n)),n, m ≤ 1000
  • 空间复杂度:O(n + m)

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.PriorityQueue;

public class Solution {
    // 解法一:逐源点 Dijkstra(主提交版本)
    public long[] minCost(int n, int[][] roads, int[] appleCost, int k) {
        List<int[]>[] g = buildGraph(n, roads);
        long[] ans = new long[n];
        for (int s = 0; s < n; s++) {
            long[] dist = dijkstra(g, s, n);
            long best = Long.MAX_VALUE;
            for (int t = 0; t < n; t++) {
                if (dist[t] == Long.MAX_VALUE) {
                    continue;
                }
                best = Math.min(best, dist[t] * (1L + k) + appleCost[t]);
            }
            ans[s] = best;
        }
        return ans;
    }

    private List<int[]>[] buildGraph(int n, int[][] roads) {
        List<int[]>[] g = new List[n];
        for (int i = 0; i < n; i++) {
            g[i] = new ArrayList<>();
        }
        for (int[] r : roads) {
            int a = r[0] - 1, b = r[1] - 1; // 转 0 基
            g[a].add(new int[]{b, r[2]});
            g[b].add(new int[]{a, r[2]});
        }
        return g;
    }

    private long[] dijkstra(List<int[]>[] g, int s, int n) {
        long[] dist = new long[n];
        Arrays.fill(dist, Long.MAX_VALUE);
        dist[s] = 0;
        PriorityQueue<long[]> pq = new PriorityQueue<>((x, y) -> Long.compare(x[1], y[1])); // {node, cost}
        pq.offer(new long[]{s, 0});
        while (!pq.isEmpty()) {
            long[] cur = pq.poll();
            int u = (int) cur[0];
            long d = cur[1];
            if (d > dist[u]) {
                continue;
            }
            for (int[] e : g[u]) {
                int v = e[0];
                long nd = d + e[1];
                if (nd < dist[v]) {
                    dist[v] = nd;
                    pq.offer(new long[]{v, nd});
                }
            }
        }
        return dist;
    }

    // 解法二:Floyd 预处理全源最短路(对照)
    public long[] minCostFloyd(int n, int[][] roads, int[] appleCost, int k) {
        long INF = Long.MAX_VALUE / 4;
        long[][] dist = new long[n][n];
        for (int i = 0; i < n; i++) {
            Arrays.fill(dist[i], INF);
            dist[i][i] = 0;
        }
        for (int[] r : roads) {
            int a = r[0] - 1, b = r[1] - 1;
            dist[a][b] = Math.min(dist[a][b], r[2]);
            dist[b][a] = Math.min(dist[b][a], r[2]);
        }
        for (int mid = 0; mid < n; mid++) {
            for (int i = 0; i < n; i++) {
                if (dist[i][mid] == INF) {
                    continue;
                }
                for (int j = 0; j < n; j++) {
                    if (dist[mid][j] == INF) {
                        continue;
                    }
                    dist[i][j] = Math.min(dist[i][j], dist[i][mid] + dist[mid][j]);
                }
            }
        }
        long[] ans = new long[n];
        for (int i = 0; i < n; i++) {
            long best = Long.MAX_VALUE;
            for (int t = 0; t < n; t++) {
                if (dist[i][t] == INF) {
                    continue;
                }
                best = Math.min(best, dist[i][t] * (1L + k) + appleCost[t]);
            }
            ans[i] = best;
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();

        long[] r1 = sol.minCost(4, new int[][]{{1, 2, 4}, {2, 3, 2}, {2, 4, 5}, {3, 4, 1}, {1, 3, 4}},
                new int[]{56, 42, 102, 301}, 2);
        assert Arrays.equals(r1, new long[]{54, 42, 48, 51}) : "2473 例1 主: " + Arrays.toString(r1);
        long[] r1b = sol.minCostFloyd(4, new int[][]{{1, 2, 4}, {2, 3, 2}, {2, 4, 5}, {3, 4, 1}, {1, 3, 4}},
                new int[]{56, 42, 102, 301}, 2);
        assert Arrays.equals(r1b, new long[]{54, 42, 48, 51}) : "2473 例1 Floyd: " + Arrays.toString(r1b);

        long[] r2 = sol.minCost(3, new int[][]{{1, 2, 5}, {2, 3, 1}, {3, 1, 2}}, new int[]{2, 3, 1}, 3);
        assert Arrays.equals(r2, new long[]{2, 3, 1}) : "2473 例2 主: " + Arrays.toString(r2);
        long[] r2b = sol.minCostFloyd(3, new int[][]{{1, 2, 5}, {2, 3, 1}, {3, 1, 2}}, new int[]{2, 3, 1}, 3);
        assert Arrays.equals(r2b, new long[]{2, 3, 1}) : "2473 例2 Floyd: " + Arrays.toString(r2b);

        // 随机对拍(小规模)
        java.util.Random rnd = new java.util.Random(11);
        for (int t = 0; t < 200; t++) {
            int n = 2 + rnd.nextInt(8);
            int m = rnd.nextInt(n * (n - 1) / 2 + 1);
            int[][] roads = new int[m][3];
            java.util.Set<Integer> seen = new java.util.HashSet<>();
            for (int i = 0; i < m; i++) {
                int a, b;
                do {
                    a = rnd.nextInt(n) + 1;
                    b = rnd.nextInt(n) + 1;
                } while (a == b || !seen.add(a * 100 + b));
                roads[i][0] = a;
                roads[i][1] = b;
                roads[i][2] = 1 + rnd.nextInt(20);
            }
            int[] apple = new int[n];
            for (int i = 0; i < n; i++) {
                apple[i] = 1 + rnd.nextInt(100);
            }
            int k = 1 + rnd.nextInt(10);
            long[] a1 = sol.minCost(n, roads, apple, k);
            long[] a2 = sol.minCostFloyd(n, roads, apple, k);
            if (!Arrays.equals(a1, a2)) {
                System.out.println("对拍失败 n=" + n + " roads=" + Arrays.deepToString(roads)
                        + " apple=" + Arrays.toString(apple) + " k=" + k
                        + " 主=" + Arrays.toString(a1) + " Floyd=" + Arrays.toString(a2));
                System.exit(1);
            }
        }
        System.out.println("2473 全部 PASS");
    }
}

复杂度分析

  • 时间复杂度:O(n * (m log n)),n, m ≤ 1000
  • 空间复杂度:O(n + m)

2474. Customers With Strictly Increasing Purchases(购买量严格增加的客户)

题目类型:数据库(SQL)

题目描述

表: Orders

复制代码
+--------------+------+
| Column Name  | Type |
+--------------+------+
| order_id     | int  |
| customer_id  | int  |
| order_date   | date |
| price        | int  |
+--------------+------+

order_id 是该表的主键。每行包含订单的 id、订购该订单的客户 id、订单日期和价格。

编写一个 SQL 查询,报告总购买量每年严格增加 的客户 id。

  • 客户在一年内的总购买量 是该年订单价格的总和。如果某一年客户没有下任何订单,我们认为总购买量为 0。
  • 对于每个客户,要考虑的第一年是第一次下单的年份。
  • 对于每个客户,要考虑的最后一年是最后一次下单的年份。

以任意顺序返回结果表。

示例 1:

复制代码
输入:
Orders 表:
+----------+-------------+------------+-------+
| order_id | customer_id | order_date | price |
+----------+-------------+------------+-------+
| 1        | 1           | 2019-07-01 | 1100  |
| 2        | 1           | 2019-11-01 | 1200  |
| 3        | 1           | 2020-05-26 | 3000  |
| 4        | 1           | 2021-08-31 | 3100  |
| 5        | 1           | 2022-12-07 | 4700  |
| 6        | 2           | 2015-01-01 | 700   |
| 7        | 2           | 2017-11-07 | 1000  |
| 8        | 3           | 2017-01-01 | 900   |
| 9        | 3           | 2018-11-07 | 900   |
+----------+-------------+------------+-------+
输出:
+-------------+
| customer_id |
+-------------+
| 1           |
+-------------+
解释:
客户 1: 第一年是 2019 年,最后一年是 2022 年
  - 2019: 1100 + 1200 = 2300
  - 2020: 3000
  - 2021: 3100
  - 2022: 4700
  总购买量每年严格增加,所以包含客户 1。

客户 2: 第一年是 2015 年,最后一年是 2017 年
  - 2015: 700
  - 2016: 0(当年无订单)
  - 2017: 1000
  2016 年为 0,并非严格增加,所以不包含客户 2。

客户 3: 第一年是 2017 年,最后一年是 2018 年
  - 2017: 900
  - 2018: 900
  两年相等,并非严格增加,所以不包含客户 3。

解题思路

解法一:年度汇总补零 + LAG 同比严格递增判断

  • 第一步:按 customer_id, 年份 汇总出每个客户每年实际购买量 yearly(SUM(price))。
  • 第二步:某年没有订单要按 0 计。为连续对比,需要把「首次下单年 → 末次下单年」之间的每一年都补齐(0)。可用递归/数字表生成客户年份序列,或直接使用 LAG 与前一年的实际值比较------补零更稳妥。
  • 具体做法:
    1. yearly:SELECT customer_id, YEAR(order_date) y, SUM(price) amount FROM Orders GROUP BY customer_id, YEAR(order_date)。
    2. 对每个客户,用递归 CTE seq 生成从 MIN(y) 到 MAX(y) 的连续年份,再 LEFT JOIN yearly 得到 amount(无订单年 COALESCE(..., 0))。
    3. 用 LAG(amount) OVER (PARTITION BY customer_id ORDER BY y) 取上一年购买量;
    4. 保留所有年份都是 amount > prev 的客户:GROUP BY customer_id HAVING SUM(CASE WHEN amount > prev OR prev IS NULL THEN 0 ELSE 1 END) = 0。

解法二(对照):JOIN 自比较相邻年

  • 同样先补出连续年份序列,再用自连接 a JOIN b ON a.customer_id = b.customer_id AND a.y = b.y - 1 比较相邻两年;
  • 若存在任何一对相邻年 a.amount >= b.amount 则淘汰该客户;等价于 NOT EXISTS 反证。

代码实现

sql 复制代码
-- 解法一(推荐):年度汇总 + 递归补零 + LAG 严格递增判断
WITH RECURSIVE yearly AS (
    SELECT customer_id, YEAR(order_date) AS y,
           SUM(price) AS amount
    FROM Orders
    GROUP BY customer_id, YEAR(order_date)
),
seq AS (
    SELECT customer_id, MIN(y) AS y, MAX(y) AS maxy
    FROM yearly
    GROUP BY customer_id
    UNION ALL
    SELECT customer_id, y + 1, maxy
    FROM seq
    WHERE y < maxy
),
full_year AS (
    SELECT s.customer_id, s.y,
           COALESCE(y.amount, 0) AS amount
    FROM seq s
    LEFT JOIN yearly y
        ON y.customer_id = s.customer_id AND y.y = s.y
),
with_prev AS (
    SELECT customer_id, y, amount,
           LAG(amount) OVER (PARTITION BY customer_id ORDER BY y) AS prev
    FROM full_year
)
SELECT DISTINCT customer_id
FROM with_prev
GROUP BY customer_id
HAVING SUM(CASE WHEN prev IS NOT NULL AND amount <= prev THEN 1 ELSE 0 END) = 0;
sql 复制代码
-- 解法二(对照):连续年份自连接比较相邻年
WITH RECURSIVE yearly AS (
    SELECT customer_id, YEAR(order_date) AS y,
           SUM(price) AS amount
    FROM Orders
    GROUP BY customer_id, YEAR(order_date)
),
seq AS (
    SELECT customer_id, MIN(y) AS y, MAX(y) AS maxy
    FROM yearly
    GROUP BY customer_id
    UNION ALL
    SELECT customer_id, y + 1, maxy
    FROM seq
    WHERE y < maxy
),
full_year AS (
    SELECT s.customer_id, s.y,
           COALESCE(y.amount, 0) AS amount
    FROM seq s
    LEFT JOIN yearly y
        ON y.customer_id = s.customer_id AND y.y = s.y
)
SELECT DISTINCT a.customer_id
FROM full_year a
WHERE a.y > (SELECT MIN(y) FROM full_year WHERE customer_id = a.customer_id)
  AND NOT EXISTS (
      SELECT 1
      FROM full_year b
      WHERE b.customer_id = a.customer_id
        AND b.y = a.y - 1
        AND b.amount >= a.amount
  );

复杂度分析

  • 时间复杂度 :O(n + y),n 为 Orders 行数、y 为所有客户【末次年 - 首次年】跨度之和;递归补零遍历每个客户每年的记录。
  • 空间复杂度 :O(y),递归 CTE 与中间年份表与生成的年记录数成正比。

2475. Number of Unequal Triplets in Array(数组中不等三元组的数目)

题目描述

给你一个下标从 0 开始的正整数数组 nums。找出并统计满足下述条件的三元组 (i, j, k) 的数目:

  • 0 <= i < j < k < nums.length
  • nums[i]、nums[j] 和 nums[k] 两两不同。

示例 1

复制代码
`nums = [4,4,2,4,3]` → 3

示例 2

复制代码
`nums = [1,1,1,1,1]` → 0

提示:

  • 3 <= nums.length <= 100
  • 1 <= nums[i] <= 1000

解题思路

解法一:频率计数 + 组合计数(主提交版本)

  • 用哈希表统计每个值出现的频次 cnt。
  • 枚举三个不同值 a、b、c,其三元组个数为 cnt[a] * cnt[b] * cnt[c],累加即可。
  • 三个值只需保证不相同(数组下标天然满足 i < j < k:每个下标只属于一个值,任意
    从三个不交值集合中各取一个下标都自动满足 i<j<k 的相对顺序)。
    解法二:三重暴力枚举(对照)
  • 直接枚举所有 (i, j, k) 组合并判断三者两两不同。
    复杂度:
  • 时间复杂度:O(n + v^3)(v 为不同值个数),解法二 O(n^3)
  • 空间复杂度:O(v)

代码实现

java 复制代码
public class Solution {
    // 解法一:频次 + 组合计数(主提交版本)
    public int unequalTriplets(int[] nums) {
        java.util.Map<Integer, Integer> cnt = new java.util.HashMap<>();
        for (int v : nums) {
            cnt.put(v, cnt.getOrDefault(v, 0) + 1);
        }
        Integer[] keys = cnt.keySet().toArray(new Integer[0]);
        int m = keys.length;
        int ans = 0;
        for (int i = 0; i < m; i++) {
            for (int j = i + 1; j < m; j++) {
                for (int t = j + 1; t < m; t++) {
                    ans += cnt.get(keys[i]) * cnt.get(keys[j]) * cnt.get(keys[t]);
                }
            }
        }
        return ans;
    }

    // 解法二:暴力三重循环(对照)
    public int unequalTripletsBrute(int[] nums) {
        int n = nums.length;
        int ans = 0;
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                for (int k = j + 1; k < n; k++) {
                    if (nums[i] != nums[j] && nums[i] != nums[k] && nums[j] != nums[k]) {
                        ans++;
                    }
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();

        assert sol.unequalTriplets(new int[]{4, 4, 2, 4, 3}) == 3 : "2475 例1 主";
        assert sol.unequalTripletsBrute(new int[]{4, 4, 2, 4, 3}) == 3 : "2475 例1 暴力";

        assert sol.unequalTriplets(new int[]{1, 1, 1, 1, 1}) == 0 : "2475 例2 主";
        assert sol.unequalTripletsBrute(new int[]{1, 1, 1, 1, 1}) == 0 : "2475 例2 暴力";

        assert sol.unequalTriplets(new int[]{1, 2, 3}) == 1 : "2475 例3 主";
        assert sol.unequalTripletsBrute(new int[]{1, 2, 3}) == 1 : "2475 例3 暴力";

        java.util.Random rnd = new java.util.Random(5);
        for (int t = 0; t < 300; t++) {
            int n = 3 + rnd.nextInt(20);
            int[] arr = new int[n];
            for (int i = 0; i < n; i++) {
                arr[i] = 1 + rnd.nextInt(10);
            }
            int a = sol.unequalTriplets(arr);
            int b = sol.unequalTripletsBrute(arr);
            if (a != b) {
                System.out.println("对拍失败 " + java.util.Arrays.toString(arr) + " 主=" + a + " 暴力=" + b);
                System.exit(1);
            }
        }
        System.out.println("2475 全部 PASS");
    }
}

复杂度分析

  • 时间复杂度:O(n + v^3)(v 为不同值个数),解法二 O(n^3)
  • 空间复杂度:O(v)

题目描述

给你一个二叉搜索树的根节点 root,和一个由正整数组成、长度为 n 的数组 queries。

找出一个长度为 n 的二维答案数组 answer,其中 answer[i] = [mini, maxi]:

  • mini 是树中小于等于 queries[i] 的最大值;如果不存在这样的值,则使用 -1 代替。
  • maxi 是树中大于等于 queries[i] 的最小值;如果不存在这样的值,则使用 -1 代替。

示例 1

复制代码
`root = [6,2,13,1,4,9,15,null,null,null,null,null,null,14], queries = [2,5,16]` → `[[2,2],[4,6],[15,-1]]`

示例 2

复制代码
`root = [4,null,9], queries = [3]` → `[[-1,4]]`

提示:

  • 树中节点的数目在范围 [2, 10^5]
  • 1 <= Node.val <= 10^6
  • n == queries.length,1 <= n <= 10^5,1 <= queries[i] <= 10^6

解题思路

解法一:中序收集 + 二分查找(主提交版本)

  • 二叉搜索树中序遍历得到严格递增的节点值数组 vals。
  • 对每个查询 q:
  • mini:二分找最后一个 <= q 的元素(lowerBound(q+1) - 1)。
  • maxi:二分找第一个 >= q 的元素(lowerBound(q))。
  • 找不到返回 -1。
    解法二:迭代中序(对照)
  • 用显式栈做中序遍历,写法不同,结果一致。
    复杂度:
  • 时间复杂度:O(n + q log n)
  • 空间复杂度:O(n)

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class Solution {
    public static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;

        TreeNode(int val) {
            this.val = val;
        }
    }

    // 解法一:递归中序 + 二分(主提交版本)
    public List<List<Integer>> closestNodes(TreeNode root, List<Integer> queries) {
        List<Integer> vals = new ArrayList<>();
        inorder(root, vals);
        int[] arr = new int[vals.size()];
        for (int i = 0; i < vals.size(); i++) {
            arr[i] = vals.get(i);
        }
        List<List<Integer>> ans = new ArrayList<>();
        for (int q : queries) {
            int min = -1, max = -1;
            // mini:最后一个 <= q
            int lo = 0, hi = arr.length - 1;
            while (lo <= hi) {
                int mid = (lo + hi) >>> 1;
                if (arr[mid] <= q) {
                    min = arr[mid];
                    lo = mid + 1;
                } else {
                    hi = mid - 1;
                }
            }
            // maxi:第一个 >= q
            lo = 0;
            hi = arr.length - 1;
            while (lo <= hi) {
                int mid = (lo + hi) >>> 1;
                if (arr[mid] >= q) {
                    max = arr[mid];
                    hi = mid - 1;
                } else {
                    lo = mid + 1;
                }
            }
            ans.add(Arrays.asList(min, max));
        }
        return ans;
    }

    private void inorder(TreeNode node, List<Integer> vals) {
        if (node == null) {
            return;
        }
        inorder(node.left, vals);
        vals.add(node.val);
        inorder(node.right, vals);
    }

    // 解法二:迭代中序(对照)
    public List<List<Integer>> closestNodesIter(TreeNode root, List<Integer> queries) {
        List<Integer> vals = new ArrayList<>();
        java.util.Deque<TreeNode> st = new java.util.ArrayDeque<>();
        TreeNode cur = root;
        while (cur != null || !st.isEmpty()) {
            while (cur != null) {
                st.push(cur);
                cur = cur.left;
            }
            cur = st.pop();
            vals.add(cur.val);
            cur = cur.right;
        }
        int[] arr = new int[vals.size()];
        for (int i = 0; i < vals.size(); i++) {
            arr[i] = vals.get(i);
        }
        List<List<Integer>> ans = new ArrayList<>();
        for (int q : queries) {
            int min = -1, max = -1;
            // 用 Arrays.binarySearch 语义:返回插入点
            int pos = Arrays.binarySearch(arr, q);
            if (pos >= 0) {
                min = arr[pos];
                max = arr[pos];
            } else {
                int ins = -pos - 1;
                if (ins - 1 >= 0) {
                    min = arr[ins - 1];
                }
                if (ins < arr.length) {
                    max = arr[ins];
                }
            }
            ans.add(Arrays.asList(min, max));
        }
        return ans;
    }

    private static TreeNode build(List<Integer> arr) {
        if (arr == null || arr.isEmpty() || arr.get(0) == null) {
            return null;
        }
        TreeNode root = new TreeNode(arr.get(0));
        java.util.Deque<TreeNode> q = new java.util.ArrayDeque<>();
        q.offer(root);
        int i = 1;
        while (!q.isEmpty() && i < arr.size()) {
            TreeNode node = q.poll();
            if (arr.get(i) != null) {
                node.left = new TreeNode(arr.get(i));
                q.offer(node.left);
            }
            i++;
            if (i < arr.size() && arr.get(i) != null) {
                node.right = new TreeNode(arr.get(i));
                q.offer(node.right);
            }
            i++;
        }
        return root;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();

        TreeNode r1 = build(Arrays.asList(6, 2, 13, 1, 4, 9, 15, null, null, null, null, null, null, 14));
        List<List<Integer>> a1 = sol.closestNodes(r1, Arrays.asList(2, 5, 16));
        List<List<Integer>> a1b = sol.closestNodesIter(r1, Arrays.asList(2, 5, 16));
        assert a1.equals(Arrays.asList(Arrays.asList(2, 2), Arrays.asList(4, 6), Arrays.asList(15, -1))) : "2476 例1 主: " + a1;
        assert a1b.equals(Arrays.asList(Arrays.asList(2, 2), Arrays.asList(4, 6), Arrays.asList(15, -1))) : "2476 例1 迭代: " + a1b;

        TreeNode r2 = build(Arrays.asList(4, null, 9));
        List<List<Integer>> a2 = sol.closestNodes(r2, Collections.singletonList(3));
        List<List<Integer>> a2b = sol.closestNodesIter(r2, Collections.singletonList(3));
        assert a2.equals(Arrays.asList(Arrays.asList(-1, 4))) : "2476 例2 主: " + a2;
        assert a2b.equals(Arrays.asList(Arrays.asList(-1, 4))) : "2476 例2 迭代: " + a2b;

        System.out.println("2476 全部 PASS");
    }
}

复杂度分析

  • 时间复杂度:O(n + q log n)
  • 空间复杂度:O(n)

2477. Minimum Fuel Cost to Report to the Capital(到达首都的最少油耗)

题目描述

给你一棵 n 个节点的树(无向、连通、无环图),每个节点表示一个城市,编号从 0 到 n-1,

恰好有 n-1 条路。0 是首都。roads[i] = [ai, bi] 表示城市 ai 和 bi 之间有一条双向路。

每个城市里有一个代表,他们都要去首都参加一个会议。每座城市里有一辆车,

给你一个整数 seats 表示每辆车里面座位的数目。

相邻城市之间一辆车的油耗是一升汽油。返回到达首都最少需要多少升汽油。

示例 1

复制代码
`roads = [[0,1],[0,2],[0,3]], seats = 5` → 3

示例 2

复制代码
`roads = [[3,1],[3,2],[1,0],[0,4],[0,5],[4,6]], seats = 2` → 7

示例 3

复制代码
`roads = [], seats = 1` → 0

提示:

  • 1 <= n <= 10^5,roads.length == n - 1
  • 1 <= seats <= 10^5

解题思路

解法一:后序 DFS 统计子树人数(主提交版本)

  • 以 0 为根,每辆车从某个子树方向驶向首都时,车上应尽量坐满。
  • 对每个子节点 u(相对父节点 p),子树 u 内有 cnt[u] 个人需要经过边 (p,u),
    至少需要 ceil(cnt[u] / seats) 辆车,即该边贡献 ceil(cnt[u] / seats) 升油。
  • 后序遍历累加子树人数并计算答案。
    解法二:按层 BFS 反向计算(对照)
  • 从叶子向根逐层累加人数,每层边贡献 ceil(cnt / seats),结果一致。
    复杂度:
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Solution {
    private long ans = 0;

    // 解法一:后序 DFS(主提交版本)
    public long minimumFuelCost(int[][] roads, int seats) {
        int n = roads.length + 1;
        List<Integer>[] g = new List[n];
        for (int i = 0; i < n; i++) {
            g[i] = new ArrayList<>();
        }
        for (int[] r : roads) {
            g[r[0]].add(r[1]);
            g[r[1]].add(r[0]);
        }
        ans = 0;
        dfs(0, -1, g, seats);
        return ans;
    }

    private long dfs(int u, int parent, List<Integer>[] g, int seats) {
        long cnt = 1; // 本节点代表
        for (int v : g[u]) {
            if (v == parent) {
                continue;
            }
            long sub = dfs(v, u, g, seats);
            ans += (sub + seats - 1) / seats; // 子节点 v 这棵子树过这条边需要的车次数
            cnt += sub;
        }
        return cnt;
    }

    // 解法二:按「度 1 剥离」的 BFS/拓扑式(对照)
    public long minimumFuelCostTopo(int[][] roads, int seats) {
        int n = roads.length + 1;
        if (n == 1) {
            return 0;
        }
        List<Integer>[] g = new List[n];
        int[] deg = new int[n];
        for (int i = 0; i < n; i++) {
            g[i] = new ArrayList<>();
        }
        for (int[] r : roads) {
            g[r[0]].add(r[1]);
            g[r[1]].add(r[0]);
            deg[r[0]]++;
            deg[r[1]]++;
        }
        long[] cnt = new long[n];
        Arrays.fill(cnt, 1);
        java.util.Deque<Integer> q = new java.util.ArrayDeque<>();
        for (int i = 1; i < n; i++) {
            if (deg[i] == 1) {
                q.offer(i);
            }
        }
        long ans2 = 0;
        boolean[] vis = new boolean[n];
        vis[0] = true;
        while (!q.isEmpty()) {
            int u = q.poll();
            if (vis[u]) {
                continue;
            }
            vis[u] = true;
            ans2 += (cnt[u] + seats - 1) / seats;
            for (int v : g[u]) {
                if (vis[v]) {
                    continue;
                }
                cnt[v] += cnt[u];
                if (--deg[v] == 1 && v != 0) {
                    q.offer(v);
                }
            }
        }
        return ans2;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();

        assert sol.minimumFuelCost(new int[][]{{0, 1}, {0, 2}, {0, 3}}, 5) == 3 : "2477 例1 主";
        assert sol.minimumFuelCostTopo(new int[][]{{0, 1}, {0, 2}, {0, 3}}, 5) == 3 : "2477 例1 拓扑";

        assert sol.minimumFuelCost(new int[][]{{3, 1}, {3, 2}, {1, 0}, {0, 4}, {0, 5}, {4, 6}}, 2) == 7 : "2477 例2 主";
        assert sol.minimumFuelCostTopo(new int[][]{{3, 1}, {3, 2}, {1, 0}, {0, 4}, {0, 5}, {4, 6}}, 2) == 7 : "2477 例2 拓扑";

        assert sol.minimumFuelCost(new int[][]{}, 1) == 0 : "2477 例3 主";
        assert sol.minimumFuelCostTopo(new int[][]{}, 1) == 0 : "2477 例3 拓扑";

        // 随机对拍
        java.util.Random rnd = new java.util.Random(3);
        for (int t = 0; t < 200; t++) {
            int n = 1 + rnd.nextInt(20);
            List<Integer> parent = new ArrayList<>();
            int[][] roads = new int[Math.max(0, n - 1)][2];
            for (int i = 1; i < n; i++) {
                int p = rnd.nextInt(i);
                parent.add(p);
                roads[i - 1][0] = p;
                roads[i - 1][1] = i;
            }
            int seats = 1 + rnd.nextInt(8);
            long a = sol.minimumFuelCost(roads, seats);
            long b = sol.minimumFuelCostTopo(roads, seats);
            if (a != b) {
                System.out.println("对拍失败 n=" + n + " roads=" + Arrays.deepToString(roads) + " seats=" + seats + " 主=" + a + " 拓扑=" + b);
                System.exit(1);
            }
        }
        System.out.println("2477 全部 PASS");
    }
}

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

2478. Number of Beautiful Partitions(完美分割的方案数)

题目描述

给你一个字符串 s,每个字符是数字 '1' 到 '9',再给你两个整数 k 和 minLength。

如果对 s 的分割满足以下条件,那么它被称为一个完美分割:

  • s 被分成 k 段互不相交的子字符串
  • 每个子字符串长度都至少为 minLength
  • 每个子字符串的第一个字符都是一个质数数字,最后一个字符都是一个非质数数字。
    质数数字为 '2'、'3'、'5' 和 '7',剩下的都是非质数数字。
    返回 s 的完美分割数目,对 10^9 + 7 取余。

示例 1

复制代码
`s = "23542185131", k = 3, minLength = 2` → 3

示例 2

复制代码
`s = "23542185131", k = 3, minLength = 3` → 1

示例 3

复制代码
`s = "3312958", k = 3, minLength = 1` → 1

提示:

  • 1 <= k, minLength <= s.length <= 1000
  • s 每个字符都为数字 '1' 到 '9' 之一

解题思路

解法一:分割点 DP + 前缀和优化(主提交版本)

  • 合法分段的边界:段首字符必须是质数数字,段尾字符必须是非质数数字。
  • 定义 dp[i][j]:前 i 个字符分成 j 段的方案数(i 为前缀长度)。
  • 转移:最后一段为 s[t..i-1],需 s[t] 为质数、s[i-1] 非质数、长度 ≥ minLength:
    dp[i][j] = sum(dp[t][j-1]),其中 t <= i - minLength 且 s[t] 为质数。
  • 用前缀和数组 pre[t] = sum(dp[t'][j-1])(只累加 st' 为质数的位置)将内层循环均摊 O(1)。
  • 注意:若第 i-1 位不是非质数,则无法作为段尾,dp 直接为 0,且此前缀和不更新。
    解法二:二重循环朴素 DP(对照)
  • 不优化前缀和,直接枚举 t 累加,结果一致。
    复杂度:
  • 时间复杂度:O(n * k),n = s.length() ≤ 1000
  • 空间复杂度:O(n * k)

代码实现

java 复制代码
public class Solution {
    private static final int MOD = 1_000_000_007;

    private boolean isPrime(char c) {
        return c == '2' || c == '3' || c == '5' || c == '7';
    }

    // 解法一:前缀和优化 DP(主提交版本)
    public int beautifulPartitions(String s, int k, int minLength) {
        int n = s.length();
        // 首尾合法性快速剪枝:首字符必须质数、尾字符必须非质数
        if (!isPrime(s.charAt(0)) || isPrime(s.charAt(n - 1))) {
            return 0;
        }
        int[][] dp = new int[n + 1][k + 1];
        dp[0][0] = 1;
        for (int j = 1; j <= k; j++) {
            // pre[x] = sum_{t=0..x-1 且 s[t] 为质数} dp[t][j-1]
            int[] pre = new int[n + 1];
            for (int i = 1; i <= n; i++) {
                pre[i] = pre[i - 1];
                if (isPrime(s.charAt(i - 1)) && dp[i - 1][j - 1] > 0) {
                    pre[i] = (pre[i] + dp[i - 1][j - 1]) % MOD;
                }
            }
            for (int i = 1; i <= n; i++) {
                // 当前位置作为段尾:s[i-1] 必须非质数
                if (isPrime(s.charAt(i - 1))) {
                    dp[i][j] = 0;
                    continue;
                }
                // 段头 t:t <= i - minLength 且 s[t] 质数,即 dp[i][j] = pre[i - minLength + 1]
                int lim = i - minLength;
                if (lim >= 0) {
                    dp[i][j] = pre[lim + 1];
                } else {
                    dp[i][j] = 0;
                }
            }
        }
        return dp[n][k];
    }

    // 解法二:朴素三重循环(对照,小规模验证用)
    public int beautifulPartitionsBrute(String s, int k, int minLength) {
        int n = s.length();
        if (!isPrime(s.charAt(0)) || isPrime(s.charAt(n - 1))) {
            return 0;
        }
        int[][] dp = new int[n + 1][k + 1];
        dp[0][0] = 1;
        for (int i = 1; i <= n; i++) {
            if (isPrime(s.charAt(i - 1))) {
                continue; // 段尾必须非质数
            }
            for (int j = 1; j <= k; j++) {
                for (int t = 0; t <= i - minLength; t++) {
                    if (t == 0 || isPrime(s.charAt(t))) {
                        dp[i][j] = (dp[i][j] + dp[t][j - 1]) % MOD;
                    }
                }
            }
        }
        return dp[n][k];
    }

    public static void main(String[] args) {
        Solution sol = new Solution();

        assert sol.beautifulPartitions("23542185131", 3, 2) == 3 : "2478 例1 主";
        assert sol.beautifulPartitionsBrute("23542185131", 3, 2) == 3 : "2478 例1 暴力";

        assert sol.beautifulPartitions("23542185131", 3, 3) == 1 : "2478 例2 主";
        assert sol.beautifulPartitionsBrute("23542185131", 3, 3) == 1 : "2478 例2 暴力";

        assert sol.beautifulPartitions("3312958", 3, 1) == 1 : "2478 例3 主";
        assert sol.beautifulPartitionsBrute("3312958", 3, 1) == 1 : "2478 例3 暴力";

        // 随机对拍(多组小规模)
        java.util.Random rnd = new java.util.Random(17);
        for (int t = 0; t < 200; t++) {
            int n = 3 + rnd.nextInt(25);
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < n; i++) {
                sb.append((char) ('1' + rnd.nextInt(9)));
            }
            String s = sb.toString();
            int k = 1 + rnd.nextInt(Math.min(5, n));
            int minL = 1 + rnd.nextInt(Math.min(6, n));
            int a = sol.beautifulPartitions(s, k, minL);
            int b = sol.beautifulPartitionsBrute(s, k, minL);
            if (a != b) {
                System.out.println("对拍失败 s=" + s + " k=" + k + " minL=" + minL + " 主=" + a + " 暴力=" + b);
                System.exit(1);
            }
        }
        System.out.println("2478 全部 PASS");
    }
}

复杂度分析

  • 时间复杂度:O(n * k),n = s.length() ≤ 1000
  • 空间复杂度:O(n * k)

2479. Maximum XOR of Two Non-Overlapping Subtrees(两个不重叠子树的最大异或值)

题目描述

有一个无向树,有 n 个节点,节点标记为从 0 到 n-1。根是节点 0。

edges[i] = [ai, bi] 表示节点 ai 和 bi 之间有一条边。

每个节点有一个相关联的值:values[i] 是第 i 个节点的值。

选择任意两个不重叠的子树,你的分数是这些子树中值的和的逐位异或。

返回能达到的最大分数;如果不可能找到两个不重叠的子树,则返回 0。

节点的子树是由该节点及其所有子节点组成的树。如果两个子树不共享任何公共节点,则它们是不重叠的。

示例 1

复制代码
`n = 6, edges = [[0,1],[0,2],[1,3],[1,4],[2,5]], values = [2,8,3,6,2,5]` → 24(16 XOR 8)

示例 2

复制代码
`n = 3, edges = [[0,1],[1,2]], values = [4,6,1]` → 0(链上子树互相包含,无两棵不重叠子树)

提示:

  • 2 <= n <= 5 * 10^4
  • values[i] <= 10^9,子树和可达 5e13

解题思路

解法一:DFS 子树和 + 增量 Trie(主提交版本)

  • 先 DFS 求出每棵子树的值和 sum[u]。
  • 关键观察:两棵子树不重叠 ⇔ 它们对应的 DFS 序区间完全互不相交
    (一棵的区间整体在另一棵的左边或右边,绝不嵌套包含)。
  • 于是按 DFS 顺序维护一个增量 Trie:
  • 访问节点 u 时,Trie 中恰好包含所有「已完全处理完」(DFS 序在 u 左侧) 的子树根 v,
    这些 v 与 u 必然不重叠;用 sum[u] 查询最大异或。
  • 处理完 u 的整棵子树后再把 sum[u] 插入 Trie,供后续(右侧)节点查询。
  • 通过这一单向增量过程,任意合法对 (u, v) 都会在遍历到两者中较晚者时被统计到。
    解法二:暴力枚举节点对(对照,仅小规模对拍)
  • 先算子树和,再枚举全部节点对 (u, v),用祖先后代关系判断是否互相包含(重叠),取最大异或。
    复杂度:
  • 时间复杂度:O(n log V)(V 为值和上界,位宽取 46)
  • 空间复杂度:O(n log V)

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;

public class Solution {
    private static final int BITS = 46; // 2^46 > 5e13,覆盖子树和上界
    private List<Integer>[] g;
    private long[] sum;
    private Trie trie;
    private long best;

    // 解法一:DFS + 增量 Trie(主提交版本)
    public long maxXor(int n, int[][] edges, int[] values) {
        g = new List[n];
        for (int i = 0; i < n; i++) {
            g[i] = new ArrayList<>();
        }
        for (int[] e : edges) {
            g[e[0]].add(e[1]);
            g[e[1]].add(e[0]);
        }
        sum = new long[n];
        dfsSum(0, -1, values);
        trie = new Trie();
        best = 0;
        solve(0, -1);
        return best;
    }

    // 增量 DFS:查询时 Trie 中只含「已完成的左侧子树根」
    private void solve(int u, int parent) {
        // 此刻 Trie 中所有节点都与 u 不重叠(区间整体在 u 左侧)
        if (u != 0) {
            best = Math.max(best, trie.query(sum[u]));
        }
        for (int v : g[u]) {
            if (v != parent) {
                solve(v, u);
            }
        }
        if (u != 0) {
            trie.insert(sum[u]); // u 子树整体处理完毕,可供给右侧节点
        }
    }

    private void dfsSum(int u, int parent, int[] values) {
        long s = values[u];
        for (int v : g[u]) {
            if (v != parent) {
                dfsSum(v, u, values);
                s += sum[v];
            }
        }
        sum[u] = s;
    }

    private static class Trie {
        Trie zero, one;

        void insert(long x) {
            Trie node = this;
            for (int i = BITS - 1; i >= 0; i--) {
                int b = (int) ((x >> i) & 1);
                if (b == 0) {
                    if (node.zero == null) {
                        node.zero = new Trie();
                    }
                    node = node.zero;
                } else {
                    if (node.one == null) {
                        node.one = new Trie();
                    }
                    node = node.one;
                }
            }
        }

        long query(long x) {
            Trie node = this;
            if (node.zero == null && node.one == null) {
                return 0;
            }
            long bestVal = 0;
            for (int i = BITS - 1; i >= 0; i--) {
                int b = (int) ((x >> i) & 1);
                int want = 1 - b;
                Trie nxt = want == 0 ? node.zero : node.one;
                if (nxt != null) {
                    bestVal |= (1L << i);
                    node = nxt;
                } else {
                    node = want == 0 ? node.one : node.zero;
                }
            }
            return bestVal;
        }
    }

    // 解法二:暴力枚举节点对(对照,仅小规模对拍)
    public long maxXorBrute(int n, int[][] edges, int[] values) {
        List<Integer>[] gg = new List[n];
        for (int i = 0; i < n; i++) {
            gg[i] = new ArrayList<>();
        }
        for (int[] e : edges) {
            gg[e[0]].add(e[1]);
            gg[e[1]].add(e[0]);
        }
        // 父节点 + 拓扑序
        int[] parent = new int[n];
        Arrays.fill(parent, -2);
        parent[0] = -1;
        Deque<Integer> st = new java.util.ArrayDeque<>();
        st.push(0);
        int[] order = new int[n];
        int oi = 0;
        while (!st.isEmpty()) {
            int u = st.pop();
            order[oi++] = u;
            for (int v : gg[u]) {
                if (parent[v] == -2) {
                    parent[v] = u;
                    st.push(v);
                }
            }
        }
        long[] sum2 = new long[n];
        for (int i = n - 1; i >= 0; i--) {
            int u = order[i];
            sum2[u] = values[u];
            for (int v : gg[u]) {
                if (parent[v] == u) {
                    sum2[u] += sum2[v];
                }
            }
        }
        long bst = 0;
        for (int u = 0; u < n; u++) {
            for (int v = u + 1; v < n; v++) {
                if (isAncestor(parent, u, v) || isAncestor(parent, v, u)) {
                    continue; // 一棵包含另一棵 → 重叠
                }
                bst = Math.max(bst, sum2[u] ^ sum2[v]);
            }
        }
        return bst;
    }

    private boolean isAncestor(int[] parent, int anc, int x) {
        while (x != -1) {
            if (x == anc) {
                return true;
            }
            x = parent[x];
        }
        return false;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();

        long r1 = sol.maxXor(6, new int[][]{{0, 1}, {0, 2}, {1, 3}, {1, 4}, {2, 5}}, new int[]{2, 8, 3, 6, 2, 5});
        long r1b = sol.maxXorBrute(6, new int[][]{{0, 1}, {0, 2}, {1, 3}, {1, 4}, {2, 5}}, new int[]{2, 8, 3, 6, 2, 5});
        assert r1 == 24 : "2479 例1 主: " + r1;
        assert r1b == 24 : "2479 例1 暴力: " + r1b;

        long r2 = sol.maxXor(3, new int[][]{{0, 1}, {1, 2}}, new int[]{4, 6, 1});
        long r2b = sol.maxXorBrute(3, new int[][]{{0, 1}, {1, 2}}, new int[]{4, 6, 1});
        assert r2 == 0 : "2479 例2 主: " + r2;
        assert r2b == 0 : "2479 例2 暴力: " + r2b;

        // 随机对拍(小规模随机树)
        java.util.Random rnd = new java.util.Random(29);
        for (int t = 0; t < 150; t++) {
            int n = 3 + rnd.nextInt(10);
            int[][] edges = new int[n - 1][2];
            for (int i = 1; i < n; i++) {
                edges[i - 1][0] = rnd.nextInt(i);
                edges[i - 1][1] = i;
            }
            int[] vs = new int[n];
            for (int i = 0; i < n; i++) {
                vs[i] = 1 + rnd.nextInt(200);
            }
            long a = sol.maxXor(n, edges, vs);
            long b = sol.maxXorBrute(n, edges, vs);
            if (a != b) {
                System.out.println("对拍失败 n=" + n + " edges=" + Arrays.deepToString(edges)
                        + " vals=" + Arrays.toString(vs) + " 主=" + a + " 暴力=" + b);
                System.exit(1);
            }
        }
        System.out.println("2479 全部 PASS");
    }
}

复杂度分析

  • 时间复杂度:O(n log V)(V 为值和上界,位宽取 46)
  • 空间复杂度:O(n log V)

2480. Form a Chemical Bond(形成化学键)

题目类型:数据库(SQL)

题目描述

表: Elements

复制代码
+-------------+---------+
| Column Name | Type    |
+-------------+---------+
| symbol      | varchar |
| type        | enum    |
| electrons   | int     |
+-------------+---------+

symbol 是该表的主键(具有唯一值的列)。该表的每一行包含一个元素的信息。

type 是 ENUM 类型,它的值是 ('Metal', 'Nonmetal', 'Noble') 之一:

  • 如果 type 是 Noble,electrons 是 0。
  • 如果 type 是 Metal,electrons 是这种元素的一个原子所能给出的电子数。
  • 如果 type 是 Nonmetal,electrons 是这种元素的一个原子所需要的电子数。

如果一个元素是 'Metal',另外一个元素是 'Nonmetal',那么它们可以形成键。

编写一个解决方案找出所有可以形成键的元素对。

以任意顺序返回结果表。

示例 1:

复制代码
输入:
Elements 表:
+--------+----------+-----------+
| symbol | type     | electrons |
+--------+----------+-----------+
| He     | Noble    | 0         |
| Na     | Metal    | 1         |
| Ca     | Metal    | 2         |
| La     | Metal    | 3         |
| Cl     | Nonmetal | 1         |
| O      | Nonmetal | 2         |
| N      | Nonmetal | 3         |
+--------+----------+-----------+
输出:
+-------+----------+
| metal | nonmetal |
+-------+----------+
| La    | Cl       |
| Ca    | Cl       |
| Na    | Cl       |
| La    | O        |
| Ca    | O        |
| Na    | O        |
| La    | N        |
| Ca    | N        |
| Na    | N        |
+-------+----------+
解释:
Metal 元素包括 La, Ca, and Na。
Nonmetal 元素包括 Cl, O, and N。
每个 Metal 元素与输出表中的 Nonmeal 元素配对。

解题思路

解法一:笛卡尔积 + 类型过滤(推荐)

  • 从 Elements 中筛出 type = 'Metal' 的元素记为 m,筛出 type = 'Nonmetal' 的元素记为 nmt。
  • 两表做笛卡尔积(CROSS JOIN),结果列别名为 metal、nonmetal。由于 Metal 与 Nonmetal 是互斥的枚举值,任意金属与任意非金属都能形成键,无需再校验电子数。
  • 输出顺序任意,直接 SELECT m.symbol AS metal, nmt.symbol AS nonmetal 即可。

解法二(对照):自连接 + 不等条件

  • 同一张表自连接:FROM Elements e1 JOIN Elements e2 ON e1.type = 'Metal' AND e2.type = 'Nonmetal',效果与解法一一致,写法上强调「配对」语义。

代码实现

sql 复制代码
-- 解法一(推荐):分类过滤 + 笛卡尔积
SELECT m.symbol AS metal, nm.symbol AS nonmetal
FROM Elements m
CROSS JOIN Elements nm
WHERE m.type = 'Metal'
  AND nm.type = 'Nonmetal';
sql 复制代码
-- 解法二(对照):单表自连接配对
SELECT e1.symbol AS metal, e2.symbol AS nonmetal
FROM Elements e1
JOIN Elements e2
  ON e1.type = 'Metal' AND e2.type = 'Nonmetal';

复杂度分析

  • 时间复杂度 :O(M * N),M 为金属元素个数、N 为非金属元素个数;输出行数即是二者乘积(如示例 3 × 3 = 9 行)。
  • 空间复杂度 :O(M * N),结果集大小与配对数量成正比。
相关推荐
文丑颜不良啊1 小时前
LeetCode2441~2460题目解析
java·leetcode
阿屿2 小时前
从零手搓 AI 伴侣(二):好感度通胀危机,与它的「货币政策」
java·人工智能
灿飞2 小时前
Kotlin与Java虚拟机十一:类与属性:构造顺序、backing field 与显式 backing field
java·后端·kotlin
aiks_yuan2 小时前
40亿个QQ号 1G内存怎么去重:Java BitSet 装不下,long 数组位图 512MB 实测 46 秒(JDK 17)
java
文丑颜不良啊2 小时前
LeetCode2521~2540题目解析
java·leetcode
杨凯凡2 小时前
Spring Framework 7 HTTP Interface 实战:声明式 RestClient、超时与测试
java·spring·http
吴名氏.3 小时前
Java转AI Agent教程(第十二周)检索与切分
java·开发语言·人工智能·ai·ai agent
瑞雪兆丰年兮3 小时前
JavaWeb-day10-SpringBootWeb案例
java·开发语言·javaweb·web
Rain的Java大神之路4 小时前
你背的Spring答案,全是5.x时代的:7.0.8源码一翻,80%的博客都过时了
java·后端·面试