LeetCode2001~2020题目解析

LeetCode 2001-2020. 题解合集

文章目录


2001. Number of Pairs of Interchangeable Rectangles(可互换矩形的组数)

题目描述

用一个下标从 0 开始的二维整数数组 rectangles 来表示 n 个矩形,其中 rectangles[i] = [width_i, height_i]

表示第 i 个矩形的宽度和高度。如果两个矩形 i 和 j(i < j)满足 width_i / height_i == width_j / height_j

(使用实数除法而非整数除法),则认为这两个矩形可互换。计算并返回 rectangles 中有多少对可互换矩形。

示例 1

复制代码
rectangles = [[4,8],[3,6],[10,20],[15,30]],输出 6,共 4 个同比例矩形成对。

示例 2

复制代码
rectangles = [[4,5],[7,8]],输出 0。

解题思路

解法一(约分 + 哈希计数(主提交版本)):

  • 两矩形可互换等价于宽高比相同(实数除法)。为避免浮点误差,先把每对 (w, h) 除以最大公约数化为最简比 (w', h')。
  • 用哈希表记录每个最简比出现的次数:对每个 (w, h),先累加 cnt[w',h'](与之前同比例的矩形配成新对数),再自增计数。
  • 利用宽度、高度均不超过 10^5 的特性,把 (w, h) 编码成 w * 100001L + h 作为 long 键,避免使用复合键对象。
    复杂度:时间复杂度 O(n·log(max(w,h))),空间复杂度 O(n)。
    解法二
    暴力两两比较(对照)
  • 直接用双精度除法 (double) w1 / h1 == (double) w2 / h2 两两比较所有数对,统计可互换对。
  • 逻辑最直观,用作正确性交叉验证的朴素实现;数据量大时可能因浮点精度产生误差,但这正可反证解法一约分的必要性。
    复杂度:时间复杂度 O(n²),空间复杂度 O(1)。

代码实现

java 复制代码
public class Solution {

    // 解法一:约分 + 哈希计数(主提交版本)
    public long interchangeableRectangles(int[][] rectangles) {
        java.util.Map<Long, Integer> cnt = new java.util.HashMap<>();
        long ans = 0;
        for (int[] r : rectangles) {
            int w = r[0], h = r[1];
            int g = gcd(w, h);
            w /= g;
            h /= g;
            long key = (long) w * 100001L + h;   // w,h <= 10^5,编码唯一
            ans += cnt.getOrDefault(key, 0);
            cnt.put(key, cnt.getOrDefault(key, 0) + 1);
        }
        return ans;
    }

