LeetCode 1501-1520. 题解合集
文章目录
- [LeetCode 1501-1520. 题解合集](#LeetCode 1501-1520. 题解合集)
-
- [1501. Countries You Can Safely Invest In(可以放心投资的国家)](#1501. Countries You Can Safely Invest In(可以放心投资的国家))
- [1502. Can Make Arithmetic Progression From Sequence(判断能否形成等差数列)](#1502. Can Make Arithmetic Progression From Sequence(判断能否形成等差数列))
- [1503. Last Moment Before All Ants Fall Out of a Plank(所有蚂蚁掉下来前的最后一刻)](#1503. Last Moment Before All Ants Fall Out of a Plank(所有蚂蚁掉下来前的最后一刻))
- [1504. Count Submatrices With All Ones(统计全 1 子矩形)](#1504. Count Submatrices With All Ones(统计全 1 子矩形))
- [1505. Minimum Possible Integer After at Most K Adjacent Swaps On Digits(最多 K 次交换相邻数位后得到的最小整数)](#1505. Minimum Possible Integer After at Most K Adjacent Swaps On Digits(最多 K 次交换相邻数位后得到的最小整数))
- [1506. Find Root of N-Ary Tree(找到 N 叉树的根节点)](#1506. Find Root of N-Ary Tree(找到 N 叉树的根节点))
- [1507. Reformat Date(转变日期格式)](#1507. Reformat Date(转变日期格式))
- [1508. Range Sum of Sorted Subarray Sums(子数组和排序后的区间和)](#1508. Range Sum of Sorted Subarray Sums(子数组和排序后的区间和))
- [1509. Minimum Difference Between Largest and Smallest Value in Three Moves(三次操作后最大值与最小值的最小差)](#1509. Minimum Difference Between Largest and Smallest Value in Three Moves(三次操作后最大值与最小值的最小差))
- [1510. Stone Game IV(石子游戏 IV)](#1510. Stone Game IV(石子游戏 IV))
- [1511. Customer Order Frequency(消费者下单频率)](#1511. Customer Order Frequency(消费者下单频率))
- [1512. Number of Good Pairs(好数对的数目)](#1512. Number of Good Pairs(好数对的数目))
- [1513. Number of Substrings With Only 1s(仅含 1 的子串数)](#1513. Number of Substrings With Only 1s(仅含 1 的子串数))
- [1514. Path with Maximum Probability(概率最大的路径)](#1514. Path with Maximum Probability(概率最大的路径))
- [1515. Best Position for a Service Centre(服务中心的最佳位置)](#1515. Best Position for a Service Centre(服务中心的最佳位置))
- [1516. Move Sub-Tree of N-Ary Tree(移动 N 叉树的子树)](#1516. Move Sub-Tree of N-Ary Tree(移动 N 叉树的子树))
- [1517. Find Users With Valid E-Mails(查找拥有有效邮箱的用户)](#1517. Find Users With Valid E-Mails(查找拥有有效邮箱的用户))
- [1518. Water Bottles(换酒问题)](#1518. Water Bottles(换酒问题))
- [1519. Number of Nodes in the Sub-Tree With the Same Label(子树中标签相同的节点数)](#1519. Number of Nodes in the Sub-Tree With the Same Label(子树中标签相同的节点数))
- [1520. Maximum Number of Non-Overlapping Substrings(最多的不重叠子字符串)](#1520. Maximum Number of Non-Overlapping Substrings(最多的不重叠子字符串))
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的格式(YYYY4 位年份、MM2 位月份、DD2 位天数)。
示例 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 所在分支重新接上);
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 中最多数目的非空子字符串,满足:
- 这些字符串互不重叠;
- 如果一个子字符串包含字符
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)。