LeetCode1901~1920题目解析

LeetCode 1901-1920. 题解合集

文章目录


1901. Find a Peak Element II(寻找峰值 II)

题目描述

一个 2D 网格中的峰值是指那些严格大于其相邻格子(上、下、左、右)的元素。

给你一个从 0 开始编号的 m x n 矩阵 mat,其中任意两个相邻格子的值都不相同。

找出任意一个峰值 mat[i][j] 并返回其位置 [i,j]。

你可以假设整个矩阵周边环绕着一圈值为 -1 的格子。

要求必须写出时间复杂度为 O(m log(n)) 或 O(n log(m)) 的算法。

示例 1

  • 输入 :mat = \[1,4,3,2]
  • 输出:0,1
  • 解释:3 和 4 都是峰值,所以 1,0 和 0,1 都是可接受的答案。

示例 2

  • 输入 :mat = \[10,20,15,21,30,14,7,16,32]
  • 输出:1,1
  • 解释 :30 和 32 都是峰值,所以 1,1 和 2,2 都是可接受的答案。
    提示:
  • - m == mat.length
  • - n == mat[i].length
  • - 1 <= m, n <= 500
  • - 1 <= mat[i][j] <= 10^5
    • 任意两个相邻元素均不相等

解题思路

解法一:按列二分 + 行内取最大,O(m log n)(主提交版本)

对列索引二分。取当前列 mid 中全局最大元素所在行 r,

比较 mat[r][mid] 与其左右邻居:若左邻更大说明峰值一定在左半部分(含 mid-1 列),

若右邻更大说明峰值在右半部分,否则 mat[r][mid] 本身就是峰值。核心是"列最大元素"的单调性:

只要某列的最大值比它右邻(或左邻)更大,该列内必存在一个峰值(沿最大值所在行向两侧攀升)。

解法二:单调爬坡直推,O(mn),对照

从 (0,0) 出发,反复向四周中值更大的相邻格子移动,

直到四周都不比当前大,即找到峰值。任意两个相邻值不同保证不会死循环,在最坏情况下退化为 O(mn),

用作正确性交叉验证的朴素实现。

代码实现

java 复制代码
import java.util.Arrays;

public class Solution {

    public int[] findPeakGrid(int[][] mat) {
        int m = mat.length, n = mat[0].length;
        int lo = 0, hi = n - 1;
        while (lo <= hi) {
            int mid = (lo + hi) >>> 1;
            int r = 0;
            for (int i = 1; i < m; i++) {          // 找第 mid 列的最大值所在行
                if (mat[i][mid] > mat[r][mid]) {
                    r = i;
                }
            }
            // 与左右邻居比较,决定二分方向
            boolean leftBigger = mid > 0 && mat[r][mid - 1] > mat[r][mid];
            boolean rightBigger = mid < n - 1 && mat[r][mid + 1] > mat[r][mid];
            if (!leftBigger && !rightBigger) {      // 当前即是峰值
                return new int[]{r, mid};
            }
            if (rightBigger) {
                lo = mid + 1;                       // 峰值可能在右侧列
            } else {
                hi = mid - 1;                       // 峰值可能在左侧列
            }
        }
        return new int[]{-1, -1};                   // 不会走到
    }

    public int[] findPeakGridWalk(int[][] mat) {
        int m = mat.length, n = mat[0].length;
        int r = 0, c = 0;
        while (true) {
            boolean moved = false;
            if (r > 0 && mat[r - 1][c] > mat[r][c]) { r--; moved = true; }
            else if (r < m - 1 && mat[r + 1][c] > mat[r][c]) { r++; moved = true; }
            else if (c > 0 && mat[r][c - 1] > mat[r][c]) { c--; moved = true; }
            else if (c < n - 1 && mat[r][c + 1] > mat[r][c]) { c++; moved = true; }
            if (!moved) {
                break;
            }
        }
        return new int[]{r, c};
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 用例 1
        int[][] m1 = {{1, 4}, {3, 2}};
        int[] r1 = s.findPeakGrid(m1);
        int[] w1 = s.findPeakGridWalk(m1);
        boolean ok1 = (m1[r1[0]][r1[1]] > (r1[0] > 0 ? m1[r1[0] - 1][r1[1]] : -1)
                && m1[r1[0]][r1[1]] > (r1[0] < m1.length - 1 ? m1[r1[0] + 1][r1[1]] : -1)
                && m1[r1[0]][r1[1]] > (r1[1] > 0 ? m1[r1[0]][r1[1] - 1] : -1)
                && m1[r1[0]][r1[1]] > (r1[1] < m1[0].length - 1 ? m1[r1[0]][r1[1] + 1] : -1));
        // 用例 2
        int[][] m2 = {{10, 20, 15}, {21, 30, 14}, {7, 16, 32}};
        int[] r2 = s.findPeakGrid(m2);
        int[] w2 = s.findPeakGridWalk(m2);
        boolean ok2 = m2[r2[0]][r2[1]] == 30 || m2[r2[0]][r2[1]] == 32;
        boolean ok3 = m2[w2[0]][w2[1]] == 30 || m2[w2[0]][w2[1]] == 32;
        System.out.println((ok1 ? "PASS" : "FAIL") + " case1 => " + Arrays.toString(r1) + " / " + Arrays.toString(w1));
        System.out.println((ok2 && ok3 ? "PASS" : "FAIL") + " case2 => " + Arrays.toString(r2) + " / " + Arrays.toString(w2));
    }
}

复杂度分析


1902. Depth of BST Given Insertion Order(给定二叉搜索树的插入顺序求深度)

题目描述

给定一个从 0 开始索引的整数类型数组 order,其长度为 n,是从 1 到 n 的所有整数的一个排列,

表示插入到一棵二叉搜索树的顺序。

二叉搜索树的定义如下:

  • 一个节点的左子树只包含键值小于该节点键值的节点。
  • 一个节点的右子树只包含键值大于该节点键值的节点。
  • 左子树和右子树须均为二叉搜索树。
    该二叉搜索树的构造方式如下:
  • order[0] 将成为该二叉搜索树的根。
  • 所有后续的元素均在维持二叉搜索树性质的前提下作为任何已存在节点的子节点插入。
    返回该二叉搜索树的深度。一棵二叉树的深度是从根节点到最远叶节点的最长路径所经节点的个数。

示例 1

  • 输入 :order = 2,1,4,3
  • 输出:3
  • 解释:该二叉搜索树的深度为 3,路径为 2->4->3。

示例 2

  • 输入 :order = 2,1,3,4
  • 输出:3
  • 解释:该二叉搜索树的深度为 3,路径为 2->3->4。

示例 3

  • 输入 :order = 1,2,3,4
  • 输出:4
  • 解释 :该二叉搜索树的深度为 4,路径为 1->2->3->4。
    提示:
  • - n == order.length
  • - 1 <= n <= 10^5
    • order 是从 1 到 n 的整数的一个排列

解题思路

解法一:有序集合维护祖先区间,O(n log n)(主提交版本)

新节点插入时,它的父节点一定是当前所有"键值区间

(lo, hi)"中能夹住它的最近祖先中最深的那一个------具体说,用 TreeMap/有序集合记录已插入节点,

对每个新值 x 找到前驱(小于 x 的最大已插入值)L 与后继(大于 x 的最小已插入值)R:

若 L 存在则 x 只能挂到 L 的右子树,若 R 存在则 x 只能挂到 R 的左子树;

父节点取 L、R 中深度更深者(因为最终只可能有一个位置放得下 x)。答案对每个新深度取最大。

解法二:直接模拟建树 + 规则推导深度,对照

朴素地按顺序建立 BST 节点并统计每个节点深度

(父节点深度 + 1),用 HashMap 存每个值的节点与深度。由于排列随机时树高期望 O(log n),

但最坏可能退化为 O(n^2)(严格升序),只作正确性对照。

代码实现

java 复制代码
import java.util.TreeMap;

public class Solution {

    public int maxDepthBST(int[] order) {
        TreeMap<Integer, Integer> depth = new TreeMap<>(); // 值 -> 深度
        int max = 1;
        depth.put(order[0], 1);
        for (int i = 1; i < order.length; i++) {
            int x = order[i];
            Integer L = depth.floorKey(x);   // 小于 x 的最大键
            Integer R = depth.ceilingKey(x); // 大于 x 的最小键
            int d = 1;
            if (L != null) {
                d = Math.max(d, depth.get(L) + 1);
            }
            if (R != null) {
                d = Math.max(d, depth.get(R) + 1);
            }
            depth.put(x, d);
            max = Math.max(max, d);
        }
        return max;
    }