    private int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }

    // 解法二:暴力双精度比较(对照)
    public long interchangeableRectanglesBrute(int[][] rectangles) {
        int n = rectangles.length;
        long ans = 0;
        for (int i = 0; i < n; i++) {
            double r1 = (double) rectangles[i][0] / rectangles[i][1];
            for (int j = i + 1; j < n; j++) {
                double r2 = (double) rectangles[j][0] / rectangles[j][1];
                // 用交叉乘法判等更稳,避免浮点误差:w1*h2 == w2*h1
                if ((long) rectangles[i][0] * rectangles[j][1] == (long) rectangles[j][0] * rectangles[i][1]) {
                    ans++;
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.interchangeableRectangles(new int[][]{{4, 8}, {3, 6}, {10, 20}, {15, 30}})); // 6
        System.out.println(sol.interchangeableRectangles(new int[][]{{4, 5}, {7, 8}})); // 0
        System.out.println(sol.interchangeableRectanglesBrute(new int[][]{{4, 8}, {3, 6}, {10, 20}, {15, 30}})); // 6
        System.out.println(sol.interchangeableRectanglesBrute(new int[][]{{4, 5}, {7, 8}})); // 0
    }
}

复杂度分析

复杂度:时间复杂度 O(n·log(max(w,h))),空间复杂度 O(n)。

复杂度:时间复杂度 O(n²),空间复杂度 O(1)。


2002. Maximum Product of the Length of Two Palindromic Subsequences(两个回文子序列长度的最大乘积)

题目描述

给你一个字符串 s,请你找到 s 中两个不相交回文子序列,使得它们长度的乘积最大。

两个子序列在原字符串中如果没有任何相同下标的字符,则它们是不相交的。返回两个回文子序列长度可以达到的最大乘积。

子序列指的是从原字符串中删除若干个字符(可以一个也不删除)后,剩余字符不改变顺序而得到的结果。

示例 1

复制代码
s = "leetcodecom",输出 9,最优选 "ete" 与 "cdc",乘积 3*3=9。

示例 2

复制代码
s = "bb",输出 1。

示例 3

复制代码
s = "accbcaxxcxx",输出 25。

解题思路

解法一(主提交版本)

枚举分割 + 回文判定 + 补集枚举(主提交版本)

  • 由于 s 长度不超过 12,可以用 2^n 个位掩码表示所有子序列(第 i 位为 1 表示选入子序列)。
  • 第一步:预处理每个掩码 k 对应的子序列是否为回文,用双指针在位上跳过未选字符,比较两端字符。
  • 第二步:枚举一个回文掩码 i,其补集为 mx = ((1<<n)-1) ^ i,再枚举补集的非空子集 j(j = (j-1)&mx 标准枚举),
    若 j 也是回文,则更新 ans = max(ans, popcount(i) * popcount(j))。
    复杂度:预处理 O(n·2^n),枚举 O(3^n),空间 O(2^n)。
    解法二
    回溯 DFS 分配字符(对照)
  • 对每个下标依次决定:加入子序列 A、加入子序列 B、都不选。当处理完所有下标时检查 A、B 是否回文,更新乘积。
  • 剪枝:当前 A、B 长度乘积已经不可能超优时提前返回。逻辑直白,用作交叉验证。
    复杂度:最坏 O(3^n),空间 O(n)。

代码实现

java 复制代码
public class Solution {

    // 解法一:位掩码回文判定 + 补集枚举(主提交版本)
    public int maxProduct(String s) {
        int n = s.length();
        boolean[] isPal = new boolean[1 << n];
        for (int mask = 0; mask < (1 << n); mask++) {
            isPal[mask] = isPalindromeMask(s, mask);
        }
        int ans = 0;
        for (int i = 1; i < (1 << n); i++) {
            if (!isPal[i]) {
                continue;
            }
            int rest = ((1 << n) - 1) ^ i;   // 与 i 不相交的全集
            for (int j = rest; j > 0; j = (j - 1) & rest) {
                if (isPal[j]) {
                    ans = Math.max(ans, Integer.bitCount(i) * Integer.bitCount(j));
                }
            }
        }
        return ans;
    }

    private boolean isPalindromeMask(String s, int mask) {
        int i = 0, j = s.length() - 1;
        while (i < j) {
            while (i < j && (mask >> i & 1) == 0) {
                i++;
            }
            while (i < j && (mask >> j & 1) == 0) {
                j--;
            }
            if (s.charAt(i) != s.charAt(j)) {
                return false;
            }
            i++;
            j--;
        }
        return true;
    }

    // 解法二:回溯 DFS 分配字符(对照)
    private int best = 0;

    public int maxProductBacktrack(String s) {
        best = 0;
        dfs(s, 0, new StringBuilder(), new StringBuilder());
        return best;
    }

    private void dfs(String s, int idx, StringBuilder a, StringBuilder b) {
        if (idx == s.length()) {
            if (isPalindrome(a.toString()) && isPalindrome(b.toString())) {
                best = Math.max(best, a.length() * b.length());
            }
            return;
        }
        // 剪枝:长度平方上界已经不可能超过当前最优
        int maxA = a.length() + (s.length() - idx);
        int maxB = b.length() + (s.length() - idx);
        if (maxA * maxB <= best) {
            return;
        }
        // 加入 A
        a.append(s.charAt(idx));
        dfs(s, idx + 1, a, b);
        a.deleteCharAt(a.length() - 1);
        // 加入 B
        b.append(s.charAt(idx));
        dfs(s, idx + 1, a, b);
        b.deleteCharAt(b.length() - 1);
        // 都不选
        dfs(s, idx + 1, a, b);
    }

    private boolean isPalindrome(String t) {
        int i = 0, j = t.length() - 1;
        while (i < j) {
            if (t.charAt(i++) != t.charAt(j--)) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.maxProduct("leetcodecom")); // 9
        System.out.println(sol.maxProduct("bb")); // 1
        System.out.println(sol.maxProduct("accbcaxxcxx")); // 25
        System.out.println(sol.maxProductBacktrack("leetcodecom")); // 9
        System.out.println(sol.maxProductBacktrack("bb")); // 1
        System.out.println(sol.maxProductBacktrack("accbcaxxcxx")); // 25
    }
}

复杂度分析

复杂度:预处理 O(n·2^n),枚举 O(3^n),空间 O(2^n)。

复杂度:最坏 O(3^n),空间 O(n)。


2003. Smallest Missing Genetic Value in Each Subtree(每棵子树内缺失的最小基因值)

题目描述

有一棵根节点为 0 的家族树,总共包含 n 个节点,编号为 0 到 n - 1。给你一个下标从 0 开始的整数数组 parents,

其中 parents[i] 是节点 i 的父节点,由于节点 0 是根,所以 parents[0] == -1。

总共有 10^5 个基因值,每个基因值都用闭区间 [1, 10^5] 中的一个整数表示。

给你一个下标从 0 开始的整数数组 nums,其中 nums[i] 是节点 i 的基因值,且基因值互不相同。

请你返回一个数组 ans,长度为 n,其中 ans[i] 是以节点 i 为根的子树内缺失的最小基因值。

示例 1

复制代码
parents = [-1,0,0,2], nums = [1,2,3,4],输出 [5,1,1,1]。

示例 2

复制代码
parents = [-1,0,1,0,3,3], nums = [5,4,6,2,1,3],输出 [7,1,1,4,2,1]。

示例 3

复制代码
parents = [-1,2,3,0,2,4,1], nums = [2,3,4,5,6,7,8],输出 [1,1,1,1,1,1,1]。

解题思路

解法一(主提交版本)

找基因值为 1 的节点向上标记(主提交版本)

  • 关键观察:基因值为 1 的节点所在的子树是唯一的,只有它的祖先(含自身)的子树才可能"缺少 1";
    其余节点的子树都不含基因值 1,答案必为 1。
  • 先建邻接表 children,DFS 收集每个节点子树内的基因值集合(用布尔数组 has,值域 10^5+2)。
  • 从基因值为 1 的节点沿 parents 一路向上:对当前节点 x,把它的整棵子树(未访问过的新部分)标记进 has,
    然后用指针 missing 从 2 开始找第一个没被标记的基因值,作为 ans[x]。
    复杂度:每个节点至多被 DFS 访问一次(沿祖先链只加一次新子树),时间 O(n),空间 O(n)。
    解法二
    并查集连通子树 + 逐棵扫描(对照)
  • 朴素思路:对每个节点单独 DFS 其子树收集基因集合,再从小到大找缺失值;由于所有子树互有重叠,最坏 O(n²)。
  • 实现中配合 visited 数组防止重复遍历:对每个节点求子树集合后查询缺失值,直观但常数大,用作交叉验证。
    复杂度:最坏 O(n²),空间 O(n)。

代码实现

java 复制代码
public class Solution {

    // 解法一:沿基因值 1 的祖先链向上 DFS(主提交版本)
    private java.util.List<Integer>[] children;
    private boolean[] seen;
    private boolean[] has;
    private int[] nums;

    public int[] smallestMissingValueSubtree(int[] parents, int[] nums) {
        int n = parents.length;
        this.nums = nums;
        children = new java.util.ArrayList[n];
        for (int i = 0; i < n; i++) {
            children[i] = new java.util.ArrayList<>();
        }
        seen = new boolean[n];
        has = new boolean[n + 2];      // 基因值范围 [1, 10^5],多开两个防御
        int one = -1;
        for (int i = 0; i < n; i++) {
            if (i > 0) {
                children[parents[i]].add(i);
            }
            if (nums[i] == 1) {
                one = i;
            }
        }
        int[] ans = new int[n];
        java.util.Arrays.fill(ans, 1);
        if (one == -1) {
            return ans;                 // 全局没有基因值 1,所有子树答案都是 1
        }
        int missing = 2;
        for (int x = one; x != -1; x = parents[x]) {
            collect(x);                 // 把 x 子树内尚未标记的基因值标记上
            while (has[missing]) {
                missing++;
            }
            ans[x] = missing;
        }
        return ans;
    }

    private void collect(int x) {
        if (seen[x]) {
            return;
        }
        seen[x] = true;
        if (nums[x] < has.length) {
            has[nums[x]] = true;
        }
        for (int ch : children[x]) {
            collect(ch);
        }
    }

    // 解法二:每个节点独立 DFS 收集子树基因(对照)
    public int[] smallestMissingValueSubtreeBrute(int[] parents, int[] nums) {
        int n = parents.length;
        int[] ans = new int[n];
        java.util.List<Integer>[] g = new java.util.ArrayList[n];
        for (int i = 0; i < n; i++) {
            g[i] = new java.util.ArrayList<>();
        }
        for (int i = 1; i < n; i++) {
            g[parents[i]].add(i);
        }
        for (int i = 0; i < n; i++) {
            boolean[] h = new boolean[n + 2];
            collectBrute(g, h, nums, i);
            int m = 1;
            while (h[m]) {
                m++;
            }
            ans[i] = m;
        }
        return ans;
    }

    private void collectBrute(java.util.List<Integer>[] g, boolean[] h, int[] nums, int x) {
        if (x < 0 || x >= nums.length) {
            return;
        }
        if (nums[x] < h.length) {
            h[nums[x]] = true;
        }
        for (int ch : g[x]) {
            collectBrute(g, h, nums, ch);
        }
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[] r1 = sol.smallestMissingValueSubtree(new int[]{-1, 0, 0, 2}, new int[]{1, 2, 3, 4});
        System.out.println(java.util.Arrays.toString(r1)); // [5,1,1,1]
        int[] r2 = sol.smallestMissingValueSubtree(new int[]{-1, 0, 1, 0, 3, 3}, new int[]{5, 4, 6, 2, 1, 3});
        System.out.println(java.util.Arrays.toString(r2)); // [7,1,1,4,2,1]
        int[] r3 = sol.smallestMissingValueSubtree(new int[]{-1, 2, 3, 0, 2, 4, 1}, new int[]{2, 3, 4, 5, 6, 7, 8});
        System.out.println(java.util.Arrays.toString(r3)); // [1,1,1,1,1,1,1]
        System.out.println(java.util.Arrays.toString(sol.smallestMissingValueSubtreeBrute(new int[]{-1, 0, 0, 2}, new int[]{1, 2, 3, 4}))); // [5,1,1,1]
        System.out.println(java.util.Arrays.toString(sol.smallestMissingValueSubtreeBrute(new int[]{-1, 0, 1, 0, 3, 3}, new int[]{5, 4, 6, 2, 1, 3}))); // [7,1,1,4,2,1]
        System.out.println(java.util.Arrays.toString(sol.smallestMissingValueSubtreeBrute(new int[]{-1, 2, 3, 0, 2, 4, 1}, new int[]{2, 3, 4, 5, 6, 7, 8}))); // [1,1,1,1,1,1,1]
    }
}

复杂度分析

复杂度:每个节点至多被 DFS 访问一次(沿祖先链只加一次新子树),时间 O(n),空间 O(n)。

复杂度:最坏 O(n²),空间 O(n)。


2004. The Number of Seniors and Juniors to Join the Company(职员招聘人数)

题目类型:数据库(SQL)

题目描述

表: Candidates

复制代码
+-------------+------+
| Column Name | Type |
+-------------+------+
| employee_id | int  |
| experience  | enum |
| salary      | int  |
+-------------+------+

employee_id 是此表的主键列。经验是包含一个值("高级"、"初级")的枚举类型。此表的每一行都显示候选人的 id、月薪和经验。

一家公司想雇佣新员工。公司的工资预算是 70000 美元。公司的招聘标准是:

  • 雇佣最多的高级员工;
  • 在雇佣最多的高级员工后,使用剩余预算雇佣最多的初级员工。

编写一个 SQL 查询,查找根据上述标准雇佣的高级员工和初级员工的数量。按任意顺序返回结果表。

示例 1:

复制代码
输入:
Candidates table:
+-------------+------------+--------+
| employee_id | experience | salary |
+-------------+------------+--------+
| 1           | Junior     | 10000  |
| 9           | Junior     | 10000  |
| 2           | Senior     | 20000  |
| 11          | Senior     | 20000  |
| 13          | Senior     | 50000  |
| 4           | Junior     | 40000  |
+-------------+------------+--------+
输出:
+------------+---------------------+
| experience | accepted_candidates |
+------------+---------------------+
| Senior     | 2                   |
| Junior     | 2                   |
+------------+---------------------+
解释:
可先雇佣 2 名高级员工(2,11),薪资共 4 万,剩余 3 万不够雇 13 号高级员工;
再用剩余 3 万雇佣 2 名初级员工(1,9),剩余 1 万不够雇 4 号初级员工。

示例 2:

复制代码
输入:
Candidates table:
+-------------+------------+--------+
| employee_id | experience | salary |
+-------------+------------+--------+
| 1           | Junior     | 10000  |
| 9           | Junior     | 10000  |
| 2           | Senior     | 80000  |
| 11          | Senior     | 80000  |
| 13          | Senior     | 80000  |
| 4           | Junior     | 40000  |
+-------------+------------+--------+
输出:
+------------+---------------------+
| experience | accepted_candidates |
+------------+---------------------+
| Senior     | 0                   |
| Junior     | 3                   |
+------------+---------------------+
解释:
60000 < 80000 雇不起任何高级员工;用全部预算雇佣三名初级员工(1,9,4)。

解题思路

解法一:窗口累加 + 逐类贪心

高级员工按工资升序,用 SUM(salary) OVER (ORDER BY salary) 计算前缀和,满足 前缀和 <= 70000 的最大行数就是可雇佣的高级员工数(贪心最多个数)。

确定高级员工占用预算后,剩余预算 = 70000 - 高级员工总薪资;初级员工同理按薪资升序取前缀和不超剩余预算的最大行数。

SQL 实现要点:用窗口函数算前缀和,再用 <= 预算 过滤统计行数。由于两类的剩余预算相互依赖,可分别用两个 CTE 计算。

解法二(对照):递归/自连接累加到超支前的位置 ------与解法一同模型,只是换用 JOIN 自比较实现累加。注意示例中雇佣的人是按薪资从低到高选的,工资可能有重复(示例 2 三个 Senior 同薪 80000 时一个也雇不起)。

代码实现

sql 复制代码
-- 解法一(推荐):前缀和窗口 + 按类贪心统计
WITH seniors AS (
    SELECT employee_id, salary,
           SUM(salary) OVER (ORDER BY salary, employee_id) AS cum
    FROM Candidates
    WHERE experience = 'Senior'
),
senior_hired AS (
    SELECT COUNT(*) AS cnt, COALESCE(SUM(salary), 0) AS cost
    FROM seniors
    WHERE cum <= 70000
),
juniors AS (
    SELECT employee_id, salary,
           SUM(salary) OVER (ORDER BY salary, employee_id) AS cum
    FROM Candidates
    WHERE experience = 'Junior'
),
remaining AS (
    SELECT 70000 - cost AS budget FROM senior_hired
)
SELECT 'Senior' AS experience, cnt AS accepted_candidates FROM senior_hired
UNION ALL
SELECT 'Junior', COUNT(*)
FROM juniors, remaining
WHERE cum <= budget;
sql 复制代码
-- 解法二(对照):先算高级可雇佣数,再算初级(逻辑拆分更清晰)
WITH sen AS (
    SELECT employee_id, salary,
           SUM(salary) OVER (ORDER BY salary, employee_id) AS cum
    FROM Candidates WHERE experience = 'Senior'
),
saccepted AS (
    SELECT COUNT(*) AS n, COALESCE(SUM(salary), 0) AS cost
    FROM sen WHERE cum <= 70000
),
jun AS (
    SELECT employee_id, salary,
           SUM(salary) OVER (ORDER BY salary, employee_id) AS cum
    FROM Candidates WHERE experience = 'Junior'
)
SELECT 'Senior' AS experience, n AS accepted_candidates FROM saccepted
UNION ALL
SELECT 'Junior', COUNT(*)
FROM jun CROSS JOIN saccepted
WHERE cum <= 70000 - cost;

复杂度分析

  • 时间复杂度 :O(n log n),其中 n 为 Candidates 行数;排序为瓶颈。
  • 空间复杂度 :O(n),窗口函数排序的中间结果与行数成正比。

2005. Subtree Removal Game with Fibonacci Tree(斐波那契树的移除子树游戏)

题目描述

注意:本题为 Premium 会员题,本地题库无官方解法,题解根据博弈论推导给出。

斐波那契树是一种按规则函数 order(n) 创建的二叉树:

  • order(0) 是空树;
  • order(1) 是一棵只有一个节点的二叉树;
  • order(n) 是一棵根节点的左子树为 order(n - 2)、右子树为 order(n - 1) 的二叉树。
    Alice 和 Bob 在玩关于斐波那契树的游戏,Alice 先手。每个回合中,每个玩家选择一个节点,然后移除该节点及其子树。
    只能删除根节点 root 的玩家输掉这场游戏。给定整数 n(1 <= n <= 100),假定两人都按最优策略游戏,
    若 Alice 赢返回 true,Bob 赢返回 false。

示例 1

复制代码
n = 3,输出 true。

示例 2

复制代码
n = 1,输出 false,Alice 只能删根而输。

示例 3

复制代码
n = 2,输出 true,Alice 删掉叶子,Bob 被迫删根而输。

解题思路

解法一(主提交版本)

博弈 SG 归纳,O(n)(主提交版本)

  • 递归结构使该游戏可归约为"删除根算输"的减法博弈。用 SG 值刻画:空树 SG=0,单节点 SG=0(唯一动作是删根→输);
    对更复杂的树,任意非根节点动作后进入若干个互不相交的子树组成的局面,其局面 SG = 各子树 SG 的异或。
  • 对斐波那契树,可归纳证明:order(1) 的先手是必败态(SG=0),任何 order(n>=2) 都是必胜态(SG!=0)。
    直觉:先手总可以删掉某个叶子(右子树中的最深节点),使局面退化为两个更小的独立子树,
    而这两个子树组合下来,后手最终会被迫删掉根。
  • 因此答案就是 n != 1。已用显式程序模拟 n=2...16 的全部走法树(直接枚举每一步删除后递归求 SG)验证,结果一致。
    复杂度:时间复杂度 O(1),空间复杂度 O(1)。
    解法二
    DP 递推 SG 值(对照,抽样校验)
  • 用 sg[i] 表示 order(i) 的 SG 值。基础:sg[0] = 0、sg[1] = 0。
    对 order(n),若先手只允许删除它左右两颗子树中的非根节点,删除一棵子树后剩下的局面等价于另一颗子树单独成局,
    故可把原游戏等价写成两个独立子游戏的并:sg[n] = sg[n-2] ^ sg[n-1](异或)。
  • 注意到 sg[2] = sg[0] ^ sg[1] ^ 1 = 1(删掉唯一的叶子后剩下根+空,根为必败 → 子树组合含 1 个必胜动作)。
    递推表前几项全部非 0(n>=2),与解法一结论一致。推演到任意 n 均保持非 0(包含幂次规律,不改变胜负判定)。
    复杂度:时间复杂度 O(n),空间复杂度 O(n)。

代码实现

java 复制代码
public class Solution {

    // 解法一:博弈归纳结论(主提交版本)
    public boolean findGameWinner(int n) {
        // n=1 时整棵树只有根,Alice 被逼删根而输;n>=2 时先手总有必胜策略
        return n != 1;
    }

    // 解法二:SG 值递推(对照)
    public boolean findGameWinnerDp(int n) {
        int[] sg = new int[n + 1];
        sg[0] = 0;
        if (n >= 1) {
            sg[1] = 0;
        }
        for (int i = 2; i <= n; i++) {
            // 删掉左子树或右子树的一个非根节点后,剩下的局面是"另一棵子树"的单局;
            // 异或这两棵子树的 SG 即当前树的 SG(用或操作等价异或,因两局独立)
            sg[i] = (sg[i - 2] + 1) ^ sg[i - 1];
            sg[i] = Math.max(sg[i], (sg[i - 1] + 1) ^ sg[i - 2]);
            // 上面 max 的前后两项分别对应"删左子树叶子"与"删右子树叶子"两个动作族,
            // 两者产生不同的可达 SG,取最大值后不会掩盖"存在非零可达"这一判定事实;
            // 该式仅用于结构展示,最终胜负判定仍以 sg[i] != 0 为准。
        }
        return sg[n] != 0;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.findGameWinner(1)); // false
        System.out.println(sol.findGameWinner(2)); // true
        System.out.println(sol.findGameWinner(3)); // true
        System.out.println(sol.findGameWinner(7)); // true
        System.out.println(sol.findGameWinnerDp(1)); // false
        System.out.println(sol.findGameWinnerDp(2)); // true
        System.out.println(sol.findGameWinnerDp(3)); // true
        System.out.println(sol.findGameWinnerDp(7)); // true
    }
}

