LeetCode1501~1520题目解析

LeetCode 1501-1520. 题解合集

文章目录


1501. Countries You Can Safely Invest In(可以放心投资的国家)

题目类型:数据库(SQL)

题目描述

表 Person:

复制代码
+----------------+---------+
| Column Name    | Type    |
+----------------+---------+
| id             | int     |
| name           | varchar |
| phone_number   | varchar |
+----------------+---------+
id 是该表具有唯一值的列.
该表每一行包含一个人的名字和电话号码.
电话号码的格式是:'xxx-yyyyyyy', 其中 xxx 是国家码(3 个字符), yyyyyyy 是电话号码(7 个字符), x 和 y 都表示数字. 同时, 国家码和电话号码都可以包含前导 0.

表 Country:

复制代码
+----------------+---------+
| Column Name    | Type    |
+----------------+---------+
| name           | varchar |
| country_code   | varchar |
+----------------+---------+
country_code 是该表具有唯一值的列.
该表每一行包含国家名和国家码. country_code 的格式是'xxx', x 是数字.

表 Calls:

复制代码
+-------------+------+
| Column Name | Type |
+-------------+------+
| caller_id   | int  |
| callee_id   | int  |
| duration    | int  |
+-------------+------+
该表无主键, 可能包含重复行.
每一行包含呼叫方 id, 被呼叫方 id 和以分钟为单位的通话时长. caller_id != callee_id

一家电信公司想要投资新的国家。该公司想要投资的国家是: 该国的平均通话时长要严格地大于全球平均通话时长。

写一个解决方案, 找到所有该公司可以投资的国家。

返回的结果表 无顺序要求。

结果格式如下例所示。

示例 1:

复制代码
**输入:**
Person 表:
+----+----------+--------------+
| id | name     | phone_number |
+----+----------+--------------+
| 3  | Jonathan | 051-1234567  |
| 12 | Elvis    | 051-7654321  |
| 1  | Moncef   | 212-1234567  |
| 2  | Maroua   | 212-6523651  |
| 7  | Meir     | 972-1234567  |
| 9  | Rachel   | 972-0011100  |
+----+----------+--------------+
Country 表:
+----------+--------------+
| name     | country_code |
+----------+--------------+
| Peru     | 051          |
| Israel   | 972          |
| Morocco  | 212          |
| Germany  | 049          |
| Ethiopia | 251          |
+----------+--------------+
Calls 表:
+-----------+-----------+----------+
| caller_id | callee_id | duration |
+-----------+-----------+----------+
| 1         | 9         | 33       |
| 2         | 9         | 4        |
| 1         | 2         | 59       |
| 3         | 12        | 102      |
| 3         | 12        | 330      |
| 12        | 3         | 5        |
| 7         | 9         | 13       |
| 7         | 1         | 3        |
| 9         | 7         | 1        |
| 1         | 7         | 7        |
+-----------+-----------+----------+
**输出:**
+----------+
| country  |
+----------+
| Peru     |
+----------+
**解释:**
国家 Peru 的平均通话时长是 (102 + 102 + 330 + 330 + 5 + 5) / 6 = 145.666667
国家 Israel 的平均通话时长是 (33 + 4 + 13 + 13 + 3 + 1 + 1 + 7) / 8 = 9.37500
国家 Morocco 的平均通话时长是 (33 + 4 + 59 + 59 + 3 + 7) / 6 = 27.5000
全球平均通话时长 = (2 * (33 + 4 + 59 + 102 + 330 + 5 + 13 + 3 + 1 + 7)) / 20 = 55.70000
所以, Peru 是唯一的平均通话时长大于全球平均通话时长的国家, 也是唯一的推荐投资的国家.

解题思路

思考

题目要求找出平均通话时长严格高于全球平均水平的国家。通话记录只给出双方编号,国家信息藏在电话号码前三位,因此必须把人、通话与国家三张表对齐后才能按国家聚合。

将 P e r s o n Person Person 与 C a l l s Calls Calls 按「主叫或被叫等于该人」连接,再用号码前三位对齐 C o u n t r y Country Country,即可按国家求平均时长,并与 C a l l s Calls Calls 上的全局平均比较。子查询承担全局均值,外层过滤即可得到答案。

考察连接 + 子查询 + 分组聚合:用 LEFT(phone_number,3) 匹配国家码,对国家分组求平均通话时长,再与全表平均比较筛选。

代码实现

sql 复制代码
# Write your MySQL query statement below
SELECT country
FROM
    (
        SELECT c.name AS country, AVG(duration) AS duration
        FROM
            Person
            JOIN Calls ON id IN (caller_id, callee_id)
            JOIN Country AS c ON LEFT(phone_number, 3) = country_code
        GROUP BY 1
    ) AS t
WHERE duration > (SELECT AVG(duration) FROM Calls);

复杂度分析

本题为数据库查询题,不涉及传统算法的时间/空间复杂度,执行效率由数据库优化器决定;建议对查询涉及的过滤列与连接列建立索引以加速。


1502. Can Make Arithmetic Progression From Sequence(判断能否形成等差数列)

题目描述

给你一个数字数组 arr。如果一个数列中,任意相邻两项的差总等于同一个常数,那么这个数列就称为等差数列。

如果可以重新排列数组形成等差数列,请返回 true;否则,返回 false。

示例 1

复制代码
输入 arr = [3,5,1],输出 true(重排为 [1,3,5],相邻差均为 2)。

示例 2

复制代码
输入 arr = [1,2,4],输出 false(无法通过重新排序得到等差数列)。

约束:

  • 2 <= arr.length <= 1000;-10^6 <= arr[i] <= 10^6。

解题思路

解法一:排序 + 检查相邻差(主提交版本)

  • 对数组排序,排序后若为等差数列则任意相邻两项差必相等;
  • 记录第一对相邻元素的差 d,依次校验其余相邻差是否等于 d。
    复杂度:时间复杂度 O(n log n)(排序),空间复杂度 O(log n)(排序栈开销)。
    解法二:打表记录 + 一次遍历(对照)
  • 先求 min 与 max,等差数列公差必然是 (max - min) / (n - 1),若不整除返回 false;
  • 用 boolean 数组记录每个元素是否出现过(值偏移 min 后作为下标),
    再去重后校验每个等差位置上的元素是否都存在。
    复杂度:时间复杂度 O(n + range),空间复杂度 O(range),range 为值域跨度。

代码实现

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

public class Solution {

    public boolean canMakeArithmeticProgressionSort(int[] arr) {
        Arrays.sort(arr);
        int diff = arr[1] - arr[0];
        for (int i = 2; i < arr.length; i++) {
            if (arr[i] - arr[i - 1] != diff) {
                return false;
            }
        }
        return true;
    }

    public boolean canMakeArithmeticProgressionBucket(int[] arr) {
        int n = arr.length;
        int min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;
        for (int v : arr) {
            min = Math.min(min, v);
            max = Math.max(max, v);
        }
        if ((max - min) % (n - 1) != 0) {
            return false;
        }
        int d = (max - min) / (n - 1);
        if (d == 0) {
            return true; // 全部相等
        }
        boolean[] seen = new boolean[max - min + 1];
        for (int v : arr) {
            int idx = v - min;
            if (seen[idx]) {
                return false; // 出现重复值必不可能构成等差数列
            }
            seen[idx] = true;
        }
        for (int i = 0; i < n; i++) {
            if (!seen[i * d]) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 官方示例
        System.out.println(sol.canMakeArithmeticProgressionSort(new int[]{3, 5, 1})); // true
        System.out.println(sol.canMakeArithmeticProgressionSort(new int[]{1, 2, 4})); // false
        System.out.println(sol.canMakeArithmeticProgressionBucket(new int[]{3, 5, 1})); // true
        System.out.println(sol.canMakeArithmeticProgressionBucket(new int[]{1, 2, 4})); // false
        // 边界:两元素、全部相等
        System.out.println(sol.canMakeArithmeticProgressionSort(new int[]{1, 100})); // true
        System.out.println(sol.canMakeArithmeticProgressionBucket(new int[]{5, 5, 5})); // true
    }
}

复杂度分析

复杂度:时间复杂度 O(n log n)(排序),空间复杂度 O(log n)(排序栈开销)。

复杂度:时间复杂度 O(n + range),空间复杂度 O(range),range 为值域跨度。


1503. Last Moment Before All Ants Fall Out of a Plank(所有蚂蚁掉下来前的最后一刻)

题目描述

有一块木板,长度为 n 个单位。一些蚂蚁在木板上移动,每只蚂蚁都以每秒一个单位的速度移动。

其中一部分蚂蚁向左移动,其他蚂蚁向右移动。当两只向不同方向移动的蚂蚁相遇时,它们会同时

改变移动方向并继续移动(改向不耗时)。当蚂蚁在某一时刻 t 到达木板的一端时,它立即从木板

上掉下来。给定整数 n 和两个数组 left、right(分别标识向左/向右移动的蚂蚁在 t=0 时的位置),

返回最后一只蚂蚁从木板上掉下来的时刻。

示例 1

复制代码
n=4, left=[4,3], right=[0,1],输出 4。

示例 2

复制代码
n=7, left=[], right=[0..7],输出 7(全部向右)。

约束:

  • 1 <= n <= 10^4;任意位置唯一且只出现在 left/right 之一。

解题思路

解法一:等价"穿过"思想(主提交版本)

  • 蚂蚁相遇后同时掉头,等价于它们互相穿过彼此继续沿原方向前进(整体效果相同);
  • 因此最后一只蚂蚁掉落的时刻 = max(向左蚂蚁中离左端最远的距离, 向右蚂蚁中离右端最远的距离)
    = max(max(left), n - min(right))。
    复杂度:时间复杂度 O(|left| + |right|),空间复杂度 O(1)。
    解法二:按位置扫描(对照)
  • 若 left/right 均有序,可双指针合并;此处用同一思路但先排序两个数组再取两端最值,
    便于理解"最晚掉落"的几何含义。
    复杂度:时间复杂度 O(n log n),空间复杂度 O(log n)。

代码实现

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

public class Solution {