    public int maxDepthBSTSimulate(int[] order) {
        int[] leftChild = new int[order.length + 1];
        int[] rightChild = new int[order.length + 1];
        int root = order[0];
        int max = 1;
        for (int i = 1; i < order.length; i++) {
            int x = order[i];
            int cur = root;
            int d = 1;
            while (true) {
                d++;
                if (x < cur) {
                    if (leftChild[cur] == 0) {
                        leftChild[cur] = x;
                        break;
                    }
                    cur = leftChild[cur];
                } else {
                    if (rightChild[cur] == 0) {
                        rightChild[cur] = x;
                        break;
                    }
                    cur = rightChild[cur];
                }
            }
            max = Math.max(max, d);
        }
        return max;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] cases = {{2, 1, 4, 3}, {2, 1, 3, 4}, {1, 2, 3, 4}};
        int[] exp = {3, 3, 4};
        boolean all = true;
        for (int i = 0; i < cases.length; i++) {
            int a = s.maxDepthBST(cases[i]);
            int b = s.maxDepthBSTSimulate(cases[i]);
            boolean ok = a == exp[i] && b == exp[i];
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1903. Largest Odd Number in String(字符串中的最大奇数)

题目描述

给你一个字符串 num,表示一个大整数。请你在字符串 num 的所有非空子字符串中找出值最大的奇数,

并以字符串形式返回。如果不存在奇数,则返回一个空字符串 ""。

子字符串是字符串中的一个连续的字符序列。

示例 1

  • 输入 :num = "52"
  • 输出:"5"
  • 解释:非空子字符串仅有 "5"、"2" 和 "52"。"5" 是其中唯一的奇数。

示例 2

  • 输入 :num = "4206"
  • 输出:""
  • 解释:在 "4206" 中不存在奇数。

示例 3

  • 输入 :num = "35427"
  • 输出:"35427"
  • 解释 :"35427" 本身就是一个奇数。
    提示:
  • - 1 <= num.length <= 10^5
    • num 仅由数字组成且不含前导零

解题思路

解法一:从右向左找第一个奇数位,O(n)(主提交版本)

一个数是否为奇数只取决于它的最后一位(最低位)是否为

奇数数字 1/3/5/7/9。若最低位是奇数则整个数就是奇数;否则无论如何截断前缀都改变不了末位,

因此从右往左扫描,找到第一个奇数数字位置 i,num[0..i] 即为数值最大的奇数子串(以它为后缀

的所有奇数子串中,前缀越长数值越大,而为了是奇数其末位必须落在该奇数位,故必然取到它)。

若扫描完也没有奇数位,返回空串。

解法二:正向扫描维护最右奇数位,对照

从左往右记录最后一个奇数数字的位置 lastOdd,

扫描完返回 num.substring(0, lastOdd + 1)。两种扫描方向等价,用于交叉验证。

代码实现

java 复制代码
public class Solution {

    public String largestOddNumber(String num) {
        for (int i = num.length() - 1; i >= 0; i--) {
            char c = num.charAt(i);
            if ((c - '0') % 2 == 1) {           // 奇数数字 1/3/5/7/9
                return num.substring(0, i + 1);
            }
        }
        return "";
    }

    public String largestOddNumberForward(String num) {
        int lastOdd = -1;
        for (int i = 0; i < num.length(); i++) {
            if ((num.charAt(i) - '0') % 2 == 1) {
                lastOdd = i;
            }
        }
        return lastOdd == -1 ? "" : num.substring(0, lastOdd + 1);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        String[] cases = {"52", "4206", "35427", "1", "2468", "999"};
        String[] exp = {"5", "", "35427", "1", "", "999"};
        boolean all = true;
        for (int i = 0; i < cases.length; i++) {
            String a = s.largestOddNumber(cases[i]);
            String b = s.largestOddNumberForward(cases[i]);
            boolean ok = a.equals(exp[i]) && b.equals(exp[i]);
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1904. The Number of Full Rounds You Have Played(你完成的完整对局数)

题目描述

一款新的在线电子游戏在近期发布,在该电子游戏中,以刻钟为周期规划若干时长为 15 分钟的游戏对局。

这意味着,在 HH:00、HH:15、HH:30 和 HH:45 将会开始一个新的对局,其中 HH 用一个从 00

到 23 的整数表示。游戏中使用 24 小时制的时钟,所以一天中最早的时间是 00:00,最晚的时间是 23:59。

给你两个字符串 startTime 和 finishTime,均符合 "HH:MM" 格式,分别表示你进入和退出游戏的确切时间,

请计算在整个游戏会话期间,你完成的完整对局的对局数。

  • 例如,如果 startTime = "05:20" 且 finishTime = "05:59",这意味着你仅仅完成从 05:30 到 05:45
    这一个完整对局。而你没有完成从 05:15 到 05:30 的完整对局,因为你是在对局开始后进入的游戏;
    同时,你也没有完成从 05:45 到 06:00 的完整对局,因为你是在对局结束前退出的游戏。
    如果 finishTime 早于 startTime,这表示你玩了个通宵(也就是从 startTime 到午夜,再从午夜到 finishTime)。
    假设你是从 startTime 进入游戏,并在 finishTime 退出游戏,请计算并返回你完成的完整对局的对局数。

示例 1

  • 输入 :startTime = "12:01", finishTime = "12:44"
  • 输出:1
  • 解释:你完成了从 12:15 到 12:30 的一个完整对局。

示例 2

  • 输入 :startTime = "20:00", finishTime = "06:00"
  • 输出:40
  • 解释:你完成了从 20:00 到 00:00 的 16 个完整的对局,以及从 00:00 到 06:00 的 24 个完整的对局。

示例 3

  • 输入 :startTime = "00:00", finishTime = "23:59"
  • 输出:95
  • 解释 :除最后一个小时你只完成了 3 个完整对局外,其余每个小时均完成了 4 场完整对局。
    提示:
    • startTime 和 finishTime 的格式为 HH:MM
  • - 00 <= HH <= 23
  • - 00 <= MM <= 59
    • startTime 和 finishTime 不相等

解题思路

解法一:转化为分钟 + 上取整/下取整对齐刻度,O(1)(主提交版本)

完整对局必须在整刻钟(0/15/30/45 分)开始并在

下一个整刻钟结束。把两个时间都换算成当天分钟数 start、finish:

  • 若跨夜(finish < start)则 finish += 1440。
    你实际能够计入"完整对局"的有效时间区间是 [ceil(start / 15) * 15, floor(finish / 15) * 15]:
    进入时间上取整到下一个开始时刻,退出时间下取整到最近一个结束时刻。答案 =
    (endAlign - startAlign) / 15,若为负则取 0。
    解法二:按刻钟格点计数,对照
    把一天划分成 96 个刻钟槽位,若跨夜则把结束时间 + 1440 后把
    区间整体移到 [start, finish],枚举每个刻钟槽位 [t, t+15) 判断是否完全落在区间内,计数即可。
    该法直观且不易出错,用于交叉验证。

代码实现

java 复制代码
public class Solution {

    public int numberOfRounds(String startTime, String finishTime) {
        int start = toMin(startTime);
        int finish = toMin(finishTime);
        if (finish < start) {
            finish += 1440;                         // 跨夜
        }
        int startAlign = ((start + 14) / 15) * 15;  // 上取整到下一个整刻钟
        int endAlign = (finish / 15) * 15;          // 下取整到最近一个整刻钟
        int ans = (endAlign - startAlign) / 15;
        return Math.max(ans, 0);
    }

    public int numberOfRoundsGrid(String startTime, String finishTime) {
        int start = toMin(startTime);
        int finish = toMin(finishTime);
        if (finish < start) {
            finish += 1440;
        }
        int cnt = 0;
        for (int t = 0; t < 1440; t += 15) {        // 96 个槽位 [t, t+15)
            if (start <= t && t + 15 <= finish) {
                cnt++;
            }
        }
        return cnt;
    }

    private int toMin(String t) {
        int h = Integer.parseInt(t.substring(0, 2));
        int m = Integer.parseInt(t.substring(3, 5));
        return h * 60 + m;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        String[] st = {"12:01", "20:00", "00:00", "00:00", "00:01", "23:59"};
        String[] ft = {"12:44", "06:00", "23:59", "00:00", "00:15", "00:01"};
        int[] exp = {1, 40, 95, 0, 1, 0};
        boolean all = true;
        for (int i = 0; i < st.length; i++) {
            // 注意第 5 个用例 start=00:01 finish=00:15:进入后下一整刻钟是 00:15,而 00:15 正是开始时刻,
            // 槽位 [00:15, 00:30) 完整落入 -> 1;第 6 个用例相等长度 0 -> 0
            // 第 4 个用例 00:00->00:00 长度 0 -> 0
            int customExp = (i == 4) ? 1 : exp[i];
            int a = s.numberOfRounds(st[i], ft[i]);
            int b = s.numberOfRoundsGrid(st[i], ft[i]);
            boolean ok = a == customExp && b == customExp;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b + " (exp " + customExp + ")");
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1905. Count Sub Islands(统计子岛屿)

题目描述

给你两个 m x n 的二进制矩阵 grid1 和 grid2,它们只包含 0(表示水域)和 1(表示陆地)。

一个岛屿是由四个方向(水平或者竖直)上相邻的 1 组成的区域。任何矩阵以外的区域都视为水域。

如果 grid2 的一个岛屿,被 grid1 的一个岛屿完全包含,也就是说 grid2 中该岛屿的每一个格子都

被 grid1 中同一个岛屿完全包含,那么我们称 grid2 中的这个岛屿为子岛屿。

请你返回 grid2 中子岛屿的数目。

示例 1

  • 输入 :grid1 = \[1,1,1,0,0,0,1,1,1,1,0,0,0,0,0,1,0,0,0,0,1,1,0,1,1],
    grid2 = [[1,1,1,0,0],[0,0,1,1,1],[0,1,0,0,0],[1,0,1,1,0],[0,1,0,1,0]]
  • 输出:3
  • 解释 :grid2 中标红的 1 区域是子岛屿,总共有 3 个子岛屿。

示例 2

  • 输入 :grid1 = \[1,0,1,0,1,1,1,1,1,1,0,0,0,0,0,1,1,1,1,1,1,0,1,0,1],
    grid2 = [[0,0,0,0,0],[1,1,1,1,1],[0,1,0,1,0],[0,1,0,1,0],[1,0,0,0,1]]
  • 输出:2
  • 解释 :grid2 中标红的 1 区域是子岛屿,总共有 2 个子岛屿。
    提示:
  • - m == grid1.length == grid2.length
  • - n == grid1[i].length == grid2[i].length
  • - 1 <= m, n <= 500
    • grid1[i][j] 和 grid2[i][j] 都要么是 0 要么是 1

解题思路

解法一:DFS 标记 + 整体判定,O(mn)(主提交版本)

遍历 grid2,遇到陆地 1 时向四方向 DFS 收集该岛屿的全部

格子,途中只要有任何一格在 grid1 中是水域,就标记该岛屿"不合法"。DFS 结束后若岛屿合法则计数 +1,

同时把访问过的格子置 0 避免重复遍历。这是标准的"两图对比 + 洪水填充"写法。

解法二:染色编号 + 集合判定,对照

先对 grid1 每个岛屿做编号染色(DFS 打上岛屿编号),

再遍历 grid2 的每个岛屿,收集它覆盖到的 grid1 岛屿编号集合:若集合大小为 1(整片都落在同一个

grid1 岛屿内部)则该 grid2 岛屿是子岛屿。两种实现思路不同,可互相验证。

代码实现

java 复制代码
public class Solution {

    private int m, n;
    private final int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

    public int countSubIslands(int[][] grid1, int[][] grid2) {
        m = grid1.length;
        n = grid1[0].length;
        int ans = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid2[i][j] == 1) {
                    boolean[] ok = {true};
                    dfs(grid1, grid2, i, j, ok);
                    if (ok[0]) {
                        ans++;
                    }
                }
            }
        }
        return ans;
    }

    private void dfs(int[][] g1, int[][] g2, int i, int j, boolean[] ok) {
        if (i < 0 || i >= m || j < 0 || j >= n || g2[i][j] != 1) {
            return;
        }
        if (g1[i][j] != 1) {
            ok[0] = false;              // 该格在 grid1 是水 -> 整岛非法
        }
        g2[i][j] = 0;                   // 标记已访问
        for (int[] d : dirs) {
            dfs(g1, g2, i + d[0], j + d[1], ok);
        }
    }

    public int countSubIslandsColor(int[][] grid1, int[][] grid2) {
        int rows = grid1.length, cols = grid1[0].length;
        int[][] id = new int[rows][cols];
        int cur = 1;
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (grid1[i][j] == 1 && id[i][j] == 0) {
                    color(grid1, id, i, j, cur);
                    cur++;
                }
            }
        }
        int ans = 0;
        boolean[][] vis = new boolean[rows][cols];
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (grid2[i][j] == 1 && !vis[i][j]) {
                    java.util.Set<Integer> set = new java.util.HashSet<>();
                    boolean[] outside = {false};
                    collect(grid2, id, vis, i, j, set, outside);
                    if (!outside[0] && set.size() == 1) {
                        ans++;
                    }
                }
            }
        }
        return ans;
    }

    private void color(int[][] g1, int[][] id, int i, int j, int c) {
        if (i < 0 || i >= id.length || j < 0 || j >= id[0].length
                || g1[i][j] != 1 || id[i][j] != 0) {
            return;
        }
        id[i][j] = c;
        for (int[] d : dirs) {
            color(g1, id, i + d[0], j + d[1], c);
        }
    }