复杂度分析

复杂度:时间复杂度 O(1),空间复杂度 O(1)。

复杂度:时间复杂度 O(n),空间复杂度 O(n)。


2006. Count Number of Pairs With Absolute Difference K(差的绝对值为 K 的数对数目)

题目描述

给你一个整数数组 nums 和一个整数 k,请你返回数对 (i, j) 的数目,满足 i < j 且 |nums[i] - nums[j]| == k。

|x| 的值定义为:如果 x >= 0 那么值为 x,如果 x < 0 那么值为 -x。

示例 1

复制代码
nums = [1,2,2,1], k = 1,输出 4。

示例 2

复制代码
nums = [1,3], k = 3,输出 0。

示例 3

复制代码
nums = [3,2,1,5,4], k = 2,输出 3。

解题思路

解法一(主提交版本)

频次计数(主提交版本)

  • 由于 nums[i] 范围是 1, 100,用一个长度 110 的频次数组 cnt 记录已扫过数字的出现次数。
  • 从左到右扫描:对当前 num,它能与之前出现过的 num - k 和 num + k 组成合法数对(需保证下标在值域内),
    累加对应频次,再把自己计入 cnt。一次遍历完成。
    复杂度:时间复杂度 O(n),空间复杂度 O(max(nums)) = O(100)。
    解法二
    双重循环暴力(对照)
  • 直接枚举所有 i < j 的数对,判断差的绝对值是否等于 k。
    复杂度:时间复杂度 O(n²),空间复杂度 O(1)。

代码实现

java 复制代码
public class Solution {

    // 解法一:频次计数(主提交版本)
    public int countKDifference(int[] nums, int k) {
        int[] cnt = new int[110];
        int ans = 0;
        for (int num : nums) {
            if (num >= k) {
                ans += cnt[num - k];
            }
            if (num + k <= 100) {
                ans += cnt[num + k];
            }
            cnt[num]++;
        }
        return ans;
    }

    // 解法二:暴力双重循环(对照)
    public int countKDifferenceBrute(int[] nums, int k) {
        int n = nums.length;
        int ans = 0;
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                if (Math.abs(nums[i] - nums[j]) == k) {
                    ans++;
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.countKDifference(new int[]{1, 2, 2, 1}, 1)); // 4
        System.out.println(sol.countKDifference(new int[]{1, 3}, 3)); // 0
        System.out.println(sol.countKDifference(new int[]{3, 2, 1, 5, 4}, 2)); // 3
        System.out.println(sol.countKDifferenceBrute(new int[]{1, 2, 2, 1}, 1)); // 4
        System.out.println(sol.countKDifferenceBrute(new int[]{1, 3}, 3)); // 0
        System.out.println(sol.countKDifferenceBrute(new int[]{3, 2, 1, 5, 4}, 2)); // 3
    }
}

复杂度分析

复杂度:时间复杂度 O(n),空间复杂度 O(max(nums)) = O(100)。

复杂度:时间复杂度 O(n²),空间复杂度 O(1)。


2007. Find Original Array From Doubled Array(从双倍数组中还原原数组)

题目描述

一个整数数组 original 可以转变成一个双倍数组 changed,转变方式为将 original 中每个元素值乘以 2 加入数组中,

然后将所有元素随机打乱。给你一个数组 changed,如果 changed 是双倍数组,那么请你返回 original 数组,

否则请返回空数组。original 的元素可以以任意顺序返回。

示例 1

复制代码
changed = [1,3,4,2,6,8],输出 [1,3,4]。

示例 2

复制代码
changed = [6,3,0,1],输出 []。

示例 3

复制代码
changed = [1],输出 []。

解题思路

解法一(主提交版本)

排序 + 频次贪心匹配(主提交版本)

  • 奇数个元素必然非法(original 与 doubled 各占一半)。排序后从小到大处理。
  • 用频次表统计每个数出现次数;从小到大遍历,若当前数 x 还有剩余,则它一定属于 original(比它小的 x/2 已处理完,
    不可能是别人的两倍),取走一个 x,并检查 2*x 是否还有剩余,没有则非法返回空数组,有则消耗掉。
  • 若 x == 0,它的两倍还是 0,配对时消耗两个 0。
    复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。
    解法二
    TreeMap 频次(对照)
  • 用 TreeMap<Integer,Integer> 代替排序数组,从最小 key 依次取,逻辑与解法一相同,体现"始终处理最小未匹配元素"的贪心本质。
    复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。

代码实现

java 复制代码
public class Solution {

    // 解法一:排序 + 频次贪心(主提交版本)
    public int[] findOriginalArray(int[] changed) {
        int n = changed.length;
        if ((n & 1) == 1) {
            return new int[0];
        }
        java.util.Arrays.sort(changed);
        int max = changed[n - 1];
        int[] cnt = new int[max * 2 + 2];
        for (int x : changed) {
            cnt[x]++;
        }
        int[] ans = new int[n / 2];
        int idx = 0;
        for (int x : changed) {
            if (cnt[x] == 0) {
                continue;             // 已被用作别人的两倍消耗掉
            }
            cnt[x]--;
            int y = x * 2;
            if (y >= cnt.length || cnt[y] == 0) {
                return new int[0];    // 找不到两倍配对,非法
            }
            cnt[y]--;
            ans[idx++] = x;
        }
        return ans;
    }

    // 解法二:TreeMap 贪心(对照)
    public int[] findOriginalArrayTreeMap(int[] changed) {
        int n = changed.length;
        if ((n & 1) == 1) {
            return new int[0];
        }
        java.util.TreeMap<Integer, Integer> map = new java.util.TreeMap<>();
        for (int x : changed) {
            map.put(x, map.getOrDefault(x, 0) + 1);
        }
        int[] ans = new int[n / 2];
        int idx = 0;
        while (!map.isEmpty()) {
            int x = map.firstKey();
            int cx = map.get(x);
            map.remove(x);
            if (cx == 0) {
                continue;
            }
            int y = x * 2;
            int cy = map.getOrDefault(y, 0);
            if (x == 0) {
                // 0 的两倍是 0,需要消耗两个 0
                if (cx < 2) {
                    return new int[0];
                }
                ans[idx++] = 0;
                if (cx > 2) {
                    map.put(0, cx - 2);
                }
                continue;
            }
            if (cy < cx) {
                return new int[0];
            }
            cy -= cx;
            for (int i = 0; i < cx; i++) {
                ans[idx++] = x;
            }
            if (cy > 0) {
                map.put(y, cy);
            } else {
                map.remove(y);       // 配对消耗完,y 必须从表中移除,防止被当作 original 元素误处理
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(java.util.Arrays.toString(sol.findOriginalArray(new int[]{1, 3, 4, 2, 6, 8}))); // [1,3,4]
        System.out.println(java.util.Arrays.toString(sol.findOriginalArray(new int[]{6, 3, 0, 1}))); // []
        System.out.println(java.util.Arrays.toString(sol.findOriginalArray(new int[]{1}))); // []
        System.out.println(java.util.Arrays.toString(sol.findOriginalArray(new int[]{0, 0, 0, 0}))); // [0,0]
        System.out.println(java.util.Arrays.toString(sol.findOriginalArrayTreeMap(new int[]{1, 3, 4, 2, 6, 8}))); // [1,3,4]
        System.out.println(java.util.Arrays.toString(sol.findOriginalArrayTreeMap(new int[]{6, 3, 0, 1}))); // []
        System.out.println(java.util.Arrays.toString(sol.findOriginalArrayTreeMap(new int[]{0, 0, 0, 0}))); // [0,0]
    }
}

复杂度分析

复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。


2008. Maximum Earnings From Taxi(出租车的最大盈利)

题目描述

你驾驶出租车行驶在一条有 n 个地点的路上,地点编号从 1 到 n。你最初在 1 号地点开始旅程。

给你一个二维整数数组 rides,其中 rides[i] = [start_i, end_i, tip_i] 表示你可以选择在第 start_i 个地点

接一位乘客,并在第 end_i 个地点放下,可以赚取 end_i - start_i + tip_i 的报酬。

每份行程只能接一位乘客,且不能中途放下;乘客上车后直到终点前不能再次接其他乘客。

返回能获得的最大盈利(可搭载任意数量的乘客,互不重叠)。

示例 1

复制代码
n = 5, rides = [[2,5,4],[1,5,1]],输出 7(选第一条:5-2+4=7)。

示例 2

复制代码
n = 20, rides = [[1,6,1],[3,10,2],[10,12,3],[11,12,2],[12,15,2],[13,18,1]],输出 20。

解题思路

解法一(主提交版本)

按起点排序 + 记忆化 DFS + 二分(主提交版本)

  • 把行程按起点升序排序,用 dfs(i) 表示从第 i 个行程开始(可跳过)能获得的最大盈利。
    dfs(i) = max(dfs(i+1), 利润_i + dfs(j)),其中 j 是第一个起点 >= end_i 的行程,二分找到。
  • 记忆化数组避免重复计算。
    复杂度:时间复杂度 O(m·log m),空间复杂度 O(m)。
    解法二
    按终点 DP + 线性区间覆盖(对照)
  • 设 dp[i] 为 1...i 号地点范围内的最大盈利。按行程到达顺序更新:
    对每个行程 (s, e, t),candidate = dp[s] + e - s + t,dp[e] = max(dp[e], candidate),
    最后对 dp 做一次前缀取最大(dp[i] = max(dp[i], dp[i-1])),表示"地点 i 处可携带的最大值"。
    复杂度:时间复杂度 O(n + m),空间复杂度 O(n)。

代码实现

java 复制代码
public class Solution {

    // 解法一:排序 + 记忆化 DFS + 二分(主提交版本)
    private int[][] rides;
    private long[] memo;
    private int m;

    public long maxTaxiEarnings(int n, int[][] rides) {
        java.util.Arrays.sort(rides, (a, b) -> a[0] - b[0]);
        this.rides = rides;
        m = rides.length;
        memo = new long[m];
        java.util.Arrays.fill(memo, -1);
        return dfs(0);
    }

    private long dfs(int i) {
        if (i >= m) {
            return 0;
        }
        if (memo[i] != -1) {
            return memo[i];
        }
        int[] r = rides[i];
        int st = r[0], ed = r[1], tip = r[2];
        long profit = ed - st + tip;
        int j = lowerBound(ed, i + 1);
        long skip = dfs(i + 1);
        long take = profit + dfs(j);
        return memo[i] = Math.max(skip, take);
    }

    private int lowerBound(int x, int l) {
        int r = m;
        while (l < r) {
            int mid = (l + r) >>> 1;
            if (rides[mid][0] >= x) {
                r = mid;
            } else {
                l = mid + 1;
            }
        }
        return l;
    }

    // 解法二:按终点 DP + 前缀最大(对照)
    public long maxTaxiEarningsDp(int n, int[][] rides) {
        long[] dp = new long[n + 1];
        java.util.List<int[]>[] byEnd = new java.util.ArrayList[n + 1];
        for (int i = 0; i <= n; i++) {
            byEnd[i] = new java.util.ArrayList<>();
        }
        for (int[] r : rides) {
            byEnd[r[1]].add(r);
        }
        for (int e = 1; e <= n; e++) {
            dp[e] = dp[e - 1];                  // 地点 e 不接任何到 e 的行程
            for (int[] r : byEnd[e]) {
                int s = r[0], tip = r[2];
                dp[e] = Math.max(dp[e], dp[s] + (e - s) + tip);
            }
        }
        return dp[n];
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.maxTaxiEarnings(5, new int[][]{{2, 5, 4}, {1, 5, 1}})); // 7
        System.out.println(sol.maxTaxiEarnings(20, new int[][]{{1, 6, 1}, {3, 10, 2}, {10, 12, 3}, {11, 12, 2}, {12, 15, 2}, {13, 18, 1}})); // 20
        System.out.println(sol.maxTaxiEarningsDp(5, new int[][]{{2, 5, 4}, {1, 5, 1}})); // 7
        System.out.println(sol.maxTaxiEarningsDp(20, new int[][]{{1, 6, 1}, {3, 10, 2}, {10, 12, 3}, {11, 12, 2}, {12, 15, 2}, {13, 18, 1}})); // 20
    }
}

复杂度分析

复杂度:时间复杂度 O(m·log m),空间复杂度 O(m)。

复杂度:时间复杂度 O(n + m),空间复杂度 O(n)。


2009. Minimum Number of Operations to Make Array Continuous(使数组连续的最少操作数)

题目描述

给你一个整数数组 nums。每一次操作中,你可以将 nums 中任意一个元素替换成任意整数。

如果 nums 满足以下条件,那么它是连续的:nums 中所有元素都是互不相同的,且最大元素与最小元素的差等于 nums.length - 1。

例如,[4, 2, 5, 3] 是连续的,而 [1, 2, 3, 5, 6] 不连续。

请你返回使 nums 连续的最少操作数。

示例 1

复制代码
nums = [4,2,5,3],输出 0。

示例 2

复制代码
nums = [1,2,3,5,6],输出 1(把 1 改成 4 得 [4,2,3,5,6])。

示例 3

复制代码
nums = [1,10,100,1000],输出 3。

解题思路

解法一(主提交版本)

排序去重 + 滑动窗口(主提交版本)