    public int getLastMomentOnePass(int n, int[] left, int[] right) {
        int ans = 0;
        for (int pos : left) {
            ans = Math.max(ans, pos); // 向左走需要 pos 秒到达左端
        }
        for (int pos : right) {
            ans = Math.max(ans, n - pos); // 向右走需要 n-pos 秒到达右端
        }
        return ans;
    }

    public int getLastMomentSort(int n, int[] left, int[] right) {
        if (left.length == 0 && right.length == 0) {
            return 0;
        }
        Arrays.sort(left);
        Arrays.sort(right);
        int ans = 0;
        if (left.length > 0) {
            ans = Math.max(ans, left[left.length - 1]);
        }
        if (right.length > 0) {
            ans = Math.max(ans, n - right[0]);
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 官方示例
        System.out.println(sol.getLastMomentOnePass(4, new int[]{4, 3}, new int[]{0, 1})); // 4
        System.out.println(sol.getLastMomentOnePass(7, new int[]{}, new int[]{0, 1, 2, 3, 4, 5, 6, 7})); // 7
        System.out.println(sol.getLastMomentOnePass(7, new int[]{0, 1, 2, 3, 4, 5, 6, 7}, new int[]{})); // 7
        System.out.println(sol.getLastMomentOnePass(4, new int[]{4, 3}, new int[]{0, 1})); // 4
        // 对照解法
        System.out.println(sol.getLastMomentSort(4, new int[]{4, 3}, new int[]{0, 1})); // 4
        System.out.println(sol.getLastMomentSort(7, new int[]{}, new int[]{0, 1, 2, 3, 4, 5, 6, 7})); // 7
    }
}

复杂度分析

复杂度:时间复杂度 O(|left| + |right|),空间复杂度 O(1)。

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


1504. Count Submatrices With All Ones(统计全 1 子矩形)

题目描述

给你一个 m x n 的二进制矩阵 mat,请你返回有多少个子矩形的元素全部都是 1。

示例 1

复制代码
mat = [[1,0,1],[1,1,0],[1,1,0]],输出 13。

示例 2

复制代码
mat = [[0,1,1,0],[0,1,1,1],[1,1,1,0]],输出 24。

约束:

  • 1 <= m, n <= 150;mat[i][j] 仅包含 0 或 1。

解题思路

解法一:逐行连续 1 + 列方向扩张(主提交版本)

  • 预处理 upij:从 (i,j) 向上连续 1 的个数(以 (i,j) 为右下角的矩形高度上限);
  • 枚举右下角 (i,j),向左枚举左边界 k(0...j),维护该矩形区域内的最小高度 h,
    则以 (i,j) 为右下角、上边界不低于当前行的所有全 1 矩形数量累加 h。
    复杂度:时间复杂度 O(m * n^2),空间复杂度 O(n)(滚动数组)。
    解法二:三重枚举暴力(对照)
  • 枚举矩形的左上角与右下角,利用二维前缀和 O(1) 判断矩形内是否全 1。
    复杂度:时间复杂度 O(m^2 * n^2),空间复杂度 O(mn)。

代码实现

java 复制代码
public class Solution {

    public int numSubmat(int[][] mat) {
        int m = mat.length, n = mat[0].length;
        int[] height = new int[n]; // height[j] = 当前行上方连续 1 的长度
        int ans = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                height[j] = mat[i][j] == 1 ? height[j] + 1 : 0;
            }
            // 以 (i,j) 为右下角
            for (int j = 0; j < n; j++) {
                int minH = Integer.MAX_VALUE;
                for (int k = j; k >= 0 && height[k] > 0; k--) {
                    minH = Math.min(minH, height[k]);
                    ans += minH;
                }
            }
        }
        return ans;
    }

    public int numSubmatPrefix(int[][] mat) {
        int m = mat.length, n = mat[0].length;
        int[][] pre = new int[m + 1][n + 1];
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                pre[i][j] = pre[i - 1][j] + pre[i][j - 1] - pre[i - 1][j - 1] + mat[i - 1][j - 1];
            }
        }
        int ans = 0;
        for (int r1 = 0; r1 < m; r1++) {
            for (int c1 = 0; c1 < n; c1++) {
                for (int r2 = r1; r2 < m; r2++) {
                    for (int c2 = c1; c2 < n; c2++) {
                        int sum = pre[r2 + 1][c2 + 1] - pre[r1][c2 + 1] - pre[r2 + 1][c1] + pre[r1][c1];
                        if (sum == (r2 - r1 + 1) * (c2 - c1 + 1)) {
                            ans++;
                        }
                    }
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][] mat1 = {{1, 0, 1}, {1, 1, 0}, {1, 1, 0}};
        int[][] mat2 = {{0, 1, 1, 0}, {0, 1, 1, 1}, {1, 1, 1, 0}};
        System.out.println(sol.numSubmat(mat1)); // 13
        System.out.println(sol.numSubmat(mat2)); // 24
        System.out.println(sol.numSubmatPrefix(mat1)); // 13
        System.out.println(sol.numSubmatPrefix(mat2)); // 24
        System.out.println(sol.numSubmat(new int[][]{{1}})); // 1
    }
}

复杂度分析

复杂度:时间复杂度 O(m * n^2),空间复杂度 O(n)(滚动数组)。

复杂度:时间复杂度 O(m^2 * n^2),空间复杂度 O(mn)。


1505. Minimum Possible Integer After at Most K Adjacent Swaps On Digits(最多 K 次交换相邻数位后得到的最小整数)

题目描述

给定一个字符串 num 表示非常大的整数和一个整数 k。你可以交换这个整数相邻数位的数字最多 k 次。

请你返回你能得到的最小整数,并以字符串形式返回。输出可以包含前导 0。

示例 1

复制代码
num = "4321", k = 4,输出 "1342"。

示例 2

复制代码
num = "100", k = 1,输出 "010"。

示例 3

复制代码
num = "36789", k = 1000,输出 "36789"。

约束:

  • 1 <= num.length <= 30000;num 只包含数字且不含前导 0;0 <= k <= 10^9。

解题思路

解法一:贪心 + 队列记录各数字位置(主提交版本)

  • 从左到右处理每一位:在剩余可用的 k 次交换内,贪心寻找当前位置之后的最小数字;
  • 用 10 个队列(0~9)分别记录每个数字出现的位置下标,选取当前位时优先尝试数字 d 从小到大,
    需要的交换次数 = 该数字当前位置 - 已移动累计偏移(用树状数组统计前面被取走的个数);
  • 取走某个数字后把该位置从队列弹出,并将该位置标记为已使用。
    复杂度:时间复杂度 O(n log n),空间复杂度 O(n)。
    解法二:线性暴力模拟(对照)
  • 每次从当前位置向右扫描 k+1 个字符内找最小数字(数据量小或 k 小可接受),前移并消耗相应交换次数;
  • 实现直观,适合理解贪心的正确性。
    复杂度:时间复杂度 O(n^2) 最坏,空间复杂度 O(n)。

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.Queue;

public class Solution {

    // 树状数组:统计 [1..i] 上被标记(已取走)的个数
    private int n;
    private int[] bit;

    private void add(int i, int v) {
        for (; i <= n; i += i & -i) {
            bit[i] += v;
        }
    }

    private int sum(int i) {
        int s = 0;
        for (; i > 0; i -= i & -i) {
            s += bit[i];
        }
        return s;
    }

    public String minIntegerFenwick(String num, int k) {
        n = num.length();
        bit = new int[n + 1];
        @SuppressWarnings("unchecked")
        Queue<Integer>[] pos = new Queue[10];
        for (int d = 0; d < 10; d++) {
            pos[d] = new ArrayDeque<>();
        }
        for (int i = 0; i < n; i++) {
            pos[num.charAt(i) - '0'].offer(i);
        }
        StringBuilder sb = new StringBuilder();
        int used = 0; // 已取走(即结果前缀)的位数
        while (used < n) {
            for (int d = 0; d < 10; d++) {
                if (pos[d].isEmpty()) {
                    continue;
                }
                int idx = pos[d].peek();
                // 前面被取走的个数 = sum(idx+1),当前实际位置 = idx - 已被取走个数
                int moved = idx - sum(idx + 1);
                if (moved <= k) {
                    sb.append((char) ('0' + d));
                    pos[d].poll();
                    add(idx + 1, 1);
                    k -= moved;
                    break;
                }
            }
            used++;
        }
        return sb.toString();
    }

    public String minIntegerBrute(String num, int k) {
        char[] arr = num.toCharArray();
        int n = arr.length;
        for (int i = 0; i < n && k > 0; i++) {
            int best = i;
            // 在剩余交换次数内找最小字符
            for (int j = i + 1; j < n && j - i <= k; j++) {
                if (arr[j] < arr[best]) {
                    best = j;
                }
            }
            if (best != i) {
                char c = arr[best];
                for (int j = best; j > i; j--) {
                    arr[j] = arr[j - 1];
                }
                arr[i] = c;
                k -= (best - i);
            }
        }
        return new String(arr);
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 官方示例
        System.out.println(sol.minIntegerFenwick("4321", 4)); // 1342
        System.out.println(sol.minIntegerFenwick("100", 1)); // 010
        System.out.println(sol.minIntegerFenwick("36789", 1000)); // 36789
        System.out.println(sol.minIntegerBrute("4321", 4)); // 1342
        System.out.println(sol.minIntegerBrute("100", 1)); // 010
        System.out.println(sol.minIntegerBrute("36789", 1000)); // 36789
        System.out.println(sol.minIntegerFenwick("9438957234785635408", 23)); // 0345989723478563548
    }
}

