LeetCode1201~1220题目解析

LeetCode 1201-1220. 题解合集

文章目录


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 9
  • 1 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^5
  • 0 <= pairs.length <= 10^5
  • 0 <= pairs[i][0], pairs[i][1] < s.length
  • s 中只含有小写英文字母

解题思路

解法一:并查集 + 分组排序(主提交版本)

可交换关系构成无向图,同一连通分量内的字符可以任意排列。

用并查集合并 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 4
  • group.length == beforeItems.length == n
  • -1 <= group[i] <= m - 1
  • 0 <= beforeItems[i].length <= n - 1
  • 0 <= beforeItems[i][j] <= n - 1
  • i != 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 5
  • t.length == s.length
  • 0 6
  • s 和 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^5
  • 2 <= k <= 10^4
  • s 中只含有小写英文字母。

解题思路

解法一:栈 + 计数(主提交版本)

用两个栈: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 <= 100
  • 0 <= 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 <= 1000
  • 1 <= 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 <= 1000
  • s 中只含有小写英文字母
  • 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 = 0
  • position[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 <= 100
  • 1 <= 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 <= 15
  • 0 <= 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)。

相关推荐
Shaoshing1 小时前
LeetCode100 283.移动零
java·数据结构·算法·leetcode
海绵宝宝转agent1 小时前
LeetCode100个人题解+java面经总结
java·开发语言·笔记·学习·算法
香蕉大香蕉(06敬畏市场)1 小时前
高频八股 · JVM(7):方法区、元空间与字符串常量池
java·jvm·ai·博客·八股·大厂实习
LazyHunter91 小时前
Spring Boot 3 微服务实践指南
java·spring boot
zzu123zsw1 小时前
从 SPI 契约到独立 ClassLoader 隔离:Spring Boot 插件热插拔架构设计与内存泄露防御
java·spring boot·后端·架构设计
文丑颜不良啊1 小时前
LeetCode1161~1180题目解析
java·leetcode
古法老登瞎折腾9 小时前
大数据智能运维实战(十一):运维 MCP 的工程化——权限、限流、审计与错误处理
java·大数据·运维·spring ai·mcp
智能运维指南10 小时前
研制过程“两张皮“怎么破:研制一体化支撑平台的定位、能力边界与落地前提
java·前端·人工智能·嘉为蓝鲸
yz_aiks11 小时前
Spring AI 2.1 ChatMemory 已过时用什么替代:迁移到 Session Memory 实战(JDBC 存储与会话压缩)
java·spring boot·大模型·ai agent·spring ai