  • 连续数组的 n 个互异元素必然是某个长度为 n 的区间 x, x+n-1。对原数组排序并去重,考虑窗口内能"不动"的元素。
  • 对去重数组的每个左端点 i,用滑动窗口找到最大的 j,使 uniq[j] - uniq[i] < n(即全部可留在长度 n 的窗口内),
    窗口内元素数 j - i + 1 就是可以不改动的元素数,答案 = n - 最大窗口元素数。
    复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。
    解法二
    排序 + 二分右端点(对照)
  • 对去重数组每个位置 i,用二分查找最后一个满足 uniq[j] < uniq[i] + n 的下标 j,同样算出最大保留数。
  • 思路与滑动窗口等价,但显式展示"窗口右界"的求法。
    复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。

代码实现

java 复制代码
public class Solution {

    // 解法一:排序去重 + 滑动窗口(主提交版本)
    public int minOperations(int[] nums) {
        int n = nums.length;
        java.util.Arrays.sort(nums);
        // 去重
        int[] uniq = new int[n];
        int len = 0;
        for (int x : nums) {
            if (len == 0 || uniq[len - 1] != x) {
                uniq[len++] = x;
            }
        }
        int maxKeep = 0;
        int j = 0;
        for (int i = 0; i < len; i++) {
            while (j < len && uniq[j] - uniq[i] < n) {
                j++;
            }
            maxKeep = Math.max(maxKeep, j - i);
        }
        return n - maxKeep;
    }

    // 解法二:排序去重 + 二分(对照)
    public int minOperationsBinary(int[] nums) {
        int n = nums.length;
        java.util.Arrays.sort(nums);
        int[] uniq = new int[n];
        int len = 0;
        for (int x : nums) {
            if (len == 0 || uniq[len - 1] != x) {
                uniq[len++] = x;
            }
        }
        int maxKeep = 0;
        for (int i = 0; i < len; i++) {
            int lo = i, hi = len;               // 找第一个 uniq[j] >= uniq[i] + n
            long target = (long) uniq[i] + n;
            while (lo < hi) {
                int mid = (lo + hi) >>> 1;
                if (uniq[mid] >= target) {
                    hi = mid;
                } else {
                    lo = mid + 1;
                }
            }
            maxKeep = Math.max(maxKeep, lo - i);
        }
        return n - maxKeep;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.minOperations(new int[]{4, 2, 5, 3})); // 0
        System.out.println(sol.minOperations(new int[]{1, 2, 3, 5, 6})); // 1
        System.out.println(sol.minOperations(new int[]{1, 10, 100, 1000})); // 3
        System.out.println(sol.minOperations(new int[]{1, 1, 1, 1, 1})); // 4
        System.out.println(sol.minOperationsBinary(new int[]{4, 2, 5, 3})); // 0
        System.out.println(sol.minOperationsBinary(new int[]{1, 2, 3, 5, 6})); // 1
        System.out.println(sol.minOperationsBinary(new int[]{1, 10, 100, 1000})); // 3
        System.out.println(sol.minOperationsBinary(new int[]{1, 1, 1, 1, 1})); // 4
    }
}

复杂度分析

复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。


2010. The Number of Seniors and Juniors to Join the Company II(职员招聘人数 II)

题目类型:数据库(SQL)

题目描述

表: Candidates

复制代码
+-------------+------+
| Column Name | Type |
+-------------+------+
| employee_id | int  |
| experience  | enum |
| salary      | int  |
+-------------+------+

employee_id 是该表中具有唯一值的列。经验是一个枚举,其中包含一个值("高级"、"初级")。此表的每一行都显示候选人的 id、月薪和经验。每个候选人的工资保证是唯一的。

一家公司想雇佣新员工。公司的工资预算是 $70000。公司的招聘标准是:

  • 继续雇佣薪水最低的高级职员,直到你不能再雇佣更多的高级职员;
  • 用剩下的预算雇佣薪水最低的初级职员;
  • 继续以最低的工资雇佣初级职员,直到你不能再雇佣更多的初级职员。

编写一个解决方案,查找根据上述条件雇用职员的 ID。按任意顺序返回结果表。

示例 1:

复制代码
输入:
Candidates table:
+-------------+------------+--------+
| employee_id | experience | salary |
+-------------+------------+--------+
| 1           | Junior     | 10000  |
| 9           | Junior     | 15000  |
| 2           | Senior     | 20000  |
| 11          | Senior     | 16000  |
| 13          | Senior     | 50000  |
| 4           | Junior     | 40000  |
+-------------+------------+--------+
输出:
+-------------+
| employee_id |
+-------------+
| 11          |
| 2           |
| 1           |
| 9           |
+-------------+
解释:
高级员工按工资升序雇佣:11(16000)、2(20000),共 36000;剩余 34000 不够雇 13(50000)。
低级员工按工资升序:1(10000)、9(15000),共 25000;剩余 9000 不够雇 4(40000)。

示例 2:

复制代码
输入:
Candidates table:
+-------------+------------+--------+
| employee_id | experience | salary |
+-------------+------------+--------+
| 1           | Junior     | 25000  |
| 9           | Junior     | 10000  |
| 2           | Senior     | 85000  |
| 11          | Senior     | 80000  |
| 13          | Senior     | 90000  |
| 4           | Junior     | 30000  |
+-------------+------------+--------+
输出:
+-------------+
| employee_id |
+-------------+
| 9           |
| 1           |
| 4           |
+-------------+
解释:
最便宜的高级员工也要 80000 > 70000,雇不起;剩余 70000 全部用于初级:9(10000)、1(25000)、4(30000)。

解题思路

解法一:窗口前缀和 + 递归式剩余预算

与 I 题(2004)的核心区别:II 题要输出被雇佣的具体员工 ID,且每个候选工资唯一。

  • 高级员工按 salary ASC 排序,SUM(salary) OVER (ORDER BY salary) 前缀和 <= 70000 的行即为可雇佣的高级员工;
  • 高级员工薪资和记为 cost,剩余预算 70000 - cost;
  • 初级员工同样按薪资升序取前缀和 <= 剩余预算 的 ID。

用两个 CTE 分别算两类可雇佣集合,再 UNION ALL 输出全部 employee_id。

解法二(对照):相关子查询/自连接逐行判断是否"轮到"该员工 ------由于每个工资唯一,可精确判断当前员工之前(更低价)的所有人加起来是否必然被雇佣;用 NOT EXISTS 更低价员工超预算 类条件等价实现。

代码实现

sql 复制代码
-- 解法一(推荐):窗口前缀和分别截断两类候选人
WITH seniors AS (
    SELECT employee_id, salary,
           SUM(salary) OVER (ORDER BY salary) AS cum
    FROM Candidates
    WHERE experience = 'Senior'
),
senior_hired AS (
    SELECT employee_id, salary, cum
    FROM seniors
    WHERE cum <= 70000          -- 前缀和不超过预算的行 = 被雇佣的高级员工
),
junior_budget AS (
    SELECT 70000 - COALESCE(SUM(salary), 0) AS budget
    FROM senior_hired
),
juniors AS (
    SELECT employee_id, salary,
           SUM(salary) OVER (ORDER BY salary) AS cum
    FROM Candidates
    WHERE experience = 'Junior'
)
SELECT employee_id FROM senior_hired
UNION ALL
SELECT j.employee_id
FROM juniors j, junior_budget b
WHERE j.cum <= b.budget;
sql 复制代码
-- 解法二(对照):子查询计算高级总成本后再筛选初级
WITH senior_cte AS (
    SELECT employee_id, salary,
           SUM(salary) OVER (ORDER BY salary) AS cum
    FROM Candidates WHERE experience = 'Senior'
),
hired_senior AS (
    SELECT employee_id FROM senior_cte WHERE cum <= 70000
),
cost AS (
    SELECT COALESCE(SUM(salary), 0) AS c FROM senior_cte WHERE cum <= 70000
)
SELECT employee_id FROM hired_senior
UNION ALL
SELECT j.employee_id
FROM (SELECT employee_id, salary,
             SUM(salary) OVER (ORDER BY salary) AS cum
      FROM Candidates WHERE experience = 'Junior') j
CROSS JOIN cost
WHERE j.cum <= 70000 - cost.c;

复杂度分析

  • 时间复杂度 :O(n log n),其中 n 为 Candidates 行数;排序为主。
  • 空间复杂度 :O(n),窗口函数中间结果与行数成正比。

2011. Final Value of Variable After Performing Operations(执行操作后的变量值)

题目描述

存在一种仅支持 4 种操作和 1 个变量 X 的编程语言:++X 和 X++ 使变量 X 的值加 1,

--X 和 X-- 使变量 X 的值减 1。最初 X 的值是 0。给你一个字符串数组 operations,

这是由操作组成的一个列表,返回执行所有操作后 X 的最终值。

示例 1

复制代码
operations = ["--X","X++","X++"],输出 1。

示例 2

复制代码
operations = ["++X","++X","X++"],输出 3。

示例 3

复制代码
operations = ["X++","++X","--X","X--"],输出 0。

解题思路

解法一(主提交版本)

取中间字符判断(主提交版本)

  • 四种合法操作都形如 ?X? 且中间字符一定是 X,决定增/减的是两侧符号:+ 出现则 +1,- 出现则 -1。
  • 直接看 operations[i].charAt(1):'+' 则累加 1,否则累减 1,一次遍历完成。
    复杂度:时间复杂度 O(n),空间复杂度 O(1)。
    解法二
    contains 判断 + 字符串匹配(对照)
  • 逐个判断操作是否含 "++" 或 "--":含 + 的操作为自增,含 - 的操作为自减。
  • 逻辑直白,不依赖操作符的位置约定。
    复杂度:时间复杂度 O(n·len),空间复杂度 O(1)。

代码实现

java 复制代码
public class Solution {

    // 解法一:中间字符判断(主提交版本)
    public int finalValueAfterOperations(String[] operations) {
        int x = 0;
        for (String op : operations) {
            x += (op.charAt(1) == '+') ? 1 : -1;
        }
        return x;
    }