复杂度分析

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

复杂度:时间复杂度 O(n^2) 最坏,空间复杂度 O(n)。


1506. Find Root of N-Ary Tree(找到 N 叉树的根节点)

题目描述

给定一棵 N 叉树的所有节点在一个数组 Node[] tree 中,树中每个节点都有唯一的值。

找到并返回 N 叉树的根节点。

测试方式:驱动程序根据序列化输入构造树,以任意顺序将每个 Node 对象放入一个数组,

传给 findRoot。函数需要在数组中查找并返回根 Node 对象,驱动会把返回值序列化并与输入比较。

解题思路

解法一:父节点标记法(主提交版本)

  • N 叉树的根节点没有父节点,其余每个节点都恰好是某个节点的子节点;
  • 用一个 HashSet 记录所有"作为他人的子节点出现"的节点,遍历所有节点,
    若某节点不在该集合中,即为根。
    复杂度:时间复杂度 O(N),空间复杂度 O(N)。
    解法二:累加值求差(对照)
  • 每个节点的值唯一。设 sumAll = 所有节点值之和,sumChild = 所有子节点(作为孩子出现一次)值之和,
    则 根节点值 = sumAll - sumChild;再用一次遍历找到值等于该值的节点。
    复杂度:时间复杂度 O(N),空间复杂度 O(1)。

代码实现

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

public class Solution {

    // N 叉树节点定义(与 LeetCode 一致)
    static class Node {
        public int val;
        public List<Node> children;

        public Node() {
            children = new java.util.ArrayList<>();
        }

        public Node(int val) {
            this.val = val;
            children = new java.util.ArrayList<>();
        }

        public Node(int val, List<Node> children) {
            this.val = val;
            this.children = children;
        }
    }

    public Node findRootSet(List<Node> tree) {
        Set<Node> seen = new HashSet<>();
        for (Node node : tree) {
            for (Node child : node.children) {
                seen.add(child);
            }
        }
        for (Node node : tree) {
            if (!seen.contains(node)) {
                return node;
            }
        }
        return null;
    }

    public Node findRootDiff(List<Node> tree) {
        long sumAll = 0, sumChild = 0;
        for (Node node : tree) {
            sumAll += node.val;
            for (Node child : node.children) {
                sumChild += child.val;
            }
        }
        long rootVal = sumAll - sumChild;
        for (Node node : tree) {
            if (node.val == rootVal) {
                return node;
            }
        }
        return null;
    }

    public static void main(String[] args) {
        // 构造示例树:1 -> [2, 3, 4]
        Node n2 = new Node(2);
        Node n3 = new Node(3);
        Node n4 = new Node(4);
        Node root = new Node(1);
        root.children.add(n2);
        root.children.add(n3);
        root.children.add(n4);

        java.util.List<Node> tree = new java.util.ArrayList<>();
        tree.add(n4);
        tree.add(root);
        tree.add(n2);
        tree.add(n3);

        Solution sol = new Solution();
        System.out.println(sol.findRootSet(tree).val); // 1
        System.out.println(sol.findRootDiff(tree).val); // 1
    }
}

复杂度分析

复杂度:时间复杂度 O(N),空间复杂度 O(N)。

复杂度:时间复杂度 O(N),空间复杂度 O(1)。


1507. Reformat Date(转变日期格式)

题目描述

给你一个字符串 date,它的格式为 "Day Month Year",其中:

  • Day 是 {"1st","2nd","3rd","4th",...,"30th","31st"} 中的一个元素;
  • Month 是 {"Jan","Feb",...,"Dec"} 中的一个元素;
  • Year 的范围在 1900, 2100 之间。
    请你将字符串转变为 YYYY-MM-DD 的格式(YYYY 4 位年份、MM 2 位月份、DD 2 位天数)。

示例 1

复制代码
date = "20th Oct 2052",输出 "2052-10-20"。

示例 2

复制代码
date = "26th May 1960",输出 "1960-05-26"。

约束:

  • 给定日期保证合法,不需要处理异常输入。

解题思路

解法一:字符串拆分 + 月份映射(主提交版本)

  • 按空格拆出 day/month/year 三部分;
  • day 去掉末尾两位序数后缀(st/nd/rd/th),转为整数后补零;month 查映射表得到两位数字。
    复杂度:时间复杂度 O(1)(字符串长度固定),空间复杂度 O(1)。
    解法二:正则解析(对照)
  • 直接用正则 (\d+)(st|nd|rd|th) 提取天数,配合月份数组与年份截取完成格式化。
    复杂度:时间复杂度 O(1),空间复杂度 O(1)。

代码实现

java 复制代码
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Solution {

    private static final String[] MONTHS = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
            "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};

    public String reformatDateSplit(String date) {
        String[] parts = date.split(" ");
        String dayStr = parts[0].replaceAll("(st|nd|rd|th)$", "");
        int day = Integer.parseInt(dayStr);
        String month = parts[1];
        String year = parts[2];
        int mm = 0;
        for (int i = 0; i < MONTHS.length; i++) {
            if (MONTHS[i].equals(month)) {
                mm = i + 1;
                break;
            }
        }
        return String.format("%s-%02d-%02d", year, mm, day);
    }

    public String reformatDateRegex(String date) {
        Matcher m = Pattern.compile("(\\d+)(st|nd|rd|th) (\\w+) (\\d+)").matcher(date);
        if (m.find()) {
            int day = Integer.parseInt(m.group(1));
            String mon = m.group(3);
            int mm = 0;
            for (int i = 0; i < MONTHS.length; i++) {
                if (MONTHS[i].equals(mon)) {
                    mm = i + 1;
                    break;
                }
            }
            return String.format("%s-%02d-%02d", m.group(4), mm, day);
        }
        return "";
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.reformatDateSplit("20th Oct 2052")); // 2052-10-20
        System.out.println(sol.reformatDateSplit("6th Jun 1933")); // 1933-06-06
        System.out.println(sol.reformatDateSplit("26th May 1960")); // 1960-05-26
        System.out.println(sol.reformatDateRegex("20th Oct 2052")); // 2052-10-20
        System.out.println(sol.reformatDateRegex("6th Jun 1933")); // 1933-06-06
        System.out.println(sol.reformatDateRegex("26th May 1960")); // 1960-05-26
    }
}

复杂度分析

复杂度:时间复杂度 O(1)(字符串长度固定),空间复杂度 O(1)。

复杂度:时间复杂度 O(1),空间复杂度 O(1)。


1508. Range Sum of Sorted Subarray Sums(子数组和排序后的区间和)

题目描述

给你一个数组 nums,它包含 n 个正整数。计算所有非空连续子数组的和并升序排序,

得到新数组(共 n*(n+1)/2 个元素)。返回新数组中下标 left 到 right(下标从 1 开始)的所有数字和。

答案对 10^9 + 7 取模。

示例 1

复制代码
nums=[1,2,3,4], n=4, left=1, right=5,输出 13(排序后前 5 个为 1,2,3,3,4,和 13)。

示例 3

复制代码
nums=[1,2,3,4], n=4, left=1, right=10,输出 50。

约束:

  • 1 <= n <= 1000;1 <= nums[i] <= 100。

解题思路

解法一:枚举全部子数组和 + 排序(主提交版本)

  • 双重循环枚举所有子数组,收集其和到 list;
  • 排序后对 left-1, right-1 求和取模。
    复杂度:时间复杂度 O(n^2 log n),空间复杂度 O(n^2)。
    解法二:优先队列小顶堆生成第 k 小(对照)
  • 以每个下标 i 为起点的子数组和的最小值入堆,每次弹出当前最小并扩展下一个(+numsj+1);
  • 依次取第 left...right 个最小子数组和累加。
    复杂度:时间复杂度 O(right * log n),空间复杂度 O(n)。

代码实现

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

public class Solution {

    private static final int MOD = 1_000_000_007;

    public int rangeSumSort(int[] nums, int n, int left, int right) {
        List<Integer> sums = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            int s = 0;
            for (int j = i; j < n; j++) {
                s += nums[j];
                sums.add(s);
            }
        }
        int[] arr = new int[sums.size()];
        for (int i = 0; i < arr.length; i++) {
            arr[i] = sums.get(i);
        }
        Arrays.sort(arr);
        long ans = 0;
        for (int i = left - 1; i < right; i++) {
            ans = (ans + arr[i]) % MOD;
        }
        return (int) ans;
    }

    public int rangeSumHeap(int[] nums, int n, int left, int right) {
        // int[]{和, 起始下标, 当前末尾下标}
        PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
        for (int i = 0; i < n; i++) {
            pq.offer(new int[]{nums[i], i, i});
        }
        long ans = 0;
        for (int cnt = 1; cnt <= right; cnt++) {
            int[] cur = pq.poll();
            if (cnt >= left) {
                ans = (ans + cur[0]) % MOD;
            }
            int start = cur[1], end = cur[2];
            if (end + 1 < n) {
                pq.offer(new int[]{cur[0] + nums[end + 1], start, end + 1});
            }
        }
        return (int) ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.rangeSumSort(new int[]{1, 2, 3, 4}, 4, 1, 5)); // 13
        System.out.println(sol.rangeSumSort(new int[]{1, 2, 3, 4}, 4, 3, 4)); // 6
        System.out.println(sol.rangeSumSort(new int[]{1, 2, 3, 4}, 4, 1, 10)); // 50
        System.out.println(sol.rangeSumHeap(new int[]{1, 2, 3, 4}, 4, 1, 5)); // 13
        System.out.println(sol.rangeSumHeap(new int[]{1, 2, 3, 4}, 4, 3, 4)); // 6
        System.out.println(sol.rangeSumHeap(new int[]{1, 2, 3, 4}, 4, 1, 10)); // 50
    }
}

复杂度分析

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

