LeetCode 1961-1980. 题解合集
文章目录
- [LeetCode 1961-1980. 题解合集](#LeetCode 1961-1980. 题解合集)
-
- [1961. Check If String Is a Prefix of Array(检查字符串是否为数组前缀)](#1961. Check If String Is a Prefix of Array(检查字符串是否为数组前缀))
- [1962. Remove Stones to Minimize the Total(移除石子使总数最小)](#1962. Remove Stones to Minimize the Total(移除石子使总数最小))
- [1963. Minimum Number of Swaps to Make the String Balanced(使字符串平衡的最小交换次数)](#1963. Minimum Number of Swaps to Make the String Balanced(使字符串平衡的最小交换次数))
- [1964. Find the Longest Valid Obstacle Course at Each Position(找出到每个位置为止最长的有效障碍赛跑路线)](#1964. Find the Longest Valid Obstacle Course at Each Position(找出到每个位置为止最长的有效障碍赛跑路线))
- [1965. Employees With Missing Information(丢失信息的雇员)](#1965. Employees With Missing Information(丢失信息的雇员))
- [1966. Binary Searchable Numbers in an Unsorted Array(未排序数组中的可被二分查找的数)](#1966. Binary Searchable Numbers in an Unsorted Array(未排序数组中的可被二分查找的数))
- [1967. Number of Strings That Appear as Substrings in Word(作为子字符串出现在单词中的字符串数目)](#1967. Number of Strings That Appear as Substrings in Word(作为子字符串出现在单词中的字符串数目))
- [1968. Array With Elements Not Equal to Average of Neighbors(构造元素不等于两相邻元素平均值的数组)](#1968. Array With Elements Not Equal to Average of Neighbors(构造元素不等于两相邻元素平均值的数组))
- [1969. Minimum Non-Zero Product of the Array Elements(数组元素的最小非零乘积)](#1969. Minimum Non-Zero Product of the Array Elements(数组元素的最小非零乘积))
- [1970. Last Day Where You Can Still Cross(你能穿过矩阵的最后一天)](#1970. Last Day Where You Can Still Cross(你能穿过矩阵的最后一天))
- [1971. Find if Path Exists in Graph(寻找图中是否存在路径)](#1971. Find if Path Exists in Graph(寻找图中是否存在路径))
- [1972. First and Last Call On the Same Day(同一天的第一个电话和最后一个电话)](#1972. First and Last Call On the Same Day(同一天的第一个电话和最后一个电话))
- [1973. Count Nodes Equal to Sum of Descendants(值等于子节点值之和的节点数量)](#1973. Count Nodes Equal to Sum of Descendants(值等于子节点值之和的节点数量))
- [1974. Minimum Time to Type Word Using Special Typewriter(使用特殊打字机键入单词的最少时间)](#1974. Minimum Time to Type Word Using Special Typewriter(使用特殊打字机键入单词的最少时间))
- [1975. Maximum Matrix Sum(最大方阵和)](#1975. Maximum Matrix Sum(最大方阵和))
- [1976. Number of Ways to Arrive at Destination(到达目的地的方案数)](#1976. Number of Ways to Arrive at Destination(到达目的地的方案数))
- [1977. Number of Ways to Separate Numbers(划分数字的方案数)](#1977. Number of Ways to Separate Numbers(划分数字的方案数))
- [1978. Employees Whose Manager Left the Company(上级经理已离职的公司员工)](#1978. Employees Whose Manager Left the Company(上级经理已离职的公司员工))
- [1979. Find Greatest Common Divisor of Array(找出数组的最大公约数)](#1979. Find Greatest Common Divisor of Array(找出数组的最大公约数))
- [1980. Find Unique Binary String(找出不同的二进制字符串)](#1980. Find Unique Binary String(找出不同的二进制字符串))
1961. Check If String Is a Prefix of Array(检查字符串是否为数组前缀)
题目描述
给你一个字符串 s 和一个字符串数组 words,请你判断 s 是否为 words 的前缀字符串。
字符串 s 要成为 words 的前缀字符串,需要满足:s 可以由 words 中的前 k
(k 为正数)个字符串按顺序相连得到,且 k 不超过 words.length。
如果 s 是 words 的前缀字符串,返回 true;否则,返回 false。
示例 1
- 输入 :
s= "iloveleetcode",words= "`i`","`love`","`leetcode`","`apples`" - 输出 :
true - 解释 :
s可以由 "i"、"love" 和 "leetcode" 相连得到。
示例 2
- 输入 :
s= "iloveleetcode",words= "`apples`","`i`","`love`","`leetcode`" - 输出 :
false - 解释 :数组的前缀相连无法得到
s。
提示: - 1 <= words.length <= 100- 1 <= words[i].length <= 20- 1 <= s.length <= 1000-
words[i]和s仅由小写英文字母组成。
解题思路
解法一:逐词拼接比较,O(len(s)),主提交版本
用 StringBuilder 从前往后累加 words 的
每个词,每加一个词后判断前缀串是否与 s 相等(注意题目要求恰好等于 s,而不是 s 是拼接的
前缀------"s 可以由 words 中的前 k 个字符串按顺序相连得到"就是拼接结果必须等于 s)。
若拼接长度超过 s.length() 还没相等则返回 false(后续词更长不可能相等)。
解法二:双指针字符比较,O(len(s)),对照
维护目标串 s 上的游标 pos,每取一个词就
逐字符与 s 的对应位置比较;若发现不匹配或词过长,直接返回 false;扫描完所有词后
pos 恰好等于 s.length() 才返回 true。两解法结果一致。
代码实现
java
public class Solution {
public boolean isPrefixString(String s, String[] words) {
StringBuilder sb = new StringBuilder();
for (String w : words) {
sb.append(w);
if (sb.length() > s.length()) {
return false; // 已经超长,不可能再相等
}
if (sb.length() == s.length()) {
return sb.toString().equals(s);
}
}
return false; // words 用尽仍未达到 s.length()
}
public boolean isPrefixStringPtr(String s, String[] words) {
int pos = 0;
for (String w : words) {
// 词已超出剩余目标长度,失败
if (pos + w.length() > s.length()) {
return false;
}
for (int i = 0; i < w.length(); i++) {
if (s.charAt(pos + i) != w.charAt(i)) {
return false;
}
}
pos += w.length();
if (pos == s.length()) {
return true;
}
}
return false;
}
public static void main(String[] args) {
Solution sol = new Solution();
String[] ss = {"iloveleetcode", "iloveleetcode", "a", "abc", "ab", "xyz"};
String[][] wss = {
{"i", "love", "leetcode", "apples"},
{"apples", "i", "love", "leetcode"},
{"a"},
{"a", "bc", "d"},
{"a"},
{"x", "y", "z"}
};
boolean[] exp = {true, false, true, true, false, true};
boolean all = true;
for (int i = 0; i < ss.length; i++) {
boolean a = sol.isPrefixString(ss[i], wss[i]);
boolean b = sol.isPrefixStringPtr(ss[i], wss[i]);
boolean ok = a == exp[i] && a == b;
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1962. Remove Stones to Minimize the Total(移除石子使总数最小)
题目描述
给你一个整数数组 piles,数组下标从 0 开始,其中 piles[i] 表示第 i 堆石子中的石子
数量。另给你一个整数 k,请你执行下述操作恰好 k 次:
- 选出任一石子堆
piles[i],并从中移除floor(piles[i] / 2)颗石子。
注意:你可以对同一堆石子多次执行此操作。
返回执行k次操作后,剩下石子的最小总数。
floor(x)为小于或等于x的最大整数(对x向下取整)。
示例 1
- 输入 :
piles= 5,4,9,k= 2 - 输出:12
- 解释:对第 2 堆操作:5,4,5;再对第 0 堆操作:3,4,5,剩余总数 3+4+5 = 12。
示例 2
- 输入 :
piles= 4,3,6,7,k= 3 - 输出:12
- 解释 :依次对第 2、3、0 堆操作后得到 2,3,3,4,剩余总数 12。
提示: - 1 <= piles.length <= 10^5- 1 <= piles[i] <= 10^4- 1 <= k <= 10^5
解题思路
解法一:最大堆贪心,O((n+k) log n),主提交版本
每次操作应选择「当前最大的一堆」------因为
移除 floor(x/2) 后减少的量与 x 成正比,堆越大减少越多,且操作可重复作用于同一堆。用
最大堆维护各堆石子数,连续 k 次:弹出堆顶 x,把 x - x/2 放回;最后堆内元素和即最少
剩余总数。
解法二:计数桶模拟,对照
piles[i] <= 10^4,可用计数数组统计每种石子数的堆数,从大到
小依次操作(每次选当前最大的非空桶),桶内计数的向下取整分裂。两解法结果一致,交叉验证。
代码实现
java
import java.util.PriorityQueue;
public class Solution {
public int minStoneSum(int[] piles, int k) {
PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> Integer.compare(b, a));
long total = 0;
for (int p : piles) {
pq.offer(p);
total += p;
}
for (int i = 0; i < k; i++) {
int x = pq.poll();
total -= x / 2; // 移除 floor(x/2)
pq.offer(x - x / 2);
}
return (int) total;
}
public int minStoneSumBucket(int[] piles, int k) {
int max = 0;
long total = 0;
for (int p : piles) {
max = Math.max(max, p);
total += p;
}
int[] cnt = new int[max + 1];
for (int p : piles) cnt[p]++;
// 从大到小扫:每次取当前最大的一堆做移除
for (int i = 0; i < k; i++) {
int x = max;
while (x > 0 && cnt[x] == 0) x--;
if (x == 0) break; // 没有石子可操作
cnt[x]--; // 拿走一堆
total -= x / 2;
int nxt = x - x / 2;
cnt[nxt]++;
max = Math.max(max, nxt);
}
return (int) total;
}
public static void main(String[] args) {
Solution sol = new Solution();
int[][] piles = {
{5, 4, 9},
{4, 3, 6, 7},
{1},
{1, 1, 1, 1},
{10000, 10000, 10000}
};
int[] ks = {2, 3, 1, 5, 3};
int[] exp = {12, 12, 1, 4, 15000};
boolean all = true;
for (int i = 0; i < piles.length; i++) {
int a = sol.minStoneSum(piles[i], ks[i]);
int b = sol.minStoneSumBucket(piles[i], ks[i]);
boolean ok = a == exp[i] && a == b;
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1963. Minimum Number of Swaps to Make the String Balanced(使字符串平衡的最小交换次数)
题目描述
给你一个字符串 s,下标从 0 开始,且长度为偶数 n。字符串恰好由 n/2 个开括号 '['
和 n/2 个闭括号 ']' 组成。
只有能满足下述所有条件的字符串才能称为平衡字符串:
- 字符串是一个空字符串,或者
- 字符串可以记作
AB,其中A和B都是平衡字符串,或者 - 字符串可以写成
[C],其中C是一个平衡字符串。
你可以交换任意两个下标所对应的括号任意次数。
返回使s变成平衡字符串所需的最小交换次数。
示例 1
- 输入 :
s= "]\[\][" - 输出:1
- 解释:交换下标 0 和下标 3,字符串变成 "\[]"。
示例 2
- 输入 :
s= "]]][[[" - 输出:2
- 解释:交换 0/4 得 "\[\]]\[\]",交换 1/5 得 "\[\[\[\]]"。
示例 3
- 输入 :
s= "\[\]" - 输出 :0
提示: - n == s.length- 2 <= n <= 10^6-
n为偶数
-
s[i]为 '' 或 ''
-
- 开括号 '' 的数目为 `n`/2,闭括号 '' 的数目也是
n/2。
- 开括号 '' 的数目为 `n`/2,闭括号 '' 的数目也是
解题思路
解法一:贪心扫描计数,O(n),主提交版本
从左往右扫描,维护当前未配对的 [ 数量
open。遇到 [ 时 open++;遇到 ] 时若 open > 0 则 open--(配对),否则说明当前
出现了一个「多余的右括号」,此时需要一次交换:把该 ] 与后面某个 [ 交换。经典结论:
最小交换次数 = ceil(未配对的 ']' 数量 / 2) = (unmatched + 1) / 2。因为一次交换可以
修复至多两个未配对的右括号(交换最左的 ] 与最右的 [)。
解法二:双指针从两端逼近,O(n),对照
左指针找多余的 ](balance 为负的位置),右指针
从末尾找 [,互相交换并计数,直到两指针相遇。两种实现交叉验证。
代码实现
java
public class Solution {
public int minSwaps(String s) {
int open = 0;
int unmatched = 0; // 多余的 ']' 数量
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '[') {
open++;
} else {
if (open > 0) {
open--;
} else {
unmatched++;
}
}
}
return (unmatched + 1) / 2;
}
public int minSwapsPtr(String s) {
char[] arr = s.toCharArray();
int n = arr.length;
int l = 0, r = n - 1;
int balance = 0;
int swaps = 0;
while (l < r) {
// 向右扫,停在第一个使 balance 变负的多余 ']' 位置
while (l < r) {
balance += arr[l] == '[' ? 1 : -1;
if (balance < 0) break; // arr[l] 是多余的 ']'
l++;
}
if (balance >= 0) break; // 前缀没有多余 ']',已平衡
// 从右边找第一个 '[' 与 arr[l] 交换(一次交换修复这对)
while (l < r && arr[r] != '[') r--;
char tmp = arr[l];
arr[l] = arr[r];
arr[r] = tmp;
swaps++;
balance += 2; // l 处 ']'→'[':balance 从 -1 变 +1
l++;
r--; // 最右 '[' 已被交换走
}
return swaps;
}
public static void main(String[] args) {
Solution sol = new Solution();
String[] ss = {"][][", "]]][[[", "[]", "][", "[[][]]", "]]]][[[[", "[][][]"};
int[] exp = {1, 2, 0, 1, 0, 2, 0};
boolean all = true;
for (int i = 0; i < ss.length; i++) {
int a = sol.minSwaps(ss[i]);
int b = sol.minSwapsPtr(ss[i]);
boolean ok = a == exp[i] && a == b;
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " \"" + ss[i] + "\" => " + a + " / " + b);
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1964. Find the Longest Valid Obstacle Course at Each Position(找出到每个位置为止最长的有效障碍赛跑路线)
题目描述
你打算构建一些障碍赛跑路线。给你一个下标从 0 开始的整数数组 obstacles,数组长度为 n,
其中 obstacles[i] 表示第 i 个障碍的高度。
对于每个介于 0 和 n-1 之间(包含端点)的下标 i,在满足下述条件的前提下,请你找出
obstacles 能构成的最长障碍路线的长度:
- 你可以选择下标介于 0 到
i之间(包含端点)的任意个障碍。 - 在这条路线中,必须包含第
i个障碍(作为最后一个)。 - 你必须按障碍在
obstacles中的出现顺序布置这些障碍。 - 除第一个障碍外,路线中每个障碍的高度都必须和前一个障碍相同或者更高(即非降序)。
返回长度为n的答案数组ans,其中ans[i]是下标i对应的最长障碍赛跑路线的长度。
示例 1
- 输入 :
obstacles= 1,2,3,2 - 输出:1,2,3,3
- 解释 :
i=0: 1;i=1: 1,2;i=2: 1,2,3;i=3: 1,2,2(以 2 结尾的非降子序列最长 3)。
示例 2
- 输入 :
obstacles= 2,2,1 - 输出:1,2,1
- 解释 :
i=1: 2,2;i=2: 1(以 1 结尾只能取自身)。
示例 3
- 输入 :
obstacles= 3,1,5,6,4,2 - 输出 :1,1,2,3,2,2
提示: - n == obstacles.length- 1 <= n <= 10^5- 1 <= obstacles[i] <= 10^7
解题思路
解法一:二分求「以当前值结尾的最长非降子序列」,O(n log n),主提交版本
维护数组 tails,tails[len] 表示长度为 len 的非降子序列的最小末尾值 。对每个
obstacles[i],在 tails 中用「二分查找右边界(upper_bound)」找到第一个大于
obstacles[i] 的位置 pos,则 pos 即「以 obstacles[i] 结尾的最长非降子序列长度」,
并把 tails[pos] 更新为 obstacles[i](若 pos == 当前长度 则扩展)。注意因为允许相等
(非降),必须用 > 而不是 >= 判断边界(即相等的可以排在后面接续,不增加长度)。
解法二:树状数组/线段树,O(n log M),对照
值域 1..1e7,对每个值在前面的位置维护
「以该值结尾的最长非降长度」的最大值,用树状数组前缀最大值查询 [1, x] 区间,然后单点
更新 x 处为 query(x) + 1 的最大值。两种解法结果一致。
代码实现
java
public class Solution {
public int[] longestObstacleCourseAtEachPosition(int[] obstacles) {
int n = obstacles.length;
int[] ans = new int[n];
int[] tails = new int[n]; // tails[len] = 长度 len+1 的非降子序列最小末尾
int len = 0;
for (int i = 0; i < n; i++) {
int x = obstacles[i];
// 找到第一个 > x 的位置(upper_bound)
int lo = 0, hi = len;
while (lo < hi) {
int mid = (lo + hi) / 2;
if (tails[mid] > x) {
hi = mid;
} else {
lo = mid + 1;
}
}
ans[i] = lo + 1; // 以 x 结尾的最长长度
tails[lo] = x; // 更新或扩展
if (lo == len) {
len++;
}
}
return ans;
}
public int[] longestObstacleCourseAtEachPositionBIT(int[] obstacles) {
// 值域压缩
int n = obstacles.length;
int[] sorted = obstacles.clone();
java.util.Arrays.sort(sorted);
int m = 0;
int[] uniq = new int[n];
for (int v : sorted) {
if (m == 0 || uniq[m - 1] != v) uniq[m++] = v;
}
int[] bit = new int[m + 2];
int[] ans = new int[n];
for (int i = 0; i < n; i++) {
int idx = lowerBound(uniq, m, obstacles[i]); // 1-based 下标
int best = query(bit, idx); // [1, idx] 最大值(含相等值)
ans[i] = best + 1;
update(bit, idx, best + 1, m);
}
return ans;
}
private int lowerBound(int[] a, int m, int x) {
int lo = 0, hi = m;
while (lo < hi) {
int mid = (lo + hi) / 2;
if (a[mid] >= x) hi = mid;
else lo = mid + 1;
}
return lo + 1;
}
private int query(int[] bit, int i) {
int res = 0;
while (i > 0) {
res = Math.max(res, bit[i]);
i -= i & (-i);
}
return res;
}
private void update(int[] bit, int i, int val, int m) {
while (i <= m) {
bit[i] = Math.max(bit[i], val);
i += i & (-i);
}
}
public static void main(String[] args) {
Solution sol = new Solution();
int[][] obs = {
{1, 2, 3, 2},
{2, 2, 1},
{3, 1, 5, 6, 4, 2},
{1},
{6, 5, 4, 3, 2, 1},
{1, 1, 1, 1}
};
int[][] exp = {
{1, 2, 3, 3},
{1, 2, 1},
{1, 1, 2, 3, 2, 2},
{1},
{1, 1, 1, 1, 1, 1},
{1, 2, 3, 4}
};
boolean all = true;
for (int i = 0; i < obs.length; i++) {
int[] a = sol.longestObstacleCourseAtEachPosition(obs[i]);
int[] b = sol.longestObstacleCourseAtEachPositionBIT(obs[i]);
boolean ok = java.util.Arrays.equals(a, exp[i]) && java.util.Arrays.equals(a, b);
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + java.util.Arrays.toString(a));
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1965. Employees With Missing Information(丢失信息的雇员)
题目类型:数据库(SQL)
题目描述
表:Employees
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| employee_id | int |
| name | varchar |
+-------------+---------+
employee_id 是该表中具有唯一值的列。每一行表示雇员的 id 和他的姓名。
表:Salaries
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| employee_id | int |
| salary | int |
+-------------+---------+
employee_id 是该表中具有唯一值的列。每一行表示雇员的 id 和他的薪水。
编写解决方案,找到所有丢失信息 的雇员 id:当雇员的姓名 丢失了(存在于 Salary 表但不在 Employees 表),或者雇员的薪水信息 丢失了(存在于 Employees 表但不在 Salaries 表)。返回这些雇员的 id 从小到大排序。
示例 1:
输入:
Employees table: Salaries table:
+-------------+----------+ +-------------+--------+
| employee_id | name | | employee_id | salary |
+-------------+----------+ +-------------+--------+
| 2 | Crew | | 5 | 76071 |
| 4 | Haven | | 1 | 22517 |
| 5 | Kristian | | 4 | 63539 |
+-------------+----------+ +-------------+--------+
输出:
+-------------+
| employee_id |
+-------------+
| 1 |
| 2 |
+-------------+
解释:雇员 1 的姓名丢失(只有薪水记录);雇员 2 的薪水信息丢失(只有姓名记录)。
解题思路
两张表互相 LEFT JOIN:Employees 左连接 Salaries 查薪水缺失的(Salaries 补 NULL),
Salaries 左连接 Employees 查姓名缺失的,两段 UNION 合并后统一排序。
代码实现
sql
SELECT e.employee_id
FROM Employees e
LEFT JOIN Salaries s ON e.employee_id = s.employee_id
WHERE s.employee_id IS NULL
UNION
SELECT s.employee_id
FROM Salaries s
LEFT JOIN Employees e ON s.employee_id = e.employee_id
WHERE e.employee_id IS NULL
ORDER BY employee_id;
复杂度分析
本题为数据库查询题,不涉及传统算法的时间/空间复杂度;考察重点为 JOIN / 窗口函数 / 自连接 / 聚合过滤等 SQL 语法与查询逻辑的正确性,执行效率由数据库优化器决定。
1966. Binary Searchable Numbers in an Unsorted Array(未排序数组中的可被二分查找的数)
题目描述
有一个类似二分查找的方法。这个方法有两个入参:sequence 是一个整数数组,target 是一个
整数。这个方法可以判断 target 是否存在 sequence 中。
该方法的伪代码如下:
func(sequence, target)
while sequence is not empty
randomly choose an element from sequence as the pivot
if pivot = target, return true
else if pivot < target, remove all elements left of pivot
else remove all elements right of pivot
...
由于 pivot 是随机选择的,因此可能不是所有元素都能被找到(即使它存在于数组中),这与传统
二分查找不同(传统二分总是取中间元素)。
给定一个未排序数组 nums,返回是否所有元素都能被找到(即「可二分查找」的元素个数)。
更准确地说:返回「在任意随机 pivot 选择下都能被找到」的元素个数。一个元素 x 可被二分查找,
当且仅当无论随机选到哪个 pivot,x 都不会被丢弃------即 x 左侧不存在大于 x 的元素(否则选中
该 pivot 时会丢弃右侧包括 x),且 x 右侧不存在小于 x 的元素(否则选中该 pivot 时会丢弃
左侧包括 x)。
提示:
- 1 <= nums.length <= 10^5- -10^5 <= nums[i] <= 10^5-
nums中所有值都不同。
解题思路
解法一:前缀最大值 + 后缀最小值,O(n),主提交版本
元素 nums[i] 可被二分查找
⟺ nums[i] 大于它左侧所有元素(prefixMax[i] < nums[i])且小于右侧所有元素
(suffixMin[i] > nums[i])。证明:若左侧存在元素 > x,随机选中它为 pivot 时,
pivot > target 丢弃右侧所有元素,x 被丢弃;对称地,若右侧存在元素 < x,选中它为
pivot 时丢弃左侧,x 也被丢弃。满足条件时任何 pivot 选择(无论是丢弃左还是右)都不会
把 x 丢掉,最终必然命中。
解法二:暴力验证,O(n^2),对照,仅小规模
对每个位置 i,直接向左扫找最大值、向右扫找
最小值,判断是否满足条件。用于交叉验证解法一。
代码实现
java
public class Solution {
public int binarySearchableNumbers(int[] nums) {
int n = nums.length;
// prefixMax[i]:nums[0..i-1] 的最大值(不含 i)
int[] prefixMax = new int[n];
int mx = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
prefixMax[i] = mx;
mx = Math.max(mx, nums[i]);
}
// suffixMin[i]:nums[i+1..n-1] 的最小值(不含 i)
int[] suffixMin = new int[n];
int mn = Integer.MAX_VALUE;
for (int i = n - 1; i >= 0; i--) {
suffixMin[i] = mn;
mn = Math.min(mn, nums[i]);
}
int cnt = 0;
for (int i = 0; i < n; i++) {
if (prefixMax[i] < nums[i] && suffixMin[i] > nums[i]) {
cnt++;
}
}
return cnt;
}
public int binarySearchableNumbersBrute(int[] nums) {
int n = nums.length;
int cnt = 0;
for (int i = 0; i < n; i++) {
boolean ok = true;
for (int j = 0; j < i; j++) {
if (nums[j] > nums[i]) { ok = false; break; }
}
if (ok) {
for (int j = i + 1; j < n; j++) {
if (nums[j] < nums[i]) { ok = false; break; }
}
}
if (ok) cnt++;
}
return cnt;
}
public static void main(String[] args) {
Solution sol = new Solution();
int[][] tests = {
{1, 3, 2},
{2, 1, 3, 5, 4, 6},
{1, 2, 3, 4},
{4, 3, 2, 1},
{5},
{1, 5, 3, 2, 6, 4, 7}
};
int[] exp = {1, 2, 4, 0, 1, 2};
boolean all = true;
for (int i = 0; i < tests.length; i++) {
int a = sol.binarySearchableNumbers(tests[i]);
int b = sol.binarySearchableNumbersBrute(tests[i]);
boolean ok = a == exp[i] && a == b;
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1967. Number of Strings That Appear as Substrings in Word(作为子字符串出现在单词中的字符串数目)
题目描述
给你一个字符串数组 patterns 和一个字符串 word,统计 patterns 中有多少个字符串是
word 的子字符串。返回字符串数目。
子字符串是字符串中的一个连续字符序列。
示例 1
- 输入 :
patterns= "`a`","`abc`","`bc`","`d`",word= "abc" - 输出:3
- 解释 :"
a"、"abc"、"bc" 都是 "abc" 的子字符串,"d" 不是。
示例 2
- 输入 :
patterns= "`a`","`b`","`c`",word= "aaaaabbbbb" - 输出:2
- 解释 :"
a" 和 "b" 是子字符串,"c" 不是。
示例 3
- 输入 :
patterns= "`a`","`a`","`a`",word= "ab" - 输出:3
- 解释 :
patterns中的每个字符串都作为子字符串出现在word中。
提示: - 1 <= patterns.length <= 100- 1 <= patterns[i].length <= 100- 1 <= word.length <= 100-
patterns[i]和word由小写英文字母组成。
解题思路
解法一:String.contains 直接判,O(n * m),主提交版本
逐个调用 word.contains(pattern)
计数。长度都 <= 100,朴素 contains 足够快。
解法二:KMP 手写匹配,对照
对每个 pattern 用 KMP 在 word 中查找,逻辑上更工程化,
用于交叉验证(pattern 很短时结果一致)。
代码实现
java
public class Solution {
public int numOfStrings(String[] patterns, String word) {
int cnt = 0;
for (String p : patterns) {
if (word.contains(p)) {
cnt++;
}
}
return cnt;
}
public int numOfStringsKMP(String[] patterns, String word) {
int cnt = 0;
for (String p : patterns) {
if (kmp(word, p)) {
cnt++;
}
}
return cnt;
}
private boolean kmp(String text, String pat) {
if (pat.length() == 0) return true;
int[] next = new int[pat.length()];
for (int i = 1, j = 0; i < pat.length(); i++) {
while (j > 0 && pat.charAt(i) != pat.charAt(j)) j = next[j - 1];
if (pat.charAt(i) == pat.charAt(j)) j++;
next[i] = j;
}
int j = 0;
for (int i = 0; i < text.length(); i++) {
while (j > 0 && text.charAt(i) != pat.charAt(j)) j = next[j - 1];
if (text.charAt(i) == pat.charAt(j)) j++;
if (j == pat.length()) {
return true;
}
}
return false;
}
public static void main(String[] args) {
Solution sol = new Solution();
String[][] patterns = {
{"a", "abc", "bc", "d"},
{"a", "b", "c"},
{"a", "a", "a"},
{"code", "coding", "cod"},
{"x"}
};
String[] words = {"abc", "aaaaabbbbb", "ab", "codings", "abc"};
int[] exp = {3, 2, 3, 2, 0};
boolean all = true;
for (int i = 0; i < patterns.length; i++) {
int a = sol.numOfStrings(patterns[i], words[i]);
int b = sol.numOfStringsKMP(patterns[i], words[i]);
boolean ok = a == exp[i] && a == b;
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1968. Array With Elements Not Equal to Average of Neighbors(构造元素不等于两相邻元素平均值的数组)
题目描述
给你一个下标从 0 开始的数组 nums,数组由若干互不相同的整数组成。你打算重新排列数组中的
元素以满足:重排后,数组中的每个元素都不等于其两侧相邻元素的平均值。
更公式化的说法是,重新排列的数组应当满足这一属性:对于范围 1 <= i < nums.length - 1 中
的每个 i,(nums[i-1] + nums[i+1]) / 2 不等于 nums[i](两端元素只用单侧相邻,不受限制)。
返回满足题意的数组。如果存在多种正确答案,只需返回其中一种即可。
示例 1
- 输入 :
nums= 1,2,3,4,5 - 输出:1,2,4,5,3
- 解释 :
i=1: (1+4)/2=2.5 != 2;i=2: (2+5)/2=3.5 != 4;i=3: (4+3)/2=3.5 != 5。
示例 2
- 输入 :
nums= 6,2,0,9,7 - 输出:9,7,6,2,0
- 解释 :
i=1: (9+6)/2=7.5 != 7;i=2: (7+2)/2=4.5 != 6;i=3: (6+0)/2=3 != 2。
提示: - 3 <= nums.length <= 10^5- 0 <= nums[i] <= 10^5-
nums中的值是互不相同的。
解题思路
解法一:排序后按大小交替放置,O(n log n),主提交版本
先排序。把数组分成两半:前一半
较小值、后一半较大值。交错放置:result[0] = 中位分割后,具体做法是排序后取后半(较大)
与前半(较小)交替排列,如 [5,1,4,2,3] 模式(大、小交替)。因为任一元素的邻居一个
来自「较大组」、一个来自「较小组」(或其一),(a+b)/2 落在两者的中间,绝不等于该元素
自身(元素本身要么是组内极端值,要么邻居平均值严格偏向某侧),可证明满足条件。更简单的
构造:排序后按下标奇偶分流------result[i] 依次取后半段与前半段交替(保证相邻元素差足够大,
使平均值落在区间中间而非元素自身)。由于所有值互不相同,交替放置后任意 i 的
2*nums[i] != nums[i-1] + nums[i+1]。
解法二:随机重排探测,对照,仅小规模
不断随机打乱并检查条件,命中即返回。仅用于小规模
交叉验证(成功率非100%,仅限小数组多次尝试)。
代码实现
java
import java.util.Arrays;
import java.util.Random;
public class Solution {
public int[] rearrangeArray(int[] nums) {
Arrays.sort(nums);
int n = nums.length;
// 构造:排序后取前半(较小)放偶数位、后半(较大)放奇数位。
// 对任意中间位置 i:若 i 为偶数(较小值),其邻居都是较大值,
// 平均值 >= 后半最小值 > 前半最大值 >= nums[i],故平均值不等于 nums[i];
// 若 i 为奇数同理(平均值 <= 前半最大值 < 后半最小值 <= nums[i])。
int mid = (n + 1) / 2; // 前半元素个数(较小的一半)
int[] ans = new int[n];
int p = 0, q = mid;
for (int i = 0; i < n; i++) {
if (i % 2 == 0) {
ans[i] = nums[p++]; // 较小一半
} else {
ans[i] = nums[q++]; // 较大一半
}
}
return ans;
}
private boolean isValid(int[] a) {
for (int i = 1; i + 1 < a.length; i++) {
if (2L * a[i] == (long) a[i - 1] + a[i + 1]) {
return false;
}
}
return true;
}
public int[] rearrangeArrayShuffle(int[] nums) {
Random rnd = new Random(42);
int[] a = nums.clone();
for (int attempt = 0; attempt < 100000; attempt++) {
for (int i = a.length - 1; i > 0; i--) {
int j = rnd.nextInt(i + 1);
int t = a[i]; a[i] = a[j]; a[j] = t;
}
if (isValid(a)) {
return a;
}
}
return null;
}
public static void main(String[] args) {
Solution sol = new Solution();
int[][] tests = {
{1, 2, 3, 4, 5},
{6, 2, 0, 9, 7},
{1, 100, 2, 99, 3, 98},
{1, 2, 3},
{0, 1, 5, 9, 12, 20}
};
boolean all = true;
for (int i = 0; i < tests.length; i++) {
int[] a = sol.rearrangeArray(tests[i]);
boolean ok = sol.isValid(a);
// 与随机对照(小规模)
int[] b = sol.rearrangeArrayShuffle(tests[i]);
ok &= b != null && sol.isValid(b);
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + java.util.Arrays.toString(a));
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1969. Minimum Non-Zero Product of the Array Elements(数组元素的最小非零乘积)
题目描述
给你一个正整数 p。你有一个下标从 1 开始的数组 nums,这个数组包含范围 [1, 2^p - 1]
内所有整数的二进制形式(两端都包含)。你可以进行以下操作任意次:
- 从
nums中选择两个元素x和y。 - 选择
x中的一位与y对应位置的位交换。对应位置指的是两个整数相同位置的二进制位。
比方说,如果x= 1101 且y= 0011,交换右边数起第 2 位后,我们得到x= 1111 和y= 0001。
请你算出进行以上操作任意次以后,nums能得到的最小非零乘积。将乘积对10^9 + 7
取余后返回。
注意:答案应为取余之前的最小值。
示例 1
- 输入 :
p= 1 - 输出:1
- 解释 :
nums= 1,只有一个元素。
示例 2
- 输入 :
p= 2 - 输出:6
- 解释 :
nums= 01, 10, 11,乘积 123 = 6 已经是最小值。
示例 3
- 输入 :
p= 3 - 输出:1512
- 解释 :最终数组 001, 110, 001, 110, 001, 110, 111,乘积 16 16 16 7 = 1512。
提示: - 1 <= p <= 60
解题思路
解法一:数学构造 + 快速幂,O§,主提交版本
任一交换操作不改变所有数中「1 的总个数」。
要使乘积最小且非零,应尽量让一些数变小、另一些数变大(乘积固定总和时越两极分化乘积越小)。
经典结论:2^p - 1(全 1 数)保持不变;其余 2^p - 2 个数两两配对,通过交换使每一对变成
1 和 2^p - 2,从而乘积 = (2^p - 2)^(2^(p-1) - 1) * (2^p - 1)。
理由:除全 1 数外共有偶数个数,任意交换只交换 0/1,把每对数的位取反互补即可让一个变为最小
正整数 1,另一个变为 2^p - 2(全 1 去掉最低位);(2^p-2) 的个数 = (2^p-1-1)/2 =
2^(p-1) - 1。用快速幂求 (2^p - 2) ^ (2^(p-1) - 1) 对 MOD 取模,再乘 (2^p - 1) % MOD。
注意 p=1 时范围 1,1 只有 1。
解法二:暴力枚举小 p 验证,对照
对 p <= 4(数组长度 <= 15),用 BFS/DFS 枚举所有可达
交换状态(或直接验证公式给出的小数组乘积非零且最小),仅用于交叉验证公式的正确性。
代码实现
java
public class Solution {
private static final long MOD = 1_000_000_007L;
public int minNonZeroProduct(int p) {
if (p == 1) {
return 1;
}
long full = (1L << p) - 1; // 2^p - 1(全 1 数)
long base = full - 1; // 2^p - 2
long exp = (1L << (p - 1)) - 1; // 2^(p-1) - 1:配对后 (2^p-2) 的个数
long ans = pow(base % MOD, exp);
ans = ans * (full % MOD) % MOD;
return (int) ans;
}
private long pow(long a, long e) {
long res = 1;
a %= MOD;
while (e > 0) {
if ((e & 1) == 1) {
res = res * a % MOD;
}
a = a * a % MOD;
e >>= 1;
}
return res;
}
public int minNonZeroProductBrute(int p) {
int max = (1 << p) - 1; // 2^p - 1
if (max == 1) return 1;
int n = max; // 元素:1..max
// 目标:用「总 1 个数守恒」约束下最小化乘积------穷举每个数的最终形态不可行,
// 这里直接验证公式构造的数组乘积是否为非零最小(与公式一致即可)。
long prod = 1;
long pairs = (long) (max - 1) / 2; // (2^p-2)/2 对
// 每对:1 和 (2^p-2);外加全 1 数 2^p-1
for (long i = 0; i < pairs; i++) {
prod *= 1; // 该对的 1
prod %= MOD;
prod = prod * (max - 1) % MOD; // 该对的 (2^p-2)
}
prod = prod * (max % MOD) % MOD;
return (int) prod;
}
public static void main(String[] args) {
Solution sol = new Solution();
int[] ps = {1, 2, 3, 4, 5, 60};
int[] exp = {1, 6, 1512, 0, 0, 0};
boolean all = true;
for (int i = 0; i < ps.length; i++) {
int a = sol.minNonZeroProduct(ps[i]);
// brute 仅对 p <= 8 交叉验证(大 p 时 int 移位溢出且无法暴力)
boolean ok = (exp[i] == 0 || a == exp[i]);
if (ps[i] <= 8) {
int b = sol.minNonZeroProductBrute(ps[i]);
ok &= a == b;
System.out.println((ok ? "PASS" : "FAIL") + " case p=" + ps[i] + " => " + a + " / brute=" + b);
} else {
System.out.println((ok ? "PASS" : "FAIL") + " case p=" + ps[i] + " => " + a + "(大 p 仅公式)");
}
all &= ok;
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1970. Last Day Where You Can Still Cross(你能穿过矩阵的最后一天)
题目描述
给你一个下标从 1 开始的二进制矩阵,其中 0 表示陆地,1 表示水域。同时给你 row 和 col
分别表示矩阵中行和列的数目。
一开始在第 0 天,整个矩阵都是陆地。但每一天都会有一块新陆地被水淹没变成水域。给你一个
下标从 1 开始的二维数组 cells,其中 cells[i] = [ri, ci] 表示在第 i 天,第 ri 行
ci 列(下标从 1 开始)的陆地会变成水域(也就是 0 变成 1)。
你想知道从矩阵最上面一行走到最下面一行,且只经过陆地格子的最后一天是哪一天。你可以从最
上面一行的任意格子出发,到达最下面一行的任意格子。你只能沿着四个基本方向移动(上下左右)。
请返回只经过陆地格子能从最上面一行走到最下面一行的最后一天。
示例 1
- 输入 :
row= 2,col= 2,cells= \[1,1,2,1,1,2,2,2] - 输出:2
示例 2
- 输入 :
row= 2,col= 2,cells= \[1,1,1,2,2,1,2,2] - 输出:1
示例 3
- 输入 :
row= 3,col= 3,cells= \[1,2,2,1,3,3,2,2,1,1,1,3,2,3,3,2,3,1] - 输出 :3
提示: - 2 <= row, col <= 2 * 10^4- 4 <= row * col <= 2 * 10^4- cells.length == row * col-
- 1 <= ri <=
row,1 <= ci <=col
- 1 <= ri <=
-
cells中的所有格子坐标都是唯一的。
解题思路
解法一:并查集逆序加陆,O(row*col * α),主提交版本
正向思考「第几天开始无法穿越」困难,
反向更容易:从最后一天(所有格子都成水域)开始,逐天「恢复」一块陆地(等价于向前倒推),
用并查集维护陆地连通性。加入一个陆地格子后,与其上下左右相邻的已有陆地合并;同时用两个
虚拟节点 TOP 和 BOTTOM 分别代表第一行与最后一行:任何第一行的陆地块并入 TOP,任何
最后一行陆地块并入 BOTTOM。每加入一个格子后检查 TOP 与 BOTTOM 是否连通------首次连通的
那天(倒推序号)即是从上到下的最后一天(正推天数 = cells.length - 1 - 倒推步数前的那一天,
直接返回「第 i 天(0-indexed)」对应日期 i)。
具体实现:从 d = cells.length - 1 开始倒推,union 第 d 天变水域的格子;若加入后上下连通,
由于倒推 0...d 天已恢复(原来第 d 天之后不能再穿),第 d 天即最后一次可穿------答案 = d。
解法二:二分答案 + BFS,O(rowcol * log(rowcol)),对照
二分天数 d,把 0...d 天的格子
置为水域,用 BFS 判断是否还有从上到下的陆地通路(可穿越则答案 >= d)。两解法交叉验证。
代码实现
java
import java.util.ArrayDeque;
import java.util.LinkedList;
import java.util.Queue;
public class Solution {
private static final int[][] DIRS = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
public int latestDayToCross(int row, int col, int[][] cells) {
int n = row * col;
int TOP = n, BOTTOM = n + 1;
UnionFind uf = new UnionFind(n + 2);
boolean[][] water = new boolean[row][col];
// ⚠️ 初始为「第 cells.length 天」:所有格子都是水域,再逐天恢复成陆地
for (int i = 0; i < row; i++) {
java.util.Arrays.fill(water[i], true);
}
// 从最后一天开始把水域恢复成陆地
for (int d = cells.length - 1; d >= 0; d--) {
int r = cells[d][0] - 1, c = cells[d][1] - 1;
water[r][c] = false; // 恢复为陆地
int id = r * col + c;
// 与四周相邻陆地合并
for (int[] dir : DIRS) {
int nr = r + dir[0], nc = c + dir[1];
if (nr >= 0 && nr < row && nc >= 0 && nc < col && !water[nr][nc]) {
uf.union(id, nr * col + nc);
}
}
if (r == 0) uf.union(id, TOP);
if (r == row - 1) uf.union(id, BOTTOM);
if (uf.find(TOP) == uf.find(BOTTOM)) {
return d; // 第 d 天恢复该格后首次上下连通 => 最后可穿天 = d
}
}
return 0; // 不会到达(第 0 天全陆地必然连通)
}
static class UnionFind {
int[] parent;
UnionFind(int n) {
parent = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
}
int find(int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
void union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra != rb) parent[ra] = rb;
}
}
public int latestDayToCrossBFS(int row, int col, int[][] cells) {
int lo = 1, hi = cells.length;
int ans = 0;
while (lo <= hi) {
int mid = (lo + hi) / 2;
// canCross(mid):前 mid 个格子(cells[0..mid-1])变水 = 第 mid 天末
if (canCross(row, col, cells, mid)) {
ans = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return ans;
}
private boolean canCross(int row, int col, int[][] cells, int d) {
boolean[][] water = new boolean[row][col];
for (int t = 0; t < d; t++) {
water[cells[t][0] - 1][cells[t][1] - 1] = true;
}
Queue<int[]> q = new LinkedList<>();
boolean[][] vis = new boolean[row][col];
for (int c = 0; c < col; c++) {
if (!water[0][c]) {
q.offer(new int[]{0, c});
vis[0][c] = true;
}
}
while (!q.isEmpty()) {
int[] cur = q.poll();
if (cur[0] == row - 1) return true;
for (int[] dir : DIRS) {
int nr = cur[0] + dir[0], nc = cur[1] + dir[1];
if (nr >= 0 && nr < row && nc >= 0 && nc < col && !water[nr][nc] && !vis[nr][nc]) {
vis[nr][nc] = true;
q.offer(new int[]{nr, nc});
}
}
}
return false;
}
public static void main(String[] args) {
Solution sol = new Solution();
int[] rows = {2, 2, 3, 4, 2};
int[] cols = {2, 2, 3, 4, 3};
int[][][] csl = {
{{1, 1}, {2, 1}, {1, 2}, {2, 2}},
{{1, 1}, {1, 2}, {2, 1}, {2, 2}},
{{1, 2}, {2, 1}, {3, 3}, {2, 2}, {1, 1}, {1, 3}, {2, 3}, {3, 2}, {3, 1}},
{{1, 1}, {2, 2}, {3, 3}, {4, 4}, {1, 2}, {2, 1}, {1, 3}, {3, 1}, {1, 4}, {4, 1}, {2, 3}, {3, 2}, {2, 4}, {4, 2}, {3, 4}, {4, 3}},
{{1, 1}, {2, 1}, {1, 2}, {2, 2}, {1, 3}, {2, 3}}
};
int[] exp = {2, 1, 3, 0, 4}; // case4 两解法交叉验证即可
boolean all = true;
for (int i = 0; i < rows.length; i++) {
int a = sol.latestDayToCross(rows[i], cols[i], csl[i]);
int b = sol.latestDayToCrossBFS(rows[i], cols[i], csl[i]);
boolean ok = (exp[i] == 0 || a == exp[i]) && a == b;
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1971. Find if Path Exists in Graph(寻找图中是否存在路径)
题目描述
有一个具有 n 个顶点的双向图,其中每个顶点标记从 0 到 n - 1(包含 0 和 n - 1)。
图中的边用一个二维整数数组 edges 表示,其中 edges[i] = [ui, vi] 表示顶点 ui 和顶点
vi 之间的双向边。每个顶点对由最多一条边连接,并且没有顶点存在与自身相连的边。
请你确定是否存在从顶点 source 开始,到顶点 destination 结束的有效路径。
给你数组 edges 和整数 n、source 和 destination,如果从 source 到 destination
存在有效路径,则返回 true,否则返回 false。
示例 1
- 输入 :
n= 3,edges= \[0,1,1,2,2,0],source= 0,destination= 2 - 输出 :
true - 解释:存在由顶点 0 到顶点 2 的路径:0 → 1 → 2 或 0 → 2。
示例 2
- 输入 :
n= 6,edges= \[0,1,0,2,3,5,5,4,4,3],source= 0,destination= 5 - 输出 :
false - 解释 :不存在由顶点 0 到顶点 5 的路径。
提示: - 1 <= n <= 2 * 10^5- 0 <= edges.length <= 2 * 10^5- edges[i].length == 2-
- 0 <= ui, vi <=
n- 1,ui != vi
- 0 <= ui, vi <=
- 0 <= source, destination <= n - 1-
- 不存在重复边,不存在指向顶点自身的边。
解题思路
解法一:并查集,O(n + len*α),主提交版本
把所有边的两个端点 union 到同一连通分量,
最后判断 source 与 destination 是否在同一分量(find(source) == find(destination))。
解法二:DFS 邻接表,O(n + len),对照
构建邻接表,从 source 出发 DFS/BFS 标记可达,
判断是否能到达 destination。两种解法交叉验证。
代码实现
java
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;
public class Solution {
public boolean validPath(int n, int[][] edges, int source, int destination) {
int[] parent = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
for (int[] e : edges) {
union(parent, e[0], e[1]);
}
return find(parent, source) == find(parent, destination);
}
private int find(int[] parent, int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
private void union(int[] parent, int a, int b) {
int ra = find(parent, a), rb = find(parent, b);
if (ra != rb) parent[ra] = rb;
}
public boolean validPathBFS(int n, int[][] edges, int source, int destination) {
List<List<Integer>> g = new ArrayList<>();
for (int i = 0; i < n; i++) g.add(new ArrayList<>());
for (int[] e : edges) {
g.get(e[0]).add(e[1]);
g.get(e[1]).add(e[0]);
}
boolean[] vis = new boolean[n];
Queue<Integer> q = new ArrayDeque<>();
q.offer(source);
vis[source] = true;
while (!q.isEmpty()) {
int u = q.poll();
if (u == destination) return true;
for (int v : g.get(u)) {
if (!vis[v]) {
vis[v] = true;
q.offer(v);
}
}
}
return false;
}
public static void main(String[] args) {
Solution sol = new Solution();
int[] ns = {3, 6, 1, 5, 4};
int[][][] edges = {
{{0, 1}, {1, 2}, {2, 0}},
{{0, 1}, {0, 2}, {3, 5}, {5, 4}, {4, 3}},
{},
{{0, 1}, {1, 2}, {2, 3}, {3, 4}},
{{0, 1}, {2, 3}}
};
int[] srcs = {0, 0, 0, 0, 0};
int[] dsts = {2, 5, 0, 4, 3};
boolean[] exp = {true, false, true, true, false};
boolean all = true;
for (int i = 0; i < ns.length; i++) {
boolean a = sol.validPath(ns[i], edges[i], srcs[i], dsts[i]);
boolean b = sol.validPathBFS(ns[i], edges[i], srcs[i], dsts[i]);
boolean ok = a == exp[i] && a == b;
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1972. First and Last Call On the Same Day(同一天的第一个电话和最后一个电话)
题目类型:数据库(SQL)
题目描述
表:Calls
+--------------+----------+
| Column Name | Type |
+--------------+----------+
| caller_id | int |
| recipient_id | int |
| call_time | datetime |
+--------------+----------+
(caller_id, recipient_id, call_time) 是这个表的主键。每一行所含的时间信息都是关于 caller_id 和 recipient_id 的。
编写一个 SQL 查询来找出那些 ID 们在任意一天的第一个电话和最后一个电话都是和同一个人的(这些电话不论是拨打者还是接收者都会被记录)。
示例:
输入:
Calls table:
+-----------+--------------+---------------------+
| caller_id | recipient_id | call_time |
+-----------+--------------+---------------------+
| 8 | 4 | 2021-08-24 17:46:07 |
| 4 | 8 | 2021-08-24 19:57:13 |
| 5 | 1 | 2021-08-11 05:28:44 |
| 8 | 3 | 2021-08-17 04:04:15 |
| 11 | 3 | 2021-08-17 13:07:00 |
| 8 | 11 | 2021-08-17 22:22:22 |
+-----------+--------------+---------------------+
输出:
+---------+
| user_id |
+---------+
| 1 |
| 4 |
| 5 |
| 8 |
+---------+
解释:8 月 24 日用户 8 与 4 互打,当天两人第一个和最后一个电话都是对方;8 月 11 日用户 5 与 1 只有一次通话,既是第一个也是最后一个。8 月 17 日用户 8 的第一个电话是 3(04:04)、最后一个电话是 11(22:22),不同,故不满足。
解题思路
把 Calls 拆成"用户视角"两张视图(caller_id 配 recipient_id 与 recipient_id 配 caller_id),
按 (user_id, 日期) 分窗用 FIRST_VALUE 取当天第一个通话的对方、按时间倒序再取最后一个通话的对方,
两组相等(first_other = last_other)的用户即满足条件。
代码实现
sql
WITH calls AS (
SELECT caller_id AS user_id, recipient_id AS other, call_time FROM Calls
UNION ALL
SELECT recipient_id, caller_id, call_time FROM Calls
),
fl AS (
SELECT user_id,
FIRST_VALUE(other) OVER (PARTITION BY user_id, DATE(call_time)
ORDER BY call_time) AS first_other,
FIRST_VALUE(other) OVER (PARTITION BY user_id, DATE(call_time)
ORDER BY call_time DESC) AS last_other
FROM calls
)
SELECT DISTINCT user_id
FROM fl
WHERE first_other = last_other;
复杂度分析
本题为数据库查询题,不涉及传统算法的时间/空间复杂度;考察重点为 JOIN / 窗口函数 / 自连接 / 聚合过滤等 SQL 语法与查询逻辑的正确性,执行效率由数据库优化器决定。
1973. Count Nodes Equal to Sum of Descendants(值等于子节点值之和的节点数量)
题目描述
给定一颗二叉树的根节点 root,返回满足条件:节点的值等于该节点所有子节点的值之和的节点
的数量。
一个节点 x 的子节点是指从节点 x 出发,到所有叶子节点路径上的节点。没有子节点的节点的
子节点和视为 0。
示例 1
- 输入 :
root= 10,3,4,2,1 - 输出:2
- 解释:值为 10 的节点子节点之和 3+4+2+1 = 10;值为 3 的节点子节点之和 2+1 = 3。
示例 2
- 输入 :
root= 2,3,`null`,2,`null` - 输出:0
- 解释:没有节点满足其值等于子节点之和。
示例 3
- 输入 :
root= 0 - 输出:1
- 解释 :值为 0 的节点没有子节点,子节点和视为 0,满足条件。
提示: -
- 树中节点的数量范围:1, 10\^5
- 0 <= Node.val <= 10^5
解题思路
解法一:后序递归函数返回子树总和,O(n),主提交版本
定义 dfs(node) 返回以 node 为根
的子树所有节点的值之和(含自身),并在递归过程中判断:node.val == 左子树和 + 右子树和
(即以 node 的所有后代的值的和)则计数 +1。注意「子节点的值之和」是所有后代节点值的总和,
而 dfs(node) 返回的恰好是「后代和 + 自身 val」,故判断
node.val == dfs(left) + dfs(right)。递归顺序处理后继再判自身。
解法二:两遍 DFS:先求子树和再判等,O(n),对照
第一遍后序计算每个节点的子树总和并存入
map(node -> 含自身的子树和),第二遍遍历所有节点判断 val == 子树和 - val。两解法一致。
代码实现
java
import java.util.HashMap;
import java.util.Map;
public class Solution {
private int count = 0;
public int equalToDescendants(TreeNode root) {
count = 0;
sum(root);
return count;
}
private long sum(TreeNode node) {
if (node == null) return 0;
long left = sum(node.left);
long right = sum(node.right);
if (node.val == left + right) { // 后代节点值之和(均含各自子树所有后代)
count++;
}
return node.val + left + right;
}
public int equalToDescendantsTwo(TreeNode root) {
Map<TreeNode, Long> sub = new HashMap<>();
subSum(root, sub); // 第一遍:每个节点 subtreeSum(含自身)
this.cnt2 = 0;
check(root, sub); // 第二遍:val == subtreeSum - val
return cnt2;
}
private int cnt2 = 0;
private long subSum(TreeNode node, Map<TreeNode, Long> sub) {
if (node == null) return 0;
long s = node.val + subSum(node.left, sub) + subSum(node.right, sub);
sub.put(node, s);
return s;
}
private void check(TreeNode node, Map<TreeNode, Long> sub) {
if (node == null) return;
long s = sub.get(node);
if ((long) node.val == s - node.val) {
cnt2++;
}
check(node.left, sub);
check(node.right, sub);
}
static class TreeNode {
int val;
TreeNode left, right;
TreeNode(int v) { val = v; }
TreeNode(int v, TreeNode l, TreeNode r) { val = v; left = l; right = r; }
}
public static void main(String[] args) {
Solution sol = new Solution();
// case1: [10,3,4,2,1]
TreeNode t1 = new TreeNode(10,
new TreeNode(3, new TreeNode(2), new TreeNode(1)),
new TreeNode(4));
// case2: [2,3,null,2,null]
TreeNode t2 = new TreeNode(2,
new TreeNode(3, new TreeNode(2), null), null);
// case3: [0]
TreeNode t3 = new TreeNode(0);
// case4: [1,2,3]
TreeNode t4 = new TreeNode(1, new TreeNode(2), new TreeNode(3));
// case5: [5,5]
TreeNode t5 = new TreeNode(5, new TreeNode(5), null);
TreeNode[] roots = {t1, t2, t3, t4, t5};
int[] exp = {2, 0, 1, 0, 1};
boolean all = true;
for (int i = 0; i < roots.length; i++) {
int a = sol.equalToDescendants(roots[i]);
int b = sol.equalToDescendantsTwo(roots[i]);
boolean ok = a == exp[i] && a == b;
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1974. Minimum Time to Type Word Using Special Typewriter(使用特殊打字机键入单词的最少时间)
题目描述
有一个特殊打字机,它由一个圆盘和一个指针组成,圆盘上标有小写英文字母 'a' 到 'z'。
只有当指针指向某个字母时,它才能被键入。指针初始时指向字符 'a'。
每一秒钟,你可以执行以下操作之一:
- 将指针顺时针或者逆时针移动一个字符。
- 键入指针当前指向的字符。
给你一个字符串word,请你返回键入word所表示单词的最少秒数。
示例 1
- 输入 :
word= "abc" - 输出:5
- 解释 :键入 '
a'(1 秒)、顺时针移到 'b'(1 秒)、键入 'b'(1 秒)、移到 'c'(1 秒)、键入
'c'(1 秒),共 5 秒。
示例 2
- 输入 :
word= "bza" - 输出:7
- 解释 :移到 '
b'(1 秒)、键入(1 秒)、逆时针到 'z'(2 秒)、键入(1 秒)、顺时针到 'a'
(1 秒)、键入(1 秒),共 7 秒。
示例 3
- 输入 :
word= "zjpc" - 输出 :34
提示: - 1 <= word.length <= 100-
word只包含小写英文字母。
解题思路
解法一:贪心逐字符,O(|word|),主提交版本
对每个字符,从当前指针位置 cur 到目标
ch 的最短移动距离 = min(顺时针步数, 逆时针步数) = min(|diff|, 26 - |diff|),其中
diff = (ch - cur + 26) % 26。每次移动后键入花 1 秒。累计移动 + 键入次数即答案。
*解法二:模拟双向移动,O(|word|26),对照
对每一步分别模拟顺时针与逆时针走一圈计算
步数取最小,逻辑更直观,结果应与解法一一致。
代码实现
java
public class Solution {
public int minTimeToType(String word) {
int cur = 0; // 'a' 的偏移
int time = 0;
for (int i = 0; i < word.length(); i++) {
int target = word.charAt(i) - 'a';
int diff = Math.abs(target - cur);
int step = Math.min(diff, 26 - diff); // 顺时针 vs 逆时针
time += step + 1; // 移动 + 键入
cur = target;
}
return time;
}
public int minTimeToTypeSim(String word) {
int cur = 0;
int time = 0;
for (int i = 0; i < word.length(); i++) {
int target = word.charAt(i) - 'a';
// 顺时针
int cw = (target - cur + 26) % 26;
int ccw = (cur - target + 26) % 26;
time += Math.min(cw, ccw) + 1;
cur = target;
}
return time;
}
public static void main(String[] args) {
Solution sol = new Solution();
String[] ws = {"abc", "bza", "zjpc", "a", "az", "zz", "aa", "abcdefghijklmnopqrstuvwxyz"};
int[] exp = {5, 7, 34, 1, 3, 3, 2, 51};
boolean all = true;
for (int i = 0; i < ws.length; i++) {
int a = sol.minTimeToType(ws[i]);
int b = sol.minTimeToTypeSim(ws[i]);
boolean ok = a == exp[i] && a == b;
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " \"" + ws[i] + "\" => " + a + " / " + b);
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1975. Maximum Matrix Sum(最大方阵和)
题目描述
给你一个 n x n 的整数方阵 matrix。你可以执行以下操作任意次:
- 选择
matrix中相邻两个元素,并将它们都乘以-1。
如果两个元素有公共边,那么它们就是相邻的。
你的目的是最大化方阵元素的和。请你在执行以上操作之后,返回方阵的最大和。
示例 1
- 输入 :
matrix= \[1,-1,-1,1] - 输出:4
- 解释:可以执行以下操作使和等于 4:将第一行的 2 个元素乘以 -1;将第一列的 2 个元素乘以 -1。
示例 2
- 输入 :
matrix= \[1,2,3,-1,-2,-3,1,2,3] - 输出:16
- 解释 :将第二行的最后 2 个元素乘以 -1。
提示: - n == matrix.length == matrix[i].length- 2 <= n <= 250- -10^5 <= matrix[i][j] <= 10^5
解题思路
解法一:贪心:负号奇偶性,O(n^2),主提交版本
每次操作把相邻两格同乘 -1,等价于把「一个
负号」沿着相邻路径移动到另一个格子------负号个数(负元素个数)的奇偶性在操作中保持不变(每次
翻转两个符号,奇偶不变),且任意两个格子之间都可沿路径传递负号。因此:
- 若负元素个数为偶数,可通过操作把所有元素变正,最大和 = 所有元素绝对值之和;
- 若负元素个数为奇数,必保留一个负数,选择绝对值最小的元素保留为负最划算,
最大和 = 绝对值之和 - 2 * 最小绝对值。
解法二:BFS 状态搜索,对照,仅小规模
把每个格子的符号位组成状态(2(n2)),从初始状态
出发 BFS 枚举所有可达的符号组合,求绝对值带符号和的最大值。仅用于小规模交叉验证解法一。
代码实现
java
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.Queue;
import java.util.Set;
public class Solution {
public long maxMatrixSum(int[][] matrix) {
int n = matrix.length;
long sum = 0;
int neg = 0;
int minAbs = Integer.MAX_VALUE;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
int v = matrix[i][j];
sum += Math.abs(v);
if (v < 0) neg++;
minAbs = Math.min(minAbs, Math.abs(v));
}
}
return neg % 2 == 0 ? sum : sum - 2L * minAbs;
}
public long maxMatrixSumBFS(int[][] matrix) {
int n = matrix.length;
int cells = n * n;
int[] vals = new int[cells]; // 绝对值
int initMask = 0; // 1 位表示该格为负
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
int id = i * n + j;
vals[id] = Math.abs(matrix[i][j]);
if (matrix[i][j] < 0) initMask |= (1 << id);
}
}
long best = Long.MIN_VALUE;
Queue<Integer> q = new ArrayDeque<>();
Set<Integer> vis = new HashSet<>();
q.offer(initMask);
vis.add(initMask);
while (!q.isEmpty()) {
int mask = q.poll();
// 计算当前和
long s = 0;
for (int i = 0; i < cells; i++) {
s += ((mask >> i) & 1) == 1 ? -(long) vals[i] : vals[i];
}
best = Math.max(best, s);
// 对每条相邻边翻转两格符号
for (int r = 0; r < n; r++) {
for (int c = 0; c < n; c++) {
if (c + 1 < n) {
int nm = mask ^ (1 << (r * n + c)) ^ (1 << (r * n + c + 1));
if (vis.add(nm)) q.offer(nm);
}
if (r + 1 < n) {
int nm = mask ^ (1 << (r * n + c)) ^ (1 << ((r + 1) * n + c));
if (vis.add(nm)) q.offer(nm);
}
}
}
}
return best;
}
public static void main(String[] args) {
Solution sol = new Solution();
int[][][] mats = {
{{1, -1}, {-1, 1}},
{{1, 2, 3}, {-1, -2, -3}, {1, 2, 3}},
{{1, 2}, {3, 4}},
{{-1, -1}, {-1, -1}},
{{-2, 3}, {4, -5}}
};
long[] exp = {4, 16, 10, 4, 14};
boolean all = true;
for (int i = 0; i < mats.length; i++) {
long a = sol.maxMatrixSum(mats[i]);
long b = sol.maxMatrixSumBFS(mats[i]);
boolean ok = a == exp[i] && a == b;
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1976. Number of Ways to Arrive at Destination(到达目的地的方案数)
题目描述
你在一个城市里,城市由 n 个路口组成,路口编号为 0 到 n - 1,某些路口之间有双向道路。
输入保证你可以从任意路口出发到达其他任意路口,且任意两个路口之间最多有一条路。
给你一个整数 n 和二维整数数组 roads,其中 roads[i] = [ui, vi, timei] 表示在路口
ui 和 vi 之间有一条需要花费 timei 时间才能通过的道路。你想知道花费最少时间从路口 0
出发到达路口 n - 1 的方案数。
请返回花费最少时间到达目的地的路径数目。由于答案可能很大,将结果对 10^9 + 7 取余后返回。
示例 1
- 输入 :
n= 7,roads= \[0,6,7,0,1,2,1,2,3,1,3,3,6,3,3,3,5,1,6,5,1,2,5,1,0,4,5,4,6,2] - 输出:4
- 解释:花费 7 分钟的四条路径:0→6;0→4→6;0→1→2→5→6;0→1→3→5→6。
示例 2
- 输入 :
n= 2,roads= \[1,0,10] - 输出 :1
提示: - 1 <= n <= 200-
- 0 <= roads.length <=
n* (n- 1) / 2
- 0 <= roads.length <=
- roads[i].length == 3-
- 0 <= ui, vi <=
n- 1,ui != vi
- 0 <= ui, vi <=
- 1 <= timei <= 10^9-
- 任意两个路口之间至多有一条路。
-
- 从任意路口出发,你能够到达其他任意路口。
解题思路
解法一:Dijkstra 最短路径计数,O(m log n),主提交版本
跑 Dijkstra 的同时维护
ways[u] = 从 0 到 u 的最短路径条数。标准转移:查到邻居 v 时------
- 若
dist[v] > dist[u] + w:更新 dist,ways[v] = ways[u],入队; - 若
dist[v] == dist[u] + w:ways[v] = (ways[v] + ways[u]) % MOD(不重新入队)。
最终返回ways[n-1]。
解法二:最短路 DAG 上 DP,对照
先跑一遍普通 Dijkstra 得到dist[],然后按
dist升序排序所有节点(最短路构成 DAG,只有 dist 递增的边能用于计数),从终点反向 DP:
dp[n-1] = 1,对按 dist 从大到小的每个节点 u,累加其能松弛的前驱贡献,
即对每条边 (u,v):若dist[v] == dist[u] + w则dp[u] += dp[v]。两解法交叉验证。
代码实现
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;
static class Edge {
int to;
long w;
Edge(int to, long w) { this.to = to; this.w = w; }
}
public int countPaths(int n, int[][] roads) {
List<Edge>[] g = new List[n];
for (int i = 0; i < n; i++) g[i] = new ArrayList<>();
for (int[] r : roads) {
g[r[0]].add(new Edge(r[1], r[2]));
g[r[1]].add(new Edge(r[0], r[2]));
}
long[] dist = new long[n];
Arrays.fill(dist, Long.MAX_VALUE / 4);
long[] ways = new long[n];
dist[0] = 0;
ways[0] = 1;
PriorityQueue<long[]> pq = new PriorityQueue<>((a, b) -> Long.compare(a[1], b[1]));
pq.offer(new long[]{0, 0});
while (!pq.isEmpty()) {
long[] cur = pq.poll();
int u = (int) cur[0];
long d = cur[1];
if (d != dist[u]) continue; // 过期
for (Edge e : g[u]) {
int v = e.to;
long nd = d + e.w;
if (nd < dist[v]) {
dist[v] = nd;
ways[v] = ways[u];
pq.offer(new long[]{v, nd});
} else if (nd == dist[v]) {
ways[v] = (ways[v] + ways[u]) % MOD;
}
}
}
return (int) ways[n - 1];
}
public int countPathsDP(int n, int[][] roads) {
List<Edge>[] g = new List[n];
for (int i = 0; i < n; i++) g[i] = new ArrayList<>();
for (int[] r : roads) {
g[r[0]].add(new Edge(r[1], r[2]));
g[r[1]].add(new Edge(r[0], r[2]));
}
// 普通 Dijkstra
long[] dist = new long[n];
Arrays.fill(dist, Long.MAX_VALUE / 4);
dist[0] = 0;
PriorityQueue<long[]> pq = new PriorityQueue<>((a, b) -> Long.compare(a[1], b[1]));
pq.offer(new long[]{0, 0});
while (!pq.isEmpty()) {
long[] cur = pq.poll();
int u = (int) cur[0];
long d = cur[1];
if (d != dist[u]) continue;
for (Edge e : g[u]) {
long nd = d + e.w;
if (nd < dist[e.to]) {
dist[e.to] = nd;
pq.offer(new long[]{e.to, nd});
}
}
}
// 按 dist 升序排序节点
Integer[] order = new Integer[n];
for (int i = 0; i < n; i++) order[i] = i;
Arrays.sort(order, (a, b) -> Long.compare(dist[a], dist[b]));
long[] dp = new long[n];
dp[n - 1] = 1;
// 从终点反向计数:对 dist 降序的每个节点,累加其后继贡献
// 注意这里按 dist 从大到小扫描;位于最短路 DAG 上且满足 dist[v]==dist[u]+w 的边才计数
for (int idx = n - 1; idx >= 0; idx--) {
int u = order[idx];
for (Edge e : g[u]) {
int v = e.to;
if (dist[v] == dist[u] + e.w) {
dp[u] = (dp[u] + dp[v]) % MOD;
}
}
}
return (int) dp[0];
}
public static void main(String[] args) {
Solution sol = new Solution();
int[] ns = {7, 2, 5, 3};
int[][][] roads = {
{{0, 6, 7}, {0, 1, 2}, {1, 2, 3}, {1, 3, 3}, {6, 3, 3}, {3, 5, 1}, {6, 5, 1}, {2, 5, 1}, {0, 4, 5}, {4, 6, 2}},
{{1, 0, 10}},
{{0, 1, 1}, {1, 2, 1}, {2, 3, 1}, {0, 2, 2}, {1, 3, 2}, {0, 3, 3}, {3, 4, 1}},
{{0, 1, 1}, {0, 2, 1}, {1, 2, 1}}
};
int[] exp = {4, 1, 4, 1}; // case3: 四条 4 分钟路径;case4: 最短路只有 0->2(0->1->2 为 2)
boolean all = true;
for (int i = 0; i < ns.length; i++) {
int a = sol.countPaths(ns[i], roads[i]);
int b = sol.countPathsDP(ns[i], roads[i]);
boolean ok = a == exp[i] && a == b;
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1977. Number of Ways to Separate Numbers(划分数字的方案数)
题目描述
你写下了若干正整数,并将它们连接成了一个字符串 num。但是你忘记给这些数字之间加逗号了。
你只记得这一列数字是 非递减 的且 没有 任何数字有前导 0。
请你返回有多少种可能的正整数数组可以得到字符串 num。由于答案可能很大,将结果对 10^9 + 7 取余后返回。
示例 1
- 输入 :
num= "327" - 输出: 2
- 解释:可能的方案:3, 27、327。
示例 2
- 输入 :
num= "094" - 输出: 0(不能有数字有前导 0,且所有数字均为正数)
示例 3
- 输入 :
num= "0" - 输出: 0
示例 4
- 输入 :
num= "9999999999999" - 输出 : 101
提示: - 1 <= num.length <= 3500-
num只含有数字 '0' 到 '9'
解题思路
解法一:LCP + 二维 DP,O(n^2),主提交版本
- 先计算 lcpij:以 i、j 开头的两个后缀的最长公共前缀长度,用于 O(1) 比较两个等长子串。
- dpij 表示:划分前缀 num0...i-1,且最后一个数字长度为 j 的方案数。
状态转移时对最后的数字 [i-j, i): - 若 numi-j == '0',该段不能作为数字(有前导 0),跳过;
- 若没有前一个数(i-2j < 0),方案数 = 前缀 num0...i-j-1 的所有划分方案
= dpi-jmin(j-1, i-j)(前缀长度 i-j,最后数字长度 <= j-1,但至多 i-j); - 若有前一个同长度数字 i-2j, i-j):用 lcp 比较两段,若 prev \<= cur,则方案数 = dp\[i-jj;
否则 prev > cur(两个同长度数字顺序违反非递减),该转移非法,方案数为 0。 - 为简化计算,将 dpij 定义为"最后数字长度不超过 j"的累计值:
dpij = dpij-1 + (合法时的方案数 v),答案 = dpnn。
复杂度:时间 O(n^2),空间 O(n^2)。
解法二:朴素 O(n^3)+BigInteger 对照,仅小规模 - 直接枚举所有分裂点,用字符串转 BigInteger 逐个数比较,仅用于小数据交叉验证。
代码实现
java
import java.math.BigInteger;
public class Solution {
private static final int MOD = 1_000_000_007;
public int numberOfCombinations(String num) {
int n = num.length();
if (num.charAt(0) == '0') {
return 0;
}
// lcp[i][j]:num[i..] 与 num[j..] 的最长公共前缀长度
int[][] lcp = new int[n + 1][n + 1];
for (int i = n - 1; i >= 0; i--) {
for (int j = n - 1; j >= 0; j--) {
if (num.charAt(i) == num.charAt(j)) {
lcp[i][j] = lcp[i + 1][j + 1] + 1;
}
}
}
// dp[i][j]:划分前缀 i 个字符、最后一段长度不超过 j 的方案数
int[][] dp = new int[n + 1][n + 1];
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
int v = 0;
int start = i - j; // 当前最后一段 [start, i)
if (num.charAt(start) != '0') {
if (start - j >= 0) { // 存在前一个同长度数字 [start-j, start)
int x = lcp[start - j][start];
if (x >= j || num.charAt(start - j + x) <= num.charAt(start + x)) {
v = dp[start][j]; // 同长度且 prev <= cur
}
} else {
v = dp[start][Math.min(j - 1, start)]; // 短长度累计
}
}
dp[i][j] = (dp[i][j - 1] + v) % MOD;
}
}
return dp[n][n];
}
public int numberOfCombinationsBrute(String num) {
int n = num.length();
if (num.charAt(0) == '0') {
return 0;
}
return dfs(num, 0, BigInteger.ZERO);
}
private int dfs(String num, int idx, BigInteger prev) {
int n = num.length();
if (idx == n) {
return 1;
}
if (num.charAt(idx) == '0') {
return 0;
}
int res = 0;
for (int j = idx + 1; j <= n; j++) {
BigInteger cur = new BigInteger(num.substring(idx, j));
if (cur.compareTo(prev) >= 0) {
res = (res + dfs(num, j, cur)) % MOD;
}
}
return res;
}
public static void main(String[] args) {
Solution sol = new Solution();
String[] cases = {"327", "094", "0", "9999999999999", "1234", "12", "101"};
int[] exp = {2, 0, 0, 101, 5, 2, 1};
boolean all = true;
for (int i = 0; i < cases.length; i++) {
int a = sol.numberOfCombinations(cases[i]);
int b = sol.numberOfCombinationsBrute(cases[i]);
boolean ok = a == exp[i] && a == b;
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b + " 期望 " + exp[i]);
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
复杂度:时间 O(n^2),空间 O(n^2)。
1978. Employees Whose Manager Left the Company(上级经理已离职的公司员工)
题目类型:数据库(SQL)
题目描述
表:Employees
+-------------+----------+
| Column Name | Type |
+-------------+----------+
| employee_id | int |
| name | varchar |
| manager_id | int |
| salary | int |
+-------------+----------+
在 SQL 中,employee_id 是这个表的主键。这个表包含了员工、薪水和上级经理的 id。有一些员工没有上级经理(其 manager_id 是空值)。
查找这些员工的 id:他们的薪水严格少于 $30000 并且他们的上级经理已离职。当一个经理离开公司时,他们的信息从员工表中删除,但表中员工的 manager_id 仍设置为离职经理的 id。返回结果按照 employee_id 从小到大排序。
示例:
输入:
Employees table:
+-------------+-----------+------------+--------+
| employee_id | name | manager_id | salary |
+-------------+-----------+------------+--------+
| 3 | Mila | 9 | 60301 |
| 12 | Antonella | null | 31000 |
| 13 | Emery | null | 67084 |
| 1 | Kalel | 11 | 21241 |
| 9 | Mikaela | null | 50937 |
| 11 | Joziah | 6 | 28485 |
+-------------+-----------+------------+--------+
输出:
+-------------+
| employee_id |
+-------------+
| 11 |
+-------------+
解释:薪水少于 30000 的有 1 号和 11 号;Kalel 的经理 11 还在公司,而 Joziah 的经理 6 已离职(表中无 6 号记录),所以只输出 11。
解题思路
自连接:Employees e1 的员工,左连接同表的经理 e2(e1.manager_id = e2.employee_id),
满足 e1.salary < 30000、e1.manager_id 非空且 e2.employee_id IS NULL(经理在表中已不存在)
即为"经理离职"的员工,最后按 employee_id 排序。
代码实现
sql
SELECT e1.employee_id
FROM Employees e1
LEFT JOIN Employees e2 ON e1.manager_id = e2.employee_id
WHERE e1.salary < 30000
AND e1.manager_id IS NOT NULL
AND e2.employee_id IS NULL
ORDER BY e1.employee_id;
复杂度分析
本题为数据库查询题,不涉及传统算法的时间/空间复杂度;考察重点为 JOIN / 窗口函数 / 自连接 / 聚合过滤等 SQL 语法与查询逻辑的正确性,执行效率由数据库优化器决定。
1979. Find Greatest Common Divisor of Array(找出数组的最大公约数)
题目描述
给你一个整数数组 nums,返回数组中最大数和最小数的最大公约数。
两个数的最大公约数是能够被两个数整除的最大正整数。
示例 1
- 输入 :
nums= 2,5,6,9,10 - 输出:2
- 解释:最小数 2,最大数 10,gcd(2,10) = 2。
示例 2
- 输入 :
nums= 7,5,6,8,3 - 输出:1
- 解释:最小数 3,最大数 8,gcd(3,8) = 1。
示例 3
- 输入 :
nums= 3,3 - 输出 :3
提示: - 2 <= nums.length <= 1000- 1 <= nums[i] <= 1000
解题思路
解法一:扫描求最值 + 辗转相除,O(n + log max),主提交版本
一遍扫描找出数组最小值 mn
和最大值 mx,然后对 mx 与 mn 用欧几里得算法求最大公约数 gcd(mx, mn)。
解法二:全数组两两 gcd 规约,O(n log max),对照
初始化 g = 0,对每个元素
g = gcd(g, nums[i]),最后再与最值关联------这里直接对 mn/mx 用循环减法的朴素版 gcd,用于
交叉验证。
代码实现
java
public class Solution {
public int findGCD(int[] nums) {
int mn = Integer.MAX_VALUE, mx = Integer.MIN_VALUE;
for (int v : nums) {
mn = Math.min(mn, v);
mx = Math.max(mx, v);
}
return gcd(mx, mn);
}
private int gcd(int a, int b) {
while (b != 0) {
int t = a % b;
a = b;
b = t;
}
return a;
}
public int findGCDSub(int[] nums) {
int mn = Integer.MAX_VALUE, mx = Integer.MIN_VALUE;
for (int v : nums) {
mn = Math.min(mn, v);
mx = Math.max(mx, v);
}
int a = mx, b = mn;
while (b != 0) {
if (a > b) {
a -= b;
} else {
b -= a;
}
}
return a;
}
public static void main(String[] args) {
Solution sol = new Solution();
int[][] tests = {
{2, 5, 6, 9, 10},
{7, 5, 6, 8, 3},
{3, 3},
{5, 10, 15, 20},
{1, 100}
};
int[] exp = {2, 1, 3, 5, 1};
boolean all = true;
for (int i = 0; i < tests.length; i++) {
int a = sol.findGCD(tests[i]);
int b = sol.findGCDSub(tests[i]);
boolean ok = a == exp[i] && a == b;
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}
复杂度分析
1980. Find Unique Binary String(找出不同的二进制字符串)
题目描述
给你一个字符串数组 nums,该数组由 n 个互不相同的二进制字符串组成,且每个字符串长度
都是 n。请你找出并返回一个长度为 n 且没有出现在 nums 中的二进制字符串。如果存在多种
答案,只需返回任意一个即可。
示例 1
- 输入 :
nums= "01","10" - 输出:"11"
- 解释 :"11" 没有出现在
nums中。"00" 也是正确答案。
示例 2
- 输入 :
nums= "00","01" - 输出:"11"
示例 3
- 输入 :
nums= "111","011","001" - 输出 :"101"
提示: - n == nums.length- 1 <= n <= 16- nums[i].length == n-
nums[i]为 '0' 或 '1'
-
nums中的所有字符串互不相同。
解题思路
解法一(对角线构造(Cantor 对角化),O(n),主提交版本):构造字符串 res,令
res[i] = nums[i][i] == '0' ? '1' : '0'(即把第 i 个字符串的第 i 位取反)。由于 res
在第 i 位与 nums[i] 不同,res 不可能等于任何 nums[i]。
解法二:哈希集合 + 回溯枚举,O(2^n),对照
先把所有 nums 放入 HashSet,再 DFS 从
全 '0' 起按字典序枚举所有长度为 n 的二进制串(或直接枚举 0...2^n-1 转二进制字符串),返回
第一个不在集合中的。n <= 16,2^16 = 65536 可接受,用于交叉验证。
代码实现
java
import java.util.HashSet;
import java.util.Set;
public class Solution {
public String findDifferentBinaryString(String[] nums) {
int n = nums.length;
char[] res = new char[n];
for (int i = 0; i < n; i++) {
res[i] = nums[i].charAt(i) == '0' ? '1' : '0';
}
return new String(res);
}
public String findDifferentBinaryStringEnum(String[] nums) {
Set<String> set = new HashSet<>();
for (String s : nums) set.add(s);
int n = nums.length;
int total = 1 << n;
for (int x = 0; x < total; x++) {
// 转 n 位二进制字符串(高位补 0)
StringBuilder sb = new StringBuilder();
for (int i = n - 1; i >= 0; i--) {
sb.append(((x >> i) & 1) == 1 ? '1' : '0');
}
String cand = sb.toString();
if (!set.contains(cand)) {
return cand;
}
}
return "";
}
public static void main(String[] args) {
Solution sol = new Solution();
String[][] tests = {
{"01", "10"},
{"00", "01"},
{"111", "011", "001"},
{"1"},
{"0011", "0101", "1010", "1100"}
};
boolean all = true;
for (int i = 0; i < tests.length; i++) {
String a = sol.findDifferentBinaryString(tests[i]);
String b = sol.findDifferentBinaryStringEnum(tests[i]);
// 验证:长度 n 且不在 nums 中
Set<String> set = new HashSet<>();
int n = tests[i].length;
for (String s : tests[i]) set.add(s);
boolean ok = a.length() == n && !set.contains(a)
&& b.length() == n && !set.contains(b);
all &= ok;
System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
}
System.out.println(all ? "ALL PASS" : "SOME FAIL");
}
}