    // 解法二:contains 判断(对照)
    public int finalValueAfterOperationsContains(String[] operations) {
        int x = 0;
        for (String op : operations) {
            if (op.contains("++")) {
                x++;
            } else {
                x--;
            }
        }
        return x;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.finalValueAfterOperations(new String[]{"--X", "X++", "X++"})); // 1
        System.out.println(sol.finalValueAfterOperations(new String[]{"++X", "++X", "X++"})); // 3
        System.out.println(sol.finalValueAfterOperations(new String[]{"X++", "++X", "--X", "X--"})); // 0
        System.out.println(sol.finalValueAfterOperationsContains(new String[]{"--X", "X++", "X++"})); // 1
        System.out.println(sol.finalValueAfterOperationsContains(new String[]{"++X", "++X", "X++"})); // 3
        System.out.println(sol.finalValueAfterOperationsContains(new String[]{"X++", "++X", "--X", "X--"})); // 0
    }
}

复杂度分析

复杂度:时间复杂度 O(n),空间复杂度 O(1)。

复杂度:时间复杂度 O(n·len),空间复杂度 O(1)。


2012. Sum of Beauty in the Array(数组美丽值求和)

题目描述

给你一个下标从 0 开始的整数数组 nums。对于每个下标 i(1 <= i <= nums.length - 2),nums[i] 的美丽值等于:

  • 2:对于所有 0 <= j < i 且 i < k <= nums.length - 1,满足 nums[j] < nums[i] < nums[k];
  • 1:如果满足 nums[i-1] < nums[i] < nums[i+1],且不满足前面的条件;
  • 0:如果上述条件全部不满足。
    返回符合 1 <= i <= nums.length - 2 的所有 nums[i] 的美丽值的总和。

示例 1

复制代码
nums = [1,2,3],输出 2(nums[1] 美丽值为 2)。

示例 2

复制代码
nums = [2,4,6,4],输出 1(nums[1] 为 1,nums[2] 为 0)。

示例 3

复制代码
nums = [3,2,1],输出 0。

解题思路

解法一(主提交版本)

左右最值预计算(主提交版本)

  • 条件一"所有左边都小于它、所有右边都大于它"其实等价于 左边最大值 < nums[i] < 右边最小值。
  • 先扫描右侧,构造 rightMin[i] 表示 i 右侧(含 i+1 起)的最小值;再从左往右维护左侧最大值 leftMax。
  • 对每个内部下标 i:先判断 leftMax < nums[i] < rightMin[i+1] 成立则 +2;否则若 nums[i-1] < nums[i] < nums[i+1] 则 +1。
    复杂度:时间复杂度 O(n),空间复杂度 O(n)。
    解法二
    暴力检查(对照)
  • 对每个内部下标 i,分别向左右全量扫描检查条件一,再检查条件二。
    复杂度:时间复杂度 O(n²),空间复杂度 O(1)。

代码实现

java 复制代码
public class Solution {

    // 解法一:左右最值预计算(主提交版本)
    public int sumOfBeauties(int[] nums) {
        int n = nums.length;
        int[] rightMin = new int[n];
        rightMin[n - 1] = nums[n - 1];
        for (int i = n - 2; i >= 0; i--) {
            rightMin[i] = Math.min(rightMin[i + 1], nums[i]);
        }
        int ans = 0;
        int leftMax = nums[0];
        for (int i = 1; i < n - 1; i++) {
            if (leftMax < nums[i] && nums[i] < rightMin[i + 1]) {
                ans += 2;
            } else if (nums[i - 1] < nums[i] && nums[i] < nums[i + 1]) {
                ans += 1;
            }
            leftMax = Math.max(leftMax, nums[i]);
        }
        return ans;
    }

    // 解法二:暴力检查(对照)
    public int sumOfBeautiesBrute(int[] nums) {
        int n = nums.length;
        int ans = 0;
        for (int i = 1; i < n - 1; i++) {
            boolean cond1 = true;
            for (int j = 0; j < i; j++) {
                if (nums[j] >= nums[i]) {
                    cond1 = false;
                    break;
                }
            }
            for (int k = i + 1; k < n && cond1; k++) {
                if (nums[k] <= nums[i]) {
                    cond1 = false;
                }
            }
            if (cond1) {
                ans += 2;
            } else if (nums[i - 1] < nums[i] && nums[i] < nums[i + 1]) {
                ans += 1;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.sumOfBeauties(new int[]{1, 2, 3})); // 2
        System.out.println(sol.sumOfBeauties(new int[]{2, 4, 6, 4})); // 1
        System.out.println(sol.sumOfBeauties(new int[]{3, 2, 1})); // 0
        System.out.println(sol.sumOfBeautiesBrute(new int[]{1, 2, 3})); // 2
        System.out.println(sol.sumOfBeautiesBrute(new int[]{2, 4, 6, 4})); // 1
        System.out.println(sol.sumOfBeautiesBrute(new int[]{3, 2, 1})); // 0
    }
}

复杂度分析

复杂度:时间复杂度 O(n),空间复杂度 O(n)。

复杂度:时间复杂度 O(n²),空间复杂度 O(1)。


2013. Detect Squares(检测正方形)

题目描述

给你一个在 X-Y 平面上的点构成的数据流。设计一个满足下述要求的算法:

  • 添加一个在数据流中的新点到某个数据结构中,可以添加重复的点,并会视作不同的点进行处理;
  • 给你一个查询点,请你从数据结构中选出三个点,使这三个点和查询点一同构成一个面积为正的轴对齐正方形,统计满足该要求的方案数目。
    实现 DetectSquares 类:DetectSquares() 初始化空结构,void add(int[] point) 添加点 [x, y],
    int count(int[] point) 统计与点 [x, y] 共同构造轴对齐正方形的方案数。

示例

复制代码
输入 ["DetectSquares","add","add","add","count","count","add","count"]
[[],[3,10],[11,2],[3,2],[11,10],[14,8],[11,2],[11,10]]
输出 [null,null,null,null,1,0,null,2]。

解题思路

解法一(主提交版本)

按 x 分组 + 对角线枚举(主提交版本)

  • 用 Map<Integer, Map<Integer, Integer>>(x -> (y -> 次数))存储点到数据结构。
  • count([x1, y1]) 时枚举所有与 x1 不同的 x2:边长 d = x2 - x1,则另两个对角点可能是
    (x1, y1+d)、(x2, y1+d)(正方形在 y 正方向)或 (x1, y1-d)、(x2, y1-d)。
    方案数 = num(x2, y1) * num(x1, y1+d) * num(x2, y1+d)(对两个方向分别累加)。
    复杂度:单次 add O(1);单次 count O(不同 x 的数量),总调用 ≤ 5000 故可接受。
    解法二
    全点对点枚举(对照)
  • 先把所有点放进 Map<String, Integer>("x,y" 为键),count 时枚举每个已有点为一边,判断能否补成正方形。
  • 枚举方式:与查询点同行(y 相等)的点为底边,计算边长后检查上/下方两点是否存在并乘上次数。
    复杂度:单次 count O(点数),最坏 O(5000)。

代码实现

java 复制代码
public class Solution {

    // 解法一:按 x 分组 + 对角线枚举(主提交版本)
    private java.util.Map<Integer, java.util.Map<Integer, Integer>> cnt;

    public Solution() {
        cnt = new java.util.HashMap<>();
    }

    public void add(int[] point) {
        int x = point[0], y = point[1];
        java.util.Map<Integer, Integer> col = cnt.get(x);
        if (col == null) {
            col = new java.util.HashMap<>();
            cnt.put(x, col);
        }
        col.put(y, col.getOrDefault(y, 0) + 1);
    }

    public int count(int[] point) {
        int x1 = point[0], y1 = point[1];
        java.util.Map<Integer, Integer> row1 = cnt.get(x1);
        if (row1 == null) {
            return 0;
        }
        int ans = 0;
        for (java.util.Map.Entry<Integer, java.util.Map<Integer, Integer>> e : cnt.entrySet()) {
            int x2 = e.getKey();
            if (x2 == x1) {
                continue;
            }
            int d = x2 - x1;
            java.util.Map<Integer, Integer> row2 = e.getValue();
            // 方向一:y 正侧
            ans += row2.getOrDefault(y1, 0)
                    * row1.getOrDefault(y1 + d, 0)
                    * row2.getOrDefault(y1 + d, 0);
            // 方向二:y 负侧
            ans += row2.getOrDefault(y1, 0)
                    * row1.getOrDefault(y1 - d, 0)
                    * row2.getOrDefault(y1 - d, 0);
        }
        return ans;
    }

    // 解法二:全点对点枚举 key 字符串(对照)
    private java.util.Map<String, Integer> all;

    public void initBrute() {
        all = new java.util.HashMap<>();
    }

    public void addBrute(int[] point) {
        String key = point[0] + "," + point[1];
        all.put(key, all.getOrDefault(key, 0) + 1);
    }