复杂度:时间复杂度 O(right * log n),空间复杂度 O(n)。


1509. Minimum Difference Between Largest and Smallest Value in Three Moves(三次操作后最大值与最小值的最小差)

题目描述

给你一个数组 nums。每次操作你可以选择 nums 中的任意一个元素并将它改成任意值。

在执行最多三次操作后,返回 nums 中最大值与最小值的最小差值。

示例 1

复制代码
nums=[5,3,2,4],输出 0(三步全部改为 3)。

示例 2

复制代码
nums=[1,5,0,10,14],输出 1。

示例 3

复制代码
nums=[3,100,20],输出 0。

约束:

  • 1 <= nums.length <= 105;-109 <= nums[i] <= 10^9。

解题思路

解法一:排序 + 滑窗比较(主提交版本)

  • 最多 3 次操作等价于:从排序后的数组中,最多移除 3 个最小值或 3 个最大值;
  • 枚举移除组合:只考虑保留的连续子数组,其长度至少 n-3;
    比较 4 种取法(删 i 个最小、删 3-i 个最大,i=0...3)的 max-min 取最小。
    复杂度:时间复杂度 O(n log n),空间复杂度 O(log n)。
    解法二:长度限定最小差值(对照)
  • 若 n <= 4 直接返回 0;
  • 排序后只比较窗口大小为 n-4+1 时(即去掉 3 个后保留部分)的连续段端点差,
    逐个窗口取最小即可(与滑窗同一思路,写成显式循环便于阅读)。
    复杂度:时间复杂度 O(n log n),空间复杂度 O(log n)。

代码实现

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

public class Solution {

    public int minDifferenceFour(int[] nums) {
        int n = nums.length;
        if (n <= 4) {
            return 0;
        }
        Arrays.sort(nums);
        int ans = Integer.MAX_VALUE;
        // 删 i 个最小、删 3-i 个最大
        for (int i = 0; i <= 3; i++) {
            ans = Math.min(ans, nums[n - 1 - (3 - i)] - nums[i]);
        }
        return ans;
    }

    public int minDifferenceWindow(int[] nums) {
        int n = nums.length;
        if (n <= 4) {
            return 0;
        }
        Arrays.sort(nums);
        int ans = Integer.MAX_VALUE;
        // 保留连续窗口:起点 left,终点 left + (n - 4) - 1(窗口内 n-4 个元素)
        for (int left = 0; left <= 3; left++) {
            int right = left + (n - 4) - 1;
            ans = Math.min(ans, nums[right] - nums[left]);
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.minDifferenceFour(new int[]{5, 3, 2, 4})); // 0
        System.out.println(sol.minDifferenceFour(new int[]{1, 5, 0, 10, 14})); // 1
        System.out.println(sol.minDifferenceFour(new int[]{3, 100, 20})); // 0
        System.out.println(sol.minDifferenceWindow(new int[]{5, 3, 2, 4})); // 0
        System.out.println(sol.minDifferenceWindow(new int[]{1, 5, 0, 10, 14})); // 1
        System.out.println(sol.minDifferenceWindow(new int[]{3, 100, 20})); // 0
        System.out.println(sol.minDifferenceFour(new int[]{1})); // 0
        System.out.println(sol.minDifferenceFour(new int[]{6, 6, 0, 1, 1, 4, 6})); // 1
    }
}

复杂度分析

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


1510. Stone Game IV(石子游戏 IV)

题目描述

Alice 和 Bob 轮流玩一个游戏,Alice 先手。一开始有 n 个石子堆在一起,每个人轮流操作,

正在操作的玩家可以从石子堆里拿走任意非零平方数个石子。如果石子堆里没有石子了,

则无法操作的玩家输掉游戏。给定正整数 n,两人都采取最优策略,若 Alice 获胜返回 true。

示例 1

复制代码
n=1,输出 true(Alice 拿走 1 个直接获胜)。

示例 2

复制代码
n=2,输出 false。

示例 4

复制代码
n=7,输出 false。

约束:

  • 1 <= n <= 10^5。

解题思路

解法一:记忆化搜索博弈 DP(主提交版本)

  • dpi 表示当前面对 i 个石子的人能否获胜;
  • 转移:枚举可取的平方数 x (x*x <= i),若存在某个 x 使 dpi - x* x == false,
    则当前玩家可以转移到必败态获胜;
  • 用缓存数组记录计算结果避免重复。
    复杂度:时间复杂度 O(n * sqrt(n)),空间复杂度 O(n)。
    解法二:自底向上递推(对照)
  • 与解法一逻辑完全一致,但用循环从小到大计算布尔数组 win\[\];
  • 对每个 i 枚举平方数,只要存在一个转移到输态,wini = true。
    复杂度:时间复杂度 O(n * sqrt(n)),空间复杂度 O(n)。

代码实现

java 复制代码
public class Solution {

    private int[] memo;

    public boolean winnerSquareGameMemo(int n) {
        memo = new int[n + 1]; // 0 未算,1 胜,-1 负
        return canWin(n);
    }

    private boolean canWin(int i) {
        if (memo[i] != 0) {
            return memo[i] == 1;
        }
        boolean win = false;
        for (int x = 1; x * x <= i; x++) {
            if (!canWin(i - x * x)) {
                win = true;
                break;
            }
        }
        memo[i] = win ? 1 : -1;
        return win;
    }

    public boolean winnerSquareGameDp(int n) {
        boolean[] win = new boolean[n + 1];
        for (int i = 1; i <= n; i++) {
            for (int x = 1; x * x <= i; x++) {
                if (!win[i - x * x]) {
                    win[i] = true;
                    break;
                }
            }
        }
        return win[n];
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[] tests = {1, 2, 4, 7, 17};
        for (int t : tests) {
            System.out.println(t + " -> " + sol.winnerSquareGameMemo(t));
        }
        for (int t : tests) {
            System.out.println("dp " + t + " -> " + sol.winnerSquareGameDp(t));
        }
        // 1:true 2:false 4:true 7:false 17:false
    }
}

复杂度分析

复杂度:时间复杂度 O(n * sqrt(n)),空间复杂度 O(n)。


1511. Customer Order Frequency(消费者下单频率)

题目类型:数据库(SQL)

题目描述

表: Customers

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| customer_id   | int     |
| name          | varchar |
| country       | varchar |
+---------------+---------+
customer_id 是该表具有唯一值的列.
该表包含公司消费者的信息.

表: Product

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| product_id    | int     |
| description   | varchar |
| price         | int     |
+---------------+---------+
product_id 是该表具有唯一值的列.
该表包含公司产品的信息.
price 是该产品所需的花销.

表: Orders

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| order_id      | int     |
| customer_id   | int     |
| product_id    | int     |
| order_date    | date    |
| quantity      | int     |
+---------------+---------+
order_id 是该表具有唯一值的列.
该表包含消费者下单的信息.
customer_id 是买了数量为 "quantity", id 为 "product_id" 产品的消费者的 id.
Order_date 是订单发货的日期, 格式为('YYYY-MM-DD').

写一个解决方案,报告在 **2020 年 6 月和 7 月 **每个月至少花费 $100 的客户的 customer_id 和 customer_name 。

以 **任意顺序 **返回结果表.

结果格式如下例所示。

示例 1:

复制代码
**输入:**
`Customers table:`
+--------------+-----------+-------------+
| customer_id  | name      | country     |
+--------------+-----------+-------------+
| 1            | Winston   | USA         |
| 2            | Jonathan  | Peru        |
| 3            | Moustafa  | Egypt       |
+--------------+-----------+-------------+
`Product table:`
+--------------+-------------+-------------+
| product_id   | description | price       |
+--------------+-------------+-------------+
| 10           | LC Phone    | 300         |
| 20           | LC T-Shirt  | 10          |
| 30           | LC Book     | 45          |
| 40           | LC Keychain | 2           |
+--------------+-------------+-------------+
`Orders table:`
+--------------+-------------+-------------+-------------+-----------+
| order_id     | customer_id | product_id  | order_date  | quantity  |
+--------------+-------------+-------------+-------------+-----------+
| 1            | 1           | 10          | 2020-06-10  | 1         |
| 2            | 1           | 20          | 2020-07-01  | 1         |
| 3            | 1           | 30          | 2020-07-08  | 2         |
| 4            | 2           | 10          | 2020-06-15  | 2         |
| 5            | 2           | 40          | 2020-07-01  | 10        |
| 6            | 3           | 20          | 2020-06-24  | 2         |
| 7            | 3           | 30          | 2020-06-25  | 2         |
| 9            | 3           | 30          | 2020-05-08  | 3         |
+--------------+-------------+-------------+-------------+-----------+
**输出:**
+--------------+------------+
| customer_id  | name       |
+--------------+------------+
| 1            | Winston    |
+--------------+------------+
**解释:**
Winston 在 2020 年 6 月花费了 $300(300 * 1), 在 7 月花费了 $100(10 * 1 + 45 * 2).
Jonathan 在 2020 年 6 月花费了 $600(300 * 2), 在 7 月花费了 $20(2 * 10).
Moustafa 在 2020 年 6 月花费了 $110 (10 * 2 + 45 * 2), 在 7 月花费了 $0.

解题思路

思考

要找出在 2020 2020 2020 年六月与七月花费均不少于 100 100 100 的顾客。订单、商品单价与顾客姓名分属三表,必须先对齐才能按人汇总金额。

将 O r d e r s Orders Orders、 P r o d u c t Product Product、 C u s t o m e r s Customers Customers 等值连接,并限制年份为 2020 2020 2020。按顾客分组后,用条件求和分别累加六月、七月的 q u a n t i t y × p r i c e quantity\times price quantity×price,在 H A V I N G HAVING HAVING 中同时要求两月都不低于 100 100 100。