    private void collect(int[][] g2, int[][] id, boolean[][] vis, int i, int j,
                         java.util.Set<Integer> set, boolean[] outside) {
        if (i < 0 || i >= g2.length || j < 0 || j >= g2[0].length || g2[i][j] != 1 || vis[i][j]) {
            return;
        }
        vis[i][j] = true;
        if (id[i][j] == 0) {
            outside[0] = true;          // 落在 grid1 水域
        } else {
            set.add(id[i][j]);
        }
        for (int[] d : dirs) {
            collect(g2, id, vis, i + d[0], j + d[1], set, outside);
        }
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] g11 = {{1, 1, 1, 0, 0}, {0, 1, 1, 1, 1}, {0, 0, 0, 0, 0}, {1, 0, 0, 0, 0}, {1, 1, 0, 1, 1}};
        int[][] g21 = {{1, 1, 1, 0, 0}, {0, 0, 1, 1, 1}, {0, 1, 0, 0, 0}, {1, 0, 1, 1, 0}, {0, 1, 0, 1, 0}};
        int[][] g12 = {{1, 0, 1, 0, 1}, {1, 1, 1, 1, 1}, {0, 0, 0, 0, 0}, {1, 1, 1, 1, 1}, {1, 0, 1, 0, 1}};
        int[][] g22 = {{0, 0, 0, 0, 0}, {1, 1, 1, 1, 1}, {0, 1, 0, 1, 0}, {0, 1, 0, 1, 0}, {1, 0, 0, 0, 1}};
        // 注意:解法一会原地修改 grid2,因此先复制再调解法一;解法二只读 grid2。
        int r1 = s.countSubIslands(copy(g11), copy(g21));
        int c1 = s.countSubIslandsColor(g11, g21);
        int r2 = s.countSubIslands(copy(g12), copy(g22));
        int c2 = s.countSubIslandsColor(g12, g22);
        System.out.println((r1 == 3 && c1 == 3 ? "PASS" : "FAIL") + " case1 => " + r1 + " / " + c1);
        System.out.println((r2 == 2 && c2 == 2 ? "PASS" : "FAIL") + " case2 => " + r2 + " / " + c2);
    }

    private static int[][] copy(int[][] a) {
        int[][] b = new int[a.length][a[0].length];
        for (int i = 0; i < a.length; i++) {
            System.arraycopy(a[i], 0, b[i], 0, a[i].length);
        }
        return b;
    }
}

复杂度分析


1906. Minimum Absolute Difference Queries(查询差绝对值的最小值)

题目描述

一个数组 a 的差绝对值的最小值定义为:min(|a[i] - a[j]|)(i != j)的最小值。

如果 a 中所有元素都相同,那么差绝对值的最小值为 -1。

比方说,数组 [5,2,3,7,2] 差绝对值的最小值是 |2 - 3| = 1。注意答案不为 0,

因为 a[i] 和 a[j] 必须不相等。

给你一个整数数组 nums 和查询数组 queries,其中 queries[i] = [li, ri]。

对于每个查询 i,计算子数组 nums[li...ri] 中差绝对值的最小值,子数组 nums[li...ri]

包含 nums 数组(下标从 0 开始)中下标在 li 和 ri 之间的所有元素(包含 li 和 ri 在内)。

请你返回 ans 数组,其中 ans[i] 是第 i 个查询的答案。

子数组是一个数组中连续的一段元素。

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

约束(原题补充):

  • 2 <= nums.length <= 10^5
  • 1 <= nums[i] <= 100
  • 1 <= queries.length <= 2 * 10^4
  • 0 <= li <= ri < nums.length

解题思路

解法一:值域前缀和 + 逐查询,O((n + q) * 100)(主提交版本)

nums[i] 取值范围只有 1~100,因此可以统计

每个值在任意前缀的出现次数:pre[v][i+1] 表示 nums[0..i] 中值 v 的出现次数。对每个查询

[l, r],考察值 1..100 中在该区间出现过的那些值(pre[v][r+1] - pre[v][l] > 0),

相邻出现的值之差的最小值即为答案;若区间内只出现一种值,答案为 -1。

解法二:位置列表 + 二分,对照

为每个值维护它在 nums 中出现的位置列表 List[v];

对每个查询二分找出每个值在 [l, r] 内是否出现(lowerBound >= l 且 <= r),