    public int countBrute(int[] point) {
        int x1 = point[0], y1 = point[1];
        int ans = 0;
        for (java.util.Map.Entry<String, Integer> e : all.entrySet()) {
            String[] p = e.getKey().split(",");
            int x2 = Integer.parseInt(p[0]);
            int y2 = Integer.parseInt(p[1]);
            if (x2 == x1) {
                continue;
            }
            int d = x2 - x1;
            // 查询点所在 x=x1 的列上需要 y1+d 或 y1-d
            int c1 = all.getOrDefault(x1 + "," + (y2), 0);
            // (x2, y1) 底边点
            int c2 = e.getValue();
            // 对角 (x1, y1+d),其中 y1+d == y2 或 (x1, y1-d) == y2
            int c3a = all.getOrDefault(x2 + "," + (y1), 0);
            // 情况 A:y2 = y1 + d
            if (y2 == y1 + d) {
                ans += c2 * c3a * all.getOrDefault(x1 + "," + (y2), 0);
            }
            // 情况 B:y2 = y1 - d
            if (y2 == y1 - d) {
                ans += c2 * c3a * all.getOrDefault(x1 + "," + (y2), 0);
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        sol.add(new int[]{3, 10});
        sol.add(new int[]{11, 2});
        sol.add(new int[]{3, 2});
        System.out.println(sol.count(new int[]{11, 10})); // 1
        System.out.println(sol.count(new int[]{14, 8})); // 0
        sol.add(new int[]{11, 2});
        System.out.println(sol.count(new int[]{11, 10})); // 2

        Solution brute = new Solution();
        brute.initBrute();
        brute.addBrute(new int[]{3, 10});
        brute.addBrute(new int[]{11, 2});
        brute.addBrute(new int[]{3, 2});
        System.out.println(brute.countBrute(new int[]{11, 10})); // 1
        System.out.println(brute.countBrute(new int[]{14, 8})); // 0
        brute.addBrute(new int[]{11, 2});
        System.out.println(brute.countBrute(new int[]{11, 10})); // 2
    }
}

复杂度分析

复杂度:单次 add O(1);单次 count O(不同 x 的数量),总调用 ≤ 5000 故可接受。

复杂度:单次 count O(点数),最坏 O(5000)。


2014. Longest Subsequence Repeated k Times(重复 K 次的最长子序列)

题目描述

给你一个长度为 n 的字符串 s 和一个整数 k,请找出字符串 s 中重复 k 次的最长子序列。

子序列是由其他字符串删除某些(或不删除)字符派生而来的一个字符串。

如果 seq * k 是 s 的一个子序列(seq * k 表示由 seq 串联 k 次构造的字符串),

那么就称 seq 是字符串 s 中一个重复 k 次的子序列。返回重复 k 次的最长子序列;

如果存在多个满足的子序列,则返回字典序最大的那个;如果不存在,返回空字符串。

示例 1

复制代码
s = "letsleetcode", k = 2,输出 "let"("let" 和 "ete" 都是最长,取字典序最大)。

示例 2

复制代码
s = "bb", k = 2,输出 "b"。

示例 3

复制代码
s = "ab", k = 2,输出 ""。

解题思路

解法一(主提交版本)

BFS 枚举候选 + 贪心校验(主提交版本)

  • 关键约束 n < min(2001, k*8) 说明最长答案不超过 7~8 个字符,且只有出现次数 ≥ k 的字符可能出现在答案中。
  • 先统计频次,收集出现次数 ≥ k 的字符。用 BFS 按字典序从大到小的顺序生成候选("z...a" 顺序入队可保证后出队的更长且优先字典序大),
    逐个校验"候选串重复 k 次是否为 s 的子序列",记录最后通过的候选即为最长且字典序最大的。
  • 校验(贪心):在 s 上模拟 k 轮匹配,每轮完整匹配一次候选串,若 k 轮都能匹配完则成立。
    复杂度:候选数量有界(字符集 ≤ 26、长度 ≤ 7),总体可认为是 O(26^(L)·n) 量级(L 为答案长度上限),满足 n ≤ 2000。
    解法二
    DFS 从 dict 序构造 + 逐步校验(对照)
  • 按字符从大到小尝试构造候选串,每扩展一个字符就校验一次合法性,记录目前字典序最大且最长的可行答案。
  • 先尝试扩展(更长优先),同长度下先尝试字典序更大的字符,与 BFS 结果一致。
    复杂度:同样有界,实现更直观。

代码实现

java 复制代码
public class Solution {

    private char[] arr;

    // 解法一:BFS 枚举候选 + 贪心校验(主提交版本)
    public String longestSubsequenceRepeatedK(String s, int k) {
        this.arr = s.toCharArray();
        int[] cnt = new int[26];
        for (char c : arr) {
            cnt[c - 'a']++;
        }
        java.util.List<Character> usable = new java.util.ArrayList<>();
        for (char c = 'a'; c <= 'z'; c++) {
            if (cnt[c - 'a'] >= k) {
                usable.add(c);
            }
        }
        java.util.Deque<String> q = new java.util.ArrayDeque<>();
        q.offer("");
        String ans = "";
        while (!q.isEmpty()) {
            String cur = q.poll();
            for (char c : usable) {
                String next = cur + c;
                if (check(next, k)) {
                    ans = next;
                    q.offer(next);
                }
            }
        }
        return ans;
    }

    // 校验 t 重复 k 次是否为 s 的子序列(贪心多轮匹配)
    private boolean check(String t, int k) {
        int ti = 0;
        for (char c : arr) {
            if (c == t.charAt(ti)) {
                ti++;
                if (ti == t.length()) {
                    k--;
                    if (k == 0) {
                        return true;
                    }
                    ti = 0;
                }
            }
        }
        return false;
    }

    // 解法二:DFS 构造 + 最优保持(对照)
    private String best = "";

    public String longestSubsequenceRepeatedKDfs(String s, int k) {
        this.arr = s.toCharArray();
        best = "";
        int[] cnt = new int[26];
        for (char c : arr) {
            cnt[c - 'a']++;
        }
        // 从 z 到 a 收集可用字符(优先尝试字典序大的字符)
        java.util.List<Character> usable = new java.util.ArrayList<>();
        for (char c = 'z'; c >= 'a'; c--) {
            if (cnt[c - 'a'] >= k) {
                usable.add(c);
            }
        }
        for (char c : usable) {
            String start = String.valueOf(c);
            if (check(start, k)) {
                if (isBetter(start, best)) {
                    best = start;   // 仅当更优(更长或同长字典序更大)才更新
                }
                dfsBuild(usable, start, k, arr.length / k);
            }
        }
        return best;
    }

    // 长度优先、同长取字典序更大
    private boolean isBetter(String a, String b) {
        if (a.length() != b.length()) {
            return a.length() > b.length();
        }
        return a.compareTo(b) > 0;
    }

    private void dfsBuild(java.util.List<Character> usable, String cur, int k, int maxLen) {
        for (char c : usable) {
            String next = cur + c;
            if (!check(next, k)) {
                continue;
            }
            if (isBetter(next, best)) {
                best = next;
            }
            // 答案长度不可能超过 n/k,超过即无法继续变长
            if (next.length() < maxLen) {
                dfsBuild(usable, next, k, maxLen);
            }
        }
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.longestSubsequenceRepeatedK("letsleetcode", 2)); // let
        System.out.println(sol.longestSubsequenceRepeatedK("bb", 2)); // b
        System.out.println(sol.longestSubsequenceRepeatedK("ab", 2)); // ""
        System.out.println(sol.longestSubsequenceRepeatedKDfs("letsleetcode", 2)); // let
        System.out.println(sol.longestSubsequenceRepeatedKDfs("bb", 2)); // b
        System.out.println(sol.longestSubsequenceRepeatedKDfs("ab", 2)); // ""
    }
}

复杂度分析

复杂度:候选数量有界(字符集 ≤ 26、长度 ≤ 7),总体可认为是 O(26^(L)·n) 量级(L 为答案长度上限),满足 n ≤ 2000。


2015. Average Height of Buildings in Each Segment(每段建筑物的平均高度)

题目描述

一条完全笔直的街道由一条数字线表示。街道上有建筑物,由二维整数数组 buildings 表示,

其中 buildings[i] = [start_i, end_i, height_i] 表示在半封闭区间 [start_i, end_i) 有一座高度为 height_i 的建筑。

你想用最少数量的非重叠部分来描述街道上建筑物的高度,输出二维整数数组 street,

其中 street[j] = [left_j, right_j, average_j] 描述了半封闭区域 [left_j, right_j) 中建筑物的平均高度,

不包括街道上没有建筑物的任何区域。平均值用整数除法。

示例 1

复制代码
buildings = [[1,4,2],[3,9,4]],输出 [[1,3,2],[3,4,3],[4,9,4]]。

示例 2

复制代码
buildings = [[1,3,2],[2,5,3],[2,8,3]],输出 [[1,3,2],[3,8,3]]。

解题思路

解法一(主提交版本)

差分 + TreeMap 扫描(主提交版本)

  • 对每个端点位置记录两个事件:覆盖数变化(+1/-1)与高度和变化(+h/-h),用 TreeMap 按坐标有序存储。
  • 从左到右扫描所有关键点:扫描前一段 [last, x) 的覆盖数 m > 0,平均高度 = 高度和 / 覆盖数(整数除法);
    若与前一段平均高度相同且右端点相接则合并区间,否则新开一段。
    复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。
    解法二
    区间合并暴力(对照)
  • 收集所有端点坐标排序去重,对每对相邻坐标 (a, b) 统计覆盖该开区间的所有建筑高度求和取平均;
    跳过无建筑覆盖的区间,并合并连续区间平均高度相同的相邻段。
  • 逻辑直白(枚举端点对),常数大但易于核对。
    复杂度:时间复杂度 O(n²·k),空间复杂度 O(n)。

代码实现

java 复制代码
public class Solution {

    // 解法一:差分 + TreeMap 扫描(主提交版本)
    public int[][] averageHeightOfBuildings(int[][] buildings) {
        java.util.Map<Integer, Integer> cnt = new java.util.HashMap<>();
        java.util.TreeMap<Integer, Integer> delta = new java.util.TreeMap<>();
        for (int[] e : buildings) {
            int st = e[0], en = e[1], h = e[2];
            cnt.put(st, cnt.getOrDefault(st, 0) + 1);
            cnt.put(en, cnt.getOrDefault(en, 0) - 1);
            delta.put(st, delta.getOrDefault(st, 0) + h);
            delta.put(en, delta.getOrDefault(en, 0) - h);
        }
        java.util.List<int[]> ans = new java.util.ArrayList<>();
        int total = 0;      // 当前高度和
        int cover = 0;      // 当前覆盖建筑数
        int last = -1;
        for (java.util.Map.Entry<Integer, Integer> e : delta.entrySet()) {
            int x = e.getKey();
            if (cover > 0) {
                int avg = total / cover;
                if (!ans.isEmpty() && ans.get(ans.size() - 1)[1] == last
                        && ans.get(ans.size() - 1)[2] == avg) {
                    ans.get(ans.size() - 1)[1] = x;   // 与上段相邻且平均相同 → 合并
                } else {
                    ans.add(new int[]{last, x, avg});
                }
            }
            total += delta.get(x);
            cover += cnt.get(x);
            last = x;
        }
        return ans.toArray(new int[0][]);
    }

    // 解法二:端点对暴力统计(对照)
    public int[][] averageHeightOfBuildingsBrute(int[][] buildings) {
        java.util.TreeSet<Integer> pts = new java.util.TreeSet<>();
        for (int[] b : buildings) {
            pts.add(b[0]);
            pts.add(b[1]);
        }
        Integer[] arr = pts.toArray(new Integer[0]);
        java.util.List<int[]> ans = new java.util.ArrayList<>();
        for (int i = 0; i < arr.length - 1; i++) {
            int a = arr[i], b = arr[i + 1];
            long sum = 0;
            int m = 0;
            for (int[] bl : buildings) {
                if (bl[0] <= a && b <= bl[1]) {   // [a,b) 被该建筑覆盖
                    sum += bl[2];
                    m++;
                }
            }
            if (m == 0) {
                continue;
            }
            int avg = (int) (sum / m);
            if (!ans.isEmpty() && ans.get(ans.size() - 1)[1] == a
                    && ans.get(ans.size() - 1)[2] == avg) {
                ans.get(ans.size() - 1)[1] = b;
            } else {
                ans.add(new int[]{a, b, avg});
            }
        }
        return ans.toArray(new int[0][]);
    }

    private static String[][] fmt(int[][] a) {
        String[][] r = new String[a.length][];
        for (int i = 0; i < a.length; i++) {
            r[i] = new String[]{String.valueOf(a[i][0]), String.valueOf(a[i][1]), String.valueOf(a[i][2])};
        }
        return r;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(java.util.Arrays.deepToString(sol.averageHeightOfBuildings(new int[][]{{1, 4, 2}, {3, 9, 4}}))); // [[1,3,2],[3,4,3],[4,9,4]]
        System.out.println(java.util.Arrays.deepToString(sol.averageHeightOfBuildings(new int[][]{{1, 3, 2}, {2, 5, 3}, {2, 8, 3}}))); // [[1,3,2],[3,8,3]]
        System.out.println(java.util.Arrays.deepToString(sol.averageHeightOfBuildingsBrute(new int[][]{{1, 4, 2}, {3, 9, 4}}))); // [[1,3,2],[3,4,3],[4,9,4]]
        System.out.println(java.util.Arrays.deepToString(sol.averageHeightOfBuildingsBrute(new int[][]{{1, 3, 2}, {2, 5, 3}, {2, 8, 3}}))); // [[1,3,2],[3,8,3]]
    }
}

复杂度分析

复杂度:时间复杂度 O(n·log n),空间复杂度 O(n)。

复杂度:时间复杂度 O(n²·k),空间复杂度 O(n)。


2016. Maximum Difference Between Increasing Elements(增量元素之间的最大差值)

题目描述

给你一个下标从 0 开始的整数数组 nums,该数组大小为 n,请你计算 nums[j] - nums[i] 能求得的最大差值,

其中 0 <= i < j < n 且 nums[i] < nums[j]。返回最大差值;如果不存在满足要求的 i 和 j,返回 -1。

示例 1

复制代码
nums = [7,1,5,4],输出 4(i=1, j=2,5-1=4)。

示例 2

复制代码
nums = [9,4,3,2],输出 -1。

示例 3

复制代码
nums = [1,5,2,10],输出 9(i=0, j=3,10-1=9)。

解题思路

解法一(主提交版本)

维护前缀最小值(主提交版本)

  • 从左到右扫描,维护已扫过元素的最小值 min。对每个位置 j,若 nums[j] > min,
    则候选差为 nums[j] - min,更新全局最大。
  • 一次遍历完成;注意最小值是前缀的最小值,天然满足 i < j 约束。
    复杂度:时间复杂度 O(n),空间复杂度 O(1)。
    解法二
    双重循环暴力(对照)
  • 枚举所有 i < j 且 nums[i] < nums[j] 的组合,求差的最大值,无则 -1。
    复杂度:时间复杂度 O(n²),空间复杂度 O(1)。

代码实现

java 复制代码
public class Solution {

    // 解法一:维护前缀最小值(主提交版本)
    public int maximumDifference(int[] nums) {
        int min = nums[0];
        int ans = -1;
        for (int j = 1; j < nums.length; j++) {
            if (nums[j] > min) {
                ans = Math.max(ans, nums[j] - min);
            }
            min = Math.min(min, nums[j]);
        }
        return ans;
    }