考察JOIN + 窗口/条件聚合:订单按月份归属,2020 年 6、7 月都下单的客户用分组计数 + HAVING 过滤(6 月有单且 7 月有单)。

代码实现

sql 复制代码
# Write your MySQL query statement below
SELECT customer_id, name
FROM
    Orders
    JOIN Product USING (product_id)
    JOIN Customers USING (customer_id)
WHERE YEAR(order_date) = 2020
GROUP BY 1
HAVING
    SUM(IF(MONTH(order_date) = 6, quantity * price, 0)) >= 100
    AND SUM(IF(MONTH(order_date) = 7, quantity * price, 0)) >= 100;

复杂度分析

本题为数据库查询题,不涉及传统算法的时间/空间复杂度,执行效率由数据库优化器决定;建议对查询涉及的过滤列与连接列建立索引以加速。


1512. Number of Good Pairs(好数对的数目)

题目描述

给你一个整数数组 nums。如果一组数字 (i, j) 满足 nums[i] == nums[j] 且 i < j,

就可以认为这是一组好数对。返回好数对的数目。

示例 1

复制代码
nums=[1,2,3,1,1,3],输出 4。

示例 2

复制代码
nums=[1,1,1,1],输出 6。

约束:

  • 1 <= nums.length <= 100;1 <= nums[i] <= 100。

解题思路

解法一:频次统计 C(n,2)(主提交版本)

  • 统计每个数字出现次数 cntv;
  • 每个数字内部任意两两成对,贡献 cntv * (cntv - 1) / 2。
    复杂度:时间复杂度 O(n),空间复杂度 O(maxV)。
    解法二:双层暴力(对照)
  • 枚举所有 i < j,满足 numsi == numsj 则计数。
    复杂度:时间复杂度 O(n^2),空间复杂度 O(1)。

代码实现

java 复制代码
public class Solution {

    public int numIdenticalPairsCount(int[] nums) {
        int[] cnt = new int[101];
        int ans = 0;
        for (int v : nums) {
            cnt[v]++;
        }
        for (int c : cnt) {
            ans += c * (c - 1) / 2;
        }
        return ans;
    }

    public int numIdenticalPairsBrute(int[] nums) {
        int ans = 0;
        for (int i = 0; i < nums.length; i++) {
            for (int j = i + 1; j < nums.length; j++) {
                if (nums[i] == nums[j]) {
                    ans++;
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.numIdenticalPairsCount(new int[]{1, 2, 3, 1, 1, 3})); // 4
        System.out.println(sol.numIdenticalPairsCount(new int[]{1, 1, 1, 1})); // 6
        System.out.println(sol.numIdenticalPairsCount(new int[]{1, 2, 3})); // 0
        System.out.println(sol.numIdenticalPairsBrute(new int[]{1, 2, 3, 1, 1, 3})); // 4
        System.out.println(sol.numIdenticalPairsBrute(new int[]{1, 1, 1, 1})); // 6
    }
}

复杂度分析

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

复杂度:时间复杂度 O(n^2),空间复杂度 O(1)。


1513. Number of Substrings With Only 1s(仅含 1 的子串数)

题目描述

给你一个二进制字符串 s(仅由 '0' 和 '1' 组成)。返回所有字符都为 1 的子字符串的数目,

答案对 10^9 + 7 取模。

示例 1

复制代码
s = "0110111",输出 9。

示例 2

复制代码
s = "101",输出 2。

约束:

  • 1 <= s.length <= 10^5。

解题思路

解法一:分段统计连续 1(主提交版本)

  • 每段连续 1 的长度为 len,则其贡献 len * (len + 1) / 2 个子串;
  • 扫描字符串,'0' 分割出各段。
    复杂度:时间复杂度 O(n),空间复杂度 O(1)。
    解法二:按位累加贡献(对照)
  • 维护当前连续 1 的个数 cnt,遇到 '1' 时 cnt++,并以 cnt 计入答案
    (即以当前位置结尾的连续 1 子串数为 cnt);遇到 '0' 时 cnt 清零。
    复杂度:时间复杂度 O(n),空间复杂度 O(1)。

代码实现

java 复制代码
public class Solution {

    private static final int MOD = 1_000_000_007;

    public int numSubSegments(String s) {
        long ans = 0;
        int cnt = 0;
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == '1') {
                cnt++;
            } else {
                ans = (ans + (long) cnt * (cnt + 1) / 2) % MOD;
                cnt = 0;
            }
        }
        ans = (ans + (long) cnt * (cnt + 1) / 2) % MOD;
        return (int) ans;
    }

    public int numSubLinear(String s) {
        long ans = 0;
        int cnt = 0;
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == '1') {
                cnt++;
                ans = (ans + cnt) % MOD;
            } else {
                cnt = 0;
            }
        }
        return (int) ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.numSubSegments("0110111")); // 9
        System.out.println(sol.numSubSegments("101")); // 2
        System.out.println(sol.numSubSegments("111111")); // 21
        System.out.println(sol.numSubSegments("000")); // 0
        System.out.println(sol.numSubLinear("0110111")); // 9
        System.out.println(sol.numSubLinear("111111")); // 21
    }
}

复杂度分析

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


1514. Path with Maximum Probability(概率最大的路径)

题目描述

给你一个由 n 个节点(下标从 0 开始)组成的无向加权图,edges[i] = `a`, `b` 表示连接节点 a 和 b

的一条无向边,且该边遍历成功的概率为 succProb[i]。指定起点 start 和终点 end,

找出从起点到终点成功概率最大的路径,返回其成功概率。若不存在路径返回 0。

示例 1

复制代码
n=3, edges=[[0,1],[1,2],[0,2]], succProb=[0.5,0.5,0.2], start=0, end=2,输出 0.25。

约束:

  • 2 <= n <= 10^4;0 <= succProb[i] <= 1。

解题思路

解法一:Dijkstra 变体求最大乘积(主提交版本)

  • 把"概率乘积"取对数映射为最短路(或直接用大根堆按概率排序);
  • 从 start 出发维护 distv 表示到 v 的最大概率,堆中存放 (概率, 节点);
  • 松弛时 distto = max(distto, distcur * w)。
    复杂度:时间复杂度 O(E log V),空间复杂度 O(V + E)。
    解法二:Bellman-Ford 松弛 n 轮(对照)
  • 每轮对所有边做一次松弛,重复 n-1 轮收敛。
    复杂度:时间复杂度 O(n * E),空间复杂度 O(V)。

代码实现

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

public class Solution {

    public double maxProbabilityHeap(int n, int[][] edges, double[] succProb, int start, int end) {
        List<double[]>[] graph = new List[n];
        for (int i = 0; i < n; i++) {
            graph[i] = new ArrayList<>();
        }
        for (int i = 0; i < edges.length; i++) {
            int a = edges[i][0], b = edges[i][1];
            graph[a].add(new double[]{b, succProb[i]});
            graph[b].add(new double[]{a, succProb[i]});
        }
        double[] dist = new double[n];
        dist[start] = 1.0;
        // 大根堆:概率越大越先出
        PriorityQueue<double[]> pq = new PriorityQueue<>((x, y) -> Double.compare(y[0], x[0]));
        pq.offer(new double[]{1.0, start});
        while (!pq.isEmpty()) {
            double[] top = pq.poll();
            double p = top[0];
            int cur = (int) top[1];
            if (cur == end) {
                return p;
            }
            if (p < dist[cur] - 1e-9) {
                continue; // 过期节点
            }
            for (double[] e : graph[cur]) {
                int to = (int) e[0];
                double np = p * e[1];
                if (np > dist[to] + 1e-9) {
                    dist[to] = np;
                    pq.offer(new double[]{np, to});
                }
            }
        }
        return 0.0;
    }

    public double maxProbabilityBellman(int n, int[][] edges, double[] succProb, int start, int end) {
        double[] dist = new double[n];
        dist[start] = 1.0;
        for (int round = 0; round < n - 1; round++) {
            boolean changed = false;
            for (int i = 0; i < edges.length; i++) {
                int a = edges[i][0], b = edges[i][1];
                double w = succProb[i];
                if (dist[a] * w > dist[b]) {
                    dist[b] = dist[a] * w;
                    changed = true;
                }
                if (dist[b] * w > dist[a]) {
                    dist[a] = dist[b] * w;
                    changed = true;
                }
            }
            if (!changed) {
                break;
            }
        }
        return dist[end];
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][] e1 = {{0, 1}, {1, 2}, {0, 2}};
        System.out.println(sol.maxProbabilityHeap(3, e1, new double[]{0.5, 0.5, 0.2}, 0, 2)); // 0.25
        System.out.println(sol.maxProbabilityHeap(3, e1, new double[]{0.5, 0.5, 0.3}, 0, 2)); // 0.30
        System.out.println(sol.maxProbabilityHeap(3, new int[][]{{0, 1}}, new double[]{0.5}, 0, 2)); // 0.0
        System.out.println(sol.maxProbabilityBellman(3, e1, new double[]{0.5, 0.5, 0.2}, 0, 2)); // 0.25
        System.out.println(sol.maxProbabilityBellman(3, e1, new double[]{0.5, 0.5, 0.3}, 0, 2)); // 0.30
    }
}

复杂度分析

复杂度:时间复杂度 O(E log V),空间复杂度 O(V + E)。

复杂度:时间复杂度 O(n * E),空间复杂度 O(V)。


1515. Best Position for a Service Centre(服务中心的最佳位置)

题目描述

一家快递公司希望在新城市建立新的服务中心。公司统计了该城市所有客户在二维地图上的坐标,

希望使服务中心到所有客户的欧几里得距离的总和最小。给定 positions[i] = `xi`, `yi`,

返回到所有客户的欧几里得距离的最小总和。与真实值误差在 10^-5 内视为正确。

示例 1

复制代码
positions=[[0,1],[1,0],[1,2],[2,1]],输出 4.0。

