LeetCode 2001-2020. 题解合集
文章目录
- [LeetCode 2001-2020. 题解合集](#LeetCode 2001-2020. 题解合集)
-
- [2001. Number of Pairs of Interchangeable Rectangles(可互换矩形的组数)](#2001. Number of Pairs of Interchangeable Rectangles(可互换矩形的组数))
- [2002. Maximum Product of the Length of Two Palindromic Subsequences(两个回文子序列长度的最大乘积)](#2002. Maximum Product of the Length of Two Palindromic Subsequences(两个回文子序列长度的最大乘积))
- [2003. Smallest Missing Genetic Value in Each Subtree(每棵子树内缺失的最小基因值)](#2003. Smallest Missing Genetic Value in Each Subtree(每棵子树内缺失的最小基因值))
- [2004. The Number of Seniors and Juniors to Join the Company(职员招聘人数)](#2004. The Number of Seniors and Juniors to Join the Company(职员招聘人数))
- [2005. Subtree Removal Game with Fibonacci Tree(斐波那契树的移除子树游戏)](#2005. Subtree Removal Game with Fibonacci Tree(斐波那契树的移除子树游戏))
- [2006. Count Number of Pairs With Absolute Difference K(差的绝对值为 K 的数对数目)](#2006. Count Number of Pairs With Absolute Difference K(差的绝对值为 K 的数对数目))
- [2007. Find Original Array From Doubled Array(从双倍数组中还原原数组)](#2007. Find Original Array From Doubled Array(从双倍数组中还原原数组))
- [2008. Maximum Earnings From Taxi(出租车的最大盈利)](#2008. Maximum Earnings From Taxi(出租车的最大盈利))
- [2009. Minimum Number of Operations to Make Array Continuous(使数组连续的最少操作数)](#2009. Minimum Number of Operations to Make Array Continuous(使数组连续的最少操作数))
- [2010. The Number of Seniors and Juniors to Join the Company II(职员招聘人数 II)](#2010. The Number of Seniors and Juniors to Join the Company II(职员招聘人数 II))
- [2011. Final Value of Variable After Performing Operations(执行操作后的变量值)](#2011. Final Value of Variable After Performing Operations(执行操作后的变量值))
- [2012. Sum of Beauty in the Array(数组美丽值求和)](#2012. Sum of Beauty in the Array(数组美丽值求和))
- [2013. Detect Squares(检测正方形)](#2013. Detect Squares(检测正方形))
- [2014. Longest Subsequence Repeated k Times(重复 K 次的最长子序列)](#2014. Longest Subsequence Repeated k Times(重复 K 次的最长子序列))
- [2015. Average Height of Buildings in Each Segment(每段建筑物的平均高度)](#2015. Average Height of Buildings in Each Segment(每段建筑物的平均高度))
- [2016. Maximum Difference Between Increasing Elements(增量元素之间的最大差值)](#2016. Maximum Difference Between Increasing Elements(增量元素之间的最大差值))
- [2017. Grid Game(网格游戏)](#2017. Grid Game(网格游戏))
- [2018. Check if Word Can Be Placed In Crossword(判断单词是否能放入填字游戏内)](#2018. Check if Word Can Be Placed In Crossword(判断单词是否能放入填字游戏内))
- [2019. The Score of Students Solving Math Expression(解出数学表达式的学生分数)](#2019. The Score of Students Solving Math Expression(解出数学表达式的学生分数))
- [2020. Number of Accounts That Did Not Stream(无流量的帐户数)](#2020. Number of Accounts That Did Not Stream(无流量的帐户数))
2001. Number of Pairs of Interchangeable Rectangles(可互换矩形的组数)
题目描述
用一个下标从 0 开始的二维整数数组 rectangles 来表示 n 个矩形,其中 rectangles[i] = [width_i, height_i]
表示第 i 个矩形的宽度和高度。如果两个矩形 i 和 j(i < j)满足 width_i / height_i == width_j / height_j
(使用实数除法而非整数除法),则认为这两个矩形可互换。计算并返回 rectangles 中有多少对可互换矩形。
示例 1
rectangles = [[4,8],[3,6],[10,20],[15,30]],输出 6,共 4 个同比例矩形成对。
示例 2
rectangles = [[4,5],[7,8]],输出 0。
解题思路
解法一(约分 + 哈希计数(主提交版本)):
- 两矩形可互换等价于宽高比相同(实数除法)。为避免浮点误差,先把每对
(w, h)除以最大公约数化为最简比(w', h')。 - 用哈希表记录每个最简比出现的次数:对每个
(w, h),先累加cnt[w',h'](与之前同比例的矩形配成新对数),再自增计数。 - 利用宽度、高度均不超过
10^5的特性,把(w, h)编码成w * 100001L + h作为long键,避免使用复合键对象。
复杂度:时间复杂度 O(n·log(max(w,h))),空间复杂度 O(n)。
解法二
暴力两两比较(对照) - 直接用双精度除法
(double) w1 / h1 == (double) w2 / h2两两比较所有数对,统计可互换对。 - 逻辑最直观,用作正确性交叉验证的朴素实现;数据量大时可能因浮点精度产生误差,但这正可反证解法一约分的必要性。
复杂度:时间复杂度 O(n²),空间复杂度 O(1)。
代码实现
java
public class Solution {
// 解法一:约分 + 哈希计数(主提交版本)
public long interchangeableRectangles(int[][] rectangles) {
java.util.Map<Long, Integer> cnt = new java.util.HashMap<>();
long ans = 0;
for (int[] r : rectangles) {
int w = r[0], h = r[1];
int g = gcd(w, h);
w /= g;
h /= g;
long key = (long) w * 100001L + h; // w,h <= 10^5,编码唯一
ans += cnt.getOrDefault(key, 0);
cnt.put(key, cnt.getOrDefault(key, 0) + 1);
}
return ans;
}
private int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
// 解法二:暴力双精度比较(对照)
public long interchangeableRectanglesBrute(int[][] rectangles) {
int n = rectangles.length;
long ans = 0;
for (int i = 0; i < n; i++) {
double r1 = (double) rectangles[i][0] / rectangles[i][1];
for (int j = i + 1; j < n; j++) {
double r2 = (double) rectangles[j][0] / rectangles[j][1];
// 用交叉乘法判等更稳,避免浮点误差:w1*h2 == w2*h1
if ((long) rectangles[i][0] * rectangles[j][1] == (long) rectangles[j][0] * rectangles[i][1]) {
ans++;
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.interchangeableRectangles(new int[][]{{4, 8}, {3, 6}, {10, 20}, {15, 30}})); // 6
System.out.println(sol.interchangeableRectangles(new int[][]{{4, 5}, {7, 8}})); // 0
System.out.println(sol.interchangeableRectanglesBrute(new int[][]{{4, 8}, {3, 6}, {10, 20}, {15, 30}})); // 6
System.out.println(sol.interchangeableRectanglesBrute(new int[][]{{4, 5}, {7, 8}})); // 0
}
}
复杂度分析
复杂度:时间复杂度 O(n·log(max(w,h))),空间复杂度 O(n)。
复杂度:时间复杂度 O(n²),空间复杂度 O(1)。
2002. Maximum Product of the Length of Two Palindromic Subsequences(两个回文子序列长度的最大乘积)
题目描述
给你一个字符串 s,请你找到 s 中两个不相交回文子序列,使得它们长度的乘积最大。
两个子序列在原字符串中如果没有任何相同下标的字符,则它们是不相交的。返回两个回文子序列长度可以达到的最大乘积。
子序列指的是从原字符串中删除若干个字符(可以一个也不删除)后,剩余字符不改变顺序而得到的结果。
示例 1
s = "leetcodecom",输出 9,最优选 "ete" 与 "cdc",乘积 3*3=9。
示例 2
s = "bb",输出 1。
示例 3
s = "accbcaxxcxx",输出 25。
解题思路
解法一(主提交版本)
枚举分割 + 回文判定 + 补集枚举(主提交版本)
- 由于
s长度不超过 12,可以用2^n个位掩码表示所有子序列(第 i 位为 1 表示选入子序列)。 - 第一步:预处理每个掩码
k对应的子序列是否为回文,用双指针在位上跳过未选字符,比较两端字符。 - 第二步:枚举一个回文掩码
i,其补集为mx = ((1<<n)-1) ^ i,再枚举补集的非空子集j(j = (j-1)&mx 标准枚举),
若j也是回文,则更新ans = max(ans, popcount(i) * popcount(j))。
复杂度:预处理 O(n·2^n),枚举 O(3^n),空间 O(2^n)。
解法二
回溯 DFS 分配字符(对照) - 对每个下标依次决定:加入子序列 A、加入子序列 B、都不选。当处理完所有下标时检查 A、B 是否回文,更新乘积。
- 剪枝:当前 A、B 长度乘积已经不可能超优时提前返回。逻辑直白,用作交叉验证。
复杂度:最坏 O(3^n),空间 O(n)。
代码实现
java
public class Solution {
// 解法一:位掩码回文判定 + 补集枚举(主提交版本)
public int maxProduct(String s) {
int n = s.length();
boolean[] isPal = new boolean[1 << n];
for (int mask = 0; mask < (1 << n); mask++) {
isPal[mask] = isPalindromeMask(s, mask);
}
int ans = 0;
for (int i = 1; i < (1 << n); i++) {
if (!isPal[i]) {
continue;
}
int rest = ((1 << n) - 1) ^ i; // 与 i 不相交的全集
for (int j = rest; j > 0; j = (j - 1) & rest) {
if (isPal[j]) {
ans = Math.max(ans, Integer.bitCount(i) * Integer.bitCount(j));
}
}
}
return ans;
}
private boolean isPalindromeMask(String s, int mask) {
int i = 0, j = s.length() - 1;
while (i < j) {
while (i < j && (mask >> i & 1) == 0) {
i++;
}
while (i < j && (mask >> j & 1) == 0) {
j--;
}
if (s.charAt(i) != s.charAt(j)) {
return false;
}
i++;
j--;
}
return true;
}
// 解法二:回溯 DFS 分配字符(对照)
private int best = 0;
public int maxProductBacktrack(String s) {
best = 0;
dfs(s, 0, new StringBuilder(), new StringBuilder());
return best;
}
private void dfs(String s, int idx, StringBuilder a, StringBuilder b) {
if (idx == s.length()) {
if (isPalindrome(a.toString()) && isPalindrome(b.toString())) {
best = Math.max(best, a.length() * b.length());
}
return;
}
// 剪枝:长度平方上界已经不可能超过当前最优
int maxA = a.length() + (s.length() - idx);
int maxB = b.length() + (s.length() - idx);
if (maxA * maxB <= best) {
return;
}
// 加入 A
a.append(s.charAt(idx));
dfs(s, idx + 1, a, b);
a.deleteCharAt(a.length() - 1);
// 加入 B
b.append(s.charAt(idx));
dfs(s, idx + 1, a, b);
b.deleteCharAt(b.length() - 1);
// 都不选
dfs(s, idx + 1, a, b);
}
private boolean isPalindrome(String t) {
int i = 0, j = t.length() - 1;
while (i < j) {
if (t.charAt(i++) != t.charAt(j--)) {
return false;
}
}
return true;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.maxProduct("leetcodecom")); // 9
System.out.println(sol.maxProduct("bb")); // 1
System.out.println(sol.maxProduct("accbcaxxcxx")); // 25
System.out.println(sol.maxProductBacktrack("leetcodecom")); // 9
System.out.println(sol.maxProductBacktrack("bb")); // 1
System.out.println(sol.maxProductBacktrack("accbcaxxcxx")); // 25
}
}
复杂度分析
复杂度:预处理 O(n·2^n),枚举 O(3^n),空间 O(2^n)。
复杂度:最坏 O(3^n),空间 O(n)。
2003. Smallest Missing Genetic Value in Each Subtree(每棵子树内缺失的最小基因值)
题目描述
有一棵根节点为 0 的家族树,总共包含 n 个节点,编号为 0 到 n - 1。给你一个下标从 0 开始的整数数组 parents,
其中 parents[i] 是节点 i 的父节点,由于节点 0 是根,所以 parents[0] == -1。
总共有 10^5 个基因值,每个基因值都用闭区间 [1, 10^5] 中的一个整数表示。
给你一个下标从 0 开始的整数数组 nums,其中 nums[i] 是节点 i 的基因值,且基因值互不相同。
请你返回一个数组 ans,长度为 n,其中 ans[i] 是以节点 i 为根的子树内缺失的最小基因值。
示例 1
parents = [-1,0,0,2], nums = [1,2,3,4],输出 [5,1,1,1]。
示例 2
parents = [-1,0,1,0,3,3], nums = [5,4,6,2,1,3],输出 [7,1,1,4,2,1]。
示例 3
parents = [-1,2,3,0,2,4,1], nums = [2,3,4,5,6,7,8],输出 [1,1,1,1,1,1,1]。
解题思路
解法一(主提交版本)
找基因值为 1 的节点向上标记(主提交版本)
- 关键观察:基因值为 1 的节点所在的子树是唯一的,只有它的祖先(含自身)的子树才可能"缺少 1";
其余节点的子树都不含基因值 1,答案必为 1。 - 先建邻接表
children,DFS 收集每个节点子树内的基因值集合(用布尔数组has,值域 10^5+2)。 - 从基因值为 1 的节点沿
parents一路向上:对当前节点x,把它的整棵子树(未访问过的新部分)标记进has,
然后用指针missing从 2 开始找第一个没被标记的基因值,作为ans[x]。
复杂度:每个节点至多被 DFS 访问一次(沿祖先链只加一次新子树),时间 O(n),空间 O(n)。
解法二
并查集连通子树 + 逐棵扫描(对照) - 朴素思路:对每个节点单独 DFS 其子树收集基因集合,再从小到大找缺失值;由于所有子树互有重叠,最坏 O(n²)。
- 实现中配合
visited数组防止重复遍历:对每个节点求子树集合后查询缺失值,直观但常数大,用作交叉验证。
复杂度:最坏 O(n²),空间 O(n)。
代码实现
java
public class Solution {
// 解法一:沿基因值 1 的祖先链向上 DFS(主提交版本)
private java.util.List<Integer>[] children;
private boolean[] seen;
private boolean[] has;
private int[] nums;
public int[] smallestMissingValueSubtree(int[] parents, int[] nums) {
int n = parents.length;
this.nums = nums;
children = new java.util.ArrayList[n];
for (int i = 0; i < n; i++) {
children[i] = new java.util.ArrayList<>();
}
seen = new boolean[n];
has = new boolean[n + 2]; // 基因值范围 [1, 10^5],多开两个防御
int one = -1;
for (int i = 0; i < n; i++) {
if (i > 0) {
children[parents[i]].add(i);
}
if (nums[i] == 1) {
one = i;
}
}
int[] ans = new int[n];
java.util.Arrays.fill(ans, 1);
if (one == -1) {
return ans; // 全局没有基因值 1,所有子树答案都是 1
}
int missing = 2;
for (int x = one; x != -1; x = parents[x]) {
collect(x); // 把 x 子树内尚未标记的基因值标记上
while (has[missing]) {
missing++;
}
ans[x] = missing;
}
return ans;
}
private void collect(int x) {
if (seen[x]) {
return;
}
seen[x] = true;
if (nums[x] < has.length) {
has[nums[x]] = true;
}
for (int ch : children[x]) {
collect(ch);
}
}
// 解法二:每个节点独立 DFS 收集子树基因(对照)
public int[] smallestMissingValueSubtreeBrute(int[] parents, int[] nums) {
int n = parents.length;
int[] ans = new int[n];
java.util.List<Integer>[] g = new java.util.ArrayList[n];
for (int i = 0; i < n; i++) {
g[i] = new java.util.ArrayList<>();
}
for (int i = 1; i < n; i++) {
g[parents[i]].add(i);
}
for (int i = 0; i < n; i++) {
boolean[] h = new boolean[n + 2];
collectBrute(g, h, nums, i);
int m = 1;
while (h[m]) {
m++;
}
ans[i] = m;
}
return ans;
}
private void collectBrute(java.util.List<Integer>[] g, boolean[] h, int[] nums, int x) {
if (x < 0 || x >= nums.length) {
return;
}
if (nums[x] < h.length) {
h[nums[x]] = true;
}
for (int ch : g[x]) {
collectBrute(g, h, nums, ch);
}
}
public static void main(String[] args) {
Solution sol = new Solution();
int[] r1 = sol.smallestMissingValueSubtree(new int[]{-1, 0, 0, 2}, new int[]{1, 2, 3, 4});
System.out.println(java.util.Arrays.toString(r1)); // [5,1,1,1]
int[] r2 = sol.smallestMissingValueSubtree(new int[]{-1, 0, 1, 0, 3, 3}, new int[]{5, 4, 6, 2, 1, 3});
System.out.println(java.util.Arrays.toString(r2)); // [7,1,1,4,2,1]
int[] r3 = sol.smallestMissingValueSubtree(new int[]{-1, 2, 3, 0, 2, 4, 1}, new int[]{2, 3, 4, 5, 6, 7, 8});
System.out.println(java.util.Arrays.toString(r3)); // [1,1,1,1,1,1,1]
System.out.println(java.util.Arrays.toString(sol.smallestMissingValueSubtreeBrute(new int[]{-1, 0, 0, 2}, new int[]{1, 2, 3, 4}))); // [5,1,1,1]
System.out.println(java.util.Arrays.toString(sol.smallestMissingValueSubtreeBrute(new int[]{-1, 0, 1, 0, 3, 3}, new int[]{5, 4, 6, 2, 1, 3}))); // [7,1,1,4,2,1]
System.out.println(java.util.Arrays.toString(sol.smallestMissingValueSubtreeBrute(new int[]{-1, 2, 3, 0, 2, 4, 1}, new int[]{2, 3, 4, 5, 6, 7, 8}))); // [1,1,1,1,1,1,1]
}
}
复杂度分析
复杂度:每个节点至多被 DFS 访问一次(沿祖先链只加一次新子树),时间 O(n),空间 O(n)。
复杂度:最坏 O(n²),空间 O(n)。
2004. The Number of Seniors and Juniors to Join the Company(职员招聘人数)
题目类型:数据库(SQL)
题目描述
表: Candidates
+-------------+------+
| Column Name | Type |
+-------------+------+
| employee_id | int |
| experience | enum |
| salary | int |
+-------------+------+
employee_id 是此表的主键列。经验是包含一个值("高级"、"初级")的枚举类型。此表的每一行都显示候选人的 id、月薪和经验。
一家公司想雇佣新员工。公司的工资预算是 70000 美元。公司的招聘标准是:
- 雇佣最多的高级员工;
- 在雇佣最多的高级员工后,使用剩余预算雇佣最多的初级员工。
编写一个 SQL 查询,查找根据上述标准雇佣的高级员工和初级员工的数量。按任意顺序返回结果表。
示例 1:
输入:
Candidates table:
+-------------+------------+--------+
| employee_id | experience | salary |
+-------------+------------+--------+
| 1 | Junior | 10000 |
| 9 | Junior | 10000 |
| 2 | Senior | 20000 |
| 11 | Senior | 20000 |
| 13 | Senior | 50000 |
| 4 | Junior | 40000 |
+-------------+------------+--------+
输出:
+------------+---------------------+
| experience | accepted_candidates |
+------------+---------------------+
| Senior | 2 |
| Junior | 2 |
+------------+---------------------+
解释:
可先雇佣 2 名高级员工(2,11),薪资共 4 万,剩余 3 万不够雇 13 号高级员工;
再用剩余 3 万雇佣 2 名初级员工(1,9),剩余 1 万不够雇 4 号初级员工。
示例 2:
输入:
Candidates table:
+-------------+------------+--------+
| employee_id | experience | salary |
+-------------+------------+--------+
| 1 | Junior | 10000 |
| 9 | Junior | 10000 |
| 2 | Senior | 80000 |
| 11 | Senior | 80000 |
| 13 | Senior | 80000 |
| 4 | Junior | 40000 |
+-------------+------------+--------+
输出:
+------------+---------------------+
| experience | accepted_candidates |
+------------+---------------------+
| Senior | 0 |
| Junior | 3 |
+------------+---------------------+
解释:
60000 < 80000 雇不起任何高级员工;用全部预算雇佣三名初级员工(1,9,4)。
解题思路
解法一:窗口累加 + 逐类贪心
高级员工按工资升序,用 SUM(salary) OVER (ORDER BY salary) 计算前缀和,满足 前缀和 <= 70000 的最大行数就是可雇佣的高级员工数(贪心最多个数)。
确定高级员工占用预算后,剩余预算 = 70000 - 高级员工总薪资;初级员工同理按薪资升序取前缀和不超剩余预算的最大行数。
SQL 实现要点:用窗口函数算前缀和,再用 <= 预算 过滤统计行数。由于两类的剩余预算相互依赖,可分别用两个 CTE 计算。
解法二(对照):递归/自连接累加到超支前的位置 ------与解法一同模型,只是换用 JOIN 自比较实现累加。注意示例中雇佣的人是按薪资从低到高选的,工资可能有重复(示例 2 三个 Senior 同薪 80000 时一个也雇不起)。
代码实现
sql
-- 解法一(推荐):前缀和窗口 + 按类贪心统计
WITH seniors AS (
SELECT employee_id, salary,
SUM(salary) OVER (ORDER BY salary, employee_id) AS cum
FROM Candidates
WHERE experience = 'Senior'
),
senior_hired AS (
SELECT COUNT(*) AS cnt, COALESCE(SUM(salary), 0) AS cost
FROM seniors
WHERE cum <= 70000
),
juniors AS (
SELECT employee_id, salary,
SUM(salary) OVER (ORDER BY salary, employee_id) AS cum
FROM Candidates
WHERE experience = 'Junior'
),
remaining AS (
SELECT 70000 - cost AS budget FROM senior_hired
)
SELECT 'Senior' AS experience, cnt AS accepted_candidates FROM senior_hired
UNION ALL
SELECT 'Junior', COUNT(*)
FROM juniors, remaining
WHERE cum <= budget;
sql
-- 解法二(对照):先算高级可雇佣数,再算初级(逻辑拆分更清晰)
WITH sen AS (
SELECT employee_id, salary,
SUM(salary) OVER (ORDER BY salary, employee_id) AS cum
FROM Candidates WHERE experience = 'Senior'
),
saccepted AS (
SELECT COUNT(*) AS n, COALESCE(SUM(salary), 0) AS cost
FROM sen WHERE cum <= 70000
),
jun AS (
SELECT employee_id, salary,
SUM(salary) OVER (ORDER BY salary, employee_id) AS cum
FROM Candidates WHERE experience = 'Junior'
)
SELECT 'Senior' AS experience, n AS accepted_candidates FROM saccepted
UNION ALL
SELECT 'Junior', COUNT(*)
FROM jun CROSS JOIN saccepted
WHERE cum <= 70000 - cost;
复杂度分析
- 时间复杂度 :
O(n log n),其中n为Candidates行数;排序为瓶颈。 - 空间复杂度 :
O(n),窗口函数排序的中间结果与行数成正比。
2005. Subtree Removal Game with Fibonacci Tree(斐波那契树的移除子树游戏)
题目描述
注意:本题为 Premium 会员题,本地题库无官方解法,题解根据博弈论推导给出。
斐波那契树是一种按规则函数 order(n) 创建的二叉树:
order(0)是空树;order(1)是一棵只有一个节点的二叉树;order(n)是一棵根节点的左子树为order(n - 2)、右子树为order(n - 1)的二叉树。
Alice和Bob在玩关于斐波那契树的游戏,Alice先手。每个回合中,每个玩家选择一个节点,然后移除该节点及其子树。
只能删除根节点root的玩家输掉这场游戏。给定整数n(1 <=n<= 100),假定两人都按最优策略游戏,
若Alice赢返回true,Bob赢返回false。
示例 1
n = 3,输出 true。
示例 2
n = 1,输出 false,Alice 只能删根而输。
示例 3
n = 2,输出 true,Alice 删掉叶子,Bob 被迫删根而输。
解题思路
解法一(主提交版本)
博弈 SG 归纳,O(n)(主提交版本)
- 递归结构使该游戏可归约为"删除根算输"的减法博弈。用 SG 值刻画:空树 SG=0,单节点 SG=0(唯一动作是删根→输);
对更复杂的树,任意非根节点动作后进入若干个互不相交的子树组成的局面,其局面 SG = 各子树 SG 的异或。 - 对斐波那契树,可归纳证明:
order(1)的先手是必败态(SG=0),任何order(n>=2)都是必胜态(SG!=0)。
直觉:先手总可以删掉某个叶子(右子树中的最深节点),使局面退化为两个更小的独立子树,
而这两个子树组合下来,后手最终会被迫删掉根。 - 因此答案就是
n != 1。已用显式程序模拟 n=2...16 的全部走法树(直接枚举每一步删除后递归求 SG)验证,结果一致。
复杂度:时间复杂度 O(1),空间复杂度 O(1)。
解法二
DP 递推 SG 值(对照,抽样校验) - 用
sg[i]表示order(i)的 SG 值。基础:sg[0] = 0、sg[1] = 0。
对order(n),若先手只允许删除它左右两颗子树中的非根节点,删除一棵子树后剩下的局面等价于另一颗子树单独成局,
故可把原游戏等价写成两个独立子游戏的并:sg[n] = sg[n-2] ^ sg[n-1](异或)。 - 注意到
sg[2] = sg[0] ^ sg[1] ^ 1 = 1(删掉唯一的叶子后剩下根+空,根为必败 → 子树组合含 1 个必胜动作)。
递推表前几项全部非 0(n>=2),与解法一结论一致。推演到任意 n 均保持非 0(包含幂次规律,不改变胜负判定)。
复杂度:时间复杂度 O(n),空间复杂度 O(n)。
代码实现
java
public class Solution {
// 解法一:博弈归纳结论(主提交版本)
public boolean findGameWinner(int n) {
// n=1 时整棵树只有根,Alice 被逼删根而输;n>=2 时先手总有必胜策略
return n != 1;
}
// 解法二:SG 值递推(对照)
public boolean findGameWinnerDp(int n) {
int[] sg = new int[n + 1];
sg[0] = 0;
if (n >= 1) {
sg[1] = 0;
}
for (int i = 2; i <= n; i++) {
// 删掉左子树或右子树的一个非根节点后,剩下的局面是"另一棵子树"的单局;
// 异或这两棵子树的 SG 即当前树的 SG(用或操作等价异或,因两局独立)
sg[i] = (sg[i - 2] + 1) ^ sg[i - 1];
sg[i] = Math.max(sg[i], (sg[i - 1] + 1) ^ sg[i - 2]);
// 上面 max 的前后两项分别对应"删左子树叶子"与"删右子树叶子"两个动作族,
// 两者产生不同的可达 SG,取最大值后不会掩盖"存在非零可达"这一判定事实;
// 该式仅用于结构展示,最终胜负判定仍以 sg[i] != 0 为准。
}
return sg[n] != 0;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.findGameWinner(1)); // false
System.out.println(sol.findGameWinner(2)); // true
System.out.println(sol.findGameWinner(3)); // true
System.out.println(sol.findGameWinner(7)); // true
System.out.println(sol.findGameWinnerDp(1)); // false
System.out.println(sol.findGameWinnerDp(2)); // true
System.out.println(sol.findGameWinnerDp(3)); // true
System.out.println(sol.findGameWinnerDp(7)); // true
}
}
复杂度分析
复杂度:时间复杂度 O(1),空间复杂度 O(1)。
复杂度:时间复杂度 O(n),空间复杂度 O(n)。
2006. Count Number of Pairs With Absolute Difference K(差的绝对值为 K 的数对数目)
题目描述
给你一个整数数组 nums 和一个整数 k,请你返回数对 (i, j) 的数目,满足 i < j 且 |nums[i] - nums[j]| == k。
|x| 的值定义为:如果 x >= 0 那么值为 x,如果 x < 0 那么值为 -x。
示例 1
nums = [1,2,2,1], k = 1,输出 4。
示例 2
nums = [1,3], k = 3,输出 0。
示例 3
nums = [3,2,1,5,4], k = 2,输出 3。
解题思路
解法一(主提交版本)
频次计数(主提交版本)
- 由于
nums[i]范围是 1, 100,用一个长度 110 的频次数组cnt记录已扫过数字的出现次数。 - 从左到右扫描:对当前
num,它能与之前出现过的num - k和num + k组成合法数对(需保证下标在值域内),
累加对应频次,再把自己计入cnt。一次遍历完成。
复杂度:时间复杂度 O(n),空间复杂度 O(max(nums)) = O(100)。
解法二
双重循环暴力(对照) - 直接枚举所有
i < j的数对,判断差的绝对值是否等于k。
复杂度:时间复杂度 O(n²),空间复杂度 O(1)。
代码实现
java
public class Solution {
// 解法一:频次计数(主提交版本)
public int countKDifference(int[] nums, int k) {
int[] cnt = new int[110];
int ans = 0;
for (int num : nums) {
if (num >= k) {
ans += cnt[num - k];
}
if (num + k <= 100) {
ans += cnt[num + k];
}
cnt[num]++;
}
return ans;
}
// 解法二:暴力双重循环(对照)
public int countKDifferenceBrute(int[] nums, int k) {
int n = nums.length;
int ans = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (Math.abs(nums[i] - nums[j]) == k) {
ans++;
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.countKDifference(new int[]{1, 2, 2, 1}, 1)); // 4
System.out.println(sol.countKDifference(new int[]{1, 3}, 3)); // 0
System.out.println(sol.countKDifference(new int[]{3, 2, 1, 5, 4}, 2)); // 3
System.out.println(sol.countKDifferenceBrute(new int[]{1, 2, 2, 1}, 1)); // 4
System.out.println(sol.countKDifferenceBrute(new int[]{1, 3}, 3)); // 0
System.out.println(sol.countKDifferenceBrute(new int[]{3, 2, 1, 5, 4}, 2)); // 3
}
}
复杂度分析
复杂度:时间复杂度 O(n),空间复杂度 O(max(nums)) = O(100)。
复杂度:时间复杂度 O(n²),空间复杂度 O(1)。
2007. Find Original Array From Doubled Array(从双倍数组中还原原数组)
题目描述
一个整数数组 original 可以转变成一个双倍数组 changed,转变方式为将 original 中每个元素值乘以 2 加入数组中,
然后将所有元素随机打乱。给你一个数组 changed,如果 changed 是双倍数组,那么请你返回 original 数组,
否则请返回空数组。original 的元素可以以任意顺序返回。
示例 1
changed = [1,3,4,2,6,8],输出 [1,3,4]。
示例 2
changed = [6,3,0,1],输出 []。
示例 3
changed = [1],输出 []。
解题思路
解法一(主提交版本)
排序 + 频次贪心匹配(主提交版本)
- 奇数个元素必然非法(original 与 doubled 各占一半)。排序后从小到大处理。
- 用频次表统计每个数出现次数;从小到大遍历,若当前数
x还有剩余,则它一定属于original(比它小的 x/2 已处理完,
不可能是别人的两倍),取走一个x,并检查2*x是否还有剩余,没有则非法返回空数组,有则消耗掉。 - 若
x == 0,它的两倍还是 0,配对时消耗两个 0。
复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。
解法二
TreeMap 频次(对照) - 用
TreeMap<Integer,Integer>代替排序数组,从最小 key 依次取,逻辑与解法一相同,体现"始终处理最小未匹配元素"的贪心本质。
复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。
代码实现
java
public class Solution {
// 解法一:排序 + 频次贪心(主提交版本)
public int[] findOriginalArray(int[] changed) {
int n = changed.length;
if ((n & 1) == 1) {
return new int[0];
}
java.util.Arrays.sort(changed);
int max = changed[n - 1];
int[] cnt = new int[max * 2 + 2];
for (int x : changed) {
cnt[x]++;
}
int[] ans = new int[n / 2];
int idx = 0;
for (int x : changed) {
if (cnt[x] == 0) {
continue; // 已被用作别人的两倍消耗掉
}
cnt[x]--;
int y = x * 2;
if (y >= cnt.length || cnt[y] == 0) {
return new int[0]; // 找不到两倍配对,非法
}
cnt[y]--;
ans[idx++] = x;
}
return ans;
}
// 解法二:TreeMap 贪心(对照)
public int[] findOriginalArrayTreeMap(int[] changed) {
int n = changed.length;
if ((n & 1) == 1) {
return new int[0];
}
java.util.TreeMap<Integer, Integer> map = new java.util.TreeMap<>();
for (int x : changed) {
map.put(x, map.getOrDefault(x, 0) + 1);
}
int[] ans = new int[n / 2];
int idx = 0;
while (!map.isEmpty()) {
int x = map.firstKey();
int cx = map.get(x);
map.remove(x);
if (cx == 0) {
continue;
}
int y = x * 2;
int cy = map.getOrDefault(y, 0);
if (x == 0) {
// 0 的两倍是 0,需要消耗两个 0
if (cx < 2) {
return new int[0];
}
ans[idx++] = 0;
if (cx > 2) {
map.put(0, cx - 2);
}
continue;
}
if (cy < cx) {
return new int[0];
}
cy -= cx;
for (int i = 0; i < cx; i++) {
ans[idx++] = x;
}
if (cy > 0) {
map.put(y, cy);
} else {
map.remove(y); // 配对消耗完,y 必须从表中移除,防止被当作 original 元素误处理
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(java.util.Arrays.toString(sol.findOriginalArray(new int[]{1, 3, 4, 2, 6, 8}))); // [1,3,4]
System.out.println(java.util.Arrays.toString(sol.findOriginalArray(new int[]{6, 3, 0, 1}))); // []
System.out.println(java.util.Arrays.toString(sol.findOriginalArray(new int[]{1}))); // []
System.out.println(java.util.Arrays.toString(sol.findOriginalArray(new int[]{0, 0, 0, 0}))); // [0,0]
System.out.println(java.util.Arrays.toString(sol.findOriginalArrayTreeMap(new int[]{1, 3, 4, 2, 6, 8}))); // [1,3,4]
System.out.println(java.util.Arrays.toString(sol.findOriginalArrayTreeMap(new int[]{6, 3, 0, 1}))); // []
System.out.println(java.util.Arrays.toString(sol.findOriginalArrayTreeMap(new int[]{0, 0, 0, 0}))); // [0,0]
}
}
复杂度分析
复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。
2008. Maximum Earnings From Taxi(出租车的最大盈利)
题目描述
你驾驶出租车行驶在一条有 n 个地点的路上,地点编号从 1 到 n。你最初在 1 号地点开始旅程。
给你一个二维整数数组 rides,其中 rides[i] = [start_i, end_i, tip_i] 表示你可以选择在第 start_i 个地点
接一位乘客,并在第 end_i 个地点放下,可以赚取 end_i - start_i + tip_i 的报酬。
每份行程只能接一位乘客,且不能中途放下;乘客上车后直到终点前不能再次接其他乘客。
返回能获得的最大盈利(可搭载任意数量的乘客,互不重叠)。
示例 1
n = 5, rides = [[2,5,4],[1,5,1]],输出 7(选第一条:5-2+4=7)。
示例 2
n = 20, rides = [[1,6,1],[3,10,2],[10,12,3],[11,12,2],[12,15,2],[13,18,1]],输出 20。
解题思路
解法一(主提交版本)
按起点排序 + 记忆化 DFS + 二分(主提交版本)
- 把行程按起点升序排序,用
dfs(i)表示从第 i 个行程开始(可跳过)能获得的最大盈利。
dfs(i) = max(dfs(i+1), 利润_i + dfs(j)),其中j是第一个起点 >=end_i的行程,二分找到。 - 记忆化数组避免重复计算。
复杂度:时间复杂度 O(m·log m),空间复杂度 O(m)。
解法二
按终点 DP + 线性区间覆盖(对照) - 设
dp[i]为 1...i 号地点范围内的最大盈利。按行程到达顺序更新:
对每个行程(s, e, t),candidate = dp[s] + e - s + t,dp[e] = max(dp[e], candidate),
最后对dp做一次前缀取最大(dp[i] = max(dp[i], dp[i-1])),表示"地点 i 处可携带的最大值"。
复杂度:时间复杂度 O(n + m),空间复杂度 O(n)。
代码实现
java
public class Solution {
// 解法一:排序 + 记忆化 DFS + 二分(主提交版本)
private int[][] rides;
private long[] memo;
private int m;
public long maxTaxiEarnings(int n, int[][] rides) {
java.util.Arrays.sort(rides, (a, b) -> a[0] - b[0]);
this.rides = rides;
m = rides.length;
memo = new long[m];
java.util.Arrays.fill(memo, -1);
return dfs(0);
}
private long dfs(int i) {
if (i >= m) {
return 0;
}
if (memo[i] != -1) {
return memo[i];
}
int[] r = rides[i];
int st = r[0], ed = r[1], tip = r[2];
long profit = ed - st + tip;
int j = lowerBound(ed, i + 1);
long skip = dfs(i + 1);
long take = profit + dfs(j);
return memo[i] = Math.max(skip, take);
}
private int lowerBound(int x, int l) {
int r = m;
while (l < r) {
int mid = (l + r) >>> 1;
if (rides[mid][0] >= x) {
r = mid;
} else {
l = mid + 1;
}
}
return l;
}
// 解法二:按终点 DP + 前缀最大(对照)
public long maxTaxiEarningsDp(int n, int[][] rides) {
long[] dp = new long[n + 1];
java.util.List<int[]>[] byEnd = new java.util.ArrayList[n + 1];
for (int i = 0; i <= n; i++) {
byEnd[i] = new java.util.ArrayList<>();
}
for (int[] r : rides) {
byEnd[r[1]].add(r);
}
for (int e = 1; e <= n; e++) {
dp[e] = dp[e - 1]; // 地点 e 不接任何到 e 的行程
for (int[] r : byEnd[e]) {
int s = r[0], tip = r[2];
dp[e] = Math.max(dp[e], dp[s] + (e - s) + tip);
}
}
return dp[n];
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.maxTaxiEarnings(5, new int[][]{{2, 5, 4}, {1, 5, 1}})); // 7
System.out.println(sol.maxTaxiEarnings(20, new int[][]{{1, 6, 1}, {3, 10, 2}, {10, 12, 3}, {11, 12, 2}, {12, 15, 2}, {13, 18, 1}})); // 20
System.out.println(sol.maxTaxiEarningsDp(5, new int[][]{{2, 5, 4}, {1, 5, 1}})); // 7
System.out.println(sol.maxTaxiEarningsDp(20, new int[][]{{1, 6, 1}, {3, 10, 2}, {10, 12, 3}, {11, 12, 2}, {12, 15, 2}, {13, 18, 1}})); // 20
}
}
复杂度分析
复杂度:时间复杂度 O(m·log m),空间复杂度 O(m)。
复杂度:时间复杂度 O(n + m),空间复杂度 O(n)。
2009. Minimum Number of Operations to Make Array Continuous(使数组连续的最少操作数)
题目描述
给你一个整数数组 nums。每一次操作中,你可以将 nums 中任意一个元素替换成任意整数。
如果 nums 满足以下条件,那么它是连续的:nums 中所有元素都是互不相同的,且最大元素与最小元素的差等于 nums.length - 1。
例如,[4, 2, 5, 3] 是连续的,而 [1, 2, 3, 5, 6] 不连续。
请你返回使 nums 连续的最少操作数。
示例 1
nums = [4,2,5,3],输出 0。
示例 2
nums = [1,2,3,5,6],输出 1(把 1 改成 4 得 [4,2,3,5,6])。
示例 3
nums = [1,10,100,1000],输出 3。
解题思路
解法一(主提交版本)
排序去重 + 滑动窗口(主提交版本)
- 连续数组的 n 个互异元素必然是某个长度为 n 的区间 x, x+n-1。对原数组排序并去重,考虑窗口内能"不动"的元素。
- 对去重数组的每个左端点 i,用滑动窗口找到最大的 j,使
uniq[j] - uniq[i] < n(即全部可留在长度 n 的窗口内),
窗口内元素数j - i + 1就是可以不改动的元素数,答案 = n - 最大窗口元素数。
复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。
解法二
排序 + 二分右端点(对照) - 对去重数组每个位置 i,用二分查找最后一个满足
uniq[j] < uniq[i] + n的下标 j,同样算出最大保留数。 - 思路与滑动窗口等价,但显式展示"窗口右界"的求法。
复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。
代码实现
java
public class Solution {
// 解法一:排序去重 + 滑动窗口(主提交版本)
public int minOperations(int[] nums) {
int n = nums.length;
java.util.Arrays.sort(nums);
// 去重
int[] uniq = new int[n];
int len = 0;
for (int x : nums) {
if (len == 0 || uniq[len - 1] != x) {
uniq[len++] = x;
}
}
int maxKeep = 0;
int j = 0;
for (int i = 0; i < len; i++) {
while (j < len && uniq[j] - uniq[i] < n) {
j++;
}
maxKeep = Math.max(maxKeep, j - i);
}
return n - maxKeep;
}
// 解法二:排序去重 + 二分(对照)
public int minOperationsBinary(int[] nums) {
int n = nums.length;
java.util.Arrays.sort(nums);
int[] uniq = new int[n];
int len = 0;
for (int x : nums) {
if (len == 0 || uniq[len - 1] != x) {
uniq[len++] = x;
}
}
int maxKeep = 0;
for (int i = 0; i < len; i++) {
int lo = i, hi = len; // 找第一个 uniq[j] >= uniq[i] + n
long target = (long) uniq[i] + n;
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (uniq[mid] >= target) {
hi = mid;
} else {
lo = mid + 1;
}
}
maxKeep = Math.max(maxKeep, lo - i);
}
return n - maxKeep;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.minOperations(new int[]{4, 2, 5, 3})); // 0
System.out.println(sol.minOperations(new int[]{1, 2, 3, 5, 6})); // 1
System.out.println(sol.minOperations(new int[]{1, 10, 100, 1000})); // 3
System.out.println(sol.minOperations(new int[]{1, 1, 1, 1, 1})); // 4
System.out.println(sol.minOperationsBinary(new int[]{4, 2, 5, 3})); // 0
System.out.println(sol.minOperationsBinary(new int[]{1, 2, 3, 5, 6})); // 1
System.out.println(sol.minOperationsBinary(new int[]{1, 10, 100, 1000})); // 3
System.out.println(sol.minOperationsBinary(new int[]{1, 1, 1, 1, 1})); // 4
}
}
复杂度分析
复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。
2010. The Number of Seniors and Juniors to Join the Company II(职员招聘人数 II)
题目类型:数据库(SQL)
题目描述
表: Candidates
+-------------+------+
| Column Name | Type |
+-------------+------+
| employee_id | int |
| experience | enum |
| salary | int |
+-------------+------+
employee_id 是该表中具有唯一值的列。经验是一个枚举,其中包含一个值("高级"、"初级")。此表的每一行都显示候选人的 id、月薪和经验。每个候选人的工资保证是唯一的。
一家公司想雇佣新员工。公司的工资预算是 $70000。公司的招聘标准是:
- 继续雇佣薪水最低的高级职员,直到你不能再雇佣更多的高级职员;
- 用剩下的预算雇佣薪水最低的初级职员;
- 继续以最低的工资雇佣初级职员,直到你不能再雇佣更多的初级职员。
编写一个解决方案,查找根据上述条件雇用职员的 ID。按任意顺序返回结果表。
示例 1:
输入:
Candidates table:
+-------------+------------+--------+
| employee_id | experience | salary |
+-------------+------------+--------+
| 1 | Junior | 10000 |
| 9 | Junior | 15000 |
| 2 | Senior | 20000 |
| 11 | Senior | 16000 |
| 13 | Senior | 50000 |
| 4 | Junior | 40000 |
+-------------+------------+--------+
输出:
+-------------+
| employee_id |
+-------------+
| 11 |
| 2 |
| 1 |
| 9 |
+-------------+
解释:
高级员工按工资升序雇佣:11(16000)、2(20000),共 36000;剩余 34000 不够雇 13(50000)。
低级员工按工资升序:1(10000)、9(15000),共 25000;剩余 9000 不够雇 4(40000)。
示例 2:
输入:
Candidates table:
+-------------+------------+--------+
| employee_id | experience | salary |
+-------------+------------+--------+
| 1 | Junior | 25000 |
| 9 | Junior | 10000 |
| 2 | Senior | 85000 |
| 11 | Senior | 80000 |
| 13 | Senior | 90000 |
| 4 | Junior | 30000 |
+-------------+------------+--------+
输出:
+-------------+
| employee_id |
+-------------+
| 9 |
| 1 |
| 4 |
+-------------+
解释:
最便宜的高级员工也要 80000 > 70000,雇不起;剩余 70000 全部用于初级:9(10000)、1(25000)、4(30000)。
解题思路
解法一:窗口前缀和 + 递归式剩余预算
与 I 题(2004)的核心区别:II 题要输出被雇佣的具体员工 ID,且每个候选工资唯一。
- 高级员工按
salary ASC排序,SUM(salary) OVER (ORDER BY salary)前缀和<= 70000的行即为可雇佣的高级员工; - 高级员工薪资和记为
cost,剩余预算70000 - cost; - 初级员工同样按薪资升序取前缀和
<= 剩余预算的 ID。
用两个 CTE 分别算两类可雇佣集合,再 UNION ALL 输出全部 employee_id。
解法二(对照):相关子查询/自连接逐行判断是否"轮到"该员工 ------由于每个工资唯一,可精确判断当前员工之前(更低价)的所有人加起来是否必然被雇佣;用 NOT EXISTS 更低价员工超预算 类条件等价实现。
代码实现
sql
-- 解法一(推荐):窗口前缀和分别截断两类候选人
WITH seniors AS (
SELECT employee_id, salary,
SUM(salary) OVER (ORDER BY salary) AS cum
FROM Candidates
WHERE experience = 'Senior'
),
senior_hired AS (
SELECT employee_id, salary, cum
FROM seniors
WHERE cum <= 70000 -- 前缀和不超过预算的行 = 被雇佣的高级员工
),
junior_budget AS (
SELECT 70000 - COALESCE(SUM(salary), 0) AS budget
FROM senior_hired
),
juniors AS (
SELECT employee_id, salary,
SUM(salary) OVER (ORDER BY salary) AS cum
FROM Candidates
WHERE experience = 'Junior'
)
SELECT employee_id FROM senior_hired
UNION ALL
SELECT j.employee_id
FROM juniors j, junior_budget b
WHERE j.cum <= b.budget;
sql
-- 解法二(对照):子查询计算高级总成本后再筛选初级
WITH senior_cte AS (
SELECT employee_id, salary,
SUM(salary) OVER (ORDER BY salary) AS cum
FROM Candidates WHERE experience = 'Senior'
),
hired_senior AS (
SELECT employee_id FROM senior_cte WHERE cum <= 70000
),
cost AS (
SELECT COALESCE(SUM(salary), 0) AS c FROM senior_cte WHERE cum <= 70000
)
SELECT employee_id FROM hired_senior
UNION ALL
SELECT j.employee_id
FROM (SELECT employee_id, salary,
SUM(salary) OVER (ORDER BY salary) AS cum
FROM Candidates WHERE experience = 'Junior') j
CROSS JOIN cost
WHERE j.cum <= 70000 - cost.c;
复杂度分析
- 时间复杂度 :
O(n log n),其中n为Candidates行数;排序为主。 - 空间复杂度 :
O(n),窗口函数中间结果与行数成正比。
2011. Final Value of Variable After Performing Operations(执行操作后的变量值)
题目描述
存在一种仅支持 4 种操作和 1 个变量 X 的编程语言:++X 和 X++ 使变量 X 的值加 1,
--X 和 X-- 使变量 X 的值减 1。最初 X 的值是 0。给你一个字符串数组 operations,
这是由操作组成的一个列表,返回执行所有操作后 X 的最终值。
示例 1
operations = ["--X","X++","X++"],输出 1。
示例 2
operations = ["++X","++X","X++"],输出 3。
示例 3
operations = ["X++","++X","--X","X--"],输出 0。
解题思路
解法一(主提交版本)
取中间字符判断(主提交版本)
- 四种合法操作都形如
?X?且中间字符一定是X,决定增/减的是两侧符号:+出现则 +1,-出现则 -1。 - 直接看
operations[i].charAt(1):'+'则累加 1,否则累减 1,一次遍历完成。
复杂度:时间复杂度 O(n),空间复杂度 O(1)。
解法二
contains 判断 + 字符串匹配(对照) - 逐个判断操作是否含
"++"或"--":含+的操作为自增,含-的操作为自减。 - 逻辑直白,不依赖操作符的位置约定。
复杂度:时间复杂度 O(n·len),空间复杂度 O(1)。
代码实现
java
public class Solution {
// 解法一:中间字符判断(主提交版本)
public int finalValueAfterOperations(String[] operations) {
int x = 0;
for (String op : operations) {
x += (op.charAt(1) == '+') ? 1 : -1;
}
return x;
}
// 解法二:contains 判断(对照)
public int finalValueAfterOperationsContains(String[] operations) {
int x = 0;
for (String op : operations) {
if (op.contains("++")) {
x++;
} else {
x--;
}
}
return x;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.finalValueAfterOperations(new String[]{"--X", "X++", "X++"})); // 1
System.out.println(sol.finalValueAfterOperations(new String[]{"++X", "++X", "X++"})); // 3
System.out.println(sol.finalValueAfterOperations(new String[]{"X++", "++X", "--X", "X--"})); // 0
System.out.println(sol.finalValueAfterOperationsContains(new String[]{"--X", "X++", "X++"})); // 1
System.out.println(sol.finalValueAfterOperationsContains(new String[]{"++X", "++X", "X++"})); // 3
System.out.println(sol.finalValueAfterOperationsContains(new String[]{"X++", "++X", "--X", "X--"})); // 0
}
}
复杂度分析
复杂度:时间复杂度 O(n),空间复杂度 O(1)。
复杂度:时间复杂度 O(n·len),空间复杂度 O(1)。
2012. Sum of Beauty in the Array(数组美丽值求和)
题目描述
给你一个下标从 0 开始的整数数组 nums。对于每个下标 i(1 <= i <= nums.length - 2),nums[i] 的美丽值等于:
- 2:对于所有
0 <= j < i且i < k <= nums.length - 1,满足nums[j] < nums[i] < nums[k]; - 1:如果满足
nums[i-1] < nums[i] < nums[i+1],且不满足前面的条件; - 0:如果上述条件全部不满足。
返回符合1 <= i <= nums.length - 2的所有nums[i]的美丽值的总和。
示例 1
nums = [1,2,3],输出 2(nums[1] 美丽值为 2)。
示例 2
nums = [2,4,6,4],输出 1(nums[1] 为 1,nums[2] 为 0)。
示例 3
nums = [3,2,1],输出 0。
解题思路
解法一(主提交版本)
左右最值预计算(主提交版本)
- 条件一"所有左边都小于它、所有右边都大于它"其实等价于
左边最大值 < nums[i] < 右边最小值。 - 先扫描右侧,构造
rightMin[i]表示i右侧(含 i+1 起)的最小值;再从左往右维护左侧最大值leftMax。 - 对每个内部下标
i:先判断leftMax < nums[i] < rightMin[i+1]成立则 +2;否则若nums[i-1] < nums[i] < nums[i+1]则 +1。
复杂度:时间复杂度 O(n),空间复杂度 O(n)。
解法二
暴力检查(对照) - 对每个内部下标
i,分别向左右全量扫描检查条件一,再检查条件二。
复杂度:时间复杂度 O(n²),空间复杂度 O(1)。
代码实现
java
public class Solution {
// 解法一:左右最值预计算(主提交版本)
public int sumOfBeauties(int[] nums) {
int n = nums.length;
int[] rightMin = new int[n];
rightMin[n - 1] = nums[n - 1];
for (int i = n - 2; i >= 0; i--) {
rightMin[i] = Math.min(rightMin[i + 1], nums[i]);
}
int ans = 0;
int leftMax = nums[0];
for (int i = 1; i < n - 1; i++) {
if (leftMax < nums[i] && nums[i] < rightMin[i + 1]) {
ans += 2;
} else if (nums[i - 1] < nums[i] && nums[i] < nums[i + 1]) {
ans += 1;
}
leftMax = Math.max(leftMax, nums[i]);
}
return ans;
}
// 解法二:暴力检查(对照)
public int sumOfBeautiesBrute(int[] nums) {
int n = nums.length;
int ans = 0;
for (int i = 1; i < n - 1; i++) {
boolean cond1 = true;
for (int j = 0; j < i; j++) {
if (nums[j] >= nums[i]) {
cond1 = false;
break;
}
}
for (int k = i + 1; k < n && cond1; k++) {
if (nums[k] <= nums[i]) {
cond1 = false;
}
}
if (cond1) {
ans += 2;
} else if (nums[i - 1] < nums[i] && nums[i] < nums[i + 1]) {
ans += 1;
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.sumOfBeauties(new int[]{1, 2, 3})); // 2
System.out.println(sol.sumOfBeauties(new int[]{2, 4, 6, 4})); // 1
System.out.println(sol.sumOfBeauties(new int[]{3, 2, 1})); // 0
System.out.println(sol.sumOfBeautiesBrute(new int[]{1, 2, 3})); // 2
System.out.println(sol.sumOfBeautiesBrute(new int[]{2, 4, 6, 4})); // 1
System.out.println(sol.sumOfBeautiesBrute(new int[]{3, 2, 1})); // 0
}
}
复杂度分析
复杂度:时间复杂度 O(n),空间复杂度 O(n)。
复杂度:时间复杂度 O(n²),空间复杂度 O(1)。
2013. Detect Squares(检测正方形)
题目描述
给你一个在 X-Y 平面上的点构成的数据流。设计一个满足下述要求的算法:
- 添加一个在数据流中的新点到某个数据结构中,可以添加重复的点,并会视作不同的点进行处理;
- 给你一个查询点,请你从数据结构中选出三个点,使这三个点和查询点一同构成一个面积为正的轴对齐正方形,统计满足该要求的方案数目。
实现DetectSquares类:DetectSquares()初始化空结构,void add(int[] point)添加点[x, y],
int count(int[] point)统计与点[x, y]共同构造轴对齐正方形的方案数。
示例
输入 ["DetectSquares","add","add","add","count","count","add","count"]
[[],[3,10],[11,2],[3,2],[11,10],[14,8],[11,2],[11,10]]
输出 [null,null,null,null,1,0,null,2]。
解题思路
解法一(主提交版本)
按 x 分组 + 对角线枚举(主提交版本)
- 用
Map<Integer, Map<Integer, Integer>>(x -> (y -> 次数))存储点到数据结构。 count([x1, y1])时枚举所有与 x1 不同的 x2:边长d = x2 - x1,则另两个对角点可能是
(x1, y1+d)、(x2, y1+d)(正方形在 y 正方向)或(x1, y1-d)、(x2, y1-d)。
方案数 =num(x2, y1) * num(x1, y1+d) * num(x2, y1+d)(对两个方向分别累加)。
复杂度:单次 add O(1);单次 count O(不同 x 的数量),总调用 ≤ 5000 故可接受。
解法二
全点对点枚举(对照)- 先把所有点放进
Map<String, Integer>("x,y" 为键),count 时枚举每个已有点为一边,判断能否补成正方形。 - 枚举方式:与查询点同行(y 相等)的点为底边,计算边长后检查上/下方两点是否存在并乘上次数。
复杂度:单次 count O(点数),最坏 O(5000)。
代码实现
java
public class Solution {
// 解法一:按 x 分组 + 对角线枚举(主提交版本)
private java.util.Map<Integer, java.util.Map<Integer, Integer>> cnt;
public Solution() {
cnt = new java.util.HashMap<>();
}
public void add(int[] point) {
int x = point[0], y = point[1];
java.util.Map<Integer, Integer> col = cnt.get(x);
if (col == null) {
col = new java.util.HashMap<>();
cnt.put(x, col);
}
col.put(y, col.getOrDefault(y, 0) + 1);
}
public int count(int[] point) {
int x1 = point[0], y1 = point[1];
java.util.Map<Integer, Integer> row1 = cnt.get(x1);
if (row1 == null) {
return 0;
}
int ans = 0;
for (java.util.Map.Entry<Integer, java.util.Map<Integer, Integer>> e : cnt.entrySet()) {
int x2 = e.getKey();
if (x2 == x1) {
continue;
}
int d = x2 - x1;
java.util.Map<Integer, Integer> row2 = e.getValue();
// 方向一:y 正侧
ans += row2.getOrDefault(y1, 0)
* row1.getOrDefault(y1 + d, 0)
* row2.getOrDefault(y1 + d, 0);
// 方向二:y 负侧
ans += row2.getOrDefault(y1, 0)
* row1.getOrDefault(y1 - d, 0)
* row2.getOrDefault(y1 - d, 0);
}
return ans;
}
// 解法二:全点对点枚举 key 字符串(对照)
private java.util.Map<String, Integer> all;
public void initBrute() {
all = new java.util.HashMap<>();
}
public void addBrute(int[] point) {
String key = point[0] + "," + point[1];
all.put(key, all.getOrDefault(key, 0) + 1);
}
public int countBrute(int[] point) {
int x1 = point[0], y1 = point[1];
int ans = 0;
for (java.util.Map.Entry<String, Integer> e : all.entrySet()) {
String[] p = e.getKey().split(",");
int x2 = Integer.parseInt(p[0]);
int y2 = Integer.parseInt(p[1]);
if (x2 == x1) {
continue;
}
int d = x2 - x1;
// 查询点所在 x=x1 的列上需要 y1+d 或 y1-d
int c1 = all.getOrDefault(x1 + "," + (y2), 0);
// (x2, y1) 底边点
int c2 = e.getValue();
// 对角 (x1, y1+d),其中 y1+d == y2 或 (x1, y1-d) == y2
int c3a = all.getOrDefault(x2 + "," + (y1), 0);
// 情况 A:y2 = y1 + d
if (y2 == y1 + d) {
ans += c2 * c3a * all.getOrDefault(x1 + "," + (y2), 0);
}
// 情况 B:y2 = y1 - d
if (y2 == y1 - d) {
ans += c2 * c3a * all.getOrDefault(x1 + "," + (y2), 0);
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
sol.add(new int[]{3, 10});
sol.add(new int[]{11, 2});
sol.add(new int[]{3, 2});
System.out.println(sol.count(new int[]{11, 10})); // 1
System.out.println(sol.count(new int[]{14, 8})); // 0
sol.add(new int[]{11, 2});
System.out.println(sol.count(new int[]{11, 10})); // 2
Solution brute = new Solution();
brute.initBrute();
brute.addBrute(new int[]{3, 10});
brute.addBrute(new int[]{11, 2});
brute.addBrute(new int[]{3, 2});
System.out.println(brute.countBrute(new int[]{11, 10})); // 1
System.out.println(brute.countBrute(new int[]{14, 8})); // 0
brute.addBrute(new int[]{11, 2});
System.out.println(brute.countBrute(new int[]{11, 10})); // 2
}
}
复杂度分析
复杂度:单次 add O(1);单次 count O(不同 x 的数量),总调用 ≤ 5000 故可接受。
复杂度:单次 count O(点数),最坏 O(5000)。
2014. Longest Subsequence Repeated k Times(重复 K 次的最长子序列)
题目描述
给你一个长度为 n 的字符串 s 和一个整数 k,请找出字符串 s 中重复 k 次的最长子序列。
子序列是由其他字符串删除某些(或不删除)字符派生而来的一个字符串。
如果 seq * k 是 s 的一个子序列(seq * k 表示由 seq 串联 k 次构造的字符串),
那么就称 seq 是字符串 s 中一个重复 k 次的子序列。返回重复 k 次的最长子序列;
如果存在多个满足的子序列,则返回字典序最大的那个;如果不存在,返回空字符串。
示例 1
s = "letsleetcode", k = 2,输出 "let"("let" 和 "ete" 都是最长,取字典序最大)。
示例 2
s = "bb", k = 2,输出 "b"。
示例 3
s = "ab", k = 2,输出 ""。
解题思路
解法一(主提交版本)
BFS 枚举候选 + 贪心校验(主提交版本)
- 关键约束
n < min(2001, k*8)说明最长答案不超过 7~8 个字符,且只有出现次数 ≥ k 的字符可能出现在答案中。 - 先统计频次,收集出现次数 ≥ k 的字符。用 BFS 按字典序从大到小的顺序生成候选("z...a" 顺序入队可保证后出队的更长且优先字典序大),
逐个校验"候选串重复 k 次是否为 s 的子序列",记录最后通过的候选即为最长且字典序最大的。 - 校验(贪心):在 s 上模拟 k 轮匹配,每轮完整匹配一次候选串,若 k 轮都能匹配完则成立。
复杂度:候选数量有界(字符集 ≤ 26、长度 ≤ 7),总体可认为是 O(26^(L)·n) 量级(L 为答案长度上限),满足 n ≤ 2000。
解法二
DFS 从 dict 序构造 + 逐步校验(对照) - 按字符从大到小尝试构造候选串,每扩展一个字符就校验一次合法性,记录目前字典序最大且最长的可行答案。
- 先尝试扩展(更长优先),同长度下先尝试字典序更大的字符,与 BFS 结果一致。
复杂度:同样有界,实现更直观。
代码实现
java
public class Solution {
private char[] arr;
// 解法一:BFS 枚举候选 + 贪心校验(主提交版本)
public String longestSubsequenceRepeatedK(String s, int k) {
this.arr = s.toCharArray();
int[] cnt = new int[26];
for (char c : arr) {
cnt[c - 'a']++;
}
java.util.List<Character> usable = new java.util.ArrayList<>();
for (char c = 'a'; c <= 'z'; c++) {
if (cnt[c - 'a'] >= k) {
usable.add(c);
}
}
java.util.Deque<String> q = new java.util.ArrayDeque<>();
q.offer("");
String ans = "";
while (!q.isEmpty()) {
String cur = q.poll();
for (char c : usable) {
String next = cur + c;
if (check(next, k)) {
ans = next;
q.offer(next);
}
}
}
return ans;
}
// 校验 t 重复 k 次是否为 s 的子序列(贪心多轮匹配)
private boolean check(String t, int k) {
int ti = 0;
for (char c : arr) {
if (c == t.charAt(ti)) {
ti++;
if (ti == t.length()) {
k--;
if (k == 0) {
return true;
}
ti = 0;
}
}
}
return false;
}
// 解法二:DFS 构造 + 最优保持(对照)
private String best = "";
public String longestSubsequenceRepeatedKDfs(String s, int k) {
this.arr = s.toCharArray();
best = "";
int[] cnt = new int[26];
for (char c : arr) {
cnt[c - 'a']++;
}
// 从 z 到 a 收集可用字符(优先尝试字典序大的字符)
java.util.List<Character> usable = new java.util.ArrayList<>();
for (char c = 'z'; c >= 'a'; c--) {
if (cnt[c - 'a'] >= k) {
usable.add(c);
}
}
for (char c : usable) {
String start = String.valueOf(c);
if (check(start, k)) {
if (isBetter(start, best)) {
best = start; // 仅当更优(更长或同长字典序更大)才更新
}
dfsBuild(usable, start, k, arr.length / k);
}
}
return best;
}
// 长度优先、同长取字典序更大
private boolean isBetter(String a, String b) {
if (a.length() != b.length()) {
return a.length() > b.length();
}
return a.compareTo(b) > 0;
}
private void dfsBuild(java.util.List<Character> usable, String cur, int k, int maxLen) {
for (char c : usable) {
String next = cur + c;
if (!check(next, k)) {
continue;
}
if (isBetter(next, best)) {
best = next;
}
// 答案长度不可能超过 n/k,超过即无法继续变长
if (next.length() < maxLen) {
dfsBuild(usable, next, k, maxLen);
}
}
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.longestSubsequenceRepeatedK("letsleetcode", 2)); // let
System.out.println(sol.longestSubsequenceRepeatedK("bb", 2)); // b
System.out.println(sol.longestSubsequenceRepeatedK("ab", 2)); // ""
System.out.println(sol.longestSubsequenceRepeatedKDfs("letsleetcode", 2)); // let
System.out.println(sol.longestSubsequenceRepeatedKDfs("bb", 2)); // b
System.out.println(sol.longestSubsequenceRepeatedKDfs("ab", 2)); // ""
}
}
复杂度分析
复杂度:候选数量有界(字符集 ≤ 26、长度 ≤ 7),总体可认为是 O(26^(L)·n) 量级(L 为答案长度上限),满足 n ≤ 2000。
2015. Average Height of Buildings in Each Segment(每段建筑物的平均高度)
题目描述
一条完全笔直的街道由一条数字线表示。街道上有建筑物,由二维整数数组 buildings 表示,
其中 buildings[i] = [start_i, end_i, height_i] 表示在半封闭区间 [start_i, end_i) 有一座高度为 height_i 的建筑。
你想用最少数量的非重叠部分来描述街道上建筑物的高度,输出二维整数数组 street,
其中 street[j] = [left_j, right_j, average_j] 描述了半封闭区域 [left_j, right_j) 中建筑物的平均高度,
不包括街道上没有建筑物的任何区域。平均值用整数除法。
示例 1
buildings = [[1,4,2],[3,9,4]],输出 [[1,3,2],[3,4,3],[4,9,4]]。
示例 2
buildings = [[1,3,2],[2,5,3],[2,8,3]],输出 [[1,3,2],[3,8,3]]。
解题思路
解法一(主提交版本)
差分 + TreeMap 扫描(主提交版本)
- 对每个端点位置记录两个事件:覆盖数变化(+1/-1)与高度和变化(+h/-h),用
TreeMap按坐标有序存储。 - 从左到右扫描所有关键点:扫描前一段
[last, x)的覆盖数m > 0,平均高度 = 高度和 / 覆盖数(整数除法);
若与前一段平均高度相同且右端点相接则合并区间,否则新开一段。
复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。
解法二
区间合并暴力(对照) - 收集所有端点坐标排序去重,对每对相邻坐标
(a, b)统计覆盖该开区间的所有建筑高度求和取平均;
跳过无建筑覆盖的区间,并合并连续区间平均高度相同的相邻段。 - 逻辑直白(枚举端点对),常数大但易于核对。
复杂度:时间复杂度 O(n²·k),空间复杂度 O(n)。
代码实现
java
public class Solution {
// 解法一:差分 + TreeMap 扫描(主提交版本)
public int[][] averageHeightOfBuildings(int[][] buildings) {
java.util.Map<Integer, Integer> cnt = new java.util.HashMap<>();
java.util.TreeMap<Integer, Integer> delta = new java.util.TreeMap<>();
for (int[] e : buildings) {
int st = e[0], en = e[1], h = e[2];
cnt.put(st, cnt.getOrDefault(st, 0) + 1);
cnt.put(en, cnt.getOrDefault(en, 0) - 1);
delta.put(st, delta.getOrDefault(st, 0) + h);
delta.put(en, delta.getOrDefault(en, 0) - h);
}
java.util.List<int[]> ans = new java.util.ArrayList<>();
int total = 0; // 当前高度和
int cover = 0; // 当前覆盖建筑数
int last = -1;
for (java.util.Map.Entry<Integer, Integer> e : delta.entrySet()) {
int x = e.getKey();
if (cover > 0) {
int avg = total / cover;
if (!ans.isEmpty() && ans.get(ans.size() - 1)[1] == last
&& ans.get(ans.size() - 1)[2] == avg) {
ans.get(ans.size() - 1)[1] = x; // 与上段相邻且平均相同 → 合并
} else {
ans.add(new int[]{last, x, avg});
}
}
total += delta.get(x);
cover += cnt.get(x);
last = x;
}
return ans.toArray(new int[0][]);
}
// 解法二:端点对暴力统计(对照)
public int[][] averageHeightOfBuildingsBrute(int[][] buildings) {
java.util.TreeSet<Integer> pts = new java.util.TreeSet<>();
for (int[] b : buildings) {
pts.add(b[0]);
pts.add(b[1]);
}
Integer[] arr = pts.toArray(new Integer[0]);
java.util.List<int[]> ans = new java.util.ArrayList<>();
for (int i = 0; i < arr.length - 1; i++) {
int a = arr[i], b = arr[i + 1];
long sum = 0;
int m = 0;
for (int[] bl : buildings) {
if (bl[0] <= a && b <= bl[1]) { // [a,b) 被该建筑覆盖
sum += bl[2];
m++;
}
}
if (m == 0) {
continue;
}
int avg = (int) (sum / m);
if (!ans.isEmpty() && ans.get(ans.size() - 1)[1] == a
&& ans.get(ans.size() - 1)[2] == avg) {
ans.get(ans.size() - 1)[1] = b;
} else {
ans.add(new int[]{a, b, avg});
}
}
return ans.toArray(new int[0][]);
}
private static String[][] fmt(int[][] a) {
String[][] r = new String[a.length][];
for (int i = 0; i < a.length; i++) {
r[i] = new String[]{String.valueOf(a[i][0]), String.valueOf(a[i][1]), String.valueOf(a[i][2])};
}
return r;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(java.util.Arrays.deepToString(sol.averageHeightOfBuildings(new int[][]{{1, 4, 2}, {3, 9, 4}}))); // [[1,3,2],[3,4,3],[4,9,4]]
System.out.println(java.util.Arrays.deepToString(sol.averageHeightOfBuildings(new int[][]{{1, 3, 2}, {2, 5, 3}, {2, 8, 3}}))); // [[1,3,2],[3,8,3]]
System.out.println(java.util.Arrays.deepToString(sol.averageHeightOfBuildingsBrute(new int[][]{{1, 4, 2}, {3, 9, 4}}))); // [[1,3,2],[3,4,3],[4,9,4]]
System.out.println(java.util.Arrays.deepToString(sol.averageHeightOfBuildingsBrute(new int[][]{{1, 3, 2}, {2, 5, 3}, {2, 8, 3}}))); // [[1,3,2],[3,8,3]]
}
}
复杂度分析
复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。
复杂度:时间复杂度 O(n²·k),空间复杂度 O(n)。
2016. Maximum Difference Between Increasing Elements(增量元素之间的最大差值)
题目描述
给你一个下标从 0 开始的整数数组 nums,该数组大小为 n,请你计算 nums[j] - nums[i] 能求得的最大差值,
其中 0 <= i < j < n 且 nums[i] < nums[j]。返回最大差值;如果不存在满足要求的 i 和 j,返回 -1。
示例 1
nums = [7,1,5,4],输出 4(i=1, j=2,5-1=4)。
示例 2
nums = [9,4,3,2],输出 -1。
示例 3
nums = [1,5,2,10],输出 9(i=0, j=3,10-1=9)。
解题思路
解法一(主提交版本)
维护前缀最小值(主提交版本)
- 从左到右扫描,维护已扫过元素的最小值
min。对每个位置j,若nums[j] > min,
则候选差为nums[j] - min,更新全局最大。 - 一次遍历完成;注意最小值是前缀的最小值,天然满足
i < j约束。
复杂度:时间复杂度 O(n),空间复杂度 O(1)。
解法二
双重循环暴力(对照) - 枚举所有
i < j且nums[i] < nums[j]的组合,求差的最大值,无则 -1。
复杂度:时间复杂度 O(n²),空间复杂度 O(1)。
代码实现
java
public class Solution {
// 解法一:维护前缀最小值(主提交版本)
public int maximumDifference(int[] nums) {
int min = nums[0];
int ans = -1;
for (int j = 1; j < nums.length; j++) {
if (nums[j] > min) {
ans = Math.max(ans, nums[j] - min);
}
min = Math.min(min, nums[j]);
}
return ans;
}
// 解法二:暴力双重循环(对照)
public int maximumDifferenceBrute(int[] nums) {
int n = nums.length;
int ans = -1;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (nums[i] < nums[j]) {
ans = Math.max(ans, nums[j] - nums[i]);
}
}
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.maximumDifference(new int[]{7, 1, 5, 4})); // 4
System.out.println(sol.maximumDifference(new int[]{9, 4, 3, 2})); // -1
System.out.println(sol.maximumDifference(new int[]{1, 5, 2, 10})); // 9
System.out.println(sol.maximumDifferenceBrute(new int[]{7, 1, 5, 4})); // 4
System.out.println(sol.maximumDifferenceBrute(new int[]{9, 4, 3, 2})); // -1
System.out.println(sol.maximumDifferenceBrute(new int[]{1, 5, 2, 10})); // 9
}
}
复杂度分析
复杂度:时间复杂度 O(n),空间复杂度 O(1)。
复杂度:时间复杂度 O(n²),空间复杂度 O(1)。
2017. Grid Game(网格游戏)
题目描述
给你一个下标从 0 开始的二维数组 grid,大小为 2 x n,其中 grid[r][c] 表示矩阵中 (r, c) 位置上的点数。
两个机器人初始位置都是 (0, 0),目标位置是 (1, n-1)。每个机器人只会向右或向下移动。
游戏开始,第一个机器人先走完 (0,0) -> (1,n-1),并收集路径上单元格的全部点数,途经后这些格子重置为 0;
然后第二个机器人再走同样路径并收集点数(路径可以相交)。
第一个机器人想让第二个机器人收集到的点数最小化,第二个机器人想最大化自己收集到的点数。
两人都发挥最佳水平的前提下,返回第二个机器人收集到的点数。
示例 1
grid = [[2,5,4],[1,5,1]],输出 4。
示例 2
grid = [[3,3,1],[8,5,2]],输出 4。
示例 3
grid = [[1,3,1,15],[1,3,3,1]],输出 7。
解题思路
解法一(主提交版本)
枚举第一机器人换行点 + 前缀和(主提交版本)
- 第一机器人从上方走,只会在某列
j处向下换行,把路径分成:上方经过0..j-1列,下方经过j..n-1列。 - 第一机器人经过的格子清零,第二机器人能拿到的 = 上方剩余部分
(j+1..n-1)之和 与 下方剩余部分(0..j-1)之和 的较大者
(第二机器人同样可以选择换行点,它必然选两条残路中更大的那条)。 - 第一机器人要使这个"较大者"最小:
ans = min_j( max(s1_after, s2_before) ),其中用前缀和 O(1) 求两侧和。
复杂度:时间复杂度 O(n),空间复杂度 O(1)。
解法二
前缀和数组 + 换行点枚举(对照) - 显式构造两行的前缀和数组,枚举换行列
j,用前缀和直接累加两侧残值,逻辑更展开。
复杂度:时间复杂度 O(n),空间复杂度 O(n)。
代码实现
java
public class Solution {
// 解法一:枚举换行点 + 滚动前缀和(主提交版本)
public long gridGame(int[][] grid) {
int n = grid[0].length;
long upRight = 0; // 上方在换行列右侧的和
for (int v : grid[0]) {
upRight += v;
}
long downLeft = 0; // 下方在换行列左侧的和
long ans = Long.MAX_VALUE;
for (int j = 0; j < n; j++) {
upRight -= grid[0][j];
ans = Math.min(ans, Math.max(upRight, downLeft));
downLeft += grid[1][j];
}
return ans;
}
// 解法二:前缀和数组(对照)
public long gridGamePrefix(int[][] grid) {
int n = grid[0].length;
long[] p0 = new long[n + 1];
long[] p1 = new long[n + 1];
for (int i = 0; i < n; i++) {
p0[i + 1] = p0[i] + grid[0][i];
p1[i + 1] = p1[i] + grid[1][i];
}
long ans = Long.MAX_VALUE;
for (int j = 0; j < n; j++) {
long upRight = p0[n] - p0[j + 1]; // 上方 j+1..n-1
long downLeft = p1[j]; // 下方 0..j-1
ans = Math.min(ans, Math.max(upRight, downLeft));
}
return ans;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.gridGame(new int[][]{{2, 5, 4}, {1, 5, 1}})); // 4
System.out.println(sol.gridGame(new int[][]{{3, 3, 1}, {8, 5, 2}})); // 4
System.out.println(sol.gridGame(new int[][]{{1, 3, 1, 15}, {1, 3, 3, 1}})); // 7
System.out.println(sol.gridGamePrefix(new int[][]{{2, 5, 4}, {1, 5, 1}})); // 4
System.out.println(sol.gridGamePrefix(new int[][]{{3, 3, 1}, {8, 5, 2}})); // 4
System.out.println(sol.gridGamePrefix(new int[][]{{1, 3, 1, 15}, {1, 3, 3, 1}})); // 7
}
}
复杂度分析
复杂度:时间复杂度 O(n),空间复杂度 O(1)。
复杂度:时间复杂度 O(n),空间复杂度 O(n)。
2018. Check if Word Can Be Placed In Crossword(判断单词是否能放入填字游戏内)
题目描述
给你一个 m x n 的矩阵 board,它代表一个填字游戏当前的状态:小写英文字母(已填入的单词)、
表示空格子的 ' ' 和表示障碍格子的 '#'。
如果满足以下条件,那么我们可以水平(从左到右或者从右到左)或竖直(从上到下或者从下到上)填入一个单词:
- 该单词不占据任何
'#'对应的格子; - 每个字母对应的格子要么是空格要么与
board中已有字母匹配; - 如果单词水平放置,那么该单词左边和右边相邻格子不能为空格或小写英文字母(必须越界或为 '#');
- 如果单词竖直放置,那么该单词上边和下边相邻格子不能为空格或小写英文字母。
给定word,判断word是否可以被放入board中。
示例 1
board = [["#"," ","#"],[" "," ","#"],["#","c"," "]], word = "abc",输出 true(从上往下)。
示例 2
board = [[" ","#","a"],[" ","#","c"],[" ","#","a"]], word = "ac",输出 false。
示例 3
board = [["#"," ","#"],[" "," ","#"],["#"," ","c"]], word = "ca",输出 true(从右到左)。
解题思路
解法一(主提交版本)
四方向起点检查(主提交版本)
- 单词要能放下的核心条件:起始格在某个方向上的"前一个格子"必须是边界或
#(不能被夹在中段);
方向上连续word.length()格全部可用(空格或与对应字母匹配),且其后一格是边界或#。 - 对每个格子作为起点枚举四个方向(右/左/下/上),满足则返回 true;任一起点失败继续。
复杂度:时间复杂度 O(m·n·(m+n)),空间复杂度 O(1)。
解法二
反转单词复用同向检查(对照) - 只检查"向右"与"向下"两个方向,分别用
word与反转后的reverse(word)尝试放置,
反转正好覆盖"从右到左"与"从下到上"两种情况,避免重复写方向逻辑。 - 检查函数
canPut(i, j, di, dj, w):从(i,j)沿(di,dj)放置w,要求起点前一格(反方向)为界/#,
且终点后一格也为界/#,中间逐格匹配。
复杂度:时间复杂度 O(m·n·(m+n)),空间复杂度 O(m·n)(反转串)。
代码实现
java
public class Solution {
// 解法一:四方向起点检查(主提交版本)
public boolean placeWordInCrossword(char[][] board, String word) {
int m = board.length, n = board[0].length;
int k = word.length();
// 四个方向:右、下、左、上
int[][] dirs = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
for (int[] d : dirs) {
if (canPlace(board, word, i, j, d[0], d[1])) {
return true;
}
}
}
}
return false;
}
private boolean canPlace(char[][] board, String word, int i, int j, int di, int dj) {
int m = board.length, n = board[0].length;
int k = word.length();
// 起点前一格(反方向)必须为边界或 '#'
int pi = i - di, pj = j - dj;
if (pi >= 0 && pi < m && pj >= 0 && pj < n && board[pi][pj] != '#') {
return false;
}
// 逐格匹配
for (int p = 0; p < k; p++) {
int x = i + di * p, y = j + dj * p;
if (x < 0 || x >= m || y < 0 || y >= n) {
return false;
}
if (board[x][y] != ' ' && board[x][y] != word.charAt(p)) {
return false;
}
}
// 终点后一格也必须是边界或 '#'
int ei = i + di * k, ej = j + dj * k;
if (ei >= 0 && ei < m && ej >= 0 && ej < n && board[ei][ej] != '#') {
return false;
}
return true;
}
// 解法二:仅向右/向下 + 反转单词(对照)
public boolean placeWordInCrosswordRev(char[][] board, String word) {
int m = board.length, n = board[0].length;
String rev = new StringBuilder(word).reverse().toString();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (canPut(board, i, j, 0, 1, word) || canPut(board, i, j, 1, 0, word)
|| canPut(board, i, j, 0, 1, rev) || canPut(board, i, j, 1, 0, rev)) {
return true;
}
}
}
return false;
}
private boolean canPut(char[][] board, int i, int j, int di, int dj, String w) {
int m = board.length, n = board[0].length;
int k = w.length();
int pi = i - di, pj = j - dj;
if (pi >= 0 && pi < m && pj >= 0 && pj < n && board[pi][pj] != '#') {
return false;
}
for (int p = 0; p < k; p++) {
int x = i + di * p, y = j + dj * p;
if (x < 0 || x >= m || y < 0 || y >= n) {
return false;
}
if (board[x][y] != ' ' && board[x][y] != w.charAt(p)) {
return false;
}
}
int ei = i + di * k, ej = j + dj * k;
if (ei >= 0 && ei < m && ej >= 0 && ej < n && board[ei][ej] != '#') {
return false;
}
return true;
}
public static void main(String[] args) {
Solution sol = new Solution();
char[][] b1 = {{'#', ' ', '#'}, {' ', ' ', '#'}, {'#', 'c', ' '}};
System.out.println(sol.placeWordInCrossword(b1, "abc")); // true
char[][] b2 = {{' ', '#', 'a'}, {' ', '#', 'c'}, {' ', '#', 'a'}};
System.out.println(sol.placeWordInCrossword(b2, "ac")); // false
char[][] b3 = {{'#', ' ', '#'}, {' ', ' ', '#'}, {'#', ' ', 'c'}};
System.out.println(sol.placeWordInCrossword(b3, "ca")); // true
char[][] b4 = {{' ', ' ', '#'}, {' ', ' ', '#'}, {' ', ' ', '#'}};
System.out.println(sol.placeWordInCrossword(b4, "abc")); // false(放不下长度 3)
System.out.println(sol.placeWordInCrosswordRev(b1, "abc")); // true
System.out.println(sol.placeWordInCrosswordRev(b2, "ac")); // false
System.out.println(sol.placeWordInCrosswordRev(b3, "ca")); // true
}
}
复杂度分析
复杂度:时间复杂度 O(m·n·(m+n)),空间复杂度 O(1)。
复杂度:时间复杂度 O(m·n·(m+n)),空间复杂度 O(m·n)(反转串)。
2019. The Score of Students Solving Math Expression(解出数学表达式的学生分数)
题目描述
给你一个字符串 s,它只包含数字 0-9、加法运算符 '+' 和乘法运算符 '*',表示一个合法的只含有个位数数字的数学表达式
(例如 "3+5*2")。有 n 位小学生计算这个表达式,遵循如下运算顺序:按照从左到右的顺序计算乘法,然后按照从左到右的顺序计算加法。
给你长度为 n 的整数数组 answers,表示每位学生提交的答案,按如下规则打分:
- 如果学生答案等于表达式的正确结果,得 5 分;
- 否则,如果答案由一处或多处错误的运算顺序计算得到,得 2 分;
- 否则得 0 分。返回所有学生的分数和。
示例 1
s = "7+3*1*2", answers = [20,13,42],输出 7。
示例 2
s = "3+5*2", answers = [13,0,10,13,13,16,16],输出 19。
示例 3
s = "6+0*1", answers = [12,9,6,4,8,6],输出 10(错误顺序也可能算出 6,仍按正确给 5 分)。
解题思路
解法一(主提交版本)
区间 DP 枚举所有可行结果(主提交版本)
- 学生只能以"部分先结合"的方式任意加括号,即任意合法的括号化方式都是一种可能的"错误运算顺序"。
用区间 DP:f[i][j]表示数字 i...j(含其间运算符)能算出的所有结果的集合。
枚举分割点 k(运算符位置),把左区间结果与右区间结果做+或*后并入f[i][j](值 > 1000 剪掉)。 - 正确结果
correct用标准"先乘后加"求值。打分:answer == correct得 5 分;
否则若answer ∈ f[0][m-1](即某种括号化能算出)得 2 分。
复杂度:区间数 O(m²),每个区间合并 O(m·V²),V ≤ 1000,运算器数 ≤ 15,完全可接受。
解法二
正确值标准求值 + 答案集合判定(对照) - 单独实现"先乘后加"的标准求值器;再用与解法一相同的区间 DP 求所有错误顺序可达值(抽成独立方法)。
- 逻辑拆分更直白,两个方法分别对照。
复杂度:同解法一。
代码实现
java
public class Solution {
// 解法一:区间 DP(主提交版本)
public int scoreOfStudents(String s, int[] answers) {
int m = (s.length() + 1) / 2; // 数字个数
int correct = evaluate(s); // 标准"先乘后加"
@SuppressWarnings("unchecked")
java.util.Set<Integer>[][] f = new java.util.HashSet[m][m];
for (int i = 0; i < m; i++) {
for (int j = i; j < m; j++) {
f[i][j] = new java.util.HashSet<>();
}
}
for (int i = 0; i < m; i++) {
f[i][i].add(s.charAt(i * 2) - '0');
}
for (int len = 2; len <= m; len++) {
for (int i = 0; i + len - 1 < m; i++) {
int j = i + len - 1;
for (int k = i; k < j; k++) {
char op = s.charAt(k * 2 + 1);
java.util.Set<Integer> left = f[i][k];
java.util.Set<Integer> right = f[k + 1][j];
for (int a : left) {
for (int b : right) {
int v = (op == '+') ? a + b : a * b;
if (v <= 1000) {
f[i][j].add(v);
}
}
}
}
}
}
java.util.Set<Integer> wrong = f[0][m - 1];
int[] cnt = new int[1001];
for (int a : answers) {
if (a <= 1000) {
cnt[a]++;
}
}
int ans = 5 * cnt[correct];
for (int i = 0; i <= 1000; i++) {
if (i != correct && wrong.contains(i)) {
ans += 2 * cnt[i];
}
}
return ans;
}
// 标准求值:先从左到右算乘法,再从左到右算加法
private int evaluate(String s) {
int n = s.length();
java.util.List<Integer> vals = new java.util.ArrayList<>();
java.util.List<Character> ops = new java.util.ArrayList<>();
vals.add(s.charAt(0) - '0');
for (int i = 1; i < n; i += 2) {
char op = s.charAt(i);
int num = s.charAt(i + 1) - '0';
if (op == '*') {
int last = vals.remove(vals.size() - 1);
vals.add(last * num);
} else {
vals.add(num);
}
ops.add(op);
}
int sum = 0;
for (int v : vals) {
sum += v;
}
return sum;
}
// 解法二:拆分求值器 + 独立区间 DP(对照)
public int scoreOfStudentsSplit(String s, int[] answers) {
int m = (s.length() + 1) / 2;
int correct = evalStandard(s);
java.util.Set<Integer> all = allResults(s);
int[] cnt = new int[1001];
for (int a : answers) {
if (a <= 1000) {
cnt[a]++;
}
}
int ans = 5 * cnt[correct];
for (int i = 0; i <= 1000; i++) {
if (i != correct && all.contains(i)) {
ans += 2 * cnt[i];
}
}
return ans;
}
private int evalStandard(String s) {
int m = (s.length() + 1) / 2;
// 用两个栈模拟:先处理乘法
java.util.Deque<Integer> st = new java.util.ArrayDeque<>();
int cur = s.charAt(0) - '0';
st.push(cur);
for (int i = 1; i < s.length(); i += 2) {
char op = s.charAt(i);
int num = s.charAt(i + 1) - '0';
if (op == '*') {
int top = st.pop();
st.push(top * num);
} else {
st.push(num);
}
}
int sum = 0;
while (!st.isEmpty()) {
sum += st.pop();
}
return sum;
}
private java.util.Set<Integer> allResults(String s) {
int m = (s.length() + 1) / 2;
@SuppressWarnings("unchecked")
java.util.Set<Integer>[][] f = new java.util.HashSet[m][m];
for (int i = 0; i < m; i++) {
for (int j = i; j < m; j++) {
f[i][j] = new java.util.HashSet<>();
}
}
for (int i = 0; i < m; i++) {
f[i][i].add(s.charAt(i * 2) - '0');
}
for (int len = 2; len <= m; len++) {
for (int i = 0; i + len - 1 < m; i++) {
int j = i + len - 1;
for (int k = i; k < j; k++) {
char op = s.charAt(k * 2 + 1);
for (int a : f[i][k]) {
for (int b : f[k + 1][j]) {
int v = (op == '+') ? a + b : a * b;
if (v <= 1000) {
f[i][j].add(v);
}
}
}
}
}
}
return f[0][m - 1];
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.scoreOfStudents("7+3*1*2", new int[]{20, 13, 42})); // 7
System.out.println(sol.scoreOfStudents("3+5*2", new int[]{13, 0, 10, 13, 13, 16, 16})); // 19
System.out.println(sol.scoreOfStudents("6+0*1", new int[]{12, 9, 6, 4, 8, 6})); // 10
System.out.println(sol.scoreOfStudentsSplit("7+3*1*2", new int[]{20, 13, 42})); // 7
System.out.println(sol.scoreOfStudentsSplit("3+5*2", new int[]{13, 0, 10, 13, 13, 16, 16})); // 19
System.out.println(sol.scoreOfStudentsSplit("6+0*1", new int[]{12, 9, 6, 4, 8, 6})); // 10
}
}
复杂度分析
复杂度:区间数 O(m²),每个区间合并 O(m·V²),V ≤ 1000,运算器数 ≤ 15,完全可接受。
2020. Number of Accounts That Did Not Stream(无流量的帐户数)
题目类型:数据库(SQL)
题目描述
表: Subscriptions
+-------------+------+
| Column Name | Type |
+-------------+------+
| account_id | int |
| start_date | date |
| end_date | date |
+-------------+------+
account_id 是此表的主键列。此表的每一行都表示帐户订阅的开始和结束日期。请注意,始终开始日期 < 结束日期。
表: Streams
+-------------+------+
| Column Name | Type |
+-------------+------+
| session_id | int |
| account_id | int |
| stream_date | date |
+-------------+------+
session_id 是该表的主键列。account_id 是订阅表中的外键。此表的每一行都包含与会话相关联的帐户和日期的信息。
编写 SQL 查询以报告在 2021 购买订阅但没有任何会话的账户数。
示例 1:
输入:
Subscriptions table:
+------------+------------+------------+
| account_id | start_date | end_date |
+------------+------------+------------+
| 9 | 2020-02-18 | 2021-10-30 |
| 3 | 2021-09-21 | 2021-11-13 |
| 11 | 2020-02-28 | 2020-08-18 |
| 13 | 2021-04-20 | 2021-09-22 |
| 4 | 2020-10-26 | 2021-05-08 |
| 5 | 2020-09-11 | 2021-01-17 |
+------------+------------+------------+
Streams table:
+------------+------------+-------------+
| session_id | account_id | stream_date |
+------------+------------+-------------+
| 14 | 9 | 2020-05-16 |
| 16 | 3 | 2021-10-27 |
| 18 | 11 | 2020-04-29 |
| 17 | 13 | 2021-08-08 |
| 19 | 4 | 2020-12-31 |
| 13 | 5 | 2021-01-05 |
+------------+------------+-------------+
输出:
+----------------+
| accounts_count |
+----------------+
| 2 |
+----------------+
解释:
用户 4 和 9 在 2021 有订阅但没有会话;用户 11 在 2021 没有订阅。
解题思路
解法一:LEFT JOIN + 空值判断
先筛选出 2021 年有订阅的账户:start_date <= '2021-12-31' AND end_date >= '2021-01-01'(订阅区间与 2021 年有交集)。把这些账户与 Streams 表 LEFT JOIN,统计没有匹配到任何会话的账户数:streams.session_id IS NULL(即该账户无任何流会话记录)。
注意题意是「在 2021 购买订阅但没有任何会话」------会话不要求一定在订阅期内,因此与其用日期区间关联 Streams,不如直接以 account_id 是否在 Streams 中出现过判断。
解法二(对照):NOT EXISTS 反连接
与解法一同条件的订阅账户,用 NOT EXISTS (SELECT 1 FROM Streams s WHERE s.account_id = ...) 排除掉有会话的账户。两种写法等价,NOT EXISTS 语义更直白。
代码实现
sql
-- 解法一(推荐):LEFT JOIN 找无匹配会话的账户
WITH active_2021 AS (
SELECT account_id
FROM Subscriptions
WHERE start_date <= '2021-12-31' AND end_date >= '2021-01-01'
)
SELECT COUNT(*) AS accounts_count
FROM active_2021 a
LEFT JOIN Streams s ON a.account_id = s.account_id
WHERE s.session_id IS NULL;
sql
-- 解法二(对照):NOT EXISTS 反连接
SELECT COUNT(*) AS accounts_count
FROM Subscriptions sub
WHERE sub.start_date <= '2021-12-31'
AND sub.end_date >= '2021-01-01'
AND NOT EXISTS (
SELECT 1 FROM Streams s WHERE s.account_id = sub.account_id
);
复杂度分析
- 时间复杂度 :
O(n + m),其中n为订阅数、m为会话数;LEFT JOIN/NOT EXISTS 走主键/外键索引后近线性扫描。 - 空间复杂度 :
O(n),CTE 中间结果与订阅表规模成正比。