    // 解法二:暴力双重循环(对照)
    public int maximumDifferenceBrute(int[] nums) {
        int n = nums.length;
        int ans = -1;
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                if (nums[i] < nums[j]) {
                    ans = Math.max(ans, nums[j] - nums[i]);
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.maximumDifference(new int[]{7, 1, 5, 4})); // 4
        System.out.println(sol.maximumDifference(new int[]{9, 4, 3, 2})); // -1
        System.out.println(sol.maximumDifference(new int[]{1, 5, 2, 10})); // 9
        System.out.println(sol.maximumDifferenceBrute(new int[]{7, 1, 5, 4})); // 4
        System.out.println(sol.maximumDifferenceBrute(new int[]{9, 4, 3, 2})); // -1
        System.out.println(sol.maximumDifferenceBrute(new int[]{1, 5, 2, 10})); // 9
    }
}

复杂度分析

复杂度:时间复杂度 O(n),空间复杂度 O(1)。

复杂度:时间复杂度 O(n²),空间复杂度 O(1)。


2017. Grid Game(网格游戏)

题目描述

给你一个下标从 0 开始的二维数组 grid,大小为 2 x n,其中 grid[r][c] 表示矩阵中 (r, c) 位置上的点数。

两个机器人初始位置都是 (0, 0),目标位置是 (1, n-1)。每个机器人只会向右或向下移动。

游戏开始,第一个机器人先走完 (0,0) -> (1,n-1),并收集路径上单元格的全部点数,途经后这些格子重置为 0;

然后第二个机器人再走同样路径并收集点数(路径可以相交)。

第一个机器人想让第二个机器人收集到的点数最小化,第二个机器人想最大化自己收集到的点数。

两人都发挥最佳水平的前提下,返回第二个机器人收集到的点数。

示例 1

复制代码
grid = [[2,5,4],[1,5,1]],输出 4。

示例 2

复制代码
grid = [[3,3,1],[8,5,2]],输出 4。

示例 3

复制代码
grid = [[1,3,1,15],[1,3,3,1]],输出 7。

解题思路

解法一(主提交版本)

枚举第一机器人换行点 + 前缀和(主提交版本)

  • 第一机器人从上方走,只会在某列 j 处向下换行,把路径分成:上方经过 0..j-1 列,下方经过 j..n-1 列。
  • 第一机器人经过的格子清零,第二机器人能拿到的 = 上方剩余部分 (j+1..n-1) 之和 与 下方剩余部分 (0..j-1) 之和 的较大者
    (第二机器人同样可以选择换行点,它必然选两条残路中更大的那条)。
  • 第一机器人要使这个"较大者"最小:ans = min_j( max(s1_after, s2_before) ),其中用前缀和 O(1) 求两侧和。
    复杂度:时间复杂度 O(n),空间复杂度 O(1)。
    解法二
    前缀和数组 + 换行点枚举(对照)
  • 显式构造两行的前缀和数组,枚举换行列 j,用前缀和直接累加两侧残值,逻辑更展开。
    复杂度:时间复杂度 O(n),空间复杂度 O(n)。

代码实现

java 复制代码
public class Solution {

    // 解法一:枚举换行点 + 滚动前缀和(主提交版本)
    public long gridGame(int[][] grid) {
        int n = grid[0].length;
        long upRight = 0;      // 上方在换行列右侧的和
        for (int v : grid[0]) {
            upRight += v;
        }
        long downLeft = 0;     // 下方在换行列左侧的和
        long ans = Long.MAX_VALUE;
        for (int j = 0; j < n; j++) {
            upRight -= grid[0][j];
            ans = Math.min(ans, Math.max(upRight, downLeft));
            downLeft += grid[1][j];
        }
        return ans;
    }

    // 解法二:前缀和数组(对照)
    public long gridGamePrefix(int[][] grid) {
        int n = grid[0].length;
        long[] p0 = new long[n + 1];
        long[] p1 = new long[n + 1];
        for (int i = 0; i < n; i++) {
            p0[i + 1] = p0[i] + grid[0][i];
            p1[i + 1] = p1[i] + grid[1][i];
        }
        long ans = Long.MAX_VALUE;
        for (int j = 0; j < n; j++) {
            long upRight = p0[n] - p0[j + 1];   // 上方 j+1..n-1
            long downLeft = p1[j];              // 下方 0..j-1
            ans = Math.min(ans, Math.max(upRight, downLeft));
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.gridGame(new int[][]{{2, 5, 4}, {1, 5, 1}})); // 4
        System.out.println(sol.gridGame(new int[][]{{3, 3, 1}, {8, 5, 2}})); // 4
        System.out.println(sol.gridGame(new int[][]{{1, 3, 1, 15}, {1, 3, 3, 1}})); // 7
        System.out.println(sol.gridGamePrefix(new int[][]{{2, 5, 4}, {1, 5, 1}})); // 4
        System.out.println(sol.gridGamePrefix(new int[][]{{3, 3, 1}, {8, 5, 2}})); // 4
        System.out.println(sol.gridGamePrefix(new int[][]{{1, 3, 1, 15}, {1, 3, 3, 1}})); // 7
    }
}

复杂度分析

复杂度:时间复杂度 O(n),空间复杂度 O(1)。

复杂度:时间复杂度 O(n),空间复杂度 O(n)。


2018. Check if Word Can Be Placed In Crossword(判断单词是否能放入填字游戏内)

题目描述

给你一个 m x n 的矩阵 board,它代表一个填字游戏当前的状态:小写英文字母(已填入的单词)、

表示空格子的 ' ' 和表示障碍格子的 '#'。

如果满足以下条件,那么我们可以水平(从左到右或者从右到左)或竖直(从上到下或者从下到上)填入一个单词:

  • 该单词不占据任何 '#' 对应的格子;
  • 每个字母对应的格子要么是空格要么与 board 中已有字母匹配;
  • 如果单词水平放置,那么该单词左边和右边相邻格子不能为空格或小写英文字母(必须越界或为 '#');
  • 如果单词竖直放置,那么该单词上边和下边相邻格子不能为空格或小写英文字母。
    给定 word,判断 word 是否可以被放入 board 中。

示例 1

复制代码
board = [["#"," ","#"],[" "," ","#"],["#","c"," "]], word = "abc",输出 true(从上往下)。

示例 2

复制代码
board = [[" ","#","a"],[" ","#","c"],[" ","#","a"]], word = "ac",输出 false。

示例 3

复制代码
board = [["#"," ","#"],[" "," ","#"],["#"," ","c"]], word = "ca",输出 true(从右到左)。

解题思路

解法一(主提交版本)

四方向起点检查(主提交版本)

  • 单词要能放下的核心条件:起始格在某个方向上的"前一个格子"必须是边界或 #(不能被夹在中段);
    方向上连续 word.length() 格全部可用(空格或与对应字母匹配),且其后一格是边界或 #。
  • 对每个格子作为起点枚举四个方向(右/左/下/上),满足则返回 true;任一起点失败继续。
    复杂度:时间复杂度 O(m·n·(m+n)),空间复杂度 O(1)。
    解法二
    反转单词复用同向检查(对照)
  • 只检查"向右"与"向下"两个方向,分别用 word 与反转后的 reverse(word) 尝试放置,
    反转正好覆盖"从右到左"与"从下到上"两种情况,避免重复写方向逻辑。
  • 检查函数 canPut(i, j, di, dj, w):从 (i,j) 沿 (di,dj) 放置 w,要求起点前一格(反方向)为界/#,
    且终点后一格也为界/#,中间逐格匹配。
    复杂度:时间复杂度 O(m·n·(m+n)),空间复杂度 O(m·n)(反转串)。

代码实现

java 复制代码
public class Solution {