同样收集出现过的值后求相邻差最小值。复杂度 O(q * 100 * log n),思路不同用于交叉验证。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Solution {

    public int[] minDifference(int[] nums, int[][] queries) {
        int n = nums.length;
        int[][] pre = new int[101][n + 1];          // pre[v][i+1] = nums[0..i] 中 v 出现次数
        for (int i = 0; i < n; i++) {
            for (int v = 1; v <= 100; v++) {
                pre[v][i + 1] = pre[v][i];
            }
            pre[nums[i]][i + 1]++;
        }
        int[] ans = new int[queries.length];
        for (int qi = 0; qi < queries.length; qi++) {
            int l = queries[qi][0], r = queries[qi][1];
            int prev = -1;
            int best = Integer.MAX_VALUE;
            for (int v = 1; v <= 100; v++) {
                if (pre[v][r + 1] - pre[v][l] > 0) {
                    if (prev != -1) {
                        best = Math.min(best, v - prev);
                    }
                    prev = v;
                }
            }
            ans[qi] = (best == Integer.MAX_VALUE) ? -1 : best;
        }
        return ans;
    }

    public int[] minDifferenceByPos(int[] nums, int[][] queries) {
        List<Integer>[] pos = new ArrayList[101];
        for (int v = 1; v <= 100; v++) {
            pos[v] = new ArrayList<>();
        }
        for (int i = 0; i < nums.length; i++) {
            pos[nums[i]].add(i);
        }
        int[] ans = new int[queries.length];
        for (int qi = 0; qi < queries.length; qi++) {
            int l = queries[qi][0], r = queries[qi][1];
            int prev = -1, best = Integer.MAX_VALUE;
            for (int v = 1; v <= 100; v++) {
                List<Integer> list = pos[v];
                int idx = lowerBound(list, l);
                if (idx < list.size() && list.get(idx) <= r) {
                    if (prev != -1) {
                        best = Math.min(best, v - prev);
                    }
                    prev = v;
                }
            }
            ans[qi] = (best == Integer.MAX_VALUE) ? -1 : best;
        }
        return ans;
    }

    private int lowerBound(List<Integer> list, int key) {
        int lo = 0, hi = list.size();
        while (lo < hi) {
            int mid = (lo + hi) >>> 1;
            if (list.get(mid) < key) {
                lo = mid + 1;
            } else {
                hi = mid;
            }
        }
        return lo;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 用例 1
        int[] nums1 = {1, 3, 4, 8};
        int[][] q1 = {{0, 1}, {1, 2}, {2, 3}, {0, 3}};
        int[] e1 = {2, 1, 4, 1};
        int[] r1 = s.minDifference(nums1, q1);
        int[] b1 = s.minDifferenceByPos(nums1, q1);
        // 用例 2
        int[] nums2 = {4, 5, 2, 2, 7, 10};
        int[][] q2 = {{2, 3}, {0, 2}, {0, 5}, {3, 5}};
        int[] e2 = {-1, 1, 1, 3};
        int[] r2 = s.minDifference(nums2, q2);
        int[] b2 = s.minDifferenceByPos(nums2, q2);
        boolean ok = Arrays.equals(r1, e1) && Arrays.equals(b1, e1)
                && Arrays.equals(r2, e2) && Arrays.equals(b2, e2);
        System.out.println((ok ? "PASS" : "FAIL") + " case1 => " + Arrays.toString(r1));
        System.out.println((ok ? "PASS" : "FAIL") + " case2 => " + Arrays.toString(r2));
        System.out.println(ok ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1907. Count Salary Categories(按分类统计薪水)

题目类型:数据库(SQL)

题目描述

表:Accounts

复制代码
+-------------+------+
| 列名        | 类型  |
+-------------+------+
| account_id  | int  |
| income      | int  |
+-------------+------+

在 SQL 中,account_id 是这个表的主键。每一行都包含一个银行帐户的月收入信息。

查询每个工资类别的银行账户数量。工资类别如下:

  • "Low Salary":所有工资严格低于 20000 美元。
  • "Average Salary":包含 在 [$20000, $50000] 范围内的所有工资。
  • "High Salary":所有工资严格大于 50000 美元。

结果表必须 包含所有三个类别,如果某个类别中没有帐户则报告 0。

示例 1:

复制代码
输入:
Accounts 表:
+------------+--------+
| account_id | income |
+------------+--------+
| 3          | 108939 |
| 2          | 12747  |
| 8          | 87709  |
| 6          | 91796  |
+------------+--------+
输出:
+----------------+----------------+
| category       | accounts_count |
+----------------+----------------+
| Low Salary     | 1              |
| Average Salary | 0              |
| High Salary    | 3              |
+----------------+----------------+

解题思路

三个类别分别查询并 UNION ALL 拼接:每个子查询固定类别名的常量列,再用 WHERE 条件统计各区间人数;

这样即使某类别没有账户也会输出 0 行。

代码实现

sql 复制代码
SELECT 'Low Salary' AS category, COUNT(*) AS accounts_count FROM Accounts WHERE income < 20000
UNION ALL
SELECT 'Average Salary', COUNT(*) FROM Accounts WHERE income BETWEEN 20000 AND 50000
UNION ALL
SELECT 'High Salary', COUNT(*) FROM Accounts WHERE income > 50000;

复杂度分析

本题为数据库查询题,不涉及传统算法的时间/空间复杂度;考察重点为 JOIN / 窗口函数 / 自连接 / 聚合过滤等 SQL 语法与查询逻辑的正确性,执行效率由数据库优化器决定。


1908. Game of Nim(Nim 游戏 II)

题目描述

Alice 和 Bob 交替进行一个游戏,由 Alice 先手。

在游戏中,共有 n 堆石头。在每个玩家的回合中,玩家需要选择任一非空石头堆,从中移除任意非零数量的石头。

如果不能移除任意的石头,就输掉游戏,同时另一人获胜。

给定一个整数数组 piles,piles[i] 为第 i 堆石头的数量,如果 Alice 能获胜返回 true,反之返回 false。

Alice 和 Bob 都会采取最优策略。

示例 1

  • 输入 :piles = 1
  • 输出 :true
  • 解释 :第一回合 Alice 移除唯一一堆中 1 块石头,Bob 无石可移,Alice 获胜。

示例 2

  • 输入 :piles = 1,1
  • 输出 :false
  • 解释 :可以证明,Bob 一定能获胜。无论 Alice 怎么取,Bob 取完剩下的一堆即可。

示例 3

  • 输入 :piles = 1,2,3
  • 输出 :false
  • 解释 :可以证明,Bob 一定能获胜。
    提示:
  • - n == piles.length
  • - 1 <= n <= 7
  • - 1 <= piles[i] <= 7
  • 进阶:你能想出一个线性时间的解决方案吗?虽然这一答案可能超出了面试所需的范围,但了解它可能会很有趣。

解题思路

解法一:Nim 和 == 异或为 0 判定,O(n),主提交版本

这是经典 Nim 游戏。设有 n 堆石头,数量为

a1, a2, ..., an。先手必胜当且仅当各堆数量的异或和(Nim-sum)不为 0:a1 ^ a2 ^ ... ^ an != 0。

原因:若异或和为 0(P-position),无论当前玩家怎么走都会把局面送给对方一个非 0 异或和;

若异或和不为 0(N-position),当前玩家总能找到一堆并从中取走适当数量使异或和变为 0,从而把

P-position 留给对手。最终全 0 局面异或和为 0 是必败局,所以先手只要初始异或和不为 0 必胜。

解法二:记忆化博弈搜索,对照

状态是 n 堆石头的数量向量(每堆 <= 7,共 7^7 种仍可枚举),

用位压缩/编码做记忆化:当前玩家存在任意一个转移使得对手面临必败态,则当前为必胜态。

该法不依赖 Nim 理论,直观地验证异或判定。

代码实现

java 复制代码
import java.util.HashMap;
import java.util.Map;

public class Solution {

    public boolean nimGame(int[] piles) {
        int xor = 0;
        for (int p : piles) {
            xor ^= p;
        }
        return xor != 0;
    }

    public boolean nimGameMemo(int[] piles) {
        Map<Long, Boolean> memo = new HashMap<>();
        return canWin(piles, memo);
    }

    private boolean canWin(int[] piles, Map<Long, Boolean> memo) {
        long key = encode(piles);
        Boolean cached = memo.get(key);
        if (cached != null) {
            return cached;
        }
        boolean win = false;
        for (int i = 0; i < piles.length && !win; i++) {
            if (piles[i] == 0) {
                continue;
            }
            for (int take = 1; take <= piles[i]; take++) {
                piles[i] -= take;
                if (!canWin(piles, memo)) {         // 对手必败 -> 我方必胜
                    win = true;
                }
                piles[i] += take;
                if (win) {
                    break;
                }
            }
        }
        memo.put(key, win);
        return win;
    }

    private long encode(int[] piles) {
        long code = 0;
        for (int p : piles) {
            code = code * 8 + p;                    // 每堆 0..7 -> 3 bit 足够,此处用基数 8
        }
        return code;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] cases = {{1}, {1, 1}, {1, 2, 3}, {2, 2}, {1, 2, 4}, {7, 7, 7}};
        boolean[] exp = {true, false, false, false, true, false};
        boolean all = true;
        for (int i = 0; i < cases.length; i++) {
            boolean a = s.nimGame(cases[i]);
            boolean b = s.nimGameMemo(cases[i]);
            boolean ok = a == exp[i] && b == exp[i];
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1909. Remove One Element to Make the Array Strictly Increasing(删除一个元素使数组严格递增)

题目描述

给你一个下标从 0 开始的整数数组 nums,如果恰好删除一个元素后,数组严格递增,那么请你返回 true,

否则返回 false。如果数组本身已经是严格递增的,请你也返回 true。

数组 nums 是严格递增的定义为:对于任意下标的 1 <= i < nums.length 都满足 nums[i - 1] < nums[i]。

示例 1

  • 输入 :nums = 1,2,10,5,7
  • 输出 :true
  • 解释 :从 nums 中删除下标 2 处的 10,得到 1,2,5,7。1,2,5,7 是严格递增的,所以返回 true。

示例 2

  • 输入 :nums = 2,3,1,2
  • 输出 :false
  • 解释 :3,1,2 是删除下标 0 处元素后得到的结果;2,1,2 是删除下标 1 处元素后得到的结果;
    2,3,2 是删除下标 2 处元素后得到的结果;2,3,1 是删除下标 3 处元素后得到的结果。
    没有任何结果数组是严格递增的,所以返回 false。

示例 3

  • 输入 :nums = 1,1,1
  • 输出 :false
  • 解释 :删除任意元素后的结果都是 1,1。1,1 不是严格递增的,所以返回 false。

示例 4

  • 输入 :nums = 1,2,3
  • 输出 :true
  • 解释 :1,2,3 已经是严格递增的,所以返回 true。
    提示:
  • - 2 <= nums.length <= 1000
  • - 1 <= nums[i] <= 1000

解题思路

解法一:首次逆序处二选一删除 + 全局校验,O(n),主提交版本

找到第一个违反严格递增的位置 i

(nums[i-1] >= nums[i])。只需尝试两种删除:删 nums[i-1] 或删 nums[i],再做一次全数组

严格递增检查即可。因为逆序只能由这两个元素之一引起(更早的位置都已严格递增),删除其中之一后

若还违反则说明需要删多于一个元素。

解法二:暴力逐位删除验证,O(n^2),对照

枚举删除每个下标 k,构造新数组并检查是否严格递增,

找到任意一个可行删除则返回 true。直观但慢,用于交叉验证解法一。

代码实现

java 复制代码
public class Solution {

    public boolean canBeIncreasing(int[] nums) {
        int n = nums.length;
        int bad = -1;
        for (int i = 1; i < n; i++) {
            if (nums[i - 1] >= nums[i]) {
                bad = i;
                break;
            }
        }
        if (bad == -1) {                // 原本就严格递增
            return true;
        }
        // 尝试删除 nums[bad-1]
        if (isStrict(nums, bad - 1)) {
            return true;
        }
        // 尝试删除 nums[bad]
        return isStrict(nums, bad);
    }

    private boolean isStrict(int[] nums, int skip) {
        int prev = Integer.MIN_VALUE;
        for (int i = 0; i < nums.length; i++) {
            if (i == skip) {
                continue;
            }
            if (nums[i] <= prev) {
                return false;
            }
            prev = nums[i];
        }
        return true;
    }

    public boolean canBeIncreasingBrute(int[] nums) {
        int n = nums.length;
        for (int k = 0; k < n; k++) {
            boolean ok = true;
            int prev = Integer.MIN_VALUE;
            for (int i = 0; i < n; i++) {
                if (i == k) {
                    continue;
                }
                if (nums[i] <= prev) {
                    ok = false;
                    break;
                }
                prev = nums[i];
            }
            if (ok) {
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] cases = {{1, 2, 10, 5, 7}, {2, 3, 1, 2}, {1, 1, 1}, {1, 2, 3}, {1, 2, 3, 3, 4}, {1, 1}};
        boolean[] exp = {true, false, false, true, true, false};
        boolean all = true;
        for (int i = 0; i < cases.length; i++) {
            boolean a = s.canBeIncreasing(cases[i]);
            boolean b = s.canBeIncreasingBrute(cases[i]);
            boolean ok = a == exp[i] && b == exp[i];
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1910. Remove All Occurrences of a Substring(删除一个字符串中所有出现的给定子字符串)

题目描述

给你两个字符串 s 和 part,请你对 s 反复执行以下操作直到所有子字符串 part 都被删除:

  • 找到 s 中最左边的子字符串 part,并将它从 s 中删除。
    请你返回从 s 中删除所有 part 子字符串以后得到的剩余字符串。
    一个子字符串是一个字符串中连续的字符序列。

示例 1

  • 输入 :s = "daabcbaabcbc", part = "abc"
  • 输出 :"dab"
  • 解释:以下操作按顺序执行:
  • s = "daabcbaabcbc",删除下标从 2 开始的 "abc",得到 s = "dabaabcbc"。
  • s = "dabaabcbc",删除下标从 4 开始的 "abc",得到 s = "dababc"。
  • s = "dababc",删除下标从 3 开始的 "abc",得到 s = "dab"。
    此时 s 中不再含有子字符串 "abc"。

示例 2

  • 输入 :s = "axxxxyyyyb", part = "xy"
  • 输出 :"ab"
  • 解释:以下操作按顺序执行:
  • s = "axxxxyyyyb",删除下标从 4 开始的 "xy",得到 s = "axxxyyyb"。
  • s = "axxxyyyb",删除下标从 3 开始的 "xy",得到 s = "axxyyb"。
  • s = "axxyyb",删除下标从 2 开始的 "xy",得到 s = "axyb"。
  • s = "axyb",删除下标从 1 开始的 "xy",得到 s = "ab"。
    此时 s 中不再含有子字符串 "xy"。
    提示:
  • - 1 <= s.length <= 1000
  • - 1 <= part.length <= 1000
    • s 和 part 只包含小写英文字母

解题思路

解法一:栈式字符串构建 + 尾部匹配,O(n * m),主提交版本

用一个可变的字符容器(StringBuilder)

从左到右读入字符,每读入一个字符后检查容器末尾是否恰好与 part 相同:相同则弹出这段(相当于删除

一次最左边出现的 part)。因为"最左边的出现"在从左构建的过程中等价于"一旦末尾匹配就立即删除",

反复删除直到不再匹配。注意删除后末尾可能与 part 形成新的匹配,因此每步都要检查末尾。

解法二:String.replaceFirst 模拟,对照

循环调用 s.replaceFirst(part, "") 直到不再包含 part。

每次删除的是最左边的 part,与原题意完全一致,最坏复杂度 O(n * m * 次数),仅作交叉验证。

代码实现

java 复制代码
public class Solution {

    public String removeOccurrences(String s, String part) {
        StringBuilder sb = new StringBuilder();
        int m = part.length();
        for (char c : s.toCharArray()) {
            sb.append(c);
            // 检查末尾是否匹配 part
            int len = sb.length();
            if (len >= m && sb.substring(len - m, len).equals(part)) {
                sb.setLength(len - m);              // 弹出匹配段
            }
        }
        return sb.toString();
    }

    public String removeOccurrencesReplace(String s, String part) {
        String cur = s;
        while (cur.contains(part)) {
            cur = cur.replaceFirst(java.util.regex.Pattern.quote(part), "");
        }
        return cur;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        String[] cs = {"daabcbaabcbc", "axxxxyyyyb", "abc", "ab", "cccttt", "aa"};
        String[] cp = {"abc", "xy", "abc", "a", "cc", "a"};
        String[] exp = {"dab", "ab", "", "b", "t", ""};
        boolean all = true;
        for (int i = 0; i < cs.length; i++) {
            String a = s.removeOccurrences(cs[i], cp[i]);
            String b = s.removeOccurrencesReplace(cs[i], cp[i]);
            boolean ok = a.equals(exp[i]) && b.equals(exp[i]);
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1911. Maximum Alternating Subsequence Sum(最大交替子序列和)

题目描述

一个下标从 0 开始的数组的交替和定义为偶数下标处元素之和减去奇数下标处元素之和。

比方说,数组 [4,2,5,3] 的交替和为 (4 + 5) - (2 + 3) = 4。

给你一个数组 nums,请你返回 nums 中任意子序列的最大交替和(子序列的下标重新从 0 开始编号)。

一个数组的子序列是从原数组中删除一些元素后(也可能一个也不删除)剩余元素不改变顺序组成的数组。

示例 1

  • 输入 :nums = 4,2,5,3
  • 输出:7
  • 解释:最优子序列为 4,2,5,交替和为 (4 + 5) - 2 = 7。

示例 2

  • 输入 :nums = 5,6,7,8
  • 输出:8
  • 解释:最优子序列为 8,交替和为 8。

示例 3

  • 输入 :nums = 6,2,1,2,4,5
  • 输出:10
  • 解释 :最优子序列为 6,1,5,交替和为 (6 + 5) - 1 = 10。
    提示:
  • - 1 <= nums.length <= 10^5
  • - 1 <= nums[i] <= 10^5

解题思路

解法一:动态规划,O(n),主提交版本

设 even 表示"当前已选子序列长度为偶数个元素(最后一个

位置是偶数下标,即该位做加法)时的最大交替和",odd 表示"已选奇数个元素(最后一位做减法)时

的最大交替和"。初始 even = 0(一个都不选),odd = -inf(无法处于奇数状态)。

遍历每个 x:

  • 新 even = max(even, odd + x) (在奇数状态后追加一个做加法的元素)
  • 新 odd = max(odd, even - x) (在偶数状态后追加一个做减法的元素)
    最终答案是 even(以加法收尾才可能最大)。注意 x > 0 保证 even 始终能从 0 出发增大。
    解法二:等价状态压缩变量,对照
    形式化为 dp0 = max(dp0, dp1 - x)、dp1 = max(dp1, dp0 + x)
    的顺序需小心,本解法采用两个独立数组 dpEven/dpOdd 记录到每位为止的最优值,再滚动实现,
    与解法一思路一致但写法更直白,便于交叉验证。

代码实现

java 复制代码
public class Solution {

    public long maxAlternatingSum(int[] nums) {
        long even = 0;          // 最后一个位置是加法位(已选偶数个元素)
        long odd = 0;           // 最后一个位置是减法位(已选奇数个元素);从 0 出发,
                                // odd + x 表示让 x 成为子序列首个元素(奇数状态本身为"空前缀")
        for (int x : nums) {
            long newEven = Math.max(even, odd + x);
            long newOdd = Math.max(odd, even - x);
            even = newEven;
            odd = newOdd;
        }
        return even;
    }

    public long maxAlternatingSumArray(int[] nums) {
        int n = nums.length;
        long[] even = new long[n + 1];   // 前 i 个元素,以加法位结尾的最大值
        long[] odd = new long[n + 1];    // 前 i 个元素,以减法位结尾的最大值
        for (int i = 1; i <= n; i++) {
            int x = nums[i - 1];
            even[i] = Math.max(even[i - 1], odd[i - 1] + x);
            odd[i] = Math.max(odd[i - 1], even[i - 1] - x);
        }
        return even[n];
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] cases = {{4, 2, 5, 3}, {5, 6, 7, 8}, {6, 2, 1, 2, 4, 5}, {1}, {1, 2, 3, 4, 5}};
        long[] exp = {7, 8, 10, 1, 5};
        boolean all = true;
        for (int i = 0; i < cases.length; i++) {
            long a = s.maxAlternatingSum(cases[i]);
            long b = s.maxAlternatingSumArray(cases[i]);
            boolean ok = a == exp[i] && b == exp[i];
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1912. Design Movie Rental System(设计电影租借系统)

题目描述

你有一个电影租借公司和 n 个电影商店。你想要实现一个电影租借系统,它支持查询、预订和返还电影的操作。

同时系统还能生成一份当前被借出电影的报告。

所有电影用二维整数数组 entries 表示,其中 entries[i] = [shopi, moviei, pricei] 表示商店 shopi

有一份电影 moviei 的拷贝,租借价格为 pricei。每个商店至多有一份编号为 moviei 的电影拷贝。

系统需要支持以下操作:

  • Search:找到拥有指定电影且未借出的商店中最便宜的 5 个。商店需要按照价格升序排序,如果价格相同,
    则 shopi 较小的商店排在前面。如果查询结果少于 5 个商店,则将它们全部返回。如果查询结果没有任何商店,
    则返回空列表。
  • Rent:从指定商店借出指定电影,题目保证指定电影在指定商店未借出。
  • Drop:在指定商店返还之前已借出的指定电影。
  • Report:返回最便宜的 5 部已借出电影(可能有重复的电影 ID),将结果用二维列表 res 返回,
    其中 res[j] = [shopj, moviej] 表示第 j 便宜的已借出电影是从商店 shopj 借出的电影 moviej。
    res 中的电影需要按价格升序排序;如果价格相同,则 shopj 较小的排在前面;如果仍然相同,
    则 moviej 较小的排在前面。如果当前借出的电影小于 5 部,则将它们全部返回。如果当前没有借出电影,
    则返回一个空的列表。
    请你实现 MovieRentingSystem 类:
  • MovieRentingSystem(int n, int[][] entries) 将 MovieRentingSystem 对象用 n 个商店和 entries
    表示的电影列表初始化。
  • List<Integer> search(int movie) 如上所述,返回未借出指定 movie 的商店列表。
  • void rent(int shop, int movie) 从指定商店 shop 借出指定电影 movie。
  • void drop(int shop, int movie) 在指定商店 shop 返还之前借出的电影 movie。
  • List<List<Integer>> report() 如上所述,返回最便宜的已借出电影列表。

示例 1

  • 输入 :

    "MovieRentingSystem", "`search`", "rent", "rent", "report", "drop", "`search`"

    \[3, \[\[0, 1, 5\], \[0, 2, 6\], \[0, 3, 7\], \[1, 1, 4\], \[1, 2, 7\], \[2, 1, 5\]\]\], \[1\], \[0, 1\], \[1, 2\], \[\], \[1, 2\], \[2\]

  • 输出 :`null`, \[1, 0, 2, null, null, \[0, 1, 1, 2], null, 0, 1]
  • 解释 :search(1) 返回 1, 0, 2(商店 1 最便宜,商店 0 和 2 价格相同按编号排序);
    rent(0,1) 后商店 0 未借出电影编号为 2,3;rent(1,2) 后商店 1 未借出的电影编号为 1;
    report() 返回 \[0,1,1,2];drop(1,2) 返还;search(2) 返回 0,1。
    提示:
  • - 1 <= n <= 3 * 10^4
  • - 1 <= entries.length <= 10^5
  • - 0 <= shopi < n
  • - 1 <= moviei, pricei <= 10^4
    • 每个商店至多有一份电影 moviei 的拷贝
    • search、rent、drop 和 report 的调用总共不超过 10^5 次

解题思路

解法一:哈希表 + 有序集合 + 堆,主提交版本

  • price:Map<(shop, movie), price> 记录每份拷贝的价格。
  • movieShops:Map<movie, TreeSet>(ShopRec = (price, shop)),维护每个电影所有
    未借出拷贝,按 (price, shop) 升序,search 时取前 5 个。
  • rented:TreeSet(MovieRec = (price, shop, movie)),维护所有已借出电影,
    report 时取前 5 个。
  • rent 时从 movieShops 中移除该 (price, shop)(惰性删除也可以,但这里用确定性移除),把
    (price, shop, movie) 加入 rented;drop 做相反操作。
    复杂度:search/report O(5 log n),rent/drop O(log n)。总调用数 10^5 完全可承受。
    解法二:按序遍历全量扫描,对照
    不维护有序集合,search 时遍历该电影所有未借出商店记录排序取前 5,
    report 时遍历全部已借出记录排序取前 5。rent/drop O(1) 更新状态,查询 O(k log k)。直观但慢,
    用于交叉验证解法一。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;

public class Solution {

    static class MovieRentingSystem {
        private static class ShopRec implements Comparable<ShopRec> {
            int price, shop;
            ShopRec(int p, int s) { price = p; shop = s; }
            public int compareTo(ShopRec o) {
                if (price != o.price) return price - o.price;
                return shop - o.shop;
            }
        }

        private static class MovieRec implements Comparable<MovieRec> {
            int price, shop, movie;
            MovieRec(int p, int s, int m) { price = p; shop = s; movie = m; }
            public int compareTo(MovieRec o) {
                if (price != o.price) return price - o.price;
                if (shop != o.shop) return shop - o.shop;
                return movie - o.movie;
            }
        }

        private Map<String, Integer> price = new HashMap<>();
        private Map<Integer, TreeSet<ShopRec>> movieShops = new HashMap<>(); // movie -> 未借出
        private TreeSet<MovieRec> rented = new TreeSet<>();                   // 全部已借出

        public MovieRentingSystem(int n, int[][] entries) {
            for (int[] e : entries) {
                int s = e[0], m = e[1], p = e[2];
                price.put(s + "," + m, p);
                movieShops.computeIfAbsent(m, k -> new TreeSet<>()).add(new ShopRec(p, s));
            }
        }

        public List<Integer> search(int movie) {
            List<Integer> res = new ArrayList<>();
            TreeSet<ShopRec> set = movieShops.get(movie);
            if (set == null) {
                return res;
            }
            for (ShopRec r : set) {
                res.add(r.shop);
                if (res.size() == 5) {
                    break;
                }
            }
            return res;
        }

        public void rent(int shop, int movie) {
            int p = price.get(shop + "," + movie);
            movieShops.get(movie).remove(new ShopRec(p, shop));
            rented.add(new MovieRec(p, shop, movie));
        }

        public void drop(int shop, int movie) {
            int p = price.get(shop + "," + movie);
            rented.remove(new MovieRec(p, shop, movie));
            movieShops.get(movie).add(new ShopRec(p, shop));
        }

        public List<List<Integer>> report() {
            List<List<Integer>> res = new ArrayList<>();
            for (MovieRec r : rented) {
                List<Integer> pair = new ArrayList<>();
                pair.add(r.shop);
                pair.add(r.movie);
                res.add(pair);
                if (res.size() == 5) {
                    break;
                }
            }
            return res;
        }
    }

    static class MovieRentingSystemScan {
        private Map<String, Integer> price = new HashMap<>();
        private Map<Integer, List<Integer>> movieShops = new HashMap<>(); // movie -> [shop, price] x N 未借出
        private List<int[]> rented = new ArrayList<>();                    // [price, shop, movie]

        public MovieRentingSystemScan(int n, int[][] entries) {
            for (int[] e : entries) {
                price.put(e[0] + "," + e[1], e[2]);
                movieShops.computeIfAbsent(e[1], k -> new ArrayList<>()).add(e[2] * 100000 + e[0]);
            }
        }

        public List<Integer> search(int movie) {
            List<Integer> list = movieShops.getOrDefault(movie, new ArrayList<>());
            List<Integer> res = new ArrayList<>();
            list.stream().sorted().limit(5).forEach(code -> res.add(code % 100000));
            return res;
        }

        public void rent(int shop, int movie) {
            int p = price.get(shop + "," + movie);
            movieShops.get(movie).remove(Integer.valueOf(p * 100000 + shop));
            rented.add(new int[]{p, shop, movie});
        }

        public void drop(int shop, int movie) {
            int p = price.get(shop + "," + movie);
            for (int i = 0; i < rented.size(); i++) {
                int[] r = rented.get(i);
                if (r[1] == shop && r[2] == movie) {
                    rented.remove(i);
                    break;
                }
            }
            movieShops.get(movie).add(p * 100000 + shop);
        }

        public List<List<Integer>> report() {
            List<int[]> sorted = new ArrayList<>(rented);
            sorted.sort((a, b) -> a[0] != b[0] ? a[0] - b[0] : (a[1] != b[1] ? a[1] - b[1] : a[2] - b[2]));
            List<List<Integer>> res = new ArrayList<>();
            for (int i = 0; i < Math.min(5, sorted.size()); i++) {
                List<Integer> pair = new ArrayList<>();
                pair.add(sorted.get(i)[1]);
                pair.add(sorted.get(i)[2]);
                res.add(pair);
            }
            return res;
        }
    }

    public static void main(String[] args) {
        // 官方案例驱动两种实现
        MovieRentingSystem a = new MovieRentingSystem(3, new int[][]{{0, 1, 5}, {0, 2, 6}, {0, 3, 7}, {1, 1, 4}, {1, 2, 7}, {2, 1, 5}});
        MovieRentingSystemScan b = new MovieRentingSystemScan(3, new int[][]{{0, 1, 5}, {0, 2, 6}, {0, 3, 7}, {1, 1, 4}, {1, 2, 7}, {2, 1, 5}});
        boolean ok = true;
        ok &= a.search(1).equals(java.util.Arrays.asList(1, 0, 2)) && b.search(1).equals(java.util.Arrays.asList(1, 0, 2));
        a.rent(0, 1); b.rent(0, 1);
        a.rent(1, 2); b.rent(1, 2);
        ok &= a.report().equals(b.report()) && a.report().toString().equals("[[0, 1], [1, 2]]");
        a.drop(1, 2); b.drop(1, 2);
        ok &= a.search(2).equals(java.util.Arrays.asList(0, 1)) && b.search(2).equals(java.util.Arrays.asList(0, 1));
        System.out.println((ok ? "PASS" : "FAIL") + " official case => search1=" + a.search(1) + " report=" + a.report() + " search2=" + a.search(2));
        System.out.println(ok ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析

复杂度:search/report O(5 log n),rent/drop O(log n)。总调用数 10^5 完全可承受。


1913. Maximum Product Difference Between Two Pairs(两个数对之间的最大乘积差)

题目描述

两个数对 (a, b) 和 (c, d) 之间的乘积差定义为 (a * b) - (c * d)。

例如,(5, 6) 和 (2, 7) 之间的乘积差是 (5 * 6) - (2 * 7) = 16。

给你一个整数数组 nums,选出四个不同的下标 w、x、y 和 z,使数对 (nums[w], nums[x])

和 (nums[y], nums[z]) 之间的乘积差取到最大值。

返回以这种方式取得的乘积差中的最大值。

示例 1

  • 输入 :nums = 5,6,2,7,4
  • 输出:34
  • 解释 :可以选出下标为 1 和 3 的元素构成第一个数对 (6, 7) 以及下标 2 和 4 构成第二个数对 (2, 4),
    乘积差是 (6 * 7) - (2 * 4) = 34。

示例 2

  • 输入 :nums = 4,2,5,9,7,4,8
  • 输出:64
  • 解释 :可以选出下标为 3 和 6 的元素构成第一个数对 (9, 8) 以及下标 1 和 5 构成第二个数对 (2, 4),
    乘积差是 (9 * 8) - (2 * 4) = 64。
    提示:
  • - 4 <= nums.length <= 10^4
  • - 1 <= nums[i] <= 10^4

解题思路

解法一:排序取最大二与最小二,O(n log n),主提交版本

乘积差 (a*b) - (c*d) 要最大,

应让第一个数对取数组中最大的两个数,第二个数对取最小的两个数(都是正整数时乘积单调)。

排序后答案 = nums[n-1] * nums[n-2] - nums[0] * nums[1]。

解法二:单次扫描维护四大最值,O(n),对照

线性扫描维护最大两个值 max1 >= max2 与

最小两个值 min1 <= min2,最后返回 max1 * max2 - min1 * min2,与解法一结果一致,

但不依赖排序,用于交叉验证。

代码实现

java 复制代码
import java.util.Arrays;

public class Solution {

    public int maxProductDifference(int[] nums) {
        Arrays.sort(nums);
        int n = nums.length;
        return nums[n - 1] * nums[n - 2] - nums[0] * nums[1];
    }

    public int maxProductDifferenceLinear(int[] nums) {
        int max1 = Integer.MIN_VALUE, max2 = Integer.MIN_VALUE;
        int min1 = Integer.MAX_VALUE, min2 = Integer.MAX_VALUE;
        for (int x : nums) {
            if (x > max1) {
                max2 = max1;
                max1 = x;
            } else if (x > max2) {
                max2 = x;
            }
            if (x < min1) {
                min2 = min1;
                min1 = x;
            } else if (x < min2) {
                min2 = x;
            }
        }
        return max1 * max2 - min1 * min2;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] cases = {{5, 6, 2, 7, 4}, {4, 2, 5, 9, 7, 4, 8}, {1, 1, 1, 1}, {10, 3, 9, 2}};
        int[] exp = {34, 64, 0, 84};
        boolean all = true;
        for (int i = 0; i < cases.length; i++) {
            int a = s.maxProductDifference(cases[i]);
            int b = s.maxProductDifferenceLinear(cases[i]);
            boolean ok = a == exp[i] && b == exp[i];
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1914. Cyclically Rotating a Grid(循环轮转矩阵)

题目描述

给你一个大小为 m x n 的整数矩阵 grid,其中 m 和 n 都是偶数;另给你一个整数 k。

矩阵由若干层组成(最外层一圈、次外层一圈......)。矩阵的循环轮转是通过分别循环轮转矩阵中的每一层

完成的。在对某一层进行一次循环旋转操作时,层中的每一个元素将会取代其逆时针方向的相邻元素。

返回执行 k 次循环轮转操作后的矩阵。

示例 1

  • 输入 :grid = \[40,10,30,20], k = 1
  • 输出:\[10,20,40,30]
  • 解释:矩阵在执行循环轮转操作时每一步的状态见官方题图。

示例 2

  • 输入 :grid = \[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16], k = 2
  • 输出:\[3,4,8,12,2,11,10,16,1,7,6,15,5,9,13,14]
  • 解释 :矩阵在执行循环轮转操作时每一步的状态见官方题图。
    提示:
  • - m == grid.length
  • - n == grid[i].length
  • - 2 <= m, n <= 50
    • m 和 n 都是偶数
  • - 1 <= m * n <= 2500
  • - 1 <= k <= 10^9
  • - 0 <= grid[i][j] <= 5000

解题思路

解法一:逐层拆环 + 旋转 k mod len,O(mn),主提交版本

对每一层(从外到内共 min(m,n)/2 层),

把该层按顺时针顺序(上边左→右、右边上→下、下边右→左、左边下→上)展开成一维数组 ring,

再整体逆时针旋转 k 个位置(等价于顺移 k,即新位置 = (i + k) % len),然后按相同顺序写回。

每层独立处理,len 为该层周长。k 很大时先取模,避免无谓移动。

解法二:逐层模拟 k 次,对照

对每层执行恰好 k % len 次"顺时针相邻取代"的单步模拟,

每次先用临时变量串行更新一圈(上边右移一位、右边下移一位、下边左移一位、左边上移一位)。

思路直观但常数大(每层每步 O(len)),仅用于交叉验证。

代码实现

java 复制代码
public class Solution {

    public int[][] rotateGrid(int[][] grid, int k) {
        int m = grid.length, n = grid[0].length;
        int[][] res = new int[m][n];
        int layers = Math.min(m, n) / 2;
        for (int L = 0; L < layers; L++) {
            // 顺时针展开当前层
            int rows = m - 2 * L, cols = n - 2 * L;
            int len = 2 * (rows + cols) - 4;
            int[] ring = new int[len];
            int idx = 0;
            int top = L, bottom = m - 1 - L, left = L, right = n - 1 - L;
            for (int c = left; c <= right; c++) ring[idx++] = grid[top][c];
            for (int r = top + 1; r <= bottom; r++) ring[idx++] = grid[r][right];
            for (int c = right - 1; c >= left; c--) ring[idx++] = grid[bottom][c];
            for (int r = bottom - 1; r > top; r--) ring[idx++] = grid[r][left];
            // 逆时针旋转 k 位:元素沿顺时针序列前进 k 个位置
            // 即新环[i] = 旧环[(i + k) % len]
            int[] rotated = new int[len];
            for (int i = 0; i < len; i++) {
                rotated[i] = ring[(i + k % len) % len];
            }
            // 写回
            idx = 0;
            for (int c = left; c <= right; c++) res[top][c] = rotated[idx++];
            for (int r = top + 1; r <= bottom; r++) res[r][right] = rotated[idx++];
            for (int c = right - 1; c >= left; c--) res[bottom][c] = rotated[idx++];
            for (int r = bottom - 1; r > top; r--) res[r][left] = rotated[idx++];
        }
        return res;
    }

    public int[][] rotateGridSim(int[][] grid, int k) {
        int m = grid.length, n = grid[0].length;
        int[][] g = new int[m][n];
        for (int i = 0; i < m; i++) {
            System.arraycopy(grid[i], 0, g[i], 0, n);
        }
        int layers = Math.min(m, n) / 2;
        for (int L = 0; L < layers; L++) {
            int rows = m - 2 * L, cols = n - 2 * L;
            int len = 2 * (rows + cols) - 4;
            int steps = k % len;
            int top = L, bottom = m - 1 - L, left = L, right = n - 1 - L;
            for (int st = 0; st < steps; st++) {
                // 逆时针旋转一圈:上边右移、右边下移、下边左移、左边上移
                int save = g[top][left];
                for (int c = left; c < right; c++) g[top][c] = g[top][c + 1];
                for (int r = top; r < bottom; r++) g[r][right] = g[r + 1][right];
                for (int c = right; c > left; c--) g[bottom][c] = g[bottom][c - 1];
                for (int r = bottom; r > top; r--) g[r][left] = g[r - 1][left];
                g[top + 1][left] = save;
            }
        }
        return g;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] g1 = {{40, 10}, {30, 20}};
        int[][] e1 = {{10, 20}, {40, 30}};
        int[][] r1 = s.rotateGrid(g1, 1);
        int[][] r1s = s.rotateGridSim(g1, 1);
        boolean ok1 = eq(r1, e1) && eq(r1s, e1);
        int[][] g2 = {{1, 2, 3, 4}, {5, 6, 7, 8}, {9, 10, 11, 12}, {13, 14, 15, 16}};
        int[][] e2 = {{3, 4, 8, 12}, {2, 11, 10, 16}, {1, 7, 6, 15}, {5, 9, 13, 14}};
        int[][] r2 = s.rotateGrid(g2, 2);
        int[][] r2s = s.rotateGridSim(g2, 2);
        boolean ok2 = eq(r2, e2) && eq(r2s, e2);
        System.out.println((ok1 ? "PASS" : "FAIL") + " case1 => " + str(r1));
        System.out.println((ok2 ? "PASS" : "FAIL") + " case2 => " + str(r2));
        System.out.println((ok1 && ok2) ? "ALL PASS" : "SOME FAIL");
    }

    private static boolean eq(int[][] a, int[][] b) {
        for (int i = 0; i < a.length; i++) {
            for (int j = 0; j < a[0].length; j++) {
                if (a[i][j] != b[i][j]) return false;
            }
        }
        return true;
    }

    private static String str(int[][] a) {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < a.length; i++) {
            sb.append(java.util.Arrays.toString(a[i]));
            if (i < a.length - 1) sb.append(",");
        }
        return sb.append("]").toString();
    }
}

复杂度分析


1915. Number of Wonderful Substrings(最美子字符串的数目)

题目描述

如果某个字符串中至多一个字母出现奇数次,则称其为最美字符串。

例如,"ccjjc" 和 "abab" 都是最美字符串,但 "ab" 不是。

给你一个字符串 word,该字符串由前十个小写英文字母组成('a' 到 'j')。

请你返回 word 中最美非空子字符串的数目。如果同样的子字符串在 word 中出现多次,

那么应当对每次出现分别计数。子字符串是字符串中的一个连续字符序列。

示例 1

  • 输入 :word = "aba"
  • 输出:4
  • 解释 :4 个最美子字符串如下所示:"a"、"b"、"a"、"aba"。

示例 2

  • 输入 :word = "aabb"
  • 输出:9
  • 解释 :9 个最美子字符串如下所示:"a"、"aa"、"aab"、"aabb"、"a"、"abb"、"b"、"bb"、"b"。

示例 3

  • 输入 :word = "he"
  • 输出:2
  • 解释 :2 个最美子字符串如下所示:"h"、"e"。
    提示:
  • - 1 <= word.length <= 10^5
    • word 由从 'a' 到 'j' 的小写英文字母组成

解题思路

解法一:前缀奇偶掩码 + 计数哈希,O(n * 10),主提交版本

用 10 位二进制掩码 mask 表示

前缀中各字母奇偶性(第 i 位 = 该字母出现次数 mod 2)。子串 [l, r] 的美态掩码 =

pre[r+1] XOR pre[l]。要求该掩码至多一位为 1,即等于 0(偶偶)或形如 1 << i。

因此遍历前缀时维护 count[preMask],每次累加 count[preMask](差为 0 的最美子串)与

count[preMask ^ (1<<i)](差恰好一位为 1 的最美子串)。注意先查询后加入自身避免计数错误。

解法二:枚举起点扩展累计,对照

对每个起点 l 向右扩展,实时维护掩码并判断是否最美,

O(n^2) 在小数据上用于交叉验证。

代码实现

java 复制代码
import java.util.HashMap;
import java.util.Map;

public class Solution {

    public long wonderfulSubstrings(String word) {
        Map<Integer, Integer> cnt = new HashMap<>();
        cnt.put(0, 1);                              // 空前缀
        int mask = 0;
        long ans = 0;
        for (char c : word.toCharArray()) {
            mask ^= (1 << (c - 'a'));
            // 1) 与某个前缀掩码相同 -> 差为 0 位 -> 偶数个奇数字母
            ans += cnt.getOrDefault(mask, 0);
            // 2) 与某个前缀掩码只差 1 位 -> 恰好一个奇数字母
            for (int i = 0; i < 10; i++) {
                ans += cnt.getOrDefault(mask ^ (1 << i), 0);
            }
            cnt.put(mask, cnt.getOrDefault(mask, 0) + 1);
        }
        return ans;
    }

    public long wonderfulSubstringsBrute(String word) {
        int n = word.length();
        long ans = 0;
        for (int l = 0; l < n; l++) {
            int mask = 0;
            for (int r = l; r < n; r++) {
                mask ^= (1 << (word.charAt(r) - 'a'));
                if (Integer.bitCount(mask) <= 1) {
                    ans++;
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        String[] cases = {"aba", "aabb", "he", "a", "aaaa"};
        long[] exp = {4, 9, 2, 1, 10};
        boolean all = true;
        for (int i = 0; i < cases.length; i++) {
            long a = s.wonderfulSubstrings(cases[i]);
            long b = s.wonderfulSubstringsBrute(cases[i]);
            boolean ok = a == exp[i] && b == exp[i];
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1916. Count Ways to Build Rooms in an Ant Colony(统计为蚁群构筑房间的不同顺序)

题目描述

你是一只蚂蚁,负责为蚁群构筑 n 间编号从 0 到 n-1 的新房间。给你一个下标从 0 开始且长度为

n 的整数数组 prevRoom 作为扩建计划。其中,prevRoom[i] 表示在构筑房间 i 之前,你必须先构筑

房间 prevRoom[i],并且这两个房间必须直接相连。房间 0 已经构筑完成,所以 prevRoom[0] = -1。

扩建计划中还有一条硬性要求,在完成所有房间的构筑之后,从房间 0 可以访问到每个房间。

你一次只能构筑一个房间。你可以在已经构筑好的房间之间自由穿行,只要这些房间是相连的。

如果房间 prevRoom[i] 已经构筑完成,那么你就可以构筑房间 i。

返回你构筑所有房间的不同顺序的数目。由于答案可能很大,请返回对 10^9 + 7 取余的结果。

示例 1

  • 输入 :prevRoom = -1,0,1
  • 输出:1
  • 解释:仅有一种方案可以完成所有房间的构筑:0 → 1 → 2。

示例 2

  • 输入 :prevRoom = -1,0,0,1,2
  • 输出:6
  • 解释 :有 6 种不同顺序:0→1→3→2→4;0→2→4→1→3;0→1→2→3→4;0→1→2→4→3;0→2→1→3→4;0→2→1→4→3。
    提示:
  • - n == prevRoom.length
  • - 2 <= n <= 10^5
  • - prevRoom[0] == -1
    • 对于所有的 1 <= i < n,都有 0 <= prevRoom[i] < n
    • 题目保证所有房间都构筑完成后,从房间 0 可以访问到每个房间

解题思路

解法一:树形 DP + 组合数,O(n),主提交版本

把依赖关系看成有根树(0 为根,边 prevRoom[i] -> i),

于是合法的构筑顺序恰是以 0 为根的所有"拓扑序"(孩子之间顺序任意,但每个孩子子树内部必须保持其

相对拓扑序)。对节点 u,记 sz[u] 为子树大小,f[u] 为子树内合法的构筑序列数。合并若干孩子

子树时:除 u 自身外共 sz[u] - 1 个位置,把每个孩子子树作为一个整体做"交错排列"------

f[u] = C(sz[u]-1, sz[c1]) * C(sz[u]-1-sz[c1], sz[c2]) * ... * f[c1] * f[c2] * ...。

即多项式插空组合:f[u] = (prod f[c]) * (sz[u]-1)! / (prod sz[c]!)。用预处理的阶乘与逆元

(费马小定理求逆)O(1) 计算组合数,一次后序遍历求解。

解法二:暴力枚举全排列校验,对照,仅小数据

n 很小时枚举 0..n-1 的全排列,检查每个排列是否

满足"每个房间出现时其 prevRoom 都已出现",计数即可,用于交叉验证解法一。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.List;

public class Solution {

    private static final int MOD = 1_000_000_007;
    private long[] fact, invFact;

    public int waysToBuildRooms(int[] prevRoom) {
        int n = prevRoom.length;
        List<Integer>[] children = new ArrayList[n];
        for (int i = 0; i < n; i++) {
            children[i] = new ArrayList<>();
        }
        for (int i = 1; i < n; i++) {
            children[prevRoom[i]].add(i);
        }
        // 预计算阶乘与逆元
        fact = new long[n + 1];
        invFact = new long[n + 1];
        fact[0] = 1;
        for (int i = 1; i <= n; i++) {
            fact[i] = fact[i - 1] * i % MOD;
        }
        invFact[n] = modPow(fact[n], MOD - 2);
        for (int i = n; i >= 1; i--) {
            invFact[i - 1] = invFact[i] * i % MOD;
        }
        long[] f = new long[n];
        int[] sz = new int[n];
        dfs(0, children, f, sz);
        return (int) f[0];
    }

    private void dfs(int u, List<Integer>[] children, long[] f, int[] sz) {
        sz[u] = 1;
        long ways = 1;
        for (int v : children[u]) {
            dfs(v, children, f, sz);
            sz[u] += sz[v];
            ways = ways * f[v] % MOD;
        }
        // f[u] = ways * (sz[u]-1)! / prod(sz[v]!)
        long prodInv = 1;
        for (int v : children[u]) {
            prodInv = prodInv * invFact[sz[v]] % MOD;
        }
        f[u] = ways * fact[sz[u] - 1] % MOD * prodInv % MOD;
    }

    private long modPow(long a, long e) {
        long r = 1;
        while (e > 0) {
            if ((e & 1) == 1) {
                r = r * a % MOD;
            }
            a = a * a % MOD;
            e >>= 1;
        }
        return r;
    }

    public int waysToBuildRoomsBrute(int[] prevRoom) {
        int n = prevRoom.length;
        // 0 号房间已建成,只需排列 1..n-1 的构筑顺序
        int[] perm = new int[n - 1];
        for (int i = 0; i < n - 1; i++) {
            perm[i] = i + 1;
        }
        int[] cnt = {0};
        permute(perm, 0, prevRoom, cnt);
        return cnt[0];
    }

    private void permute(int[] perm, int idx, int[] prevRoom, int[] cnt) {
        int n = perm.length;
        if (idx == n) {
            boolean[] built = new boolean[prevRoom.length];
            built[0] = true;
            for (int x : perm) {
                if (!built[prevRoom[x]]) {
                    return;
                }
                built[x] = true;
            }
            cnt[0] = (cnt[0] + 1) % MOD;
            return;
        }
        for (int i = idx; i < n; i++) {
            swap(perm, idx, i);
            permute(perm, idx + 1, prevRoom, cnt);
            swap(perm, idx, i);
        }
    }

    private void swap(int[] a, int i, int j) {
        int t = a[i]; a[i] = a[j]; a[j] = t;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] cases = {{-1, 0, 1}, {-1, 0, 0, 1, 2}, {-1, 0, 0, 0, 0}};
        int[] exp = {1, 6, 24};
        boolean all = true;
        for (int i = 0; i < cases.length; i++) {
            int a = s.waysToBuildRooms(cases[i]);
            int b = s.waysToBuildRoomsBrute(cases[i]);
            boolean ok = a == exp[i] && b == exp[i];
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1917. Leetcodify Friends Recommendations(Leetcodify 好友推荐)

题目类型:数据库(SQL)

题目描述

表:Listens

复制代码
+-------------+---------+
| Column Name | Type    |
+-------------+---------+
| user_id     | int     |
| song_id     | int     |
| day         | date    |
+-------------+---------+

这个表没有主键,可能存在重复项。每一行表示用户 user_id 在 day 这一天收听的歌曲 song_id。

表:Friendship

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| user1_id      | int     |
| user2_id      | int     |
+---------------+---------+

(user1_id, user2_id) 是这个表的主键,且 user1_id < user2_id。

写出 SQL 语句,为 Leetcodify 用户推荐好友。将符合条件的用户 x 推荐给用户 y:

  • 用户 x 和 y 不是好友,且
  • 用户 x 和 y 在同一天收听了相同的三首或更多不同歌曲。

好友推荐是单向 的:如果 x 和 y 需互相推荐,结果表需要两行(x 推荐 y、y 推荐 x)。结果表不得出现重复项。

示例 1:

复制代码
输入:
Listens 表:                                   Friendship 表:
+---------+---------+------------+   +----------+----------+
| user_id | song_id | day        |   | user1_id | user2_id |
+---------+---------+------------+   +----------+----------+
| 1       | 10      | 2021-03-15 |   | 1        | 2        |
| 1       | 11      | 2021-03-15 |   +----------+----------+
| 1       | 12      | 2021-03-15 |
| 2       | 10      | 2021-03-15 |
| 2       | 11      | 2021-03-15 |
| 2       | 12      | 2021-03-15 |
| 3       | 10      | 2021-03-15 |
| 3       | 11      | 2021-03-15 |
| 3       | 12      | 2021-03-15 |
| 4       | 10      | 2021-03-15 |
| 4       | 11      | 2021-03-15 |
| 4       | 13      | 2021-03-15 |
| 5       | 10      | 2021-03-16 |
| 5       | 11      | 2021-03-16 |
| 5       | 12      | 2021-03-16 |
+---------+---------+------------+
输出:
+---------+----------------+
| user_id | recommended_id |
+---------+----------------+
| 1       | 3              |
| 2       | 3              |
| 3       | 1              |
| 3       | 2              |
+---------+----------------+

解释:用户 1/2/3 在同一天听了歌曲 10、11、12;1 和 2 已是好友不推荐,其余互相推荐。

解题思路

① 对 Listens 去重(DISTINCT)避免重复行造成误判;

② 自连接按"同一天、同一首歌、不同用户"配对,分组统计共同歌曲数 >= 3 的 (x, y) 对;

③ LEFT JOIN 友谊表排除已是好友的对(注意友谊表存的是 user1_id < user2_id)。

代码实现

sql 复制代码
WITH songs AS (
    SELECT DISTINCT user_id, song_id, day FROM Listens
),
pairs AS (
    SELECT a.user_id AS x, b.user_id AS y
    FROM songs a
    JOIN songs b
      ON a.day = b.day
     AND a.song_id = b.song_id
     AND a.user_id != b.user_id
    GROUP BY a.user_id, b.user_id, a.day
    HAVING COUNT(*) >= 3
)
SELECT p.x AS user_id, p.y AS recommended_id
FROM pairs p
LEFT JOIN Friendship f
  ON f.user1_id = p.x AND f.user2_id = p.y
WHERE f.user1_id IS NULL;

复杂度分析

本题为数据库查询题,不涉及传统算法的时间/空间复杂度;考察重点为 JOIN / 窗口函数 / 自连接 / 聚合过滤等 SQL 语法与查询逻辑的正确性,执行效率由数据库优化器决定。


1918. Kth Smallest Subarray Sum(第 K 小的子数组和)

题目描述

给你一个长度为 n 的整型数组 nums 和一个数值 k,返回第 k 小的子数组和。

子数组是指数组中一个非空且不间断的子序列。子数组和则指子数组中所有元素的和。

示例 1

  • 输入 :nums = 2,1,3, k = 4
  • 输出:3
  • 解释 :2,1,3 的子数组为:2 和为 2;1 和为 1;3 和为 3;2,1 和为 3;1,3 和为 4;2,1,3 和为 6。
    最小子数组和的升序排序为 1, 2, 3, 3, 4, 6。第 4 小的子数组和为 3。

示例 2

  • 输入 :nums = 3,3,5,5, k = 7
  • 输出:10
  • 解释 :3,3,5,5 的子数组和为:3, 3, 5, 5, 6, 8, 10, 11, 13, 16。
    最小子数组和的升序排序为 3, 3, 5, 5, 6, 8, 10, 11, 13, 16。第 7 小的子数组和为 10。
    提示:
  • - n == nums.length
  • - 1 <= n <= 2 * 10^4
  • - 1 <= nums[i] <= 5 * 10^4
    • 1 <= k <= n * (n + 1) / 2

解题思路

解法一:二分答案 + 滑窗计数,O(n log S),主提交版本

子数组和均为非负(numsi > 0),

因此对于阈值 mid,可用双指针(滑动窗口)统计"和 <= mid 的子数组个数":

枚举右端点 r,维护满足 sum(l..r) <= mid 的最小左端点 l,那么以 r 结尾且和 <= mid 的子数组

有 r - l + 1 个。二分查找最小的 mid 使得"和 <= mid 的子数组数 >= k",该 mid 即第 k 小和。

注意子数组"和相同"时二分找到的是首次使计数 >= k 的位置,正是第 k 小的值。

解法二:暴力枚举全部子数组和排序,对照

生成所有 n(n+1)/2 个子数组和并排序后取第 k 个,

O(n^2 log n),仅用于小规模交叉验证。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Solution {

    public int kthSmallestSubarraySum(int[] nums, int k) {
        int lo = 1, hi = 0;
        for (int x : nums) {
            hi += x;
        }
        while (lo < hi) {
            int mid = lo + (hi - lo) / 2;
            if (countLe(nums, mid) >= k) {
                hi = mid;
            } else {
                lo = mid + 1;
            }
        }
        return lo;
    }

    // 统计"子数组和 <= limit"的子数组数量(双指针滑窗)
    private long countLe(int[] nums, int limit) {
        long cnt = 0;
        long sum = 0;
        int l = 0;
        for (int r = 0; r < nums.length; r++) {
            sum += nums[r];
            while (sum > limit) {
                sum -= nums[l];
                l++;
            }
            cnt += r - l + 1;   // 以 r 结尾、和 <= limit 的子数组数
        }
        return cnt;
    }

    public int kthSmallestSubarraySumBrute(int[] nums, int k) {
        int n = nums.length;
        List<Integer> sums = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            int s = 0;
            for (int j = i; j < n; j++) {
                s += nums[j];
                sums.add(s);
            }
        }
        Collections.sort(sums);
        return sums.get(k - 1);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[] n1 = {2, 1, 3};
        int[] n2 = {3, 3, 5, 5};
        int[] n3 = {1};
        boolean ok = true;
        int a1 = s.kthSmallestSubarraySum(n1, 4);
        int b1 = s.kthSmallestSubarraySumBrute(n1, 4);
        ok &= a1 == 3 && b1 == 3;
        int a2 = s.kthSmallestSubarraySum(n2, 7);
        int b2 = s.kthSmallestSubarraySumBrute(n2, 7);
        ok &= a2 == 10 && b2 == 10;
        int a3 = s.kthSmallestSubarraySum(n3, 1);
        int b3 = s.kthSmallestSubarraySumBrute(n3, 1);
        ok &= a3 == 1 && b3 == 1;
        System.out.println((ok ? "PASS" : "FAIL") + " case1 => " + a1 + " / " + b1);
        System.out.println((ok ? "PASS" : "FAIL") + " case2 => " + a2 + " / " + b2);
        System.out.println((ok ? "PASS" : "FAIL") + " case3 => " + a3 + " / " + b3);
        System.out.println(ok ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1919. Leetcodify Similar Friends(兴趣相同的朋友)

题目类型:数据库(SQL)

题目描述

表:Listens

复制代码
+-------------+---------+
| Column Name | Type    |
+-------------+---------+
| user_id     | int     |
| song_id     | int     |
| day         | date    |
+-------------+---------+

该表没有主键,因此会存在重复的行。每一行表示用户 user_id 在某天 day 听了某首歌曲 song_id。

表:Friendship

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| user1_id      | int     |
| user2_id      | int     |
+---------------+---------+

(user1_id, user2_id) 是该表的主键,且 user1_id < user2_id。

请写一段 SQL 查询获取到兴趣相同的朋友。用户 x 和用户 y 是兴趣相同的朋友,需满足:它们是朋友,且在同一天内听过相同的歌曲且数量大于等于三首。结果表格式与源数据表相同(user1_id < user2_id)。

示例 1:

复制代码
输入:
Listens table:            Friendship table:
+---------+---------+------------+   +----------+----------+
| user_id | song_id | day        |   | user1_id | user2_id |
+---------+---------+------------+   +----------+----------+
| 1       | 10      | 2021-03-15 |   | 1        | 2        |
| 1       | 11      | 2021-03-15 |   | 2        | 4        |
| 1       | 12      | 2021-03-15 |   | 2        | 5        |
| 2       | 10      | 2021-03-15 |   +----------+----------+
| 2       | 11      | 2021-03-15 |
| 2       | 12      | 2021-03-15 |
| 3       | 10      | 2021-03-15 |
| 3       | 11      | 2021-03-15 |
| 3       | 12      | 2021-03-15 |
| 4       | 10      | 2021-03-15 |
| 4       | 11      | 2021-03-15 |
| 4       | 13      | 2021-03-15 |
| 5       | 10      | 2021-03-16 |
| 5       | 11      | 2021-03-16 |
| 5       | 12      | 2021-03-16 |
+---------+---------+------------+
输出:
+----------+----------+
| user1_id | user2_id |
+----------+----------+
| 1        | 2        |
+----------+----------+

解释:用户 1 和 2 是朋友且同一天听了 10、11、12 共 3 首相同歌曲,是兴趣相同的朋友。

解题思路

以 Friendship 为主表(保证 user1_id < user2_id 且双方确实是朋友),

连接两张去重后的 Listens(a 对应用户 1、b 对应用户 2),条件为同一天同一首歌,

按朋友对分组统计 COUNT(*) >= 3。

代码实现

sql 复制代码
WITH songs AS (
    SELECT DISTINCT user_id, song_id, day FROM Listens
)
SELECT f.user1_id, f.user2_id
FROM Friendship f
JOIN songs a ON f.user1_id = a.user_id
JOIN songs b ON f.user2_id = b.user_id
            AND a.day = b.day
            AND a.song_id = b.song_id
GROUP BY f.user1_id, f.user2_id, a.day
HAVING COUNT(*) >= 3;

复杂度分析

本题为数据库查询题,不涉及传统算法的时间/空间复杂度;考察重点为 JOIN / 窗口函数 / 自连接 / 聚合过滤等 SQL 语法与查询逻辑的正确性,执行效率由数据库优化器决定。


1920. Build Array from Permutation(基于排列构建数组)

题目描述

给你一个从 0 开始的排列 nums(下标也从 0 开始)。请你构建一个同样长度的数组 ans,其中,

对于每个 i(0 <= i < nums.length),都满足 ans[i] = nums[nums[i]]。返回构建好的数组 ans。

从 0 开始的排列 nums 是一个由 0 到 nums.length - 1(0 和 nums.length - 1 也包含在内)

的不同整数组成的数组。

示例 1

  • 输入 :nums = 0,2,1,5,3,4
  • 输出:0,1,2,4,5,3
  • 解释 :ans = `nums`\[`nums[0]`, nums`nums[1]`, ..., nums`nums[5]`] = 0,1,2,4,5,3。

示例 2

  • 输入 :nums = 5,0,1,2,3,4
  • 输出:4,5,0,1,2,3
  • 解释 :ans = `nums`\[`nums[0]`, nums`nums[1]`, ..., nums`nums[5]`] = 4,5,0,1,2,3。
    提示:
  • - 1 <= nums.length <= 1000
  • - 0 <= nums[i] < nums.length
    • nums 中的元素互不相同
  • 进阶:你能在不使用额外空间的情况下解决此问题吗(即 O(1) 内存)?

解题思路

解法一:直接构造,O(n) 空间,主提交版本

按题意 ans[i] = nums[nums[i]] 直接遍历构造新数组。

解法二:原地 O(1) 内存,对照进阶要求

利用值域只有 n(即 numsi < n = nums.length),

把每个位置编码为 nums[i] = nums[i] + n * (nums[nums[i]] % n),即高位存最终值、低位存原值;

最后整体除以 n 得到结果。由于 nums 是 0...n-1 的排列,n * x + v 中 n 足够大不会产生混淆,

这一技巧在恢复时先取 % n 还原原值再取 / n 得到新值。直接原地修改后最后一步还原即可。

代码实现

java 复制代码
public class Solution {

    public int[] buildArray(int[] nums) {
        int n = nums.length;
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) {
            ans[i] = nums[nums[i]];
        }
        return ans;
    }

    public int[] buildArrayInPlace(int[] nums) {
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            nums[i] += n * (nums[nums[i]] % n);
        }
        for (int i = 0; i < n; i++) {
            nums[i] /= n;
        }
        return nums;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[] c1 = {0, 2, 1, 5, 3, 4};
        int[] e1 = {0, 1, 2, 4, 5, 3};
        int[] c2 = {5, 0, 1, 2, 3, 4};
        int[] e2 = {4, 5, 0, 1, 2, 3};
        java.util.Arrays.equals(s.buildArray(c1), e1);
        boolean ok = java.util.Arrays.equals(s.buildArray(c1), e1)
                && java.util.Arrays.equals(s.buildArrayInPlace(c1.clone()), e1)
                && java.util.Arrays.equals(s.buildArray(c2), e2)
                && java.util.Arrays.equals(s.buildArrayInPlace(c2.clone()), e2);
        System.out.println((ok ? "PASS" : "FAIL") + " case1 => " + java.util.Arrays.toString(s.buildArray(c1)));
        System.out.println((ok ? "PASS" : "FAIL") + " case2 => " + java.util.Arrays.toString(s.buildArray(c2)));
        System.out.println(ok ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析

相关推荐
Pniubi34 分钟前
力扣3无重复字符的最长字串(滑动窗口)
算法·leetcode·职场和发展
tealcwu36 分钟前
【Unity基础】为什么游戏需要物理按键映射?从 Unity Input Manager 到跨引擎输入设计
java·游戏·unity
vipxieliang1 小时前
Spring Boot 性能优化实战:从启动加速到运行时调优
java·spring boot
AI全栈学习助手1 小时前
告别“转圈等待”:Spring Boot + SSE 让大模型像 ChatGPT 一样流式输出
java·人工智能·spring boot·后端·aigc
天天被压力1 小时前
【A股分钟级高频数据完全手册 #02】Python调用股票1分钟数据接口:Pro版最新分时取数与字段解析
java·人工智能·python
代码不停1 小时前
Spring Aop
java·数据库·spring
道尔柯南1 小时前
C++ STL 入门详解:从认识标准模板库到掌握学习方法
java·c++·学习方法
杨凯凡1 小时前
Spring Boot 4 升级为什么卡在 Jackson 3?包名、配置与兼容迁移指南
java·spring boot 4·技术升级·jackson 3·spring framework
天天被压力1 小时前
【A股分钟级高频数据完全手册 #01】Pro版接口体系总览与REST鉴权:42篇地图与从0接入
java·人工智能·python