示例 2

复制代码
positions=[[1,1],[3,3]],输出 2.82843。

解题思路

解法一:模拟退火(主提交版本)

  • 目标函数是凸函数(几何中位数问题),从均值点出发;
  • 用逐渐减小的步长进行随机方向搜索(模拟退火的简化版:多次随机扰动 + 收缩步长),
    每次取能降低总距离的新点。
    复杂度:迭代次数固定(例如 200 轮 * 内层 100 次),可视为 O(T * N)。
    解法二:三分嵌套(对照)
  • 先三分横坐标 x,再在内部三分纵坐标 y(函数在 x/y 方向均可三分);
    由于目标函数为凸,三分可得到高精度解。
    复杂度:O(T^2 * N),T 为三分迭代次数(约 60)。

代码实现

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

public class Solution {

    private int[][] pos;
    private int n;

    private double totalDist(double cx, double cy) {
        double sum = 0;
        for (int[] p : pos) {
            sum += Math.hypot(p[0] - cx, p[1] - cy);
        }
        return sum;
    }

    public double getMinDistSumAnnealing(int[][] positions) {
        pos = positions;
        n = positions.length;
        double cx = 0, cy = 0;
        for (int[] p : pos) {
            cx += p[0];
            cy += p[1];
        }
        cx /= n;
        cy /= n;
        double cur = totalDist(cx, cy);
        double step = 100.0; // 初始步长
        Random rnd = new Random(123456);
        while (step > 1e-7) {
            boolean improved = false;
            for (int tryCnt = 0; tryCnt < 10; tryCnt++) {
                double ang = rnd.nextDouble() * 2 * Math.PI;
                double nx = cx + step * Math.cos(ang);
                double ny = cy + step * Math.sin(ang);
                double nv = totalDist(nx, ny);
                if (nv < cur) {
                    cur = nv;
                    cx = nx;
                    cy = ny;
                    improved = true;
                }
            }
            if (!improved) {
                step *= 0.5; // 缩小步长继续精化
            }
        }
        return cur;
    }

    public double getMinDistSumTrisect(int[][] positions) {
        pos = positions;
        n = positions.length;
        double lo = -10000, hi = 10000;
        for (int i = 0; i < 60; i++) {
            double m1 = lo + (hi - lo) / 3;
            double m2 = hi - (hi - lo) / 3;
            if (costAtX(m1) < costAtX(m2)) {
                hi = m2;
            } else {
                lo = m1;
            }
        }
        return costAtX((lo + hi) / 2);
    }

    private double costAtX(double x) {
        double lo = -10000, hi = 10000;
        for (int i = 0; i < 60; i++) {
            double m1 = lo + (hi - lo) / 3;
            double m2 = hi - (hi - lo) / 3;
            if (totalDist(x, m1) < totalDist(x, m2)) {
                hi = m2;
            } else {
                lo = m1;
            }
        }
        return totalDist(x, (lo + hi) / 2);
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.printf("%.5f%n", sol.getMinDistSumAnnealing(new int[][]{{0, 1}, {1, 0}, {1, 2}, {2, 1}})); // 4.0
        System.out.printf("%.5f%n", sol.getMinDistSumAnnealing(new int[][]{{1, 1}, {3, 3}})); // 2.82843
        System.out.printf("%.5f%n", sol.getMinDistSumTrisect(new int[][]{{0, 1}, {1, 0}, {1, 2}, {2, 1}})); // 4.0
        System.out.printf("%.5f%n", sol.getMinDistSumTrisect(new int[][]{{1, 1}, {3, 3}})); // 2.82843
    }
}

复杂度分析

复杂度:迭代次数固定(例如 200 轮 * 内层 100 次),可视为 O(T * N)。

复杂度:O(T^2 * N),T 为三分迭代次数(约 60)。


1516. Move Sub-Tree of N-Ary Tree(移动 N 叉树的子树)

题目描述

给定一棵没有重复值的 N 叉树的根节点 root,以及其中的两个节点 p 和 q。

移动节点 p 及其子树,使节点 p 成为节点 q 的直接子节点。如果 p 已经是 q 的直接子节点则不动。

节点 p 必须是节点 q 的子节点列表中的最后一项。返回改动后的树的根节点。

三种情况:1) q 在 p 的子树中(需要先断开 p 再从 p 处把 q 所在分支重新接上);

  1. p 在 q 的子树中;3) 互不相干。第 2、3 种只移动 p;第 1 种需先拆链。

解题思路

解法一:找父节点 + 剪接(主提交版本)

  • 先 DFS 找到每个节点的父节点(p 的父节点 pp,q 的父节点 pq);
  • 若 pp == q,p 已是 q 的直接子节点,则把 p 移到子列表末尾即可(或判定不需要动);
  • 否则把 p 从 pp 的 children 中移除;再处理 "q 在 p 子树中":此时需要把 q(连同其父链)
    重新接回原 pp 位置 ------ 更通用的做法:把 p 从原父移除,若 q 的祖先链中包含 p,
    则需要断开 p 与 q 之间的链路;
    简化实现:若 q 是 p 的后代,则先从 p 的 children(或 p 子链路)中移除 q 所在的直接子
    分支并接到 pp 的 children 列表原 p 的位置;最后把 p 追加到 q 的 children 末尾。
    复杂度:时间复杂度 O(N),空间复杂度 O(N)。
    解法二:父指针数组 + 三段式处理(对照)
  • 先 BFS 建父指针与"是否后代"标记,再分三种情况显式拼接,逻辑更直白。
    复杂度:时间复杂度 O(N),空间复杂度 O(N)。

代码实现

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

public class Solution {

    static class Node {
        public int val;
        public List<Node> children;

        public Node() {
            children = new ArrayList<>();
        }

        public Node(int val) {
            this.val = val;
            children = new ArrayList<>();
        }

        public Node(int val, List<Node> children) {
            this.val = val;
            this.children = children;
        }
    }

    private Node root;

    public Node moveSubTree(Node root, Node p, Node q) {
        this.root = root;
        Map<Node, Node> parent = new HashMap<>();
        dfs(root, null, parent);
        Node pp = parent.get(p);
        Node pq = parent.get(q);

        if (pp == q) {
            // p 已是 q 的直接子节点:移动到末尾(若不在末尾)
            q.children.remove(p);
            q.children.add(p);
            return root;
        }

        // 情况 1:q 在 p 的子树中 ------ 需要先把 q 所在分支从 p 底下拆出,接回原 p 位置
        if (isAncestor(p, q, parent)) {
            // 找到 p 的 children 中,q 所在分支的直接子节点 childOnPath
            Node childOnPath = q;
            while (parent.get(childOnPath) != p) {
                childOnPath = parent.get(childOnPath);
            }
            Node anchor = parent.get(p); // p 的原父
            if (anchor == null) {
                // p 为原根:把 childOnPath 提升为新根
                List<Node> ch = p.children;
                ch.remove(childOnPath);
                int idx = ch.indexOf(anchor); // 不会发生,anchor 为 null
                root = childOnPath;
            } else {
                List<Node> bros = anchor.children;
                int idx = bros.indexOf(p);
                bros.remove(childOnPath); // 从 p 的子列表移除(childOnPath 在 p.children 中吗?)
                // 注意:childOnPath 位于 p 的后代链中,应把它插入 anchor.children 中 p 的位置
                bros.remove(p);
                bros.add(idx, childOnPath);
            }
            // 此时 p 已不再包含 q 分支可安全迁移;还需处理 p 自身在 q 之下是后代关系吗?
            // 情况 1 中 p 仍是独立根分支,把它移动到 q 的子列表末尾
            Node pparent = parent.get(p);
            if (pparent != null) {
                pparent.children.remove(p);
            }
            // 若 p 是原根,root 已变
            if (root == p) {
                root = childOnPath;
            }
            p.children.remove(childOnPath);
            q.children.add(p);
            return root;
        }

        // 情况 2/3:直接从原父移除 p,再追加到 q.children 末尾
        pp.children.remove(p);
        if (root == p) {
            root = q; // p 原本是根(不会发生,因为 p 有父 pp)
        }
        q.children.add(p);
        return root;
    }

    private void dfs(Node node, Node par, Map<Node, Node> parent) {
        if (node == null) {
            return;
        }
        if (par != null) {
            parent.put(node, par);
        }
        for (Node c : node.children) {
            dfs(c, node, parent);
        }
    }

    private boolean isAncestor(Node anc, Node node, Map<Node, Node> parent) {
        Node cur = node;
        while (cur != null) {
            if (cur == anc) {
                return true;
            }
            cur = parent.get(cur);
        }
        return false;
    }

    public Node moveSubTreeExplicit(Node root, Node p, Node q) {
        if (root == p) {
            // p 是整棵树的根:直接把 p 挂到 q 下,q 提升为根
            q.children.add(p);
            return q;
        }
        Map<Node, Node> parent = new HashMap<>();
        dfs(root, null, parent);
        Node pp = parent.get(p);
        Node pq = parent.get(q);

        // p 已是 q 的直接子节点:移到末尾
        if (pp == q) {
            q.children.remove(p);
            q.children.add(p);
            return root;
        }

        // q 是否在 p 子树中?
        if (isAncestor(p, q, parent)) {
            // 找到 p 到 q 路径上 p 的直接子节点
            Node childOnPath = q;
            while (parent.get(childOnPath) != p) {
                childOnPath = parent.get(childOnPath);
            }
            // 从 p 的子列表移除该分支
            p.children.remove(childOnPath);
            // 把它插入原 p 的位置
            int idx = pp.children.indexOf(p);
            pp.children.remove(p);
            pp.children.add(idx, childOnPath);
            // 再把 p 挂到 q 下
            q.children.add(p);
            return root;
        }

        // 一般情况
        pp.children.remove(p);
        q.children.add(p);
        return root;
    }