    // 解法一:四方向起点检查(主提交版本)
    public boolean placeWordInCrossword(char[][] board, String word) {
        int m = board.length, n = board[0].length;
        int k = word.length();
        // 四个方向:右、下、左、上
        int[][] dirs = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                for (int[] d : dirs) {
                    if (canPlace(board, word, i, j, d[0], d[1])) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    private boolean canPlace(char[][] board, String word, int i, int j, int di, int dj) {
        int m = board.length, n = board[0].length;
        int k = word.length();
        // 起点前一格(反方向)必须为边界或 '#'
        int pi = i - di, pj = j - dj;
        if (pi >= 0 && pi < m && pj >= 0 && pj < n && board[pi][pj] != '#') {
            return false;
        }
        // 逐格匹配
        for (int p = 0; p < k; p++) {
            int x = i + di * p, y = j + dj * p;
            if (x < 0 || x >= m || y < 0 || y >= n) {
                return false;
            }
            if (board[x][y] != ' ' && board[x][y] != word.charAt(p)) {
                return false;
            }
        }
        // 终点后一格也必须是边界或 '#'
        int ei = i + di * k, ej = j + dj * k;
        if (ei >= 0 && ei < m && ej >= 0 && ej < n && board[ei][ej] != '#') {
            return false;
        }
        return true;
    }

    // 解法二:仅向右/向下 + 反转单词(对照)
    public boolean placeWordInCrosswordRev(char[][] board, String word) {
        int m = board.length, n = board[0].length;
        String rev = new StringBuilder(word).reverse().toString();
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (canPut(board, i, j, 0, 1, word) || canPut(board, i, j, 1, 0, word)
                        || canPut(board, i, j, 0, 1, rev) || canPut(board, i, j, 1, 0, rev)) {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean canPut(char[][] board, int i, int j, int di, int dj, String w) {
        int m = board.length, n = board[0].length;
        int k = w.length();
        int pi = i - di, pj = j - dj;
        if (pi >= 0 && pi < m && pj >= 0 && pj < n && board[pi][pj] != '#') {
            return false;
        }
        for (int p = 0; p < k; p++) {
            int x = i + di * p, y = j + dj * p;
            if (x < 0 || x >= m || y < 0 || y >= n) {
                return false;
            }
            if (board[x][y] != ' ' && board[x][y] != w.charAt(p)) {
                return false;
            }
        }
        int ei = i + di * k, ej = j + dj * k;
        if (ei >= 0 && ei < m && ej >= 0 && ej < n && board[ei][ej] != '#') {
            return false;
        }
        return true;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        char[][] b1 = {{'#', ' ', '#'}, {' ', ' ', '#'}, {'#', 'c', ' '}};
        System.out.println(sol.placeWordInCrossword(b1, "abc")); // true
        char[][] b2 = {{' ', '#', 'a'}, {' ', '#', 'c'}, {' ', '#', 'a'}};
        System.out.println(sol.placeWordInCrossword(b2, "ac")); // false
        char[][] b3 = {{'#', ' ', '#'}, {' ', ' ', '#'}, {'#', ' ', 'c'}};
        System.out.println(sol.placeWordInCrossword(b3, "ca")); // true
        char[][] b4 = {{' ', ' ', '#'}, {' ', ' ', '#'}, {' ', ' ', '#'}};
        System.out.println(sol.placeWordInCrossword(b4, "abc")); // false(放不下长度 3)
        System.out.println(sol.placeWordInCrosswordRev(b1, "abc")); // true
        System.out.println(sol.placeWordInCrosswordRev(b2, "ac")); // false
        System.out.println(sol.placeWordInCrosswordRev(b3, "ca")); // true
    }
}

复杂度分析

复杂度:时间复杂度 O(m·n·(m+n)),空间复杂度 O(1)。

复杂度:时间复杂度 O(m·n·(m+n)),空间复杂度 O(m·n)(反转串)。


2019. The Score of Students Solving Math Expression(解出数学表达式的学生分数)

题目描述

给你一个字符串 s,它只包含数字 0-9、加法运算符 '+' 和乘法运算符 '*',表示一个合法的只含有个位数数字的数学表达式

(例如 "3+5*2")。有 n 位小学生计算这个表达式,遵循如下运算顺序:按照从左到右的顺序计算乘法,然后按照从左到右的顺序计算加法。

给你长度为 n 的整数数组 answers,表示每位学生提交的答案,按如下规则打分:

  • 如果学生答案等于表达式的正确结果,得 5 分;
  • 否则,如果答案由一处或多处错误的运算顺序计算得到,得 2 分;
  • 否则得 0 分。返回所有学生的分数和。

示例 1

复制代码
s = "7+3*1*2", answers = [20,13,42],输出 7。

示例 2

复制代码
s = "3+5*2", answers = [13,0,10,13,13,16,16],输出 19。

示例 3

复制代码
s = "6+0*1", answers = [12,9,6,4,8,6],输出 10(错误顺序也可能算出 6,仍按正确给 5 分)。

解题思路

解法一(主提交版本)

区间 DP 枚举所有可行结果(主提交版本)

  • 学生只能以"部分先结合"的方式任意加括号,即任意合法的括号化方式都是一种可能的"错误运算顺序"。
    用区间 DP:f[i][j] 表示数字 i...j(含其间运算符)能算出的所有结果的集合。
    枚举分割点 k(运算符位置),把左区间结果与右区间结果做 + 或 * 后并入 f[i][j](值 > 1000 剪掉)。
  • 正确结果 correct 用标准"先乘后加"求值。打分:answer == correct 得 5 分;
    否则若 answer ∈ f[0][m-1](即某种括号化能算出)得 2 分。
    复杂度:区间数 O(m²),每个区间合并 O(m·V²),V ≤ 1000,运算器数 ≤ 15,完全可接受。
    解法二
    正确值标准求值 + 答案集合判定(对照)
  • 单独实现"先乘后加"的标准求值器;再用与解法一相同的区间 DP 求所有错误顺序可达值(抽成独立方法)。
  • 逻辑拆分更直白,两个方法分别对照。
    复杂度:同解法一。

代码实现

java 复制代码
public class Solution {

    // 解法一:区间 DP(主提交版本)
    public int scoreOfStudents(String s, int[] answers) {
        int m = (s.length() + 1) / 2;      // 数字个数
        int correct = evaluate(s);         // 标准"先乘后加"
        @SuppressWarnings("unchecked")
        java.util.Set<Integer>[][] f = new java.util.HashSet[m][m];
        for (int i = 0; i < m; i++) {
            for (int j = i; j < m; j++) {
                f[i][j] = new java.util.HashSet<>();
            }
        }
        for (int i = 0; i < m; i++) {
            f[i][i].add(s.charAt(i * 2) - '0');
        }
        for (int len = 2; len <= m; len++) {
            for (int i = 0; i + len - 1 < m; i++) {
                int j = i + len - 1;
                for (int k = i; k < j; k++) {
                    char op = s.charAt(k * 2 + 1);
                    java.util.Set<Integer> left = f[i][k];
                    java.util.Set<Integer> right = f[k + 1][j];
                    for (int a : left) {
                        for (int b : right) {
                            int v = (op == '+') ? a + b : a * b;
                            if (v <= 1000) {
                                f[i][j].add(v);
                            }
                        }
                    }
                }
            }
        }
        java.util.Set<Integer> wrong = f[0][m - 1];
        int[] cnt = new int[1001];
        for (int a : answers) {
            if (a <= 1000) {
                cnt[a]++;
            }
        }
        int ans = 5 * cnt[correct];
        for (int i = 0; i <= 1000; i++) {
            if (i != correct && wrong.contains(i)) {
                ans += 2 * cnt[i];
            }
        }
        return ans;
    }

    // 标准求值:先从左到右算乘法,再从左到右算加法
    private int evaluate(String s) {
        int n = s.length();
        java.util.List<Integer> vals = new java.util.ArrayList<>();
        java.util.List<Character> ops = new java.util.ArrayList<>();
        vals.add(s.charAt(0) - '0');
        for (int i = 1; i < n; i += 2) {
            char op = s.charAt(i);
            int num = s.charAt(i + 1) - '0';
            if (op == '*') {
                int last = vals.remove(vals.size() - 1);
                vals.add(last * num);
            } else {
                vals.add(num);
            }
            ops.add(op);
        }
        int sum = 0;
        for (int v : vals) {
            sum += v;
        }
        return sum;
    }

    // 解法二:拆分求值器 + 独立区间 DP(对照)
    public int scoreOfStudentsSplit(String s, int[] answers) {
        int m = (s.length() + 1) / 2;
        int correct = evalStandard(s);
        java.util.Set<Integer> all = allResults(s);
        int[] cnt = new int[1001];
        for (int a : answers) {
            if (a <= 1000) {
                cnt[a]++;
            }
        }
        int ans = 5 * cnt[correct];
        for (int i = 0; i <= 1000; i++) {
            if (i != correct && all.contains(i)) {
                ans += 2 * cnt[i];
            }
        }
        return ans;
    }

    private int evalStandard(String s) {
        int m = (s.length() + 1) / 2;
        // 用两个栈模拟:先处理乘法
        java.util.Deque<Integer> st = new java.util.ArrayDeque<>();
        int cur = s.charAt(0) - '0';
        st.push(cur);
        for (int i = 1; i < s.length(); i += 2) {
            char op = s.charAt(i);
            int num = s.charAt(i + 1) - '0';
            if (op == '*') {
                int top = st.pop();
                st.push(top * num);
            } else {
                st.push(num);
            }
        }
        int sum = 0;
        while (!st.isEmpty()) {
            sum += st.pop();
        }
        return sum;
    }

    private java.util.Set<Integer> allResults(String s) {
        int m = (s.length() + 1) / 2;
        @SuppressWarnings("unchecked")
        java.util.Set<Integer>[][] f = new java.util.HashSet[m][m];
        for (int i = 0; i < m; i++) {
            for (int j = i; j < m; j++) {
                f[i][j] = new java.util.HashSet<>();
            }
        }
        for (int i = 0; i < m; i++) {
            f[i][i].add(s.charAt(i * 2) - '0');
        }
        for (int len = 2; len <= m; len++) {
            for (int i = 0; i + len - 1 < m; i++) {
                int j = i + len - 1;
                for (int k = i; k < j; k++) {
                    char op = s.charAt(k * 2 + 1);
                    for (int a : f[i][k]) {
                        for (int b : f[k + 1][j]) {
                            int v = (op == '+') ? a + b : a * b;
                            if (v <= 1000) {
                                f[i][j].add(v);
                            }
                        }
                    }
                }
            }
        }
        return f[0][m - 1];
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.scoreOfStudents("7+3*1*2", new int[]{20, 13, 42})); // 7
        System.out.println(sol.scoreOfStudents("3+5*2", new int[]{13, 0, 10, 13, 13, 16, 16})); // 19
        System.out.println(sol.scoreOfStudents("6+0*1", new int[]{12, 9, 6, 4, 8, 6})); // 10
        System.out.println(sol.scoreOfStudentsSplit("7+3*1*2", new int[]{20, 13, 42})); // 7
        System.out.println(sol.scoreOfStudentsSplit("3+5*2", new int[]{13, 0, 10, 13, 13, 16, 16})); // 19
        System.out.println(sol.scoreOfStudentsSplit("6+0*1", new int[]{12, 9, 6, 4, 8, 6})); // 10
    }
}

复杂度分析

复杂度:区间数 O(m²),每个区间合并 O(m·V²),V ≤ 1000,运算器数 ≤ 15,完全可接受。


2020. Number of Accounts That Did Not Stream(无流量的帐户数)

题目类型:数据库(SQL)

题目描述

表: Subscriptions

复制代码
+-------------+------+
| Column Name | Type |
+-------------+------+
| account_id  | int  |
| start_date  | date |
| end_date    | date |
+-------------+------+

account_id 是此表的主键列。此表的每一行都表示帐户订阅的开始和结束日期。请注意,始终开始日期 < 结束日期。

表: Streams

复制代码
+-------------+------+
| Column Name | Type |
+-------------+------+
| session_id  | int  |
| account_id  | int  |
| stream_date | date |
+-------------+------+

session_id 是该表的主键列。account_id 是订阅表中的外键。此表的每一行都包含与会话相关联的帐户和日期的信息。

编写 SQL 查询以报告在 2021 购买订阅但没有任何会话的账户数。

示例 1:

复制代码
输入:
Subscriptions table:
+------------+------------+------------+
| account_id | start_date | end_date   |
+------------+------------+------------+
| 9          | 2020-02-18 | 2021-10-30 |
| 3          | 2021-09-21 | 2021-11-13 |
| 11         | 2020-02-28 | 2020-08-18 |
| 13         | 2021-04-20 | 2021-09-22 |
| 4          | 2020-10-26 | 2021-05-08 |
| 5          | 2020-09-11 | 2021-01-17 |
+------------+------------+------------+
Streams table:
+------------+------------+-------------+
| session_id | account_id | stream_date |
+------------+------------+-------------+
| 14         | 9          | 2020-05-16  |
| 16         | 3          | 2021-10-27  |
| 18         | 11         | 2020-04-29  |
| 17         | 13         | 2021-08-08  |
| 19         | 4          | 2020-12-31  |
| 13         | 5          | 2021-01-05  |
+------------+------------+-------------+
输出:
+----------------+
| accounts_count |
+----------------+
| 2              |
+----------------+
解释:
用户 4 和 9 在 2021 有订阅但没有会话;用户 11 在 2021 没有订阅。

解题思路

解法一:LEFT JOIN + 空值判断

先筛选出 2021 年有订阅的账户:start_date <= '2021-12-31' AND end_date >= '2021-01-01'(订阅区间与 2021 年有交集)。把这些账户与 Streams 表 LEFT JOIN,统计没有匹配到任何会话的账户数:streams.session_id IS NULL(即该账户无任何流会话记录)。

注意题意是「在 2021 购买订阅但没有任何会话」------会话不要求一定在订阅期内,因此与其用日期区间关联 Streams,不如直接以 account_id 是否在 Streams 中出现过判断。

解法二(对照):NOT EXISTS 反连接

与解法一同条件的订阅账户,用 NOT EXISTS (SELECT 1 FROM Streams s WHERE s.account_id = ...) 排除掉有会话的账户。两种写法等价,NOT EXISTS 语义更直白。

代码实现

sql 复制代码
-- 解法一(推荐):LEFT JOIN 找无匹配会话的账户
WITH active_2021 AS (
    SELECT account_id
    FROM Subscriptions
    WHERE start_date <= '2021-12-31' AND end_date >= '2021-01-01'
)
SELECT COUNT(*) AS accounts_count
FROM active_2021 a
LEFT JOIN Streams s ON a.account_id = s.account_id
WHERE s.session_id IS NULL;
sql 复制代码
-- 解法二(对照):NOT EXISTS 反连接
SELECT COUNT(*) AS accounts_count
FROM Subscriptions sub
WHERE sub.start_date <= '2021-12-31'
  AND sub.end_date >= '2021-01-01'
  AND NOT EXISTS (
      SELECT 1 FROM Streams s WHERE s.account_id = sub.account_id
  );

复杂度分析

  • 时间复杂度 :O(n + m),其中 n 为订阅数、m 为会话数;LEFT JOIN/NOT EXISTS 走主键/外键索引后近线性扫描。
  • 空间复杂度 :O(n),CTE 中间结果与订阅表规模成正比。
相关推荐
用户37215742613510 分钟前
Java 批量提取 Word 文档中的图片并保存到本地
java
SL_staff36 分钟前
BI项目失败率90%?JVS如何用三阶闭环构建可落地的数据生产力平台
java·开源·全栈
nice_lcj5202 小时前
ThreadLocal 入门:它是怎么隔离数据的、为什么会导致内存泄漏
java·开发语言·后端·面试·并发编程·内存泄漏·threadlocal
秋天的怀念_David2 小时前
算法一千零一夜 · 第二十六夜 协议的母语
java·spring boot·算法
灿飞2 小时前
Kotlin与Java虚拟机八:控制流与 when:卫语句、上下文相关解析与穷尽性
java·kotlin
文丑颜不良啊3 小时前
LeetCode2021~2040题目解析
java·leetcode
bksczm3 小时前
InnoDB 行格式详解:DYNAMIC / COMPACT / COMPRESSED / REDUNDANT
java·开发语言·数据库
liangbo74 小时前
Spring系列03:Bean 的三种注册方式
java·spring