LeetCode 1201-1220. 题解合集
文章目录
- [LeetCode 1201-1220. 题解合集](#LeetCode 1201-1220. 题解合集)
-
- [1201. Ugly Number III(丑数 III)](#1201. Ugly Number III(丑数 III))
- [1202. Smallest String with Swaps(交换字符串中的元素)](#1202. Smallest String with Swaps(交换字符串中的元素))
- [1203. Sort Items by Groups Respecting Dependencies(项目管理)](#1203. Sort Items by Groups Respecting Dependencies(项目管理))
- [1204. Last Person to Fit in the Bus(最后一个能进入巴士的人)](#1204. Last Person to Fit in the Bus(最后一个能进入巴士的人))
- [1205. Monthly Transactions II(每月交易 II 🔒)](#1205. Monthly Transactions II(每月交易 II 🔒))
- [1206. Design Skiplist(设计跳表)](#1206. Design Skiplist(设计跳表))
- [1207. Unique Number of Occurrences(独一无二的出现次数)](#1207. Unique Number of Occurrences(独一无二的出现次数))
- [1208. Get Equal Substrings Within Budget(尽可能使字符串相等)](#1208. Get Equal Substrings Within Budget(尽可能使字符串相等))
- [1209. Remove All Adjacent Duplicates in String II(删除字符串中的所有相邻重复项 II)](#1209. Remove All Adjacent Duplicates in String II(删除字符串中的所有相邻重复项 II))
- [1210. Minimum Moves to Reach Target with Rotations(穿过迷宫的最少移动次数)](#1210. Minimum Moves to Reach Target with Rotations(穿过迷宫的最少移动次数))
- [1211. Queries Quality and Percentage(查询结果的质量和占比)](#1211. Queries Quality and Percentage(查询结果的质量和占比))
- [1212. Team Scores in Football Tournament(查询球队积分 🔒)](#1212. Team Scores in Football Tournament(查询球队积分 🔒))
- [1213. Intersection of Three Sorted Arrays(三个有序数组的交集)](#1213. Intersection of Three Sorted Arrays(三个有序数组的交集))
- [1214. Two Sum BSTs(查找两棵二叉搜索树之和)](#1214. Two Sum BSTs(查找两棵二叉搜索树之和))
- [1215. Stepping Numbers(步进数)](#1215. Stepping Numbers(步进数))
- [1216. Valid Palindrome III(验证回文串 III)](#1216. Valid Palindrome III(验证回文串 III))
- [1217. Minimum Cost to Move Chips to The Same Position(玩筹码)](#1217. Minimum Cost to Move Chips to The Same Position(玩筹码))
- [1218. Longest Arithmetic Subsequence of Given Difference(最长定差子序列)](#1218. Longest Arithmetic Subsequence of Given Difference(最长定差子序列))
- [1219. Path with Maximum Gold(黄金矿工)](#1219. Path with Maximum Gold(黄金矿工))
- [1220. Count Vowels Permutation(统计元音字母序列的数目)](#1220. Count Vowels Permutation(统计元音字母序列的数目))
1201. Ugly Number III(丑数 III)
题目描述
丑数是可以被 a 或 b 或 c 整除的 正整数 。
给你四个整数:n 、a 、b 、c ,请你设计一个算法来找出第 n 个丑数。
示例 1
输入:n = 3, a = 2, b = 3, c = 5
输出:4
解释:丑数序列为 2, 3, 4, 5, 6, 8, 9, 10... 其中第 3 个是 4。
示例 2
输入:n = 4, a = 2, b = 3, c = 4
输出:6
解释:丑数序列为 2, 3, 4, 6, 8, 9, 10, 12... 其中第 4 个是 6。
示例 3
输入:n = 5, a = 2, b = 11, c = 13
输出:10
解释:丑数序列为 2, 4, 6, 8, 10, 11, 12, 13... 其中第 5 个是 10。
提示:
1 91 18- 本题结果在
[1, 2 * 109]的范围内
解题思路
解法一:容斥 + 二分查找(主提交版本)
定义 count(x) = <= x 的丑数个数。根据容斥原理:
count(x) = x/a + x/b + x/c - x/lcm(a,b) - x/lcm(a,c) - x/lcm(b,c) + x/lcm(a,b,c)
由于 count 随 x 单调不减,二分查找最小的 x 使得 count(x) >= n 即可。
复杂度:时间 O(log(2e9) * 常数),空间 O(1)。
解法二:线性扫描候选(对照)
暴力从小到大的整数逐个判断是否能被 a/b/c 整除并计数,直到第 n 个。
仅作为正确性对照,n 较大时会超时。
复杂度:时间 O(答案),空间 O(1)。
代码实现
java
public class Solution {
private long gcd(long x, long y) {
return y == 0 ? x : gcd(y, x % y);
}
private long lcm(long x, long y) {
return x / gcd(x, y) * y;
}
public int nthUglyNumber(int n, int a, int b, int c) {
long ab = lcm(a, b);
long ac = lcm(a, c);
long bc = lcm(b, c);
long abc = lcm(ab, c);
long lo = 1, hi = 2000000000L;
while (lo < hi) {
long mid = lo + (hi - lo) / 2;
long cnt = mid / a + mid / b + mid / c
- mid / ab - mid / ac - mid / bc
+ mid / abc;
if (cnt >= n) {
hi = mid;
} else {
lo = mid + 1;
}
}
return (int) lo;
}
public int nthUglyNumberLinear(int n, int a, int b, int c) {
int cnt = 0;
int x = 1;
while (true) {
if (x % a == 0 || x % b == 0 || x % c == 0) {
cnt++;
if (cnt == n) {
return x;
}
}
x++;
}
}
public static void main(String[] args) {
Solution s = new Solution();
// 解法一
System.out.println(s.nthUglyNumber(3, 2, 3, 5)); // 4
System.out.println(s.nthUglyNumber(4, 2, 3, 4)); // 6
System.out.println(s.nthUglyNumber(5, 2, 11, 13)); // 10
// 解法二(小数据对照)
System.out.println(s.nthUglyNumberLinear(3, 2, 3, 5)); // 4
System.out.println(s.nthUglyNumberLinear(4, 2, 3, 4)); // 6
System.out.println(s.nthUglyNumberLinear(5, 2, 11, 13)); // 10
}
}
复杂度分析
复杂度:时间 O(log(2e9) * 常数),空间 O(1)。
复杂度:时间 O(答案),空间 O(1)。
1202. Smallest String with Swaps(交换字符串中的元素)
题目描述
给你一个字符串 s,以及该字符串中的一些「索引对」数组 pairs,其中 pairs[i] = [a, b] 表示字符串中的两个索引(编号从 0 开始)。
你可以 任意多次交换 在 pairs 中任意一对索引处的字符。
返回在经过若干次交换后,s 可以变成的按字典序最小的字符串。
示例 1
输入:s = "dcab", pairs = [[0,3],[1,2]]
输出:"bacd"
解释:
交换 s[0] 和 s[3], s = "bcad"
交换 s[1] 和 s[2], s = "bacd"
示例 2
输入:s = "dcab", pairs = [[0,3],[1,2],[0,2]]
输出:"abcd"
解释:
交换 s[0] 和 s[3], s = "bcad"
交换 s[0] 和 s[2], s = "acbd"
交换 s[1] 和 s[2], s = "abcd"
示例 3
输入:s = "cba", pairs = [[0,1],[1,2]]
输出:"abc"
解释:
交换 s[0] 和 s[1], s = "bca"
交换 s[1] 和 s[2], s = "bac"
交换 s[0] 和 s[1], s = "abc"
提示:
1 <= s.length <= 10^50 <= pairs.length <= 10^50 <= pairs[i][0], pairs[i][1] < s.lengths中只含有小写英文字母
解题思路
解法一:并查集 + 分组排序(主提交版本)
可交换关系构成无向图,同一连通分量内的字符可以任意排列。
用并查集合并 pairs 中的索引,再对每个连通分量单独取出字符排序后填回。
复杂度:时间 O(n + E·α + Σk·logk),近似 O(n log n),空间 O(n)。
解法二:BFS 连通分量(对照)
从无向图构建邻接表,BFS 找出每个连通分量,同样取出排序填回。
复杂度:时间 O(n + E + Σk·logk),空间 O(n + E)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Solution {
private int[] parent;
private int find(int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]]; // 路径压缩
x = parent[x];
}
return x;
}
private void union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra != rb) {
parent[ra] = rb;
}
}
public String smallestStringWithSwaps(String s, List<List<Integer>> pairs) {
int n = s.length();
parent = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
}
for (List<Integer> p : pairs) {
union(p.get(0), p.get(1));
}
Map<Integer, List<Integer>> groups = new HashMap<>();
for (int i = 0; i < n; i++) {
int r = find(i);
groups.computeIfAbsent(r, k -> new ArrayList<>()).add(i);
}
char[] cs = s.toCharArray();
char[] ans = new char[n];
for (List<Integer> idx : groups.values()) {
List<Character> chars = new ArrayList<>();
for (int i : idx) {
chars.add(cs[i]);
}
Collections.sort(chars);
Collections.sort(idx);
for (int j = 0; j < idx.size(); j++) {
ans[idx.get(j)] = chars.get(j);
}
}
return new String(ans);
}
public String smallestStringWithSwapsBfs(String s, List<List<Integer>> pairs) {
int n = s.length();
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) {
adj.add(new ArrayList<>());
}
for (List<Integer> p : pairs) {
adj.get(p.get(0)).add(p.get(1));
adj.get(p.get(1)).add(p.get(0));
}
char[] cs = s.toCharArray();
char[] ans = new char[n];
boolean[] vis = new boolean[n];
for (int i = 0; i < n; i++) {
if (vis[i]) {
continue;
}
// BFS 收集连通分量
List<Integer> comp = new ArrayList<>();
List<Integer> queue = new ArrayList<>();
queue.add(i);
vis[i] = true;
int head = 0;
while (head < queue.size()) {
int u = queue.get(head++);
comp.add(u);
for (int v : adj.get(u)) {
if (!vis[v]) {
vis[v] = true;
queue.add(v);
}
}
}
List<Character> chars = new ArrayList<>();
for (int u : comp) {
chars.add(cs[u]);
}
Collections.sort(chars);
Collections.sort(comp);
for (int j = 0; j < comp.size(); j++) {
ans[comp.get(j)] = chars.get(j);
}
}
return new String(ans);
}
public static void main(String[] args) {
Solution s = new Solution();
List<List<Integer>> p1 = new ArrayList<>();
p1.add(Arrays.asList(0, 3));
p1.add(Arrays.asList(1, 2));
System.out.println(s.smallestStringWithSwaps("dcab", p1)); // bacd
System.out.println(s.smallestStringWithSwapsBfs("dcab", p1)); // bacd
List<List<Integer>> p2 = new ArrayList<>();
p2.add(Arrays.asList(0, 3));
p2.add(Arrays.asList(1, 2));
p2.add(Arrays.asList(0, 2));
System.out.println(s.smallestStringWithSwaps("dcab", p2)); // abcd
System.out.println(s.smallestStringWithSwapsBfs("dcab", p2)); // abcd
List<List<Integer>> p3 = new ArrayList<>();
p3.add(Arrays.asList(0, 1));
p3.add(Arrays.asList(1, 2));
System.out.println(s.smallestStringWithSwaps("cba", p3)); // abc
System.out.println(s.smallestStringWithSwapsBfs("cba", p3)); // abc
}
}
复杂度分析
复杂度:时间 O(n + E·α + Σk·logk),近似 O(n log n),空间 O(n)。
复杂度:时间 O(n + E + Σk·logk),空间 O(n + E)。
1203. Sort Items by Groups Respecting Dependencies(项目管理)
题目描述
有 n 个项目,每个项目或者不属于任何小组,或者属于 m 个小组之一。group[i] 表示第 i 个项目所属的小组,如果第 i 个项目不属于任何小组,则 group[i] 等于 -1。项目和小组都是从零开始编号的。可能存在小组不负责任何项目,即没有任何项目属于这个小组。
请你帮忙按要求安排这些项目的进度,并返回排序后的项目列表:
- 同一小组的项目,排序后在列表中彼此相邻。
- 项目之间存在一定的依赖关系,我们用一个列表
beforeItems来表示,其中beforeItems[i]表示在进行第i个项目前(位于第i个项目左侧)应该完成的所有项目。
如果存在多个解决方案,只需要返回其中任意一个即可。如果没有合适的解决方案,就请返回一个 空列表 。
示例 1
输入:n = 8, m = 2, group = [-1,-1,1,0,0,1,0,-1], beforeItems = [[],[6],[5],[6],[3,6],[],[],[]]
输出:[6,3,4,1,5,2,0,7]
示例 2
输入:n = 8, m = 2, group = [-1,-1,1,0,0,1,0,-1], beforeItems = [[],[6],[5],[6],[3],[],[4],[]]
输出:[]
解释:与示例 1 大致相同,但是在排序后的列表中,4 必须放在 6 的前面。
提示:
1 4group.length == beforeItems.length == n-1 <= group[i] <= m - 10 <= beforeItems[i].length <= n - 10 <= beforeItems[i][j] <= n - 1i != beforeItems[i][j]beforeItems[i]不含重复元素
解题思路
解法一:双拓扑排序(组间 + 组内)(主提交版本)
为每个组建一个"组图"(未分组的项目各自成组,编号从 m 起递增),
然后:1) 对组间依赖做拓扑排序;2) 对每个组内部的项目依赖做拓扑排序。
组间顺序必须满足相邻(组间拓扑序),组内按内部拓扑序输出。
复杂度:时间 O(n + m + Σ|beforeItems|),空间 O(n + m)。
解法二:依赖关系校准(对照)
先按依赖做一次全项目拓扑排序校验合法性,再按分组归类输出(未分组独立成组),
最后按组间依赖顺序拼接。实现更直白但重复排序。
复杂度:时间 O(n + m + E),空间 O(n + m)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Solution {
public int[] sortItems(int n, int m, int[] group, List<List<Integer>> beforeItems) {
// 1. 给未分组的项目分配独立"虚拟组"
int maxGroup = m;
for (int i = 0; i < n; i++) {
if (group[i] == -1) {
group[i] = maxGroup++;
}
}
// 2. 组图 + 组内图
List<List<Integer>> groupAdj = new ArrayList<>();
List<List<Integer>> itemAdj = new ArrayList<>();
for (int i = 0; i < maxGroup; i++) {
groupAdj.add(new ArrayList<>());
}
for (int i = 0; i < n; i++) {
itemAdj.add(new ArrayList<>());
}
int[] groupDeg = new int[maxGroup];
int[] itemDeg = new int[n];
for (int i = 0; i < n; i++) {
int gi = group[i];
for (int pre : beforeItems.get(i)) {
int gp = group[pre];
if (gi == gp) {
itemAdj.get(pre).add(i);
itemDeg[i]++;
} else {
groupAdj.get(gp).add(gi);
groupDeg[gi]++;
}
}
}
// 3. 组拓扑序
List<Integer> groupOrder = topo(maxGroup, groupAdj, groupDeg);
if (groupOrder.size() != maxGroup) {
return new int[0];
}
// 4. 组内拓扑序
Map<Integer, List<Integer>> groupItems = new HashMap<>();
for (int i = 0; i < n; i++) {
groupItems.computeIfAbsent(group[i], k -> new ArrayList<>()).add(i);
}
int[] ans = new int[n];
int idx = 0;
for (int g : groupOrder) {
List<Integer> items = groupItems.getOrDefault(g, new ArrayList<>());
if (items.isEmpty()) {
continue;
}
// 对组内子图做拓扑:仅保留组内边
int[] inDeg = new int[n];
List<List<Integer>> gAdj = new ArrayList<>();
for (int j = 0; j < n; j++) {
gAdj.add(new ArrayList<>());
}
for (int u : items) {
for (int v : itemAdj.get(u)) {
if (group[v] == g) {
gAdj.get(u).add(v);
inDeg[v]++;
}
}
}
List<Integer> order = topoItems(items, gAdj, inDeg);
if (order.size() != items.size()) {
return new int[0];
}
for (int u : order) {
ans[idx++] = u;
}
}
return ans;
}
private List<Integer> topo(int size, List<List<Integer>> adj, int[] deg) {
List<Integer> res = new ArrayList<>();
int[] q = new int[size];
int head = 0, tail = 0;
for (int i = 0; i < size; i++) {
if (deg[i] == 0) {
q[tail++] = i;
}
}
while (head < tail) {
int u = q[head++];
res.add(u);
for (int v : adj.get(u)) {
if (--deg[v] == 0) {
q[tail++] = v;
}
}
}
return res;
}
private List<Integer> topoItems(List<Integer> items, List<List<Integer>> adj, int[] deg) {
List<Integer> res = new ArrayList<>();
int[] q = new int[items.size()];
int head = 0, tail = 0;
// 用集合加速查找
java.util.Set<Integer> set = new java.util.HashSet<>(items);
for (int u : items) {
if (deg[u] == 0) {
q[tail++] = u;
}
}
while (head < tail) {
int u = q[head++];
res.add(u);
for (int v : adj.get(u)) {
if (--deg[v] == 0) {
q[tail++] = v;
}
}
}
return res;
}
public int[] sortItemsGroupTopo(int n, int m, int[] group, List<List<Integer>> beforeItems) {
int[] g2 = new int[n];
int maxGroup = m;
for (int i = 0; i < n; i++) {
if (group[i] == -1) {
group[i] = maxGroup++;
}
g2[i] = group[i];
}
// 全量项目图(校验整体合法性)
List<List<Integer>> allAdj = new ArrayList<>();
for (int i = 0; i < n; i++) {
allAdj.add(new ArrayList<>());
}
int[] allDeg = new int[n];
for (int i = 0; i < n; i++) {
for (int pre : beforeItems.get(i)) {
allAdj.get(pre).add(i);
allDeg[i]++;
}
}
List<Integer> full = topo(n, allAdj, allDeg.clone());
if (full.size() != n) {
return new int[0];
}
// 分组归类(key = 组号),组内保持拓扑序(full 顺序)
Map<Integer, List<Integer>> map = new HashMap<>();
for (int u : full) {
map.computeIfAbsent(group[u], k -> new ArrayList<>()).add(u);
}
// 组间依赖
List<List<Integer>> gAdj = new ArrayList<>();
for (int i = 0; i < maxGroup; i++) {
gAdj.add(new ArrayList<>());
}
int[] gDeg = new int[maxGroup];
java.util.Set<String> seen = new java.util.HashSet<>();
for (int i = 0; i < n; i++) {
for (int pre : beforeItems.get(i)) {
if (group[pre] != group[i]) {
String key = group[pre] + "->" + group[i];
if (seen.add(key)) {
gAdj.get(group[pre]).add(group[i]);
gDeg[group[i]]++;
}
}
}
}
List<Integer> gOrder = topo(maxGroup, gAdj, gDeg);
if (gOrder.size() != maxGroup) {
return new int[0];
}
int[] ans = new int[n];
int idx = 0;
for (int g : gOrder) {
for (int u : map.getOrDefault(g, new ArrayList<>())) {
ans[idx++] = u;
}
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
int n = 8, m = 2;
int[] group = {-1, -1, 1, 0, 0, 1, 0, -1};
List<List<Integer>> before = new ArrayList<>();
before.add(new ArrayList<>());
before.add(Arrays.asList(6));
before.add(Arrays.asList(5));
before.add(Arrays.asList(6));
before.add(Arrays.asList(3, 6));
before.add(new ArrayList<>());
before.add(new ArrayList<>());
before.add(new ArrayList<>());
System.out.println(java.util.Arrays.toString(s.sortItems(n, m, group.clone(), before))); // 一种合法排序
System.out.println(java.util.Arrays.toString(s.sortItemsGroupTopo(n, m, group.clone(), before)));
int[] group2 = {-1, -1, 1, 0, 0, 1, 0, -1};
List<List<Integer>> before2 = new ArrayList<>();
before2.add(new ArrayList<>());
before2.add(Arrays.asList(6));
before2.add(Arrays.asList(5));
before2.add(Arrays.asList(6));
before2.add(Arrays.asList(3));
before2.add(new ArrayList<>());
before2.add(Arrays.asList(4));
before2.add(new ArrayList<>());
System.out.println(java.util.Arrays.toString(s.sortItems(8, 2, group2, before2))); // []
System.out.println(java.util.Arrays.toString(s.sortItemsGroupTopo(8, 2, group2, before2))); // []
}
}
复杂度分析
复杂度:时间 O(n + m + Σ|beforeItems|),空间 O(n + m)。
复杂度:时间 O(n + m + E),空间 O(n + m)。
1204. Last Person to Fit in the Bus(最后一个能进入巴士的人)
题目类型:数据库(SQL)
题目描述
表: Queue
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| person_id | int |
| person_name | varchar |
| weight | int |
| turn | int |
+-------------+---------+
person_id 是这个表具有唯一值的列。
该表展示了所有候车乘客的信息。
表中 person_id 和 turn 列将包含从 1 到 n 的所有数字,其中 n 是表中的行数。
turn 决定了候车乘客上巴士的顺序,其中 turn=1 表示第一个上巴士,turn=n 表示最后一个上巴士。
weight 表示候车乘客的体重,以千克为单位。
有一队乘客在等着上巴士。然而,巴士有1000 千克 的重量限制,所以其中一部分乘客可能无法上巴士。
编写解决方案找出 最后一个 上巴士且不超过重量限制的乘客,并报告 person_name 。题目测试用例确保顺位第一的人可以上巴士且不会超重。
返回结果格式如下所示。
示例 1:
**输入:**
Queue 表
+-----------+-------------+--------+------+
| person_id | person_name | weight | turn |
+-----------+-------------+--------+------+
| 5 | Alice | 250 | 1 |
| 4 | Bob | 175 | 5 |
| 3 | Alex | 350 | 2 |
| 6 | John Cena | 400 | 3 |
| 1 | Winston | 500 | 6 |
| 2 | Marie | 200 | 4 |
+-----------+-------------+--------+------+
**输出:**
+-------------+
| person_name |
+-------------+
| John Cena |
+-------------+
**解释:**
为了简化,Queue 表按 turn 列由小到大排序。
+------+----+-----------+--------+--------------+
| Turn | ID | Name | Weight | Total Weight |
+------+----+-----------+--------+--------------+
| 1 | 5 | Alice | 250 | 250 |
| 2 | 3 | Alex | 350 | 600 |
| 3 | 6 | John Cena | 400 | 1000 | (最后一个上巴士)
| 4 | 2 | Marie | 200 | 1200 | (无法上巴士)
| 5 | 4 | Bob | 175 | ___ |
| 6 | 1 | Winston | 500 | ___ |
+------+----+-----------+--------+--------------+
解题思路
思考
上车顺序由 t u r n turn turn 唯一确定,能否上车只取决于到该人为止的体重前缀和是否不超过 1000 1000 1000。自连接把每个人与所有不晚于他的人配对,按人聚合后用 H A V I N G HAVING HAVING 筛出仍能上车的人,再取 t u r n turn turn 最大者即为最后上车的人。
考察自连接 + 分组聚合 + HAVING + ORDER BY LIMIT:让每个人与其前面所有(含自己)排队者比较,累计重量不超过 1000 且 turn 最大者即为最后一个能上车的人。
代码实现
sql
# Write your MySQL query statement below
SELECT a.person_name
FROM
Queue AS a,
Queue AS b
WHERE a.turn >= b.turn
GROUP BY a.person_id
HAVING SUM(b.weight) <= 1000
ORDER BY a.turn DESC
LIMIT 1;
复杂度分析
本题为数据库查询题,不涉及传统算法的时间/空间复杂度,执行效率由数据库优化器决定;建议对查询涉及的过滤列与连接列建立索引以加速。
1205. Monthly Transactions II(每月交易 II 🔒)
题目类型:数据库(SQL)
题目描述
Transactions 记录表
+----------------+---------+
| Column Name | Type |
+----------------+---------+
| id | int |
| country | varchar |
| state | enum |
| amount | int |
| trans_date | date |
+----------------+---------+
id 是这个表的主键。
该表包含有关传入事务的信息。
状态列是枚举类型,值为 [approved、declined] 其中之一的列。
Chargebacks 表
+----------------+---------+
| Column Name | Type |
+----------------+---------+
| trans_id | int |
| trans_date | date |
+----------------+---------+
退单包含有关放置在事务表中的某些事务的传入退单的基本信息。
trans_id 是 transactions 表的 id 列的外键(reference 列)。
每项退单都对应于之前进行的交易,即使未经批准。
编写一个解决方案,找出每个国家/地区的每月交易信息:已批准交易的数量及其总金额、退单的数量及其总金额。
**注意:**在你的解决方案中,只需显示给定月份和国家,忽略所有为零的行。
以 任意顺序 返回结果表。
结果格式如下所示。
示例 1:
**输入:**
Transactions 表:
+-----+---------+----------+--------+------------+
| id | country | state | amount | trans_date |
+-----+---------+----------+--------+------------+
| 101 | US | approved | 1000 | 2019-05-18 |
| 102 | US | declined | 2000 | 2019-05-19 |
| 103 | US | approved | 3000 | 2019-06-10 |
| 104 | US | declined | 4000 | 2019-06-13 |
| 105 | US | approved | 5000 | 2019-06-15 |
+-----+---------+----------+--------+------------+
Chargebacks 表:
+----------+------------+
| trans_id | trans_date |
+----------+------------+
| 102 | 2019-05-29 |
| 101 | 2019-06-30 |
| 105 | 2019-09-18 |
+----------+------------+
**输出:**
+---------+---------+----------------+-----------------+------------------+-------------------+
| month | country | approved_count | approved_amount | chargeback_count | chargeback_amount |
+---------+---------+----------------+-----------------+------------------+-------------------+
| 2019-05 | US | 1 | 1000 | 1 | 2000 |
| 2019-06 | US | 2 | 8000 | 1 | 1000 |
| 2019-09 | US | 0 | 0 | 1 | 5000 |
+---------+---------+----------------+-----------------+------------------+-------------------+
解题思路
思考
月度报表既要统计当月批准的交易,也要把退款记在退款发生的月份,二者日期字段不同,不宜在原表上直接分组。
将退款改写成与交易同构的行(状态为 chargeback、日期为退款日)后与原交易做并集,所有事件便落在同一套 ( m o n t h , c o u n t r y ) (month, country) (month,country) 维度上。
为此用 U N I O N UNION UNION 拼出事件表,再按月与国家汇总批准与退款的笔数和金额,并用 H A V I N G HAVING HAVING 丢掉两端均为零的组。
考察UNION 合并三表统计:分别统计"可退款交易数与金额"、"批准交易数与金额",按月份与国家分组后合并、用聚合函数补齐空值(IFNULL 或 SUM 配 CASE WHEN)。
代码实现
sql
# Write your MySQL query statement below
WITH
T AS (
SELECT * FROM Transactions
UNION
SELECT id, country, 'chargeback', amount, c.trans_date
FROM
Transactions AS t
JOIN Chargebacks AS c ON t.id = c.trans_id
)
SELECT
DATE_FORMAT(trans_date, '%Y-%m') AS month,
country,
SUM(state = 'approved') AS approved_count,
SUM(IF(state = 'approved', amount, 0)) AS approved_amount,
SUM(state = 'chargeback') AS chargeback_count,
SUM(IF(state = 'chargeback', amount, 0)) AS chargeback_amount
FROM T
GROUP BY 1, 2
HAVING approved_amount OR chargeback_amount;
复杂度分析
本题为数据库查询题,不涉及传统算法的时间/空间复杂度,执行效率由数据库优化器决定;建议对查询涉及的过滤列与连接列建立索引以加速。
1206. Design Skiplist(设计跳表)
题目描述
不使用任何库函数,设计一个 跳表 。
跳表 是在 O(log(`n`)`)` 时间内完成增加、删除、搜索操作的数据结构。跳表相比于树堆与红黑树,其功能与性能相当,并且跳表的代码长度相较下更短,其设计思想与链表相似。 例如,一个跳表包含 `[30, 40, 50, 60, 70, 90]` ,然后增加 `80`、`45` 到跳表中,以下图的方式操作: 跳表中有很多层,每一层是一个短的链表。在第一层的作用下,增加、删除和搜索操作的时间复杂度不超过 O(n)。跳表的每一个操作的平均时间复杂度是 O(log(n)),空间复杂度是 O(`n`)。
了解更多 : https://oi-wiki.org/ds/skiplist/
在本题中,你的设计应该要包含这些函数:
bool search(int target): 返回target是否存在于跳表中。void add(int num): 插入一个元素到跳表。bool erase(int num): 在跳表中删除一个值,如果num不存在,直接返回false. 如果存在多个num,删除其中任意一个即可。
注意,跳表中可能存在多个相同的值,你的代码需要处理这种情况。
示例 1
输入
["Skiplist", "add", "add", "add", "search", "add", "search", "erase", "erase", "search"]
[[], [1], [2], [3], [0], [4], [1], [0], [1], [1]]
输出
[null, null, null, null, false, null, true, false, true, false]
解释
Skiplist skiplist = new Skiplist();
skiplist.add(1);
skiplist.add(2);
skiplist.add(3);
skiplist.search(0); // 返回 false
skiplist.add(4);
skiplist.search(1); // 返回 true
skiplist.erase(0); // 返回 false,0 不在跳表中
skiplist.erase(1); // 返回 true
skiplist.search(1); // 返回 false,1 已被擦除
提示:
0 4- 调用
search,add,erase操作次数不大于5 * 104
解题思路
解法一:标准跳表(多层链表 + 随机层数)(主提交版本)
每个节点包含 val、forward 数组(指向各层的后继)。
add 时按随机生成的层数在每层插入;search/erase 从高层向下逐层查找。
删除时若某层前驱后继相同则断开该层链接。
复杂度:期望时间 O(log n),空间 O(n)。
解法二:Treap / 有序集合适配(对照)
用 java.util.TreeMap<Integer, Integer> 统计每个值的出现次数,
逻辑等价的平衡树实现。作为正确性对照。
复杂度:时间 O(log n),空间 O(n)。
代码实现
java
import java.util.concurrent.ThreadLocalRandom;
public class Solution {
static class SkiplistNode {
int val;
SkiplistNode[] forward;
SkiplistNode(int val, int level) {
this.val = val;
this.forward = new SkiplistNode[level + 1];
}
}
static class Skiplist {
private static final int MAX_LEVEL = 16;
private static final double P = 0.5;
private SkiplistNode head;
private int level;
public Skiplist() {
head = new SkiplistNode(-1, MAX_LEVEL);
level = 0;
}
public boolean search(int target) {
SkiplistNode cur = head;
for (int i = level; i >= 0; i--) {
while (cur.forward[i] != null && cur.forward[i].val < target) {
cur = cur.forward[i];
}
}
cur = cur.forward[0];
return cur != null && cur.val == target;
}
public void add(int num) {
SkiplistNode[] update = new SkiplistNode[MAX_LEVEL + 1];
SkiplistNode cur = head;
for (int i = level; i >= 0; i--) {
while (cur.forward[i] != null && cur.forward[i].val < num) {
cur = cur.forward[i];
}
update[i] = cur;
}
int lv = randomLevel();
if (lv > level) {
for (int i = level + 1; i <= lv; i++) {
update[i] = head;
}
level = lv;
}
SkiplistNode node = new SkiplistNode(num, lv);
for (int i = 0; i <= lv; i++) {
node.forward[i] = update[i].forward[i];
update[i].forward[i] = node;
}
}
public boolean erase(int num) {
SkiplistNode[] update = new SkiplistNode[MAX_LEVEL + 1];
SkiplistNode cur = head;
for (int i = level; i >= 0; i--) {
while (cur.forward[i] != null && cur.forward[i].val < num) {
cur = cur.forward[i];
}
update[i] = cur;
}
cur = cur.forward[0];
if (cur == null || cur.val != num) {
return false;
}
for (int i = 0; i <= level; i++) {
if (update[i].forward[i] == cur) {
update[i].forward[i] = cur.forward[i];
}
}
while (level > 0 && head.forward[level] == null) {
level--;
}
return true;
}
private int randomLevel() {
int lv = 0;
while (lv < MAX_LEVEL && ThreadLocalRandom.current().nextDouble() < P) {
lv++;
}
return lv;
}
}
static class SkiplistTree {
private java.util.TreeMap<Integer, Integer> map;
public SkiplistTree() {
map = new java.util.TreeMap<>();
}
public boolean search(int target) {
return map.containsKey(target);
}
public void add(int num) {
map.put(num, map.getOrDefault(num, 0) + 1);
}
public boolean erase(int num) {
if (!map.containsKey(num)) {
return false;
}
int c = map.get(num);
if (c == 1) {
map.remove(num);
} else {
map.put(num, c - 1);
}
return true;
}
}
private static void run(String[] ops, int[][] args) {
Skiplist list = new Skiplist();
SkiplistTree tree = new SkiplistTree();
boolean ok = true;
for (int i = 0; i < ops.length; i++) {
String op = ops[i];
if ("add".equals(op)) {
list.add(args[i][0]);
tree.add(args[i][0]);
} else if ("search".equals(op)) {
boolean a = list.search(args[i][0]);
boolean b = tree.search(args[i][0]);
if (a != b) {
System.out.println("MISMATCH search " + args[i][0] + " skip=" + a + " tree=" + b);
ok = false;
}
System.out.println((i - 1 >= 0 ? "" : "") + "search(" + args[i][0] + ")=" + a);
} else if ("erase".equals(op)) {
boolean a = list.erase(args[i][0]);
boolean b = tree.erase(args[i][0]);
if (a != b) {
System.out.println("MISMATCH erase " + args[i][0]);
ok = false;
}
System.out.println("erase(" + args[i][0] + ")=" + a);
}
}
System.out.println("一致性: " + (ok ? "PASS" : "FAIL"));
}
public static void main(String[] args) {
// 官方示例
String[] ops = {"add", "add", "add", "search", "add", "search", "erase", "erase", "search"};
int[][] arg = {{1}, {2}, {3}, {0}, {4}, {1}, {0}, {1}, {1}};
run(ops, arg);
// 期望: search(0)=false, search(1)=true, erase(0)=false, erase(1)=true, search(1)=false
// 重复值
String[] ops2 = {"add", "add", "erase", "erase", "search"};
int[][] arg2 = {{5}, {5}, {5}, {5}, {5}};
run(ops2, arg2);
// 期望: erase(5)=true(第一次), erase(5)=true(仍有第二个5), search(5)=false
}
}
复杂度分析
复杂度:期望时间 O(log n),空间 O(n)。
复杂度:时间 O(log n),空间 O(n)。
1207. Unique Number of Occurrences(独一无二的出现次数)
题目描述
给你一个整数数组 arr,如果每个数的出现次数都是独一无二的,就返回 true;否则返回 false。
示例 1
输入:arr = [1,2,2,1,1,3]
输出:true
解释:在该数组中,1 出现了 3 次,2 出现了 2 次,3 只出现了 1 次。没有两个数的出现次数相同。
示例 2
输入:arr = [1,2]
输出:false
示例 3
输入:arr = [-3,0,1,-3,1,1,1,-3,10,0]
输出:true
提示:
1 <= arr.length <= 1000-1000 <= arr[i] <= 1000
解题思路
解法一:Map 计数 + Set 去重(主提交版本)
先用 HashMap 统计每个数的出现次数,再检查频次集合大小是否等于不同数字个数。
复杂度:时间 O(n),空间 O(n)。
解法二:数组计数版(对照)
由于值域范围小(-1000~1000),用偏移数组统计频次,再用 boolean\[\] 标记
出现过的频次数。复杂度:时间 O(n + V),空间 O(V)。
代码实现
java
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class Solution {
public boolean uniqueOccurrences(int[] arr) {
Map<Integer, Integer> cnt = new HashMap<>();
for (int x : arr) {
cnt.put(x, cnt.getOrDefault(x, 0) + 1);
}
Set<Integer> seen = new HashSet<>();
for (int c : cnt.values()) {
if (!seen.add(c)) {
return false;
}
}
return true;
}
public boolean uniqueOccurrencesArray(int[] arr) {
int[] cnt = new int[2001];
for (int x : arr) {
cnt[x + 1000]++;
}
boolean[] mark = new boolean[arr.length + 1];
for (int i = 0; i < cnt.length; i++) {
if (cnt[i] == 0) {
continue;
}
if (mark[cnt[i]]) {
return false;
}
mark[cnt[i]] = true;
}
return true;
}
public static void main(String[] args) {
Solution s = new Solution();
int[] a1 = {1, 2, 2, 1, 1, 3};
int[] a2 = {1, 2};
int[] a3 = {-3, 0, 1, -3, 1, 1, 1, -3, 10, 0};
System.out.println(s.uniqueOccurrences(a1)); // true
System.out.println(s.uniqueOccurrencesArray(a1)); // true
System.out.println(s.uniqueOccurrences(a2)); // false
System.out.println(s.uniqueOccurrencesArray(a2)); // false
System.out.println(s.uniqueOccurrences(a3)); // true
System.out.println(s.uniqueOccurrencesArray(a3)); // true
}
}
复杂度分析
复杂度:时间 O(n),空间 O(n)。
出现过的频次数。复杂度:时间 O(n + V),空间 O(V)。
1208. Get Equal Substrings Within Budget(尽可能使字符串相等)
题目描述
给你两个长度相同的字符串,s 和 t。
将 s 中的第 i 个字符变到 t 中的第 i 个字符需要 |s[i] - t[i]| 的开销(开销可能为 0),也就是两个字符的 ASCII 码值的差的绝对值。
用于变更字符串的最大预算是 maxCost。在转化字符串时,总开销应当小于等于该预算,这也意味着字符串的转化可能是不完全的。
如果你可以将 s 的子字符串转化为它在 t 中对应的子字符串,则返回可以转化的最大长度。
如果 s 中没有子字符串可以转化成 t 中对应的子字符串,则返回 0。
示例 1
输入:s = "abcd", t = "bcdf", maxCost = 3
输出:3
解释:s 中的 "abc" 可以变为 "bcd"。开销为 3,所以最大长度为 3。
示例 2
输入:s = "abcd", t = "cdef", maxCost = 3
输出:1
解释:s 中的任一字符要想变成 t 中对应的字符,其开销都是 2。因此,最大长度为` 1。`
示例 3
输入:s = "abcd", t = "acde", maxCost = 0
输出:1
解释:a -> a, cost = 0,字符串未发生变化,所以最大长度为 1。
提示:
1 5t.length == s.length0 6s和t都只含小写英文字母。
解题思路
解法一:滑动窗口(主提交版本)
令 diffi = |si-ti|,问题转化为:最长连续子数组且和 <= maxCost。
用双指针维护窗口 l, r 的和,不超过预算时右移,超过时左移。
复杂度:时间 O(n),空间 O(n)(diff 数组,可优化为 O(1))。
解法二:二分长度 + 前缀和(对照)
对窗口长度做二分,check(mid) 判断是否存在长度为 mid 且和 <= maxCost 的窗口。
复杂度:时间 O(n log n),空间 O(n)。
代码实现
java
public class Solution {
public int equalSubstring(String s, String t, int maxCost) {
int n = s.length();
int[] diff = new int[n];
for (int i = 0; i < n; i++) {
diff[i] = Math.abs(s.charAt(i) - t.charAt(i));
}
int l = 0, r = 0, sum = 0, ans = 0;
while (r < n) {
sum += diff[r];
while (sum > maxCost) {
sum -= diff[l];
l++;
}
ans = Math.max(ans, r - l + 1);
r++;
}
return ans;
}
public int equalSubstringBinary(String s, String t, int maxCost) {
int n = s.length();
int[] pre = new int[n + 1];
for (int i = 0; i < n; i++) {
pre[i + 1] = pre[i] + Math.abs(s.charAt(i) - t.charAt(i));
}
int lo = 0, hi = n;
while (lo < hi) {
int mid = (lo + hi + 1) >>> 1;
boolean ok = false;
for (int i = 0; i + mid <= n; i++) {
if (pre[i + mid] - pre[i] <= maxCost) {
ok = true;
break;
}
}
if (ok) {
lo = mid;
} else {
hi = mid - 1;
}
}
return lo;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.equalSubstring("abcd", "bcdf", 3)); // 3
System.out.println(s.equalSubstringBinary("abcd", "bcdf", 3)); // 3
System.out.println(s.equalSubstring("abcd", "cdef", 3)); // 1
System.out.println(s.equalSubstringBinary("abcd", "cdef", 3)); // 1
System.out.println(s.equalSubstring("abcd", "acde", 0)); // 1
System.out.println(s.equalSubstringBinary("abcd", "acde", 0)); // 1
}
}
复杂度分析
复杂度:时间 O(n),空间 O(n)(diff 数组,可优化为 O(1))。
复杂度:时间 O(n log n),空间 O(n)。
1209. Remove All Adjacent Duplicates in String II(删除字符串中的所有相邻重复项 II)
题目描述
给你一个字符串 s,「k 倍重复项删除操作」将会从 s 中选择 k 个相邻且相等的字母,并删除它们,使被删去的字符串的左侧和右侧连在一起。
你需要对 s 重复进行无限次这样的删除操作,直到无法继续为止。
在执行完所有删除操作后,返回最终得到的字符串。
本题答案保证唯一。
示例 1
输入:s = "abcd", k = 2
输出:"abcd"
解释:没有要删除的内容。
示例 2
输入:s = "deeedbbcccbdaa", k = 3
输出:"aa"
解释:
先删除 "eee" 和 "ccc",得到 "ddbbbdaa"
再删除 "bbb",得到 "dddaa"
最后删除 "ddd",得到 "aa"
示例 3
输入:s = "pbbcggttciiippooaais", k = 2
输出:"ps"
提示:
1 <= s.length <= 10^52 <= k <= 10^4s中只含有小写英文字母。
解题思路
解法一:栈 + 计数(主提交版本)
用两个栈:charStack 存字符,cntStack 存当前字符连续出现的次数。
扫描字符 c:若与栈顶相同则计数+1;若计数 == k 则把字符栈与计数栈各弹出 k 次
(等价于删除这 k 个字符);否则入栈。
复杂度:时间 O(n),空间 O(n)。
解法二:字符+计数合并栈(数组模拟,对照)
栈元素为 字符, 连续次数,扫描时若栈顶字符相同则递增计数,
计数达到 k 就弹出栈顶;否则压入新元素。最后按栈序拼接。
复杂度:时间 O(n),空间 O(n)。
代码实现
java
import java.util.ArrayDeque;
import java.util.Deque;
public class Solution {
public String removeDuplicates(String s, int k) {
Deque<Character> cs = new ArrayDeque<>();
Deque<Integer> cnts = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (!cs.isEmpty() && cs.peek() == c) {
int nc = cnts.pop() + 1;
if (nc == k) {
cs.pop();
} else {
cnts.push(nc);
}
} else {
cs.push(c);
cnts.push(1);
}
}
StringBuilder sb = new StringBuilder();
Deque<Character> tmp = new ArrayDeque<>();
while (!cs.isEmpty()) {
tmp.push(cs.pop());
}
while (!tmp.isEmpty()) {
sb.append(tmp.pop());
}
return sb.toString();
}
public String removeDuplicatesMerge(String s, int k) {
Deque<int[]> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (!stack.isEmpty() && stack.peek()[0] == c) {
stack.peek()[1]++;
if (stack.peek()[1] == k) {
stack.pop();
}
} else {
stack.push(new int[]{c, 1});
}
}
StringBuilder sb = new StringBuilder();
Deque<int[]> rev = new ArrayDeque<>();
while (!stack.isEmpty()) {
rev.push(stack.pop());
}
while (!rev.isEmpty()) {
int[] e = rev.pop();
for (int i = 0; i < e[1]; i++) {
sb.append((char) e[0]);
}
}
return sb.toString();
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.removeDuplicates("abcd", 2)); // abcd
System.out.println(s.removeDuplicatesMerge("abcd", 2)); // abcd
System.out.println(s.removeDuplicates("deeedbbcccbdaa", 3)); // aa
System.out.println(s.removeDuplicatesMerge("deeedbbcccbdaa", 3)); // aa
System.out.println(s.removeDuplicates("pbbcggttciiippooaais", 2)); // ps
System.out.println(s.removeDuplicatesMerge("pbbcggttciiippooaais", 2)); // ps
}
}
复杂度分析
复杂度:时间 O(n),空间 O(n)。
1210. Minimum Moves to Reach Target with Rotations(穿过迷宫的最少移动次数)
题目描述
你还记得那条风靡全球的贪吃蛇吗?
我们在一个 n*n 的网格上构建了新的迷宫地图,蛇的长度为 2,也就是说它会占去两个单元格。蛇会从左上角((0, 0) 和 (0, 1))开始移动。我们用 0 表示空单元格,用 1 表示障碍物。蛇需要移动到迷宫的右下角((n-1, n-2) 和 (n-1, n-1))。
每次移动,蛇可以这样走:
- 如果没有障碍,则向右移动一个单元格。并仍然保持身体的水平/竖直状态。
- 如果没有障碍,则向下移动一个单元格。并仍然保持身体的水平/竖直状态。
- 如果它处于水平状态并且其下面的两个单元都是空的,就顺时针旋转 90 度。蛇从(
(r, c)、(r, c+1))移动到 ((r, c)、(r+1, c))。 - 如果它处于竖直状态并且其右面的两个单元都是空的,就逆时针旋转 90 度。蛇从(
(r, c)、(r+1, c))移动到((r, c)、(r, c+1))。
返回蛇抵达目的地所需的最少移动次数。
如果无法到达目的地,请返回-1。
示例 1
输入:grid = [[0,0,0,0,0,1],
[1,1,0,0,1,0],
[0,0,0,0,1,1],
[0,0,1,0,1,0],
[0,1,1,0,0,0],
[0,1,1,0,0,0]]
输出:11
解释:
一种可能的解决方案是 [右, 右, 顺时针旋转, 右, 下, 下, 下, 下, 逆时针旋转, 右, 下]。
示例 2
输入:grid = [[0,0,1,1,1,1],
[0,0,0,0,1,1],
[1,1,0,0,0,1],
[1,1,1,0,0,1],
[1,1,1,0,0,1],
[1,1,1,0,0,0]]
输出:9
提示:
2 <= n <= 1000 <= grid[i][j] <= 1- 蛇保证从空单元格开始出发。
解题思路
解法一:BFS 状态搜索(主提交版本)
状态表示为 (r, c, dir),dir=0 水平(尾部在 (r,c),头在 (r,c+1)),
dir=1 竖直(尾部在 (r,c),头在 (r+1,c))。BFS 扩展 4 种动作:
右移、下移、顺时针旋转(水平)、逆时针旋转(竖直)。
复杂度:时间 O(n^2),空间 O(n^2)。
解法二:双向 BFS / 三维 DP(对照)
用三维 visitedrc2 标记 + 队列迭代的等价实现(也只列出标准 BFS),
这里以"带状态扩展的显式队列"为对照实现(逻辑同解法一,仅编码不同)。
复杂度:时间 O(n^2),空间 O(n^2)。
代码实现
java
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
public class Solution {
public int minimumMoves(int[][] grid) {
int n = grid.length;
boolean[][][] vis = new boolean[n][n][2];
Deque<int[]> q = new ArrayDeque<>();
q.offer(new int[]{0, 0, 0}); // 尾部位置 + 方向
vis[0][0][0] = true;
int steps = 0;
while (!q.isEmpty()) {
int size = q.size();
for (int t = 0; t < size; t++) {
int[] cur = q.poll();
int r = cur[0], c = cur[1], d = cur[2];
// 检查是否到达目标
if (r == n - 1 && c == n - 2 && d == 0) {
return steps;
}
if (d == 0) { // 水平
// 右移
if (c + 2 < n && grid[r][c + 2] == 0 && !vis[r][c + 1][0]) {
vis[r][c + 1][0] = true;
q.offer(new int[]{r, c + 1, 0});
}
// 下移
if (r + 1 < n && grid[r + 1][c] == 0 && grid[r + 1][c + 1] == 0 && !vis[r + 1][c][0]) {
vis[r + 1][c][0] = true;
q.offer(new int[]{r + 1, c, 0});
}
// 顺时针旋转(水平 -> 竖直)
if (r + 1 < n && grid[r + 1][c] == 0 && grid[r + 1][c + 1] == 0 && !vis[r][c][1]) {
vis[r][c][1] = true;
q.offer(new int[]{r, c, 1});
}
} else { // 竖直
// 右移
if (c + 1 < n && grid[r][c + 1] == 0 && grid[r + 1][c + 1] == 0 && !vis[r][c + 1][1]) {
vis[r][c + 1][1] = true;
q.offer(new int[]{r, c + 1, 1});
}
// 下移
if (r + 2 < n && grid[r + 2][c] == 0 && !vis[r + 1][c][1]) {
vis[r + 1][c][1] = true;
q.offer(new int[]{r + 1, c, 1});
}
// 逆时针旋转(竖直 -> 水平)
if (c + 1 < n && grid[r][c + 1] == 0 && grid[r + 1][c + 1] == 0 && !vis[r][c][0]) {
vis[r][c][0] = true;
q.offer(new int[]{r, c, 0});
}
}
}
steps++;
}
return -1;
}
public int minimumMovesBfs2(int[][] grid) {
int n = grid.length;
boolean[][][] vis = new boolean[n][n][2];
Deque<int[]> q = new ArrayDeque<>();
q.offer(new int[]{0, 0, 0});
vis[0][0][0] = true;
int steps = 0;
while (!q.isEmpty()) {
for (int sz = q.size(); sz > 0; sz--) {
int[] cur = q.poll();
int r = cur[0], c = cur[1], d = cur[2];
if (r == n - 1 && c == n - 2 && d == 0) {
return steps;
}
if (d == 0) {
// 右
if (c + 2 < n && grid[r][c + 2] == 0 && !vis[r][c + 1][0]) {
vis[r][c + 1][0] = true;
q.offer(new int[]{r, c + 1, 0});
}
// 下
if (r + 1 < n && grid[r + 1][c] == 0 && grid[r + 1][c + 1] == 0 && !vis[r + 1][c][0]) {
vis[r + 1][c][0] = true;
q.offer(new int[]{r + 1, c, 0});
}
// 旋转
if (r + 1 < n && grid[r + 1][c] == 0 && grid[r + 1][c + 1] == 0 && !vis[r][c][1]) {
vis[r][c][1] = true;
q.offer(new int[]{r, c, 1});
}
} else {
// 右
if (c + 1 < n && grid[r][c + 1] == 0 && grid[r + 1][c + 1] == 0 && !vis[r][c + 1][1]) {
vis[r][c + 1][1] = true;
q.offer(new int[]{r, c + 1, 1});
}
// 下
if (r + 2 < n && grid[r + 2][c] == 0 && !vis[r + 1][c][1]) {
vis[r + 1][c][1] = true;
q.offer(new int[]{r + 1, c, 1});
}
// 旋转
if (c + 1 < n && grid[r][c + 1] == 0 && grid[r + 1][c + 1] == 0 && !vis[r][c][0]) {
vis[r][c][0] = true;
q.offer(new int[]{r, c, 0});
}
}
}
steps++;
}
return -1;
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] g1 = {
{0, 0, 0, 0, 0, 1},
{1, 1, 0, 0, 1, 0},
{0, 0, 0, 0, 1, 1},
{0, 0, 1, 0, 1, 0},
{0, 1, 1, 0, 0, 0},
{0, 1, 1, 0, 0, 0}
};
System.out.println(s.minimumMoves(g1)); // 11
System.out.println(s.minimumMovesBfs2(g1)); // 11
int[][] g2 = {
{0, 0, 1, 1, 1, 1},
{0, 0, 0, 0, 1, 1},
{1, 1, 0, 0, 0, 1},
{1, 1, 1, 0, 0, 1},
{1, 1, 1, 0, 0, 1},
{1, 1, 1, 0, 0, 0}
};
System.out.println(s.minimumMoves(g2)); // 9
System.out.println(s.minimumMovesBfs2(g2)); // 9
}
}
复杂度分析
复杂度:时间 O(n^2),空间 O(n^2)。
1211. Queries Quality and Percentage(查询结果的质量和占比)
题目类型:数据库(SQL)
题目描述
Queries 表:
+-------------+---------+
| Column Name | Type |
+-------------+---------+
| query_name | varchar |
| result | varchar |
| position | int |
| rating | int |
+-------------+---------+
此表可能有重复的行。
此表包含了一些从数据库中收集的查询信息。
"位置"(`position`)列的值为 **1** 到 **500** 。
"评分"(`rating`)列的值为 **1** 到 **5** 。评分小于 3 的查询被定义为质量很差的查询。
将查询结果的质量 quality 定义为:
各查询结果的评分与其位置之间比率的平均值。
将劣质查询百分比 poor_query_percentage 定义为:
评分小于 3 的查询结果占全部查询结果的百分比。
编写解决方案,找出每次的 query_name 、 quality 和 poor_query_percentage。
quality 和 poor_query_percentage 都应 四舍五入到小数点后两位 。
以 任意顺序 返回结果表。
结果格式如下所示:
示例 1:
**输入:**
Queries table:
+------------+-------------------+----------+--------+
| query_name | result | position | rating |
+------------+-------------------+----------+--------+
| Dog | Golden Retriever | 1 | 5 |
| Dog | German Shepherd | 2 | 5 |
| Dog | Mule | 200 | 1 |
| Cat | Shirazi | 5 | 2 |
| Cat | Siamese | 3 | 3 |
| Cat | Sphynx | 7 | 4 |
+------------+-------------------+----------+--------+
**输出:**
+------------+---------+-----------------------+
| query_name | quality | poor_query_percentage |
+------------+---------+-----------------------+
| Dog | 2.50 | 33.33 |
| Cat | 0.66 | 33.33 |
+------------+---------+-----------------------+
**解释:**
Dog 查询结果的质量为 ((5 / 1) + (5 / 2) + (1 / 200)) / 3 = 2.50
Dog 查询结果的劣质查询百分比为 (1 / 3) * 100 = 33.33
Cat 查询结果的质量为 ((2 / 5) + (3 / 3) + (4 / 7)) / 3 = 0.66
Cat 查询结果的劣质查询百分比为 (1 / 3) * 100 = 33.33
解题思路
思考
质量是 r a t i n g / p o s i t i o n rating/position rating/position 的均值,劣质占比是 r a t i n g < 3 rating < 3 rating<3 的比例。二者都是按查询名聚合的标量,直接 G R O U P B Y q u e r y _ n a m e GROUP\ BY\ query\_name GROUP BY query_name,用 A V G AVG AVG 分别计算比值与布尔均值,再 R O U N D ROUND ROUND 到两位。空查询名按题意排除。
考察ROUND + AVG + SUM + GROUP BY:quality = 每个查询的 rating/position 均值,poor_query_percentage = rating<3 的查询占比,均按 query_name 分组。
代码实现
sql
# Write your MySQL query statement below
SELECT
query_name,
ROUND(AVG(rating / position), 2) AS quality,
ROUND(AVG(rating < 3) * 100, 2) AS poor_query_percentage
FROM Queries
WHERE query_name IS NOT NULL
GROUP BY 1;
复杂度分析
本题为数据库查询题,不涉及传统算法的时间/空间复杂度,执行效率由数据库优化器决定;建议对查询涉及的过滤列与连接列建立索引以加速。
1212. Team Scores in Football Tournament(查询球队积分 🔒)
题目类型:数据库(SQL)
题目描述
表: Teams
+---------------+----------+
| Column Name | Type |
+---------------+----------+
| team_id | int |
| team_name | varchar |
+---------------+----------+
team_id 是该表具有唯一值的列。
表中的每一行都代表一支独立足球队。
表: Matches
+---------------+---------+
| Column Name | Type |
+---------------+---------+
| match_id | int |
| host_team | int |
| guest_team | int |
| host_goals | int |
| guest_goals | int |
+---------------+---------+
match_id 是该表具有唯一值的列。
表中的每一行都代表一场已结束的比赛。
比赛的主客队分别由它们自己的 id 表示,他们的进球由 host_goals 和 guest_goals 分别表示。
你希望在所有比赛之后计算所有球队的比分。积分奖励方式如下:
- 如果球队赢了比赛(即比对手进更多的球),就得 **3** 分。
- 如果双方打成平手(即,与对方得分相同),则得 **1** 分。
- 如果球队输掉了比赛(例如,比对手少进球),就 **不得分** 。
编写解决方案,以找出每个队的 team_id,team_name 和 num_points。
返回的结果根据 num_points** 降序排序**,如果有两队积分相同,那么这两队按 team_id 升序排序。
返回结果格式如下。
示例 1:
`**输入:**
Teams `table:
+-----------+--------------+
| team_id | team_name |
+-----------+--------------+
| 10 | Leetcode FC |
| 20 | NewYork FC |
| 30 | Atlanta FC |
| 40 | Chicago FC |
| 50 | Toronto FC |
+-----------+--------------+
`Matches `table:
+------------+--------------+---------------+-------------+--------------+
| match_id | host_team | guest_team | host_goals | guest_goals |
+------------+--------------+---------------+-------------+--------------+
| 1 | 10 | 20 | 3 | 0 |
| 2 | 30 | 10 | 2 | 2 |
| 3 | 10 | 50 | 5 | 1 |
| 4 | 20 | 30 | 1 | 0 |
| 5 | 50 | 30 | 1 | 0 |
+------------+--------------+---------------+-------------+--------------+
**输出:**
+------------+--------------+---------------+
| team_id | team_name | num_points |
+------------+--------------+---------------+
| 10 | Leetcode FC | 7 |
| 20 | NewYork FC | 3 |
| 50 | Toronto FC | 3 |
| 30 | Atlanta FC | 1 |
| 40 | Chicago FC | 0 |
+------------+--------------+---------------+
解题思路
思考
一支球队可能从未出场,内连接会丢掉其零分记录。左连接 T e a m s Teams Teams 与 M a t c h e s Matches Matches,条件为担任主队或客队,使无赛球队仍保留一行。
积分只取决于该队在该场是主是客以及进球比较:胜 3 3 3、平 1 1 1、负 0 0 0。 C A S E CASE CASE 按行给出当场得分,再按 t e a m _ i d team\_id team_id 求和。
最后按积分降序、 t e a m _ i d team\_id team_id 升序输出。左连接保证名单完整, C A S E CASE CASE 把胜平负规则写成可聚合的标量。
考察外连接 + 汇总:每队主/客场分别统计胜平负,用 CASE WHEN 计 3/1/0 分,UNION ALL 后按 team_id 汇总并按分数降序、id 升序排序。
代码实现
sql
# Write your MySQL query statement below
SELECT
team_id,
team_name,
SUM(
CASE
WHEN team_id = host_team
AND host_goals > guest_goals THEN 3
WHEN team_id = guest_team
AND guest_goals > host_goals THEN 3
WHEN host_goals = guest_goals THEN 1
ELSE 0
END
) AS num_points
FROM
Teams
LEFT JOIN Matches ON team_id = host_team OR team_id = guest_team
GROUP BY 1
ORDER BY 3 DESC, 1;
复杂度分析
本题为数据库查询题,不涉及传统算法的时间/空间复杂度,执行效率由数据库优化器决定;建议对查询涉及的过滤列与连接列建立索引以加速。
1213. Intersection of Three Sorted Arrays(三个有序数组的交集)
题目描述
给出三个均为 严格递增排列 的整数数组 arr1,arr2 和 arr3。返回一个由 仅 在这三个数组中 同时出现 的整数所构成的有序数组。
示例 1
输入: arr1 = [1,2,3,4,5], arr2 = [1,2,5,7,9], arr3 = [1,3,4,5,8]
输出: [1,5]
解释: 只有 1 和 5 同时在这三个数组中出现.
示例 2
输入: arr1 = [197,418,523,876,1356], arr2 = [501,880,1593,1710,1870], arr3 = [521,682,1337,1395,1764]
输出: []
提示:
1 <= arr1.length, arr2.length, arr3.length <= 10001 <= arr1[i], arr2[i], arr3[i] <= 2000
解题思路
解法一:三指针归并(主提交版本)
三个指针 i、j、k 分别指向三个数组,比较当前三个值:
相等则加入结果并同时前进;否则让最小值的指针前进。因为三个数组都严格递增,该过程保证不漏不重。
复杂度:时间 O(A+B+C),空间 O(1)(不计输出)。
解法二:哈希计数(对照)
用一个 Map 统计每个数字在三个数组中出现的次数,最后收集出现次数为 3 的数字并按序输出。
复杂度:时间 O(A+B+C),空间 O(不同数字个数)。
代码实现
java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Solution {
public List<Integer> arraysIntersection(int[] arr1, int[] arr2, int[] arr3) {
List<Integer> res = new ArrayList<>();
int i = 0, j = 0, k = 0;
while (i < arr1.length && j < arr2.length && k < arr3.length) {
int a = arr1[i], b = arr2[j], c = arr3[k];
if (a == b && b == c) {
res.add(a);
i++;
j++;
k++;
} else if (a <= b && a <= c) {
i++;
} else if (b <= a && b <= c) {
j++;
} else {
k++;
}
}
return res;
}
public List<Integer> arraysIntersectionMap(int[] arr1, int[] arr2, int[] arr3) {
Map<Integer, Integer> cnt = new HashMap<>();
for (int x : arr1) {
cnt.put(x, cnt.getOrDefault(x, 0) + 1);
}
for (int x : arr2) {
cnt.put(x, cnt.getOrDefault(x, 0) + 1);
}
for (int x : arr3) {
cnt.put(x, cnt.getOrDefault(x, 0) + 1);
}
List<Integer> res = new ArrayList<>();
for (Map.Entry<Integer, Integer> e : cnt.entrySet()) {
if (e.getValue() == 3) {
res.add(e.getKey());
}
}
res.sort((x, y) -> x - y);
return res;
}
public static void main(String[] args) {
Solution s = new Solution();
int[] a1 = {1, 2, 3, 4, 5};
int[] a2 = {1, 2, 5, 7, 9};
int[] a3 = {1, 3, 4, 5, 8};
System.out.println(s.arraysIntersection(a1, a2, a3));
System.out.println(s.arraysIntersectionMap(a1, a2, a3));
int[] b1 = {197, 418, 523, 876, 1356};
int[] b2 = {501, 880, 1593, 1710, 1870};
int[] b3 = {521, 682, 1337, 1395, 1764};
System.out.println(s.arraysIntersection(b1, b2, b3));
System.out.println(s.arraysIntersectionMap(b1, b2, b3));
int[] c1 = {1, 2, 4};
int[] c2 = {2, 4, 8};
int[] c3 = {2, 3, 4};
System.out.println(s.arraysIntersection(c1, c2, c3));
System.out.println(s.arraysIntersectionMap(c1, c2, c3));
}
}
复杂度分析
复杂度:时间 O(A+B+C),空间 O(1)(不计输出)。
复杂度:时间 O(A+B+C),空间 O(不同数字个数)。
1214. Two Sum BSTs(查找两棵二叉搜索树之和)
题目描述
给出两棵二叉搜索树的根节点 root1 和 root2 ,请你从两棵树中各找出一个节点,使得这两个节点的值之和等于目标值 Target。
如果可以找到返回 True,否则返回 False。
示例 1
输入:root1 = [2,1,4], root2 = [1,0,3], target = 5
输出:true
解释:2 加 3 和为 5 。
示例 2
输入:root1 = [0,-10,10], root2 = [5,1,7,0,2], target = 18
输出:false
提示:
- 每棵树上节点数在
[1, 5000]范围内。 -109 9
解题思路
解法一:中序遍历 + 双指针(主提交版本)
对两棵 BST 分别中序遍历得到有序列表,然后双指针:i 从头、j 从尾,和大于 target 时 j--,小于时 i++。
复杂度:时间 O(A+B),空间 O(A+B)。
解法二:哈希集合(对照)
遍历第一棵树把所有值存入 HashSet,遍历第二棵树时检查 target - val 是否在集合中。
复杂度:时间 O(A+B),空间 O(A)。
代码实现
java
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Solution {
public boolean twoSumBSTs(TreeNode root1, TreeNode root2, int target) {
List<Integer> l1 = new ArrayList<>();
List<Integer> l2 = new ArrayList<>();
inorder(root1, l1);
inorder(root2, l2);
int i = 0, j = l2.size() - 1;
while (i < l1.size() && j >= 0) {
int sum = l1.get(i) + l2.get(j);
if (sum == target) {
return true;
} else if (sum < target) {
i++;
} else {
j--;
}
}
return false;
}
private void inorder(TreeNode node, List<Integer> list) {
if (node == null) {
return;
}
inorder(node.left, list);
list.add(node.val);
inorder(node.right, list);
}
public boolean twoSumBSTsSet(TreeNode root1, TreeNode root2, int target) {
Set<Integer> vals = new HashSet<>();
collect(root1, vals);
return check(root2, vals, target);
}
private void collect(TreeNode node, Set<Integer> set) {
if (node == null) {
return;
}
set.add(node.val);
collect(node.left, set);
collect(node.right, set);
}
private boolean check(TreeNode node, Set<Integer> set, int target) {
if (node == null) {
return false;
}
if (set.contains(target - node.val)) {
return true;
}
return check(node.left, set, target) || check(node.right, set, target);
}
public static void main(String[] args) {
Solution s = new Solution();
// root1 = [2,1,4], root2 = [1,0,3], target = 5
TreeNode r1 = new TreeNode(2);
r1.left = new TreeNode(1);
r1.right = new TreeNode(4);
TreeNode r2 = new TreeNode(1);
r2.left = new TreeNode(0);
r2.right = new TreeNode(3);
System.out.println(s.twoSumBSTs(r1, r2, 5));
System.out.println(s.twoSumBSTsSet(r1, r2, 5));
System.out.println(s.twoSumBSTs(r1, r2, 18));
System.out.println(s.twoSumBSTsSet(r1, r2, 18));
}
}
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
}
复杂度分析
复杂度:时间 O(A+B),空间 O(A+B)。
复杂度:时间 O(A+B),空间 O(A)。
1215. Stepping Numbers(步进数)
题目描述
如果一个整数上的每一位数字与其相邻位上的数字的绝对差都是 1,那么这个数就是一个「步进数」。
例如,321 是一个 步进数,而 421 不是。
给你两个整数,low 和 high,请你找出在 [low, high] 范围内的所有 步进数,并返回 排序后 的结果。
示例 1
输入:low = 0, high = 21
输出:[0,1,2,3,4,5,6,7,8,9,10,12,21]
示例 2
输入:low = 10, high = 15
输出:[10,12]
提示:
0 9
解题思路
解法一:BFS 构造(主提交版本)
以 1~9 为种子,每次在当前数末尾追加"最后一位 ± 1"得到下一个步进数(BFS 队列)。
只保留 <= high 的候选,落在 low, high 内的收集,最后排序输出(含 0 的单独处理)。
复杂度:时间 O(位数 * 2 * 层数)≈常数,空间 O(候选数)。
解法二:DFS 构造(对照)
同样的扩展规则写成递归 DFS,从 1~9 出发逐位扩展并收集,最后统一排序。
复杂度:与 BFS 相同。
代码实现
java
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class Solution {
public List<Integer> countSteppingNumbers(int low, int high) {
List<Integer> res = new ArrayList<>();
if (low == 0) {
res.add(0);
}
Queue<Long> q = new LinkedList<>();
for (int d = 1; d <= 9; d++) {
q.offer((long) d);
}
while (!q.isEmpty()) {
long cur = q.poll();
if (cur > high) {
continue;
}
if (cur >= low) {
res.add((int) cur);
}
long last = cur % 10;
long base = cur * 10;
if (last - 1 >= 0) {
long next = base + (last - 1);
if (next <= high) {
q.offer(next);
}
}
if (last + 1 <= 9) {
long next = base + (last + 1);
if (next <= high) {
q.offer(next);
}
}
}
res.sort((x, y) -> x - y);
return res;
}
public List<Integer> countSteppingNumbersDfs(int low, int high) {
List<Integer> res = new ArrayList<>();
if (low == 0) {
res.add(0);
}
for (int d = 1; d <= 9; d++) {
dfs(d, low, high, res);
}
res.sort((x, y) -> x - y);
return res;
}
private void dfs(long cur, int low, int high, List<Integer> res) {
if (cur > high) {
return;
}
if (cur >= low) {
res.add((int) cur);
}
long last = cur % 10;
if (last - 1 >= 0) {
dfs(cur * 10 + (last - 1), low, high, res);
}
if (last + 1 <= 9) {
dfs(cur * 10 + (last + 1), low, high, res);
}
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.countSteppingNumbers(0, 21));
System.out.println(s.countSteppingNumbersDfs(0, 21));
System.out.println(s.countSteppingNumbers(10, 15));
System.out.println(s.countSteppingNumbersDfs(10, 15));
System.out.println(s.countSteppingNumbers(0, 0));
System.out.println(s.countSteppingNumbersDfs(0, 0));
}
}
复杂度分析
复杂度:时间 O(位数 * 2 * 层数)≈常数,空间 O(候选数)。
1216. Valid Palindrome III(验证回文串 III)
题目描述
给出一个字符串 s 和一个整数 k,若这个字符串是一个「k 回文 」,则返回 true 。
如果可以通过从字符串中删去最多 k 个字符将其转换为回文,那么这个字符串就是一个「k 回文 」。
示例 1
输入:s = "abcdeca", k = 2
输出:true
解释:删去字符 "b" 和 "e"。
示例 2
输入:s = "abbababa", k = 1
输出:true
提示:
1 <= s.length <= 1000s中只含有小写英文字母1 <= k <= s.length
解题思路
解法一:最长回文子序列 DP(主提交版本)
问题等价于:s 中保留一个回文子序列,其余字符全部删除。若"需要删除的字符数 = n - L"
不超过 k,即为 k 回文。最长回文子序列用经典区间 DP 计算:
dpij = si==sj ? dpi+1j-1+2 : max(dpi+1j, dpij-1)。
复杂度:时间 O(n^2),空间 O(n^2)(可滚动到 O(n))。
解法二:记忆化搜索(对照)
solve(i, j) = 使 si...j 变成回文所需最少删除次数:
si==sj -> solve(i+1,j-1);否则 1 + min(solve(i+1,j), solve(i,j-1))。
最后判断 solve(0,n-1) <= k。
复杂度:时间 O(n^2),空间 O(n^2)。
代码实现
java
public class Solution {
public boolean isValidPalindrome(String s, int k) {
int n = s.length();
int[][] dp = new int[n][n];
for (int i = 0; i < n; i++) {
dp[i][i] = 1; // 单字符自身是长度为 1 的回文
}
for (int len = 2; len <= n; len++) {
for (int i = 0; i + len - 1 < n; i++) {
int j = i + len - 1;
if (s.charAt(i) == s.charAt(j)) {
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
}
}
}
return n - dp[0][n - 1] <= k;
}
public boolean isValidPalindromeMemo(String s, int k) {
int n = s.length();
int[][] memo = new int[n][n];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
memo[i][j] = -1;
}
}
return solve(s, 0, n - 1, memo) <= k;
}
private int solve(String s, int i, int j, int[][] memo) {
if (i >= j) {
return 0;
}
if (memo[i][j] != -1) {
return memo[i][j];
}
int ans;
if (s.charAt(i) == s.charAt(j)) {
ans = solve(s, i + 1, j - 1, memo);
} else {
ans = 1 + Math.min(solve(s, i + 1, j, memo), solve(s, i, j - 1, memo));
}
memo[i][j] = ans;
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.isValidPalindrome("abcdeca", 2));
System.out.println(s.isValidPalindromeMemo("abcdeca", 2));
System.out.println(s.isValidPalindrome("abbababa", 1));
System.out.println(s.isValidPalindromeMemo("abbababa", 1));
System.out.println(s.isValidPalindrome("abc", 2));
System.out.println(s.isValidPalindromeMemo("abc", 2));
}
}
复杂度分析
复杂度:时间 O(n^2),空间 O(n^2)(可滚动到 O(n))。
复杂度:时间 O(n^2),空间 O(n^2)。
1217. Minimum Cost to Move Chips to The Same Position(玩筹码)
题目描述
有 n 个筹码。第 i 个筹码的位置是 position[i] 。
我们需要把所有筹码移到同一个位置。在一步中,我们可以将第 i 个筹码的位置从 position[i] 改变为:
position[i] + 2或position[i] - 2,此时cost = 0position[i] + 1或position[i] - 1,此时cost = 1
返回将所有筹码移动到同一位置上所需要的 最小代价 。
示例 1
输入:position = [1,2,3]
输出:1
解释:第一步:将位置3的筹码移动到位置1,成本为0。
第二步:将位置2的筹码移动到位置1,成本= 1。
总成本是1。
示例 2
输入:position = [2,2,2,3,3]
输出:2
解释:我们可以把位置3的两个筹码移到位置2。每一步的成本为1。总成本= 2。
示例 3
输入:position = [1,1000000000]
输出:1
提示:
1 <= position.length <= 1001 <= position[i] <= 10^9
解题思路
解法一:奇偶统计(主提交版本)
移动 ±2 代价为 0,意味着同奇偶性的位置可以免费互相到达;跨奇偶需要 1 代价。
因此把全部筹码集中到"奇数位置"的花费 = 偶数筹码个数;集中到"偶数位置"的花费 = 奇数筹码个数。
取二者较小值即可。
复杂度:时间 O(n),空间 O(1)。
解法二:模拟枚举目标(对照)
枚举一个目标位置(取任一筹码所在位置即可,由于同奇偶免费,只需考虑奇/偶两类代表),
统计其他筹码与它奇偶性不同的人数作为代价,取最小。等价实现亦可在 (min, max) 范围内枚举。
复杂度:时间 O(n * 范围) 或 O(n),空间 O(1)。
代码实现
java
public class Solution {
public int minCostToMoveChips(int[] position) {
int odd = 0, even = 0;
for (int p : position) {
if ((p & 1) == 1) {
odd++;
} else {
even++;
}
}
return Math.min(odd, even);
}
public int minCostToMoveChipsEnum(int[] position) {
int min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;
for (int p : position) {
min = Math.min(min, p);
max = Math.max(max, p);
}
int best = Integer.MAX_VALUE;
for (int target = min; target <= max; target++) {
int cost = 0;
for (int p : position) {
int d = Math.abs(p - target);
if ((d & 1) == 1) {
cost++; // 奇数距离只能靠 +-1 移动,代价 1
}
}
best = Math.min(best, cost);
}
return best;
}
public static void main(String[] args) {
Solution s = new Solution();
int[] p1 = {1, 2, 3};
System.out.println(s.minCostToMoveChips(p1));
System.out.println(s.minCostToMoveChipsEnum(p1));
int[] p2 = {2, 2, 2, 3, 3};
System.out.println(s.minCostToMoveChips(p2));
System.out.println(s.minCostToMoveChipsEnum(p2));
int[] p3 = {1, 1000000000};
System.out.println(s.minCostToMoveChips(p3));
System.out.println(s.minCostToMoveChipsEnum(p3));
}
}
复杂度分析
复杂度:时间 O(n),空间 O(1)。
复杂度:时间 O(n * 范围) 或 O(n),空间 O(1)。
1218. Longest Arithmetic Subsequence of Given Difference(最长定差子序列)
题目描述
给你一个整数数组 arr 和一个整数 difference,请你找出并返回 arr 中最长等差子序列的长度,该子序列中相邻元素之间的差等于 difference 。
子序列 是指在不改变其余元素顺序的情况下,通过删除一些元素或不删除任何元素而从 arr 派生出来的序列。
示例 1
输入:arr = [1,2,3,4], difference = 1
输出:4
解释:最长的等差子序列是 [1,2,3,4]。
示例 2
输入:arr = [1,3,5,7], difference = 1
输出:1
解释:最长的等差子序列是任意单个元素。
示例 3
输入:arr = [1,5,7,8,5,3,4,2,1], difference = -2
输出:4
解释:最长的等差子序列是 [7,5,3,1]。
提示:
1 5-104 4
解题思路
解法一:哈希 DP(主提交版本)
令 dpx 表示以数值 x 结尾的最长定差子序列长度。顺序遍历 arr:
dparr\[i] = dparr\[i - difference] + 1,全程取最大值。
因为差是固定的,前一元素一定是 arri-difference,用 Map 即可 O(1) 转移。
复杂度:时间 O(n),空间 O(n)。
解法二:朴素枚举子序列(对照)
对每个起点枚举其后所有满足差为 difference 的元素做二维 DP/暴力计数。
仅作为正确性对照,n 较大时会超时。
复杂度:时间 O(n^2),空间 O(1)。
代码实现
java
import java.util.HashMap;
import java.util.Map;
public class Solution {
public int longestSubsequence(int[] arr, int difference) {
Map<Integer, Integer> dp = new HashMap<>();
int best = 0;
for (int x : arr) {
int prev = dp.getOrDefault(x - difference, 0);
int cur = prev + 1;
dp.put(x, cur);
best = Math.max(best, cur);
}
return best;
}
public int longestSubsequenceNaive(int[] arr, int difference) {
int n = arr.length;
int best = 1;
for (int i = 0; i < n; i++) {
int len = 1, need = arr[i] + difference;
for (int j = i + 1; j < n; j++) {
if (arr[j] == need) {
len++;
need += difference;
}
}
best = Math.max(best, len);
}
return best;
}
public static void main(String[] args) {
Solution s = new Solution();
int[] a1 = {1, 2, 3, 4};
System.out.println(s.longestSubsequence(a1, 1));
System.out.println(s.longestSubsequenceNaive(a1, 1));
int[] a2 = {1, 3, 5, 7};
System.out.println(s.longestSubsequence(a2, 1));
System.out.println(s.longestSubsequenceNaive(a2, 1));
int[] a3 = {1, 5, 7, 8, 5, 3, 4, 2, 1};
System.out.println(s.longestSubsequence(a3, -2));
System.out.println(s.longestSubsequenceNaive(a3, -2));
}
}
复杂度分析
复杂度:时间 O(n),空间 O(n)。
复杂度:时间 O(n^2),空间 O(1)。
1219. Path with Maximum Gold(黄金矿工)
题目描述
你要开发一座金矿,地质勘测学家已经探明了这座金矿中的资源分布,并用大小为 m * n 的网格 grid 进行了标注。每个单元格中的整数就表示这一单元格中的黄金数量;如果该单元格是空的,那么就是 0。
为了使收益最大化,矿工需要按以下规则来开采黄金:
- 每当矿工进入一个单元,就会收集该单元格中的所有黄金。
- 矿工每次可以从当前位置向上下左右四个方向走。
- 每个单元格只能被开采(进入)一次。
- 不得开采(进入)黄金数目为
0的单元格。 - 矿工可以从网格中 任意一个 有黄金的单元格出发或者是停止。
示例 1
输入:grid = [[0,6,0],[5,8,7],[0,9,0]]
输出:24
解释:
[[0,6,0],
[5,8,7],
[0,9,0]]
一种收集最多黄金的路线是:9 -> 8 -> 7。
示例 2
输入:grid = [[1,0,7],[2,0,6],[3,4,5],[0,3,0],[9,0,20]]
输出:28
解释:
[[1,0,7],
[2,0,6],
[3,4,5],
[0,3,0],
[9,0,20]]
一种收集最多黄金的路线是:1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7。
提示:
1 <= grid.length, grid[i].length <= 150 <= grid[i][j] <= 100- 最多 25 个单元格中有黄金。
解题思路
解法一:回溯 DFS(主提交版本)
从每个非 0 单元格出发深搜,用一个 visited 防止重复,路径上累加黄金并更新全局最大值。
因为格子数很少(最多 25 个黄金格),回溯完全可行。
复杂度:时间 O(4^k)(k 为黄金格数),空间 O(m*n)。
解法二:回溯 + 原地标记(对照)
同上,但用"访问后临时置 0、回溯时恢复"的方式代替 visited 数组,省一层空间。
复杂度:时间 O(4^k),空间 O(1)(不计递归栈)。
代码实现
java
public class Solution {
private int best;
public int getMaximumGold(int[][] grid) {
int m = grid.length, n = grid[0].length;
best = 0;
boolean[][] vis = new boolean[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] > 0) {
dfs(grid, i, j, vis, 0);
}
}
}
return best;
}
private void dfs(int[][] grid, int i, int j, boolean[][] vis, int sum) {
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length
|| vis[i][j] || grid[i][j] == 0) {
return;
}
vis[i][j] = true;
sum += grid[i][j];
best = Math.max(best, sum);
dfs(grid, i + 1, j, vis, sum);
dfs(grid, i - 1, j, vis, sum);
dfs(grid, i, j + 1, vis, sum);
dfs(grid, i, j - 1, vis, sum);
vis[i][j] = false;
}
public int getMaximumGoldInPlace(int[][] grid) {
int m = grid.length, n = grid[0].length;
best = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] > 0) {
dfsInPlace(grid, i, j, 0);
}
}
}
return best;
}
private void dfsInPlace(int[][] grid, int i, int j, int sum) {
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] == 0) {
return;
}
int gold = grid[i][j];
grid[i][j] = 0; // 临时置 0 充当 visited
sum += gold;
best = Math.max(best, sum);
dfsInPlace(grid, i + 1, j, sum);
dfsInPlace(grid, i - 1, j, sum);
dfsInPlace(grid, i, j + 1, sum);
dfsInPlace(grid, i, j - 1, sum);
grid[i][j] = gold; // 回溯恢复
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] g1 = {{0, 6, 0}, {5, 8, 7}, {0, 9, 0}};
System.out.println(s.getMaximumGold(g1));
System.out.println(s.getMaximumGoldInPlace(g1));
int[][] g2 = {{1, 0, 7}, {2, 0, 6}, {3, 4, 5}, {0, 3, 0}, {9, 0, 20}};
System.out.println(s.getMaximumGold(g2));
System.out.println(s.getMaximumGoldInPlace(g2));
int[][] g3 = {{10}};
System.out.println(s.getMaximumGold(g3));
System.out.println(s.getMaximumGoldInPlace(g3));
}
}
复杂度分析
复杂度:时间 O(4^k)(k 为黄金格数),空间 O(m*n)。
复杂度:时间 O(4^k),空间 O(1)(不计递归栈)。
1220. Count Vowels Permutation(统计元音字母序列的数目)
题目描述
给你一个整数 n,请你帮忙统计一下我们可以按下述规则形成多少个长度为 n 的字符串:
- 字符串中的每个字符都应当是小写元音字母(
'a','e','i','o','u') - 每个元音
'a'后面都只能跟着'e' - 每个元音
'e'后面只能跟着'a'或者是'i' - 每个元音
'i'后面 不能 再跟着另一个'i' - 每个元音
'o'后面只能跟着'i'或者是'u' - 每个元音
'u'后面只能跟着'a'
由于答案可能会很大,所以请你返回 模10^9 + 7之后的结果。
示例 1
输入:n = 1
输出:5
解释:所有可能的字符串分别是:"a", "e", "i" , "o" 和 "u"。
示例 2
输入:n = 2
输出:10
解释:所有可能的字符串分别是:"ae", "ea", "ei", "ia", "ie", "io", "iu", "oi", "ou" 和 "ua"。
示例 3
输入:n = 5
输出:68
提示:
1 <= n <= 2 * 10^4
解题思路
解法一:滚动数组 DP(主提交版本)
逆推状态转移:dpFullij 表示长度为 i、以元音 j 结尾的合法串数量。
由"最后一字符是谁"按规则反推前一字符集合:
以 u 结尾 <- 前一步可以是 a/o;以 a 结尾 <- 前一步可以是 e/i/u;以 e 结尾 <- 前一步可以是 a/i;
以 i 结尾 <- 前一步可以是 e/o;以 o 结尾 <- 前一步只能是 i。
每轮只用上一轮,用 5 个变量滚动即可(这里用 int5 数组实现)。
复杂度:时间 O(n*5),空间 O(5)。
解法二:矩阵快速幂(对照)
把状态转移写成 5 阶矩阵,长度为 n 的总数等于初始向量乘转移矩阵^(n-1) 后求和。
适用于 n 极大的场景。这里直接实现矩阵乘幂。
复杂度:时间 O(5^3 * log n),空间 O(5^2)。
代码实现
java
public class Solution {
private static final int MOD = 1000000007;
public int countVowelPermutation(int n) {
// 下标 0:a, 1:e, 2:i, 3:o, 4:u
long[] dp = new long[5];
for (int j = 0; j < 5; j++) {
dp[j] = 1;
}
for (int len = 2; len <= n; len++) {
long[] ndp = new long[5];
ndp[4] = (dp[2] + dp[3]) % MOD; // 以 u 结尾 <- i, o
ndp[0] = (dp[1] + dp[2] + dp[4]) % MOD; // 以 a 结尾 <- e, i, u
ndp[1] = (dp[0] + dp[2]) % MOD; // 以 e 结尾 <- a, i
ndp[2] = (dp[1] + dp[3]) % MOD; // 以 i 结尾 <- e, o
ndp[3] = dp[2] % MOD; // 以 o 结尾 <- i
dp = ndp;
}
long ans = 0;
for (long v : dp) {
ans = (ans + v) % MOD;
}
return (int) ans;
}
public int countVowelPermutationMatrix(int n) {
// 转移矩阵 T[j][i]:从元音 i 到元音 j(i->j 是否允许)
long[][] T = new long[5][5];
// a->e : T[e][a]; e->a,i : T[a][e], T[i][e]; i->a,e,o,u : ...
T[1][0] = 1; // a -> e
T[0][1] = 1; // e -> a
T[2][1] = 1; // e -> i
T[0][2] = 1; // i -> a
T[1][2] = 1; // i -> e
T[3][2] = 1; // i -> o
T[4][2] = 1; // i -> u
T[2][3] = 1; // o -> i
T[4][3] = 1; // o -> u
T[0][4] = 1; // u -> a
long[][] P = powMat(T, n - 1);
// 初始向量全 1(长度 1),答案 = sum over j of (T^(n-1) * 1)[j]
long ans = 0;
for (int j = 0; j < 5; j++) {
long s = 0;
for (int i = 0; i < 5; i++) {
s = (s + P[j][i]) % MOD;
}
ans = (ans + s) % MOD;
}
return (int) ans;
}
private long[][] mulMat(long[][] A, long[][] B) {
int n = A.length;
long[][] C = new long[n][n];
for (int i = 0; i < n; i++) {
for (int k = 0; k < n; k++) {
if (A[i][k] == 0) {
continue;
}
for (int j = 0; j < n; j++) {
C[i][j] = (C[i][j] + A[i][k] * B[k][j]) % MOD;
}
}
}
return C;
}
private long[][] powMat(long[][] base, long e) {
int n = base.length;
long[][] R = new long[n][n];
for (int i = 0; i < n; i++) {
R[i][i] = 1;
}
while (e > 0) {
if ((e & 1) == 1) {
R = mulMat(R, base);
}
base = mulMat(base, base);
e >>= 1;
}
return R;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.countVowelPermutation(1));
System.out.println(s.countVowelPermutationMatrix(1));
System.out.println(s.countVowelPermutation(2));
System.out.println(s.countVowelPermutationMatrix(2));
System.out.println(s.countVowelPermutation(5));
System.out.println(s.countVowelPermutationMatrix(5));
System.out.println(s.countVowelPermutation(144));
System.out.println(s.countVowelPermutationMatrix(144));
}
}
复杂度分析
复杂度:时间 O(n*5),空间 O(5)。
复杂度:时间 O(5^3 * log n),空间 O(5^2)。