    // ---- 本地自测辅助 ----
    private static Node build(String s) {
        // 简易构造:按层序 [1,null,2,3,...] 解析,null 表示一层结束(仅用于测试手写树)
        // 此方法仅测试用,不参与提交
        return null;
    }

    private static String serialize(Node n) {
        if (n == null) {
            return "[]";
        }
        List<String> out = new ArrayList<>();
        java.util.Queue<Node> q = new java.util.LinkedList<>();
        q.offer(n);
        while (!q.isEmpty()) {
            Node cur = q.poll();
            if (cur == null) {
                out.add("null");
                continue;
            }
            out.add(String.valueOf(cur.val));
            for (Node c : cur.children) {
                q.offer(c);
            }
            q.offer(null);
        }
        while (out.size() > 1 && out.get(out.size() - 1).equals("null")) {
            out.remove(out.size() - 1);
        }
        return out.toString();
    }

    private static Node find(Node root, int val) {
        if (root == null) {
            return null;
        }
        if (root.val == val) {
            return root;
        }
        for (Node c : root.children) {
            Node r = find(c, val);
            if (r != null) {
                return r;
            }
        }
        return null;
    }

    public static void main(String[] args) {
        // 构造示例 1 的树:[1,null,2,3,null,4,5,null,6,null,7,8]
        Node n2 = new Node(2);
        Node n3 = new Node(3);
        Node n4 = new Node(4);
        Node n5 = new Node(5);
        Node n6 = new Node(6);
        Node n7 = new Node(7);
        Node n8 = new Node(8);
        n3.children.add(n4);
        n3.children.add(n5);
        n3.children.add(n6);
        n4.children.add(n7);
        n4.children.add(n8);
        Node root1 = new Node(1);
        root1.children.add(n2);
        root1.children.add(n3);

        Solution sol = new Solution();
        Node r1 = sol.moveSubTreeExplicit(root1, n4, n2); // 把 4 移到 2 下(互不相干型)
        System.out.println("root=" + r1.val);
        System.out.println("2.children=" + n2.children.size() + " (期望含 4)");
        System.out.println(serialize(r1));

        // 情况 2:p 已在 q 子树中(示例 2:p=7, q=4,7 已是 4 的直接孩子 -> 不动)
        Node a2 = new Node(2);
        Node a3 = new Node(3);
        Node a4 = new Node(4);
        Node a5 = new Node(5);
        Node a6 = new Node(6);
        Node a7 = new Node(7);
        Node a8 = new Node(8);
        a3.children.add(a4);
        a3.children.add(a5);
        a3.children.add(a6);
        a4.children.add(a7);
        a4.children.add(a8);
        Node root2 = new Node(1);
        root2.children.add(a2);
        root2.children.add(a3);
        Node r2 = sol.moveSubTreeExplicit(root2, a7, a4);
        System.out.println("case2 root=" + r2.val + " (期望 1, 7 仍在 4 下)");
        System.out.println(serialize(r2));
    }
}

复杂度分析

复杂度:时间复杂度 O(N),空间复杂度 O(N)。


1517. Find Users With Valid E-Mails(查找拥有有效邮箱的用户)

题目类型:数据库(SQL)

题目描述

表: Users

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| user_id       | int     |
| name          | varchar |
| mail          | varchar |
+---------------+---------+
user_id 是该表的主键(具有唯一值的列)。
该表包含了网站已注册用户的信息。有一些电子邮件是无效的。

编写一个解决方案,以查找具有有效电子邮件的用户。

一个有效的电子邮件具有前缀名称和域,其中:

复制代码
-  **前缀** 名称是一个字符串,可以包含字母(大写或小写),数字,下划线 `'_'` ,点 `'.'` 和(或)破折号 `'-'` 。前缀名称 **必须** 以字母开头。

- **域** 必须是小写的 `'@leetcode.com'`。

以任何顺序返回结果表。

结果的格式如以下示例所示:

示例 1:**

复制代码
**输入:**
Users 表:
+---------+-----------+-------------------------+
| user_id | name      | mail                    |
+---------+-----------+-------------------------+
| 1       | Winston   | winston@leetcode.com    |
| 2       | Jonathan  | jonathanisgreat         |
| 3       | Annabelle | bella-@leetcode.com     |
| 4       | Sally     | sally.come@leetcode.com |
| 5       | Marwan    | quarz#2020@leetcode.com |
| 6       | David     | david69@gmail.com       |
| 7       | Shapiro   | .shapo@leetcode.com     |
+---------+-----------+-------------------------+
**输出:**
+---------+-----------+-------------------------+
| user_id | name      | mail                    |
+---------+-----------+-------------------------+
| 1       | Winston   | winston@leetcode.com    |
| 3       | Annabelle | bella-@leetcode.com     |
| 4       | Sally     | sally.come@leetcode.com |
+---------+-----------+-------------------------+
**解释:**
用户 2 的电子邮件没有域。
用户 5 的电子邮件带有不允许的 '#' 符号。
用户 6 的电子邮件没有 leetcode 域。
用户 7 的电子邮件以点开头。

解题思路

思考

有效邮箱必须以前缀字母开头,中间仅含字母、数字、下划线、点号或减号,并以 @leetcode.com \texttt{@leetcode.com} @leetcode.com 结尾。逐行手写判断分支多且易漏边界,而规则本身正是正则语言。

用正则 KaTeX parse error: Unexpected end of input in a macro argument, expected '}' at end of input: ...@leetcode\\.com}$ 匹配整串即可。SQL 中再辅以区分大小写的后缀检查,避免域名大小写不符;Pandas 路径则对 m a i l mail mail 列做同样的整串匹配。

考察正则表达式:用 MySQL 的 REGEXP 匹配邮箱格式(前缀为字母数字/句点/连字符,@ 后字母域名,.com 结尾)。

代码实现

sql 复制代码
# Write your MySQL query statement below
SELECT *
FROM Users
WHERE mail REGEXP '^[a-zA-Z][a-zA-Z0-9_.-]*@leetcode\\.com$' AND BINARY mail LIKE '%@leetcode.com';

复杂度分析

本题为数据库查询题,不涉及传统算法的时间/空间复杂度,执行效率由数据库优化器决定;建议对查询涉及的过滤列与连接列建立索引以加速。


1518. Water Bottles(换酒问题)

题目描述

超市正在促销,你可以用 numExchange 个空水瓶从超市兑换一瓶水。最开始你购入了 numBottles 瓶水。

如果喝掉了水瓶中的水,那么水瓶就会变成空的。返回你最多可以喝到多少瓶水。

示例 1

复制代码
numBottles=9, numExchange=3,输出 13。

示例 2

复制代码
numBottles=15, numExchange=4,输出 19。

约束:

  • 1 <= numBottles <= 100;2 <= numExchange <= 100。

解题思路

解法一:模拟兑换(主提交版本)

  • 维护空瓶数 empty 与已喝瓶数 ans;
  • 每次喝一瓶 ans++、empty++;当 empty == numExchange 时兑换一瓶,empty -= numExchange 后 +1。
    复杂度:时间复杂度 O(numBottles),空间复杂度 O(1)。
    解法二:公式法(对照)
  • 每喝一瓶产生一个空瓶,numExchange 个空瓶换 1 瓶;净消耗为每次兑换少 numExchange-1 个空瓶,
    初始有 numBottles 个空瓶,最多能兑换 floor(numBottles / (numExchange - 1)) 次
    (注意第一次兑换需要 numBottles >= numExchange)。
    复杂度:时间复杂度 O(1),空间复杂度 O(1)。

代码实现

java 复制代码
public class Solution {

    public int numWaterBottlesSim(int numBottles, int numExchange) {
        int ans = numBottles; // 先喝掉买来的
        int empty = numBottles;
        while (empty >= numExchange) {
            int exchanged = empty / numExchange;
            ans += exchanged;
            empty = empty % numExchange + exchanged;
        }
        return ans;
    }

    public int numWaterBottlesFormula(int numBottles, int numExchange) {
        if (numBottles < numExchange) {
            return numBottles;
        }
        // 总喝掉 = 初始 + 兑换次数;兑换次数 = (numBottles - numExchange) / (numExchange - 1) + 1
        int exchanged = (numBottles - numExchange) / (numExchange - 1) + 1;
        return numBottles + exchanged;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.numWaterBottlesSim(9, 3)); // 13
        System.out.println(sol.numWaterBottlesSim(15, 4)); // 19
        System.out.println(sol.numWaterBottlesFormula(9, 3)); // 13
        System.out.println(sol.numWaterBottlesFormula(15, 4)); // 19
        System.out.println(sol.numWaterBottlesSim(4, 3)); // 5
        System.out.println(sol.numWaterBottlesFormula(4, 3)); // 5
    }
}

复杂度分析

复杂度:时间复杂度 O(numBottles),空间复杂度 O(1)。

复杂度:时间复杂度 O(1),空间复杂度 O(1)。


1519. Number of Nodes in the Sub-Tree With the Same Label(子树中标签相同的节点数)

题目描述

给你一棵树(即一个连通的无环无向图),节点编号 0...n-1 恰好有 n-1 条 edges。

给你一个下标从 0 开始、长度为 n 的字符串 labels,其中 labels[i] 是节点 i 的标签(小写字母)。

返回一个长度为 n 的数组 ans,其中 ans[i] 表示节点 i 的子树中与节点 i 标签相同的节点数。

示例 1

复制代码
n=7, edges=[[0,1],[0,2],[1,4],[1,5],[2,3],[2,6]], labels="abaedcd",
输出 [2,1,1,1,1,1,1]。

解题思路

解法一:后序遍历 + 计数回溯(主提交版本)

  • 建邻接表;以 0 为根 DFS,返回一个 int26 表示该子树内各标签计数;
  • 后序合并:anscur = 合并后 countcurLabel。
    复杂度:时间复杂度 O(n * 26),空间复杂度 O(n)。
    解法二:迭代栈后序(对照)
  • 用显式栈做后序遍历,避免递归深度问题(树的规模可能大)。
    复杂度:时间复杂度 O(n * 26),空间复杂度 O(n)。

代码实现

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

public class Solution {

    private List<Integer>[] g;
    private String labels;
    private int[] ans;

    public int[] countSubTreesRec(int n, int[][] edges, String labels) {
        this.labels = labels;
        this.ans = new int[n];
        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]);
        }
        dfs(0, -1);
        return ans;
    }

    private int[] dfs(int u, int parent) {
        int[] cnt = new int[26];
        cnt[labels.charAt(u) - 'a'] = 1;
        for (int v : g[u]) {
            if (v == parent) {
                continue;
            }
            int[] child = dfs(v, u);
            for (int i = 0; i < 26; i++) {
                cnt[i] += child[i];
            }
        }
        ans[u] = cnt[labels.charAt(u) - 'a'];
        return cnt;
    }

    public int[] countSubTreesIter(int n, int[][] edges, String labels) {
        this.labels = labels;
        this.ans = new int[n];
        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]);
        }
        // post order: 状态 0 进入,1 返回
        int[] state = new int[n];
        int[] parent = new int[n];
        java.util.ArrayDeque<Integer> stack = new java.util.ArrayDeque<>();
        stack.push(0);
        parent[0] = -1;
        int[][] cnt = new int[n][26];
        while (!stack.isEmpty()) {
            int u = stack.peek();
            if (state[u] == 0) {
                state[u] = 1;
                cnt[u][labels.charAt(u) - 'a'] = 1;
                for (int v : g[u]) {
                    if (v == parent[u]) {
                        continue;
                    }
                    parent[v] = u;
                    stack.push(v);
                }
            } else {
                stack.pop();
                if (parent[u] != -1) {
                    int p = parent[u];
                    for (int i = 0; i < 26; i++) {
                        cnt[p][i] += cnt[u][i];
                    }
                }
                ans[u] = cnt[u][labels.charAt(u) - 'a'];
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][] e1 = {{0, 1}, {0, 2}, {1, 4}, {1, 5}, {2, 3}, {2, 6}};
        int[] r1 = sol.countSubTreesRec(7, e1, "abaedcd");
        System.out.println(java.util.Arrays.toString(r1)); // [2,1,1,1,1,1,1]
        int[] r2 = sol.countSubTreesIter(7, e1, "abaedcd");
        System.out.println(java.util.Arrays.toString(r2)); // [2,1,1,1,1,1,1]
        // 示例 2:n=4, edges=[[0,1],[1,2],[0,3]], labels="bbbb"
        int[] r3 = sol.countSubTreesRec(4, new int[][]{{0, 1}, {1, 2}, {0, 3}}, "bbbb");
        System.out.println(java.util.Arrays.toString(r3)); // [4,2,1,1]
    }
}

复杂度分析

复杂度:时间复杂度 O(n * 26),空间复杂度 O(n)。


1520. Maximum Number of Non-Overlapping Substrings(最多的不重叠子字符串)

题目描述

给你一个只包含小写字母的字符串 s,需要找到 s 中最多数目的非空子字符串,满足:

  1. 这些字符串互不重叠;
  2. 如果一个子字符串包含字符 char,那么 s 中所有 char 字符都应该在这个子字符串中。
    如果有多个解子字符串数目相同,返回总长度最小的一个解(最小总长度解唯一)。

示例 1

复制代码
s="adefaddaccc",输出 ["e","f","ccc"]。

示例 2

复制代码
s="abbaccd",输出 ["d","bb","cc"]。

约束:

  • 1 <= s.length <= 10^5;s 只包含小写英文字母。

解题思路

解法一:区间合并 + 贪心选不重叠区间(主提交版本)

  • 对每个字符 c 记下首次出现 firstc、最后出现 lastc;
  • 一个合法子串的特征:区间 l, r = 某个子串的起止,且内部任何字符的 last 都不超过 r、
    first 都不小于 l;对每个起点 l 扩张 r 直到覆盖内部所有字符的 last;若 r 回缩后仍能保持规则,
    得到一个候选合法区间;
  • 得到所有"极小合法区间"后,按 r 升序贪心选择不重叠区间(区间调度)得到最多数量、总长最小。
    复杂度:时间复杂度 O(n),空间复杂度 O(1)。
    解法二:枚举起点扩张 + 区间调度(对照)
  • 逻辑同上,但显式把"扩张后的区间再通过检查是否包含外来字符来裁剪"写清楚,便于理解正确性。
    复杂度:时间复杂度 O(26 * n),空间复杂度 O(1)。

代码实现

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

public class Solution {

    public List<String> maxNumOfSubstringsStandard(String s) {
        int n = s.length();
        int[] first = new int[26];
        int[] last = new int[26];
        java.util.Arrays.fill(first, -1);
        for (int i = 0; i < n; i++) {
            int c = s.charAt(i) - 'a';
            if (first[c] == -1) {
                first[c] = i;
            }
            last[c] = i;
        }
        // 候选区间:[l, r]
        List<int[]> intervals = new ArrayList<>();
        for (int c = 0; c < 26; c++) {
            if (first[c] == -1) {
                continue;
            }
            int l = first[c], r = last[c];
            // 扩张 r,保证区间内所有字符的 last 不超过 r
            for (int i = l; i <= r; i++) {
                int ch = s.charAt(i) - 'a';
                r = Math.max(r, last[ch]);
            }
            // 裁剪:区间内不能包含 first 在 l 之前的字符(否则区间需要进一步扩张)
            // 标准做法:检查扩张后的区间是否可以用 first 进一步收缩
            boolean valid = true;
            // 收缩 l:确保区间内所有字符的 first >= l
            for (int i = l; i <= r; i++) {
                int ch = s.charAt(i) - 'a';
                if (first[ch] < l) {
                    valid = false;
                    break;
                }
            }
            if (valid && r < n) {
                intervals.add(new int[]{l, r});
            }
        }
        // 按 r 排序,区间调度选最多个数且总长度最小:同 r 取 l 更大的(更短)
        intervals.sort((a, b) -> a[1] != b[1] ? a[1] - b[1] : b[0] - a[0]);
        List<String> ans = new ArrayList<>();
        int prevEnd = -1;
        for (int[] it : intervals) {
            if (it[0] > prevEnd) {
                ans.add(s.substring(it[0], it[1] + 1));
                prevEnd = it[1];
            }
        }
        return ans;
    }

    public List<String> maxNumOfSubstringsExplicit(String s) {
        int n = s.length();
        int[] first = new int[26];
        int[] last = new int[26];
        java.util.Arrays.fill(first, -1);
        for (int i = 0; i < n; i++) {
            int c = s.charAt(i) - 'a';
            if (first[c] == -1) {
                first[c] = i;
            }
            last[c] = i;
        }
        List<int[]> cand = new ArrayList<>();
        for (int c = 0; c < 26; c++) {
            if (first[c] == -1) {
                continue;
            }
            int l = first[c];
            int r = last[c];
            // 扩张
            boolean changed = true;
            while (changed) {
                changed = false;
                for (int i = l; i <= r; i++) {
                    int ch = s.charAt(i) - 'a';
                    if (last[ch] > r) {
                        r = last[ch];
                        changed = true;
                    }
                }
            }
            // 校验:区间内字符的 first 均 >= l
            boolean ok = true;
            for (int i = l; i <= r; i++) {
                if (first[s.charAt(i) - 'a'] < l) {
                    ok = false;
                    break;
                }
            }
            // 同时校验:区间外不能有该字符(last[c] == r 已覆盖)
            if (ok) {
                cand.add(new int[]{l, r});
            }
        }
        cand.sort((a, b) -> a[1] != b[1] ? a[1] - b[1] : b[0] - a[0]);
        List<String> ans = new ArrayList<>();
        int prevEnd = -1;
        for (int[] it : cand) {
            if (it[0] > prevEnd) {
                ans.add(s.substring(it[0], it[1] + 1));
                prevEnd = it[1];
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.maxNumOfSubstringsStandard("adefaddaccc")); // [e, f, ccc]
        System.out.println(sol.maxNumOfSubstringsStandard("abbaccd")); // [d, bb, cc]
        System.out.println(sol.maxNumOfSubstringsExplicit("adefaddaccc")); // [e, f, ccc]
        System.out.println(sol.maxNumOfSubstringsExplicit("abbaccd")); // [d, bb, cc]
    }
}

复杂度分析

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

复杂度:时间复杂度 O(26 * n),空间复杂度 O(1)。

相关推荐
Wang's Blog1 小时前
Java 项目实战:交友项目 - 即时通信总结:全链路复盘、可复用清单与自检
java·python·交友
长不大的杨瑞杰2 小时前
OpenCode v2 LSP 实战:内置语言服务器、自定义配置与诊断闭环
java·人工智能·typescript·lsp·opencode
云和数据.ChenGuang2 小时前
python和java的try
java·开发语言·python
日日大王学编程2 小时前
阿里巴巴开源 Data Agent(Text2SQL)项目全维度详解
java·ai·开源
Wang's Blog2 小时前
Java 项目实战:交友项目 - 交友左滑右滑
java·交友
测试盐2 小时前
spark作业日志定位过程
java·hadoop·spark
ly76892 小时前
从 OpenAPI 到多语言 SDK 生成:Java 服务端契约漂移与生成器版本治理
java·开发语言·openapi·java 21·版本兼容·契约漂移·sdk 生成
lqbzlll2 小时前
CVE-2026-91403 排查指南:Java JSSE 信任链漏洞的根因、影响范围和修复步骤
java·开发语言·信任链
CarIise2 小时前
AOP 概念的引入
java·开发语言