LeetCode1261~1280题目解析

LeetCode 1261-1280. 题解合集

文章目录


1261. Find Elements in a Contaminated Binary Tree(在受污染的二叉树中查找元素)

题目描述

给出一个满足下述规则的二叉树:

  • root.val == 0
  • 对于任意 treeNode:
  • 如果 treeNode.val 为 x 且 treeNode.left != null,那么 treeNode.left.val == 2 * x + 1
  • 如果 treeNode.val 为 x 且 treeNode.right != null,那么 treeNode.right.val == 2 * x + 2
    现在这个二叉树受到「污染」,所有的 treeNode.val 都变成了 -1。
    请你先还原二叉树,然后实现 FindElements 类:
  • FindElements(TreeNode* root) 用受污染的二叉树初始化对象,你需要先把它还原。
  • bool find(int target) 判断目标值 target 是否存在于还原后的二叉树中并返回结果。

示例 1

复制代码
输入:
["FindElements","find","find"]
[[[-1,null,-1]],[1],[2]]
输出:
[null,false,true]
解释:
FindElements findElements = new FindElements([-1,null,-1]);
findElements.find(1); // return False
findElements.find(2); // return True

示例 2

复制代码
输入:
["FindElements","find","find","find"]
[[[-1,-1,-1,-1,-1]],[1],[3],[5]]
输出:
[null,true,true,false]
解释:
FindElements findElements = new FindElements([-1,-1,-1,-1,-1]);
findElements.find(1); // return True
findElements.find(3); // return True
findElements.find(5); // return False

示例 3

复制代码
输入:
["FindElements","find","find","find","find"]
[[[-1,null,-1,-1,null,-1]],[2],[3],[4],[5]]
输出:
[null,true,false,false,true]
解释:
FindElements findElements = new FindElements([-1,null,-1,-1,null,-1]);
findElements.find(2); // return True
findElements.find(3); // return False
findElements.find(4); // return False
findElements.find(5); // return True

提示:

  • TreeNode.val == -1
  • 二叉树的高度不超过 20
  • 节点的总数在 [1, 104] 之间
  • 调用 find() 的总次数在 [1, 104] 之间
  • 0 6

解题思路

解法一:DFS 还原 + 哈希集合(主提交版本)

从根节点 val=0 出发 DFS:左子 2x+1,右子 2x+2,把所有还原值存入 HashSet。

find 直接查集合。

复杂度:构造 O(n),find O(1),空间 O(n)。

解法二:路径位运算(对照)

不预建集合,利用规则反推:target 从二进制高位到低位,每一位对应向左/右走。

find(target) 时按路径走,若中途遇到空缺节点返回 false。

复杂度:find O(log target),构造 O(n),空间 O(log target)(递归)。

代码实现

java 复制代码
import java.util.HashSet;
import java.util.Set;

public class Solution {

    public static class FindElements {
        private Set<Integer> vals;

        public FindElements(TreeNode root) {
            vals = new HashSet<>();
            if (root != null) {
                root.val = 0;
            }
            recover(root, 0);
        }

        private void recover(TreeNode node, int val) {
            if (node == null) {
                return;
            }
            node.val = val;
            vals.add(val);
            recover(node.left, val * 2 + 1);
            recover(node.right, val * 2 + 2);
        }

        public boolean find(int target) {
            return vals.contains(target);
        }
    }

    public static class FindElementsWalk {
        private TreeNode root;

        public FindElementsWalk(TreeNode root) {
            this.root = root;
        }

        public boolean find(int target) {
            if (target < 0) {
                return false;
            }
            // 树中值即"num+1 的二进制去掉最高位"对应的路径编号。
            // bin(target+1) 去掉首位后,'0' -> 左子,'1' -> 右子。
            String bits = Integer.toBinaryString(target + 1);
            TreeNode node = root;
            for (int i = 1; i < bits.length(); i++) {
                if (bits.charAt(i) == '0') {
                    node = node.left;
                } else {
                    node = node.right;
                }
                if (node == null) {
                    return false;
                }
            }
            return true;
        }
    }

    public static void main(String[] args) {
        // 示例 1:[-1,null,-1]
        TreeNode r1 = new TreeNode(-1);
        r1.right = new TreeNode(-1);
        FindElements fe1 = new FindElements(r1);
        System.out.println(fe1.find(1)); // false
        System.out.println(fe1.find(2)); // true
        FindElementsWalk fw1 = new FindElementsWalk(r1);
        System.out.println(fw1.find(1)); // false
        System.out.println(fw1.find(2)); // true

        // 示例 2:[-1,-1,-1,-1,-1]
        TreeNode r2 = new TreeNode(-1);
        r2.left = new TreeNode(-1);
        r2.right = new TreeNode(-1);
        r2.left.left = new TreeNode(-1);
        r2.left.right = new TreeNode(-1);
        FindElements fe2 = new FindElements(r2);
        System.out.println(fe2.find(1)); // true
        System.out.println(fe2.find(3)); // true
        System.out.println(fe2.find(5)); // false
        FindElementsWalk fw2 = new FindElementsWalk(r2);
        System.out.println(fw2.find(1)); // true
        System.out.println(fw2.find(3)); // true
        System.out.println(fw2.find(5)); // false
    }
}

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;

    TreeNode(int x) {
        val = x;
    }
}

复杂度分析

复杂度:构造 O(n),find O(1),空间 O(n)。

复杂度:find O(log target),构造 O(n),空间 O(log target)(递归)。


1262. Greatest Sum Divisible by Three(可被三整除的最大和)

题目描述

给你一个整数数组 nums,请你找出并返回能被三整除的元素 最大和。

示例 1

复制代码
输入:nums = [3,6,5,1,8]
输出:18
解释:选出数字 3, 6, 1 和 8,它们的和是 18(可被 3 整除的最大和)。

示例 2

复制代码
输入:nums = [4]
输出:0
解释:4 不能被 3 整除,所以无法选出数字,返回 0。

示例 3

复制代码
输入:nums = [1,2,3,4,4]
输出:12
解释:选出数字 1, 3, 4 以及 4,它们的和是 12(可被 3 整除的最大和)。

提示:

  • 1 4
  • 1 4

解题思路

解法一:余数 DP(主提交版本)

dpr 表示当前处理到某个前缀时"元素和模 3 余 r"的最大和。

每个数 x 可选可不选:dp = max( dp, dp_prev(r - x%3 + 3)%3 + x )。

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

解法二:排序贪心(对照)

先求总和 S。若 S%3==0 直接返回 S;否则删除最小的、余数匹配的元素来修正:

若余 1,删除最小的 1 个余 1 的数或最小的 2 个余 2 的数;若余 2,删除最小的 1 个余 2 或 2 个余 1。

用排序/选择取最小。

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

代码实现

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

public class Solution {

    public int maxSumDivThree(int[] nums) {
        int[] dp = new int[3];
        dp[1] = Integer.MIN_VALUE / 2;
        dp[2] = Integer.MIN_VALUE / 2;
        for (int x : nums) {
            int[] ndp = dp.clone();
            for (int r = 0; r < 3; r++) {
                int need = (x % 3 + r) % 3;
                ndp[(r + x % 3) % 3] = Math.max(ndp[(r + x % 3) % 3], dp[r] + x);
            }
            dp = ndp;
        }
        return dp[0];
    }

    public int maxSumDivThreeGreedy(int[] nums) {
        int total = 0;
        for (int x : nums) {
            total += x;
        }
        int r = total % 3;
        if (r == 0) {
            return total;
        }
        Arrays.sort(nums);
        // 收集余 1 与余 2 的最小若干数
        java.util.List<Integer> r1 = new java.util.ArrayList<>();
        java.util.List<Integer> r2 = new java.util.ArrayList<>();
        for (int x : nums) {
            if (x % 3 == 1) {
                r1.add(x);
            } else if (x % 3 == 2) {
                r2.add(x);
            }
        }
        int best = 0;
        if (r == 1) {
            // 删 1 个余 1,或 2 个余 2
            if (r1.size() >= 1) {
                best = Math.max(best, total - r1.get(0));
            }
            if (r2.size() >= 2) {
                best = Math.max(best, total - r2.get(0) - r2.get(1));
            }
        } else { // r == 2
            if (r2.size() >= 1) {
                best = Math.max(best, total - r2.get(0));
            }
            if (r1.size() >= 2) {
                best = Math.max(best, total - r1.get(0) - r1.get(1));
            }
        }
        return best;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.maxSumDivThree(new int[]{3, 6, 5, 1, 8}));
        System.out.println(s.maxSumDivThreeGreedy(new int[]{3, 6, 5, 1, 8}));
        System.out.println(s.maxSumDivThree(new int[]{4}));
        System.out.println(s.maxSumDivThreeGreedy(new int[]{4}));
        System.out.println(s.maxSumDivThree(new int[]{1, 2, 3, 4, 4}));
        System.out.println(s.maxSumDivThreeGreedy(new int[]{1, 2, 3, 4, 4}));
    }
}

复杂度分析

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

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


1263. Minimum Moves to Move a Box to Their Target Location(推箱子)

题目描述

「推箱子」是一款风靡全球的益智小游戏,玩家需要将箱子推到仓库中的目标位置。

游戏地图用大小为 m x n 的网格 grid 表示,其中每个元素可以是墙、地板或者是箱子。

现在你将作为玩家参与游戏,按规则将箱子 'B' 移动到目标位置 'T' :

  • 玩家用字符 'S' 表示,只要他在地板上,就可以在网格中向上、下、左、右四个方向移动。
  • 地板用字符 '.' 表示,意味着可以自由行走。
  • 墙用字符 '#' 表示,意味着障碍物,不能通行。
  • 箱子仅有一个,用字符 'B' 表示。相应地,网格上有一个目标位置 'T'。
  • 玩家需要站在箱子旁边,然后沿着箱子的方向进行移动,此时箱子会被移动到相邻的地板单元格。记作一次「推动」。
  • 玩家无法越过箱子。
    返回将箱子推到目标位置的最小 推动 次数,如果无法做到,请返回 -1。

示例 1

复制代码
输入:grid = [["#","#","#","#","#","#"],
["#","T","#","#","#","#"],
["#",".",".","B",".","#"],
["#",".","#","#",".","#"],
["#",".",".",".","S","#"],
["#","#","#","#","#","#"]]
输出:3
解释:我们只需要返回推箱子的次数。

示例 2

复制代码
输入:grid = [["#","#","#","#","#","#"],
["#","T","#","#","#","#"],
["#",".",".","B",".","#"],
["#","#","#","#",".","#"],
["#",".",".",".","S","#"],
["#","#","#","#","#","#"]]
输出:-1

示例 3

复制代码
输入:grid = [["#","#","#","#","#","#"],
["#","T",".",".","#","#"],
["#",".","#","B",".","#"],
["#",".",".",".",".","#"],
["#",".",".",".","S","#"],
["#","#","#","#","#","#"]]
输出:5
解释:向下、向左、向左、向上再向上。

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 20
  • grid 仅包含字符 '.', '#', 'S' , 'T', 以及 'B'。
  • grid 中 'S', 'B' 和 'T' 各只能出现一个。

解题思路

解法一:双层 BFS(主提交版本)

状态 = (箱子位置, 玩家位置)。BFS 按"推动次数"分层:

箱子不动时,玩家在可活动区域内做次级 BFS(不计推动次数);

当玩家能走到箱子的某一侧并可在推的方向站定时,推动箱子一格,推动次数 +1 入队。

用 (boxR, boxC, manR, manC) 编码状态去重。

复杂度:时间 O((mn)^2) 级,空间 O((m n)^2)。

解法二:状态压缩 BFS + 双端队列(对照)

复用"箱子在 4 方向可推"的判据,仅在推动时计步,等价实现,编码上更紧凑。

复杂度:同解法一。

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Queue;
import java.util.Set;

public class Solution {

    private char[][] g;
    private int m, n;

    public int minPushBox(char[][] grid) {
        m = grid.length;
        n = grid[0].length;
        g = grid;
        int[] box = null, man = null, target = null;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 'B') {
                    box = new int[]{i, j};
                } else if (grid[i][j] == 'S') {
                    man = new int[]{i, j};
                } else if (grid[i][j] == 'T') {
                    target = new int[]{i, j};
                }
            }
        }
        // 状态:箱子位置 + 玩家位置 -> 最小推动次数
        Set<String> vis = new HashSet<>();
        Queue<int[]> q = new ArrayDeque<>(); // [boxR, boxC, manR, manC]
        q.offer(new int[]{box[0], box[1], man[0], man[1]});
        vis.add(key(box[0], box[1], man[0], man[1]));
        int steps = 0;
        int[] dr = {1, -1, 0, 0};
        int[] dc = {0, 0, 1, -1};
        while (!q.isEmpty()) {
            int size = q.size();
            for (int s = 0; s < size; s++) {
                int[] cur = q.poll();
                int br = cur[0], bc = cur[1], mr = cur[2], mc = cur[3];
                if (br == target[0] && bc == target[1]) {
                    return steps;
                }
                // 玩家在 (mr,mc),箱子在 (br,bc):玩家可到达的格子集合(不含箱子)
                Set<Integer> reach = reachable(mr, mc, br, bc);
                for (int d = 0; d < 4; d++) {
                    int nbr = br + dr[d], nbc = bc + dc[d];
                    // 推箱子方向 d:玩家必须站在箱子反方向 (br - dr[d], bc - dc[d])
                    int standR = br - dr[d], standC = bc - dc[d];
                    if (!inBound(nbr, nbc) || g[nbr][nbc] == '#') {
                        continue;
                    }
                    if (!inBound(standR, standC) || g[standR][standC] == '#') {
                        continue;
                    }
                    if (!reach.contains(standR * n + standC)) {
                        continue;
                    }
                    String nk = key(nbr, nbc, br, bc);
                    if (!vis.contains(nk)) {
                        vis.add(nk);
                        q.offer(new int[]{nbr, nbc, br, bc});
                    }
                }
            }
            steps++;
        }
        return -1;
    }

    private String key(int br, int bc, int mr, int mc) {
        return br + "," + bc + "," + mr + "," + mc;
    }

    private boolean inBound(int r, int c) {
        return r >= 0 && r < m && c >= 0 && c < n;
    }

    // 玩家从 (sr, sc) 出发能到达的格子集合((br, bc) 视为障碍)
    private Set<Integer> reachable(int sr, int sc, int br, int bc) {
        Set<Integer> res = new HashSet<>();
        Queue<int[]> q = new ArrayDeque<>();
        q.offer(new int[]{sr, sc});
        res.add(sr * n + sc);
        int[] dr = {1, -1, 0, 0};
        int[] dc = {0, 0, 1, -1};
        while (!q.isEmpty()) {
            int[] cur = q.poll();
            for (int d = 0; d < 4; d++) {
                int nr = cur[0] + dr[d], nc = cur[1] + dc[d];
                if (!inBound(nr, nc) || g[nr][nc] == '#' || (nr == br && nc == bc)) {
                    continue;
                }
                int k = nr * n + nc;
                if (!res.contains(k)) {
                    res.add(k);
                    q.offer(new int[]{nr, nc});
                }
            }
        }
        return res;
    }

    public int minPushBoxCompact(char[][] grid) {
        m = grid.length;
        n = grid[0].length;
        g = grid;
        int[] box = null, man = null, target = null;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 'B') {
                    box = new int[]{i, j};
                } else if (grid[i][j] == 'S') {
                    man = new int[]{i, j};
                } else if (grid[i][j] == 'T') {
                    target = new int[]{i, j};
                }
            }
        }
        boolean[][][][] vis = new boolean[m][n][m][n];
        Queue<int[]> q = new ArrayDeque<>();
        q.offer(new int[]{box[0], box[1], man[0], man[1]});
        vis[box[0]][box[1]][man[0]][man[1]] = true;
        int steps = 0;
        int[] dr = {1, -1, 0, 0};
        int[] dc = {0, 0, 1, -1};
        while (!q.isEmpty()) {
            int size = q.size();
            while (size-- > 0) {
                int[] cur = q.poll();
                int br = cur[0], bc = cur[1], mr = cur[2], mc = cur[3];
                if (br == target[0] && bc == target[1]) {
                    return steps;
                }
                Set<Integer> reach = reachable(mr, mc, br, bc);
                for (int d = 0; d < 4; d++) {
                    int nbr = br + dr[d], nbc = bc + dc[d];
                    int sr = br - dr[d], sc = bc - dc[d];
                    if (!inBound(nbr, nbc) || g[nbr][nbc] == '#') {
                        continue;
                    }
                    if (!inBound(sr, sc) || g[sr][sc] == '#') {
                        continue;
                    }
                    if (!reach.contains(sr * n + sc)) {
                        continue;
                    }
                    if (!vis[nbr][nbc][br][bc]) {
                        vis[nbr][nbc][br][bc] = true;
                        q.offer(new int[]{nbr, nbc, br, bc});
                    }
                }
            }
            steps++;
        }
        return -1;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        char[][] g1 = {
                {'#', '#', '#', '#', '#', '#'},
                {'#', 'T', '#', '#', '#', '#'},
                {'#', '.', '.', 'B', '.', '#'},
                {'#', '.', '#', '#', '.', '#'},
                {'#', '.', '.', '.', 'S', '#'},
                {'#', '#', '#', '#', '#', '#'}
        };
        System.out.println(s.minPushBox(g1));
        System.out.println(s.minPushBoxCompact(g1));
        char[][] g2 = {
                {'#', '#', '#', '#', '#', '#'},
                {'#', 'T', '#', '#', '#', '#'},
                {'#', '.', '.', 'B', '.', '#'},
                {'#', '#', '#', '#', '.', '#'},
                {'#', '.', '.', '.', 'S', '#'},
                {'#', '#', '#', '#', '#', '#'}
        };
        System.out.println(s.minPushBox(g2));
        System.out.println(s.minPushBoxCompact(g2));
        char[][] g3 = {
                {'#', '#', '#', '#', '#', '#'},
                {'#', 'T', '.', '.', '#', '#'},
                {'#', '.', '#', 'B', '.', '#'},
                {'#', '.', '.', '.', '.', '#'},
                {'#', '.', '.', '.', 'S', '#'},
                {'#', '#', '#', '#', '#', '#'}
        };
        System.out.println(s.minPushBox(g3));
        System.out.println(s.minPushBoxCompact(g3));
    }
}

复杂度分析

复杂度:时间 O((mn)^2) 级,空间 O((mn)^2)。


1264. Page Recommendations(页面推荐 🔒)

题目类型:数据库(SQL)

题目描述

朋友关系列表: Friendship

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| user1_id      | int     |
| user2_id      | int     |
+---------------+---------+
(user1_id, user2_id) 是这张表具有唯一值的列的组合。
这张表的每一行代表着 user1_id 和 user2_id 之间存在着朋友关系。

喜欢列表: Likes

复制代码
+-------------+---------+
| Column Name | Type    |
+-------------+---------+
| user_id     | int     |
| page_id     | int     |
+-------------+---------+
(user_id, page_id) 是这张表具有唯一值的列的组合。
这张表的每一行代表着 user_id 喜欢 page_id。

编写解决方案,向user_id = 1 的用户,推荐其朋友们喜欢的页面。不要推荐该用户已经喜欢的页面。

以 任意顺序 返回结果,其中不应当包含重复项。

返回结果的格式如下例所示。

示例 1:

复制代码
**输入:**
Friendship table:
+----------+----------+
| user1_id | user2_id |
+----------+----------+
| 1        | 2        |
| 1        | 3        |
| 1        | 4        |
| 2        | 3        |
| 2        | 4        |
| 2        | 5        |
| 6        | 1        |
+----------+----------+
 
Likes table:
+---------+---------+
| user_id | page_id |
+---------+---------+
| 1       | 88      |
| 2       | 23      |
| 3       | 24      |
| 4       | 56      |
| 5       | 11      |
| 6       | 33      |
| 2       | 77      |
| 3       | 77      |
| 6       | 88      |
+---------+---------+

**输出:**
+------------------+
| recommended_page |
+------------------+
| 23               |
| 24               |
| 56               |
| 33               |
| 77               |
+------------------+
**解释:**
用户1 同 用户2, 3, 4, 6 是朋友关系。
推荐页面为: 页面23 来自于 用户2, 页面24 来自于 用户3, 页面56 来自于 用户3 以及 页面33 来自于 用户6。
页面77 同时被 用户2 和 用户3 推荐。
页面88 没有被推荐,因为 用户1 已经喜欢了它。

解题思路

思考

推荐页是好友喜欢而自己未喜欢的页面。好友关系无向,先把与用户 1 1 1 相连的两端并成好友表,再连接 L i k e s Likes Likes,最后用子查询排除用户 1 1 1 已喜欢的 p a g e _ i d page\_id page_id。

考察UNION + NOT IN 子查询:先找出 user1=1 的所有朋友(两列 UNION),再取朋友喜欢的页面中排除本人已喜欢的页面,用 DISTINCT 去重。

代码实现

sql 复制代码
SELECT DISTINCT page_id AS recommended_page
FROM Likes
WHERE user_id IN (
        SELECT user2_id AS friend_id FROM Friendship WHERE user1_id = 1
        UNION
        SELECT user1_id AS friend_id FROM Friendship WHERE user2_id = 1
    )
    AND page_id NOT IN (
        SELECT page_id FROM Likes WHERE user_id = 1
    );

复杂度分析

本题为数据库查询题,不涉及传统算法的时间/空间复杂度,执行效率由数据库优化器决定;建议对查询涉及的过滤列与连接列建立索引以加速。


1265. Print Immutable Linked List in Reverse(逆序打印不可变链表)

题目描述

给您一个不可变的链表,使用下列接口逆序打印每个节点的值:

  • ImmutableListNode: 描述不可变链表的接口,链表的头节点已给出。
    您需要使用以下函数来访问此链表(您 不能 直接访问 ImmutableListNode):
  • ImmutableListNode.printValue():打印当前节点的值。
  • ImmutableListNode.getNext():返回下一个节点。
    输入只用来内部初始化链表。您不可以通过修改链表解决问题。也就是说,您只能通过上述 API 来操作链表。

示例 1

复制代码
输入:head = [1,2,3,4]
输出:[4,3,2,1]

示例 2

复制代码
输入:head = [0,-4,-1,3,-5]
输出:[-5,3,-1,-4,0]

示例 3

复制代码
输入:head = [-2,0,6,4,4,-6]
输出:[-6,4,4,6,0,-2]

提示:

  • 链表的长度在 [1, 1000] 之间。
  • 每个节点的值在 [-1000, 1000] 之间。
  • 进阶:
  • 您是否可以:
  • 使用常数级空间复杂度解决问题?
  • 使用线性级时间复杂度和低于线性级空间复杂度解决问题?

解题思路

解法一:递归(主提交版本)

先递归打印后继,再打印当前节点值。空间 O(n)(递归栈),时间 O(n)。

解法二:显式栈迭代(对照)

用栈把节点顺序压入,再依次弹出打印,达到逆序。空间 O(n),时间 O(n)。

代码实现

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

public class Solution {

    public void printLinkedListInReverse(ImmutableListNode head) {
        if (head == null) {
            return;
        }
        printLinkedListInReverse(head.getNext());
        head.printValue();
    }

    public void printLinkedListInReverseStack(ImmutableListNode head) {
        Stack<ImmutableListNode> stack = new Stack<>();
        ImmutableListNode cur = head;
        while (cur != null) {
            stack.push(cur);
            cur = cur.getNext();
        }
        while (!stack.isEmpty()) {
            stack.pop().printValue();
        }
    }

    public static void main(String[] args) {
        int[] vals = {1, 2, 3, 4};
        ImmutableListNode head = build(vals);
        System.out.print("递归: ");
        new Solution().printLinkedListInReverse(head);
        System.out.println();
        System.out.print("栈:   ");
        new Solution().printLinkedListInReverseStack(head);
        System.out.println();
    }

    private static ImmutableListNode build(int[] vals) {
        ImmutableListNodeImpl head = null, tail = null;
        for (int v : vals) {
            ImmutableListNodeImpl node = new ImmutableListNodeImpl(v);
            if (head == null) {
                head = node;
            } else {
                tail.next = node;
            }
            tail = node;
        }
        return head;
    }

    static class ImmutableListNodeImpl implements ImmutableListNode {
        int val;
        ImmutableListNodeImpl next;

        ImmutableListNodeImpl(int v) {
            val = v;
        }

        @Override
        public void printValue() {
            System.out.print(val + " ");
        }

        @Override
        public ImmutableListNode getNext() {
            return next;
        }
    }
}

interface ImmutableListNode {
    void printValue();

    ImmutableListNode getNext();
}

复杂度分析

先递归打印后继,再打印当前节点值。空间 O(n)(递归栈),时间 O(n)。

用栈把节点顺序压入,再依次弹出打印,达到逆序。空间 O(n),时间 O(n)。


1266. Minimum Time Visiting All Points(访问所有点的最小时间)

题目描述

平面上有 n 个点,点的位置用整数坐标表示 points[i] = [xi, yi] 。请你计算访问所有这些点需要的 最小时间(以秒为单位)。

你需要按照下面的规则在平面上移动:

  • 每一秒内,你可以:
  • 沿水平方向移动一个单位长度,或者
  • 沿竖直方向移动一个单位长度,或者
  • 跨过对角线移动 sqrt(2) 个单位长度(可以看作在一秒内向水平和竖直方向各移动一个单位长度)。
  • 必须按照数组中出现的顺序来访问这些点。
  • 在访问某个点时,可以经过该点后面出现的点,但经过的那些点不算作有效访问。

示例 1

复制代码
输入:points = [[1,1],[3,4],[-1,0]]
输出:7
解释:一条最佳的访问路径是: [1,1] -> [2,2] -> [3,3] -> [3,4] -> [2,3] -> [1,2] -> [0,1] -> [-1,0]
从 [1,1] 到 [3,4] 需要 3 秒
从 [3,4] 到 [-1,0] 需要 4 秒
一共需要 7 秒

示例 2

复制代码
输入:points = [[3,2],[-2,2]]
输出:5

提示:

  • points.length == n
  • 1 <= n <= 100
  • points[i].length == 2
  • -1000 <= points[i][0], points[i][1] <= 1000

解题思路

解法一:切比雪夫距离(主提交版本)

从 (x1,y1) 到 (x2,y2),先走 min(|dx|,|dy|) 步对角,再走 |dx|-|dy| 步直线,

总秒数 = max(|dx|, |dy|)。累加相邻点的时间即可。

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

解法二:逐秒模拟(对照)

对每一段路程,循环:若横纵坐标都没对齐则同时各走一步(对角),否则朝未对齐方向走一步,

计数器累加。逻辑直观但较慢。

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

代码实现

java 复制代码
public class Solution {

    public int minTimeToVisitAllPoints(int[][] points) {
        int ans = 0;
        for (int i = 1; i < points.length; i++) {
            int dx = Math.abs(points[i][0] - points[i - 1][0]);
            int dy = Math.abs(points[i][1] - points[i - 1][1]);
            ans += Math.max(dx, dy);
        }
        return ans;
    }

    public int minTimeToVisitAllPointsSim(int[][] points) {
        int ans = 0;
        for (int i = 1; i < points.length; i++) {
            int x = points[i - 1][0], y = points[i - 1][1];
            int tx = points[i][0], ty = points[i][1];
            while (x != tx || y != ty) {
                if (x != tx) {
                    x += (tx > x) ? 1 : -1;
                }
                if (y != ty) {
                    y += (ty > y) ? 1 : -1;
                }
                ans++;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] p1 = {{1, 1}, {3, 4}, {-1, 0}};
        System.out.println(s.minTimeToVisitAllPoints(p1));
        System.out.println(s.minTimeToVisitAllPointsSim(p1));
        int[][] p2 = {{3, 2}, {-2, 2}};
        System.out.println(s.minTimeToVisitAllPoints(p2));
        System.out.println(s.minTimeToVisitAllPointsSim(p2));
        int[][] p3 = {{0, 0}};
        System.out.println(s.minTimeToVisitAllPoints(p3));
        System.out.println(s.minTimeToVisitAllPointsSim(p3));
    }
}

复杂度分析

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

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


1267. Count Servers that Communicate(统计参与通信的服务器)

题目描述

这里有一幅服务器分布图,服务器的位置标识在 m * n 的整数矩阵网格 grid 中,1 表示单元格上有服务器,0 表示没有。

如果两台服务器位于同一行或者同一列,我们就认为它们之间可以进行通信。

请你统计并返回能够与至少一台其他服务器进行通信的服务器的数量。

示例 1

复制代码
输入:grid = [[1,0],[0,1]]
输出:0
解释:没有一台服务器能与其他服务器进行通信。

示例 2

复制代码
输入:grid = [[1,0],[1,1]]
输出:3
解释:所有这些服务器都至少可以与一台别的服务器进行通信。

示例 3

复制代码
输入:grid = [[1,1,0,0],[0,0,1,0],[0,0,1,0],[0,0,0,1]]
输出:4
解释:第一行的两台服务器互相通信,第三列的两台服务器互相通信,但右下角的服务器无法与其他服务器通信。

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m <= 250
  • 1 <= n <= 250
  • grid[i][j] == 0 or 1

解题思路

解法一:行/列计数(主提交版本)

先统计每行、每列的服务器总数;再遍历每个 1 格子,

若所在行服务器数 > 1 或所在列服务器数 > 1,则该服务器能通信,计数 +1。

复杂度:时间 O(mn),空间 O(m+n)。
解法二:连通块 BFS(对照)
把同行/同列的服务器视作连边,BFS 找连通分量;连通分量大小 > 1 的全部服务器都可通信。
复杂度:时间 O(m
n*(m+n)),空间 O(m*n)。

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.Queue;

public class Solution {

    public int countServers(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        int[] rows = new int[m];
        int[] cols = new int[n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1) {
                    rows[i]++;
                    cols[j]++;
                }
            }
        }
        int res = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1 && (rows[i] > 1 || cols[j] > 1)) {
                    res++;
                }
            }
        }
        return res;
    }

    public int countServersBfs(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        boolean[][] vis = new boolean[m][n];
        int res = 0;
        int[] dr = {1, -1, 0, 0};
        int[] dc = {0, 0, 1, -1};
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1 && !vis[i][j]) {
                    Queue<int[]> q = new ArrayDeque<>();
                    q.offer(new int[]{i, j});
                    vis[i][j] = true;
                    int cnt = 0;
                    while (!q.isEmpty()) {
                        int[] cur = q.poll();
                        cnt++;
                        // 扫描同行/同列的所有服务器入队
                        for (int c = 0; c < n; c++) {
                            if (grid[cur[0]][c] == 1 && !vis[cur[0]][c]) {
                                vis[cur[0]][c] = true;
                                q.offer(new int[]{cur[0], c});
                            }
                        }
                        for (int r = 0; r < m; r++) {
                            if (grid[r][cur[1]] == 1 && !vis[r][cur[1]]) {
                                vis[r][cur[1]] = true;
                                q.offer(new int[]{r, cur[1]});
                            }
                        }
                    }
                    if (cnt > 1) {
                        res += cnt;
                    }
                }
            }
        }
        return res;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] g1 = {{1, 0}, {0, 1}};
        System.out.println(s.countServers(g1));
        System.out.println(s.countServersBfs(g1));
        int[][] g2 = {{1, 0}, {1, 1}};
        System.out.println(s.countServers(g2));
        System.out.println(s.countServersBfs(g2));
        int[][] g3 = {{1, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}};
        System.out.println(s.countServers(g3));
        System.out.println(s.countServersBfs(g3));
    }
}

复杂度分析

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


题目描述

给你一个产品数组 products 和一个字符串 searchWord ,products 数组中每个产品都是一个字符串。

请你设计一个推荐系统,在依次输入单词 searchWord 的每一个字母后,推荐 products 数组中前缀与 searchWord 相同的最多三个产品。如果前缀相同的可推荐产品超过三个,请按字典序返回最小的三个。

请你以二维列表的形式,返回在输入 searchWord 每个字母后相应的推荐产品的列表。

示例 1

复制代码
输入:products = ["mobile","mouse","moneypot","monitor","mousepad"], searchWord = "mouse"
输出:[
["mobile","moneypot","monitor"],
["mobile","moneypot","monitor"],
["mouse","mousepad"],
["mouse","mousepad"],
["mouse","mousepad"]
]
解释:按字典序排序后的产品列表是 ["mobile","moneypot","monitor","mouse","mousepad"]
输入 m 和 mo,由于所有产品的前缀都相同,所以系统返回字典序最小的三个产品 ["mobile","moneypot","monitor"]
输入 mou, mous 和 mouse 后系统都返回 ["mouse","mousepad"]

示例 2

复制代码
输入:products = ["havana"], searchWord = "havana"
输出:[["havana"],["havana"],["havana"],["havana"],["havana"],["havana"]]

示例 3

复制代码
输入:products = ["bags","baggage","banner","box","cloths"], searchWord = "bags"
输出:[["baggage","bags","banner"],["baggage","bags","banner"],["baggage","bags"],["bags"]]

示例 4

复制代码
输入:products = ["havana"], searchWord = "tatiana"
输出:[[],[],[],[],[],[],[]]

提示:

  • 1 <= products.length <= 1000
  • 1 <= &Sigma; products[i].length <= 2 * 10^4
  • products[i] 中所有的字符都是小写英文字母。
  • 1 <= searchWord.length <= 1000
  • searchWord 中所有字符都是小写英文字母。

解题思路

解法一:排序 + 二分前缀(主提交版本)

对 products 排序。对每个前缀,用二分找到第一个 >= 前缀的下标,再从该下标取最多 3 个

以该前缀开头的产品(不满足就停止)。

复杂度:时间 O(n log n + |searchWord| * (log n + 3)),空间 O(n)。

解法二:字典树 Trie(对照)

构建 Trie,每个节点保存"以该前缀开头的最小 3 个产品"(建树时维护),查询时直接取节点列表。

复杂度:时间 O(总字符数 + 查询),空间 O(总字符数 * 3)。

代码实现

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

public class Solution {

    public List<List<String>> suggestedProducts(String[] products, String searchWord) {
        Arrays.sort(products);
        List<List<String>> res = new ArrayList<>();
        StringBuilder prefix = new StringBuilder();
        for (int i = 0; i < searchWord.length(); i++) {
            prefix.append(searchWord.charAt(i));
            String p = prefix.toString();
            int start = lowerBound(products, p);
            List<String> list = new ArrayList<>();
            for (int j = start; j < products.length && list.size() < 3; j++) {
                if (products[j].startsWith(p)) {
                    list.add(products[j]);
                } else {
                    break;
                }
            }
            res.add(list);
        }
        return res;
    }

    private int lowerBound(String[] arr, String key) {
        int lo = 0, hi = arr.length;
        while (lo < hi) {
            int mid = (lo + hi) >>> 1;
            if (arr[mid].compareTo(key) < 0) {
                lo = mid + 1;
            } else {
                hi = mid;
            }
        }
        return lo;
    }

    public List<List<String>> suggestedProductsTrie(String[] products, String searchWord) {
        TrieNode root = new TrieNode();
        for (String s : products) {
            insert(root, s);
        }
        List<List<String>> res = new ArrayList<>();
        TrieNode cur = root;
        for (int i = 0; i < searchWord.length(); i++) {
            int c = searchWord.charAt(i) - 'a';
            if (cur != null) {
                cur = cur.children[c];
            }
            if (cur == null) {
                res.add(new ArrayList<String>());
            } else {
                List<String> list = new ArrayList<>(cur.top3);
                Collections.sort(list);
                if (list.size() > 3) {
                    list = list.subList(0, 3);
                }
                res.add(list);
            }
        }
        return res;
    }

    private void insert(TrieNode root, String s) {
        TrieNode cur = root;
        for (char ch : s.toCharArray()) {
            int c = ch - 'a';
            if (cur.children[c] == null) {
                cur.children[c] = new TrieNode();
            }
            cur = cur.children[c];
            if (!cur.top3.contains(s)) {
                cur.top3.add(s);
                Collections.sort(cur.top3);
                if (cur.top3.size() > 3) {
                    cur.top3.remove(cur.top3.size() - 1);
                }
            }
        }
    }

    static class TrieNode {
        TrieNode[] children = new TrieNode[26];
        List<String> top3 = new ArrayList<>();
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        String[] p1 = {"mobile", "mouse", "moneypot", "monitor", "mousepad"};
        System.out.println(s.suggestedProducts(p1, "mouse"));
        System.out.println(s.suggestedProductsTrie(p1, "mouse"));
        String[] p2 = {"havana"};
        System.out.println(s.suggestedProducts(p2, "tatiana"));
        System.out.println(s.suggestedProductsTrie(p2, "tatiana"));
        String[] p3 = {"bags", "baggage", "banner", "box", "cloths"};
        System.out.println(s.suggestedProducts(p3, "bags"));
        System.out.println(s.suggestedProductsTrie(p3, "bags"));
    }
}

复杂度分析

复杂度:时间 O(n log n + |searchWord| * (log n + 3)),空间 O(n)。

复杂度:时间 O(总字符数 + 查询),空间 O(总字符数 * 3)。


1269. Number of Ways to Stay in the Same Place After Some Steps(停在原地的方案数)

题目描述

有一个长度为 arrLen 的数组,开始有一个指针在索引 0 处。

每一步操作中,你可以将指针向左或向右移动 1 步,或者停在原地(指针不能被移动到数组范围外)。

给你两个整数 steps 和 arrLen ,请你计算并返回:在恰好执行 steps 次操作以后,指针仍然指向索引 0 处的方案数。

由于答案可能会很大,请返回方案数 模 10^9 + 7 后的结果。

示例 1

复制代码
输入:steps = 3, arrLen = 2
输出:4
解释:3 步后,总共有 4 种不同的方法可以停在索引 0 处。
向右,向左,不动
不动,向右,向左
向右,不动,向左
不动,不动,不动

示例 2

复制代码
输入:steps = 2, arrLen = 4
输出:2
解释:2 步后,总共有 2 种不同的方法可以停在索引 0 处。
向右,向左
不动,不动

示例 3

复制代码
输入:steps = 4, arrLen = 2
输出:8

提示:

  • 1 <= steps <= 500
  • 1 6

解题思路

解法一:滚动 DP(主提交版本)

dppos 表示当前步数下位于 pos 的方案数。每步转移:

ndppos = dppos-1 + dppos + dppos+1。

由于 steps 步内最远只能到 steps 位置,取 limit = min(arrLen - 1, steps) 压缩状态。

复杂度:时间 O(steps * limit),空间 O(limit)。

解法二:记忆化搜索(对照)

solve(steps, pos):剩余 steps 步、当前在 pos 的方案数。递归 + 记忆化。

复杂度:时间 O(steps * limit),空间 O(steps * limit)。

代码实现

java 复制代码
public class Solution {

    private static final long MOD = 1000000007L;

    public int numWays(int steps, int arrLen) {
        int limit = Math.min(arrLen - 1, steps);
        long[] dp = new long[limit + 1];
        dp[0] = 1;
        for (int s = 1; s <= steps; s++) {
            long[] ndp = new long[limit + 1];
            for (int pos = 0; pos <= limit; pos++) {
                long v = dp[pos]; // 原地
                if (pos - 1 >= 0) {
                    v += dp[pos - 1]; // 从左边来(左移)
                }
                if (pos + 1 <= limit) {
                    v += dp[pos + 1]; // 从右边来(右移)
                }
                ndp[pos] = v % MOD;
            }
            dp = ndp;
        }
        return (int) dp[0];
    }

    public int numWaysMemo(int steps, int arrLen) {
        int limit = Math.min(arrLen - 1, steps);
        Long[][] memo = new Long[steps + 1][limit + 1];
        return (int) solve(steps, 0, limit, memo);
    }

    private long solve(int steps, int pos, int limit, Long[][] memo) {
        if (steps == 0) {
            return pos == 0 ? 1 : 0;
        }
        if (pos > steps) {
            return 0; // 剩余步数不足以回到 0
        }
        if (memo[steps][pos] != null) {
            return memo[steps][pos];
        }
        long v = solve(steps - 1, pos, limit, memo); // 原地
        if (pos - 1 >= 0) {
            v += solve(steps - 1, pos - 1, limit, memo);
        }
        if (pos + 1 <= limit) {
            v += solve(steps - 1, pos + 1, limit, memo);
        }
        memo[steps][pos] = v % MOD;
        return memo[steps][pos];
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.numWays(3, 2));
        System.out.println(s.numWaysMemo(3, 2));
        System.out.println(s.numWays(2, 4));
        System.out.println(s.numWaysMemo(2, 4));
        System.out.println(s.numWays(4, 2));
        System.out.println(s.numWaysMemo(4, 2));
        System.out.println(s.numWays(27, 7));
        System.out.println(s.numWaysMemo(27, 7));
    }
}

复杂度分析

复杂度:时间 O(steps * limit),空间 O(limit)。

复杂度:时间 O(steps * limit),空间 O(steps * limit)。


1270. All People Report to the Given Manager(向公司 CEO 汇报工作的所有人 🔒)

题目类型:数据库(SQL)

题目描述

员工表:Employees

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| employee_id   | int     |
| employee_name | varchar |
| manager_id    | int     |
+---------------+---------+
employee_id 是这个表具有唯一值的列。
这个表中每一行中,employee_id 表示职工的 ID,employee_name 表示职工的名字,manager_id 表示该职工汇报工作的直线经理。
这个公司 CEO 是 employee_id = 1 的人。

编写解决方案,找出所有直接或间接向公司 CEO 汇报工作的职工的 employee_id 。

由于公司规模较小,经理之间的间接关系 不超过 3 个经理 。

可以以 任何顺序 返回无重复项的结果。

返回结果示例如下。

示例 1:

复制代码
**输入:**
`Employees `table:
+-------------+---------------+------------+
| employee_id | employee_name | manager_id |
+-------------+---------------+------------+
| 1           | Boss          | 1          |
| 3           | Alice         | 3          |
| 2           | Bob           | 1          |
| 4           | Daniel        | 2          |
| 7           | Luis          | 4          |
| 8           | Jhon          | 3          |
| 9           | Angela        | 8          |
| 77          | Robert        | 1          |
+-------------+---------------+------------+
**输出:**
+-------------+
| employee_id |
+-------------+
| 2           |
| 77          |
| 4           |
| 7           |
+-------------+
**解释:**
公司 CEO 的 employee_id 是 1.
employee_id 是 2 和 77 的职员直接汇报给公司 CEO。
employee_id 是 4 的职员间接汇报给公司 CEO 4 --> 2 --> 1 。
employee_id 是 7 的职员间接汇报给公司 CEO 7 --> 4 --> 2 --> 1 。
employee_id 是 3, 8 ,9 的职员不会直接或间接的汇报给公司 CEO。 

解题思路

思考

公司层级至多三层且 CEO 的编号为 1 1 1。两次自连接沿 m a n a g e r _ i d manager\_id manager_id 走到上上级,若其为 1 1 1 且本人不是 1 1 1,则该员工直接或间接向 CEO 汇报。连接次数与层级上限一致。

考察递归 CTE 或多次连接:从 employee_id=1 出发按 manager_id 链路递归找出所有间接下属,输出其 name。SQL Server/Oracle 用递归 CTE,MySQL 8 亦支持。

代码实现

sql 复制代码
# Write your MySQL query statement below
SELECT e1.employee_id
FROM
    Employees AS e1
    JOIN Employees AS e2 ON e1.manager_id = e2.employee_id
    JOIN Employees AS e3 ON e2.manager_id = e3.employee_id
WHERE e1.employee_id != 1 AND e3.manager_id = 1;

复杂度分析

本题为数据库查询题,不涉及传统算法的时间/空间复杂度,执行效率由数据库优化器决定;建议对查询涉及的过滤列与连接列建立索引以加速。


1271. Hexspeak(十六进制魔术数字)

题目描述

你有一个十进制数字,请按照此规则将它变成「十六进制魔术数字」:首先将它变成字母大写的十六进制字符串,然后将所有的数字 0 变成字母 O ,将数字 1 变成字母 I 。

如果一个数字在转换后只包含 {"A", "B", "C", "D", "E", "F", "I", "O"} ,那么我们就认为这个转换是有效的。

给你一个字符串 num ,它表示一个十进制数 N,如果它的十六进制魔术数字转换是有效的,请返回转换后的结果,否则返回 "ERROR" 。

示例 1

复制代码
输入:num = "257"
输出:"IOI"
解释:257 的十六进制表示是 101 。

示例 2

复制代码
输入:num = "3"
输出:"ERROR"

提示:

  • 1 <= N <= 10^12
  • 给定字符串不会有前导 0 。
  • 结果中的所有字母都应该是大写字母。

解题思路

解法一:Long 转换 + 逐字符校验(主提交版本)

用 Long.parseLong 解析十进制,Long.toHexString 得到小写十六进制,逐个字符替换 0/1 并校验其余字符。

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

解法二:手工进制转换(对照)

用除 16 取余的方式自行构造大写十六进制串,再做同样替换校验。

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

代码实现

java 复制代码
public class Solution {

    public String toHexspeak(String num) {
        long v = Long.parseLong(num);
        String hex = Long.toHexString(v).toUpperCase();
        StringBuilder sb = new StringBuilder();
        for (char c : hex.toCharArray()) {
            if (c == '0') {
                sb.append('O');
            } else if (c == '1') {
                sb.append('I');
            } else if (c >= 'A' && c <= 'F') {
                sb.append(c);
            } else {
                return "ERROR";
            }
        }
        return sb.toString();
    }

    public String toHexspeakManual(String num) {
        long v = Long.parseLong(num);
        char[] map = {'0', '1', '2', '3', '4', '5', '6', '7',
                '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
        StringBuilder sb = new StringBuilder();
        while (v > 0) {
            sb.append(map[(int) (v % 16)]);
            v /= 16;
        }
        sb.reverse();
        StringBuilder res = new StringBuilder();
        for (char c : sb.toString().toCharArray()) {
            if (c == '0') {
                res.append('O');
            } else if (c == '1') {
                res.append('I');
            } else if (c >= 'A' && c <= 'F') {
                res.append(c);
            } else {
                return "ERROR";
            }
        }
        return res.toString();
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.toHexspeak("257"));
        System.out.println(s.toHexspeakManual("257"));
        System.out.println(s.toHexspeak("3"));
        System.out.println(s.toHexspeakManual("3"));
        System.out.println(s.toHexspeak("10"));
        System.out.println(s.toHexspeakManual("10"));
        System.out.println(s.toHexspeak("6199912085092")); // 0x5A2D1CE
        System.out.println(s.toHexspeakManual("6199912085092"));
    }
}

复杂度分析

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


1272. Remove Interval(删除区间)

题目描述

实数集合可以表示为若干不相交区间的并集,其中每个区间的形式为 [a, b)(左闭右开),表示满足 a <= x < b 的所有实数 x 的集合。如果某个区间 [a, b) 中包含实数 x ,则称实数 x 在集合中。

给你一个 有序的 不相交区间列表 intervals 。intervals 表示一个实数集合,其中每一项 intervals[i] = [ai, bi] 都表示一个区间 [ai, bi) 。再给你一个要删除的区间 toBeRemoved 。

返回 一组实数,该实数表示intervals 中 删除 了 toBeRemoved 的部分 。换句话说,返回实数集合,并满足集合中的每个实数 x 都在 intervals 中,但不在 toBeRemoved 中。你的答案应该是一个如上所述的 有序的 不相连的间隔列表 。

示例 1

复制代码
输入:intervals = [[0,2],[3,4],[5,7]], toBeRemoved = [1,6]
输出:[[0,1],[6,7]]

示例 2

复制代码
输入:intervals = [[0,5]], toBeRemoved = [2,3]
输出:[[0,2],[3,5]]

示例 3

复制代码
输入:intervals = [[-5,-4],[-3,-2],[1,2],[3,5],[8,9]], toBeRemoved = [-1,4]
输出:[[-5,-4],[-3,-2],[4,5],[8,9]]

提示:

  • 1 4
  • -109 i i 9

解题思路

解法一:线性扫描切分(主提交版本)

对每个区间 [a, b),计算其与 [t1, t2) 的交集并切除:

若完全不交,直接保留;若部分交叠,保留 [a, t1) 和/或 [t2, b)(左闭右开,注意端点相等不加入)。

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

解法二:分类讨论切分(对照)

把删除区间对每个 interval 的相交情况分为:左切、右切、全切、切割成两段,按情况处理。

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

代码实现

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

public class Solution {

    public List<List<Integer>> removeInterval(int[][] intervals, int[] toBeRemoved) {
        int t1 = toBeRemoved[0], t2 = toBeRemoved[1];
        List<List<Integer>> res = new ArrayList<>();
        for (int[] it : intervals) {
            int a = it[0], b = it[1];
            if (b <= t1 || a >= t2) {
                // 完全不在删除区间内
                add(res, a, b);
            } else {
                // 左侧保留部分 [a, min(b, t1))
                if (a < t1) {
                    add(res, a, Math.min(b, t1));
                }
                // 右侧保留部分 [max(a, t2), b)
                if (b > t2) {
                    add(res, Math.max(a, t2), b);
                }
            }
        }
        return res;
    }

    private void add(List<List<Integer>> res, int x, int y) {
        if (x < y) {
            List<Integer> seg = new ArrayList<>(2);
            seg.add(x);
            seg.add(y);
            res.add(seg);
        }
    }

    public List<List<Integer>> removeIntervalCate(int[][] intervals, int[] toBeRemoved) {
        int t1 = toBeRemoved[0], t2 = toBeRemoved[1];
        List<List<Integer>> res = new ArrayList<>();
        for (int[] it : intervals) {
            int a = it[0], b = it[1];
            if (b <= t1 || a >= t2) {
                add(res, a, b);
            } else if (a >= t1 && b <= t2) {
                // 完全被删除
            } else if (a < t1 && b <= t2) {
                // 左侧被切除 [a, t1)
                add(res, a, t1);
            } else if (a >= t1 && b > t2) {
                // 右侧被切除 [t2, b)
                add(res, t2, b);
            } else {
                // 中间被切除,剩两段
                add(res, a, t1);
                add(res, t2, b);
            }
        }
        return res;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.removeInterval(new int[][]{{0, 2}, {3, 4}, {5, 7}}, new int[]{1, 6}));
        System.out.println(s.removeIntervalCate(new int[][]{{0, 2}, {3, 4}, {5, 7}}, new int[]{1, 6}));
        System.out.println(s.removeInterval(new int[][]{{0, 5}}, new int[]{2, 3}));
        System.out.println(s.removeIntervalCate(new int[][]{{0, 5}}, new int[]{2, 3}));
        System.out.println(s.removeInterval(new int[][]{{-5, -4}, {-3, -2}, {1, 2}, {3, 5}, {8, 9}}, new int[]{-1, 4}));
        System.out.println(s.removeIntervalCate(new int[][]{{-5, -4}, {-3, -2}, {1, 2}, {3, 5}, {8, 9}}, new int[]{-1, 4}));
    }
}

复杂度分析

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


1273. Delete Tree Nodes(删除树节点)

题目描述

给你一棵以节点 0 为根节点的树,定义如下:

  • 节点的总数为 nodes 个;
  • 第 i 个节点的值为 value[i] ;
  • 第 i 个节点的父节点是 parent[i] 。
    请你删除节点值之和为 0 的每一棵子树。
    在完成所有删除之后,返回树中剩余节点的数目。

示例 1

复制代码
输入:nodes = 7, parent = [-1,0,0,1,2,2,2], value = [1,-2,4,0,-2,-1,-1]
输出:2

示例 2

复制代码
输入:nodes = 7, parent = [-1,0,0,1,2,2,2], value = [1,-2,4,0,-2,-1,-2]
输出:6

示例 3

复制代码
输入:nodes = 5, parent = [-1,0,1,0,0], value = [-672,441,18,728,378]
输出:5

示例 4

复制代码
输入:nodes = 5, parent = [-1,0,0,1,1], value = [-686,-842,616,-739,-746]
输出:5

提示:

  • 1 <= nodes <= 10^4
  • parent.length == nodes
  • 0 <= parent[i] <= nodes - 1
  • parent[0] == -1 表示节点 0 是树的根。
  • value.length == nodes
  • -10^5 <= value[i] <= 10^5
  • 题目输入数据 保证 是一棵 有效的树 。

解题思路

解法一:后序统计(主提交版本)

建立孩子列表,从根 DFS:先递归计算子树和与子树内保留节点数。

若整棵子树和为 0,则该子树整体删除(保留 0 个节点);否则保留 1 + 子节点保留数。

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

解法二:逆阶段顺序上推(对照)

按 parent 从叶子到根的顺序(父节点下标不一定小于子节点,用拓扑序或按深度排序)累计。

这里用"从 n-1 到 1 倒序累加贡献":对每个节点把子树和传给父节点,同时如果遇到和为 0 的子树计数删除。

简化实现:先自底向上累计子树和标记删除子树大小,再从根统计存活节点。

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

代码实现

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

public class Solution {

    public int deleteTreeNodes(int nodes, int[] parent, int[] value) {
        List<List<Integer>> children = new ArrayList<>();
        for (int i = 0; i < nodes; i++) {
            children.add(new ArrayList<>());
        }
        for (int i = 1; i < nodes; i++) {
            children.get(parent[i]).add(i);
        }
        int[] keep = dfs(children, 0, value);
        return keep[1];
    }

    // 返回 {子树和, 保留节点数}
    private int[] dfs(List<List<Integer>> children, int u, int[] value) {
        int sum = value[u];
        int count = 1;
        for (int v : children.get(u)) {
            int[] child = dfs(children, v, value);
            sum += child[0];
            count += child[1];
        }
        if (sum == 0) {
            return new int[]{0, 0}; // 整棵子树删除
        }
        return new int[]{sum, count};
    }

    public int deleteTreeNodesBottomUp(int nodes, int[] parent, int[] value) {
        // 子树和(自底向上累计)
        int[] sum = new int[nodes];
        for (int i = 0; i < nodes; i++) {
            sum[i] = value[i];
        }
        // 由于父节点编号不一定小于子节点,需要按深度分层或用计数器。
        // 简便做法:统计每个节点孩子数,用"孩子数归零"的拓扑序。
        int[] childCount = new int[nodes];
        for (int i = 1; i < nodes; i++) {
            childCount[parent[i]]++;
        }
        java.util.Queue<Integer> q = new java.util.LinkedList<>();
        for (int i = 0; i < nodes; i++) {
            if (childCount[i] == 0) {
                q.offer(i);
            }
        }
        int[] deleted = new int[nodes];
        while (!q.isEmpty()) {
            int u = q.poll();
            if (sum[u] == 0) {
                deleted[u] = 1;
            }
            if (u == 0) {
                break;
            }
            int p = parent[u];
            if (deleted[u] == 0) {
                sum[p] += sum[u];
            }
            childCount[p]--;
            if (childCount[p] == 0) {
                q.offer(p);
            }
        }
        // 统计未被删除的节点
        int res = 0;
        for (int i = 0; i < nodes; i++) {
            if (deleted[i] == 0) {
                res++;
            }
        }
        return res;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.deleteTreeNodes(7, new int[]{-1, 0, 0, 1, 2, 2, 2}, new int[]{1, -2, 4, 0, -2, -1, -1}));
        System.out.println(s.deleteTreeNodesBottomUp(7, new int[]{-1, 0, 0, 1, 2, 2, 2}, new int[]{1, -2, 4, 0, -2, -1, -1}));
        System.out.println(s.deleteTreeNodes(7, new int[]{-1, 0, 0, 1, 2, 2, 2}, new int[]{1, -2, 4, 0, -2, -1, -2}));
        System.out.println(s.deleteTreeNodesBottomUp(7, new int[]{-1, 0, 0, 1, 2, 2, 2}, new int[]{1, -2, 4, 0, -2, -1, -2}));
        System.out.println(s.deleteTreeNodes(5, new int[]{-1, 0, 1, 0, 0}, new int[]{-672, 441, 18, 728, 378}));
        System.out.println(s.deleteTreeNodesBottomUp(5, new int[]{-1, 0, 1, 0, 0}, new int[]{-672, 441, 18, 728, 378}));
    }
}

复杂度分析

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


1274. Number of Ships in a Rectangle(矩形内船只的数目)

题目描述

(此题是 交互式问题 )

在用笛卡尔坐标系表示的二维海平面上,有一些船。每一艘船都在一个整数点上,且每一个整数点最多只有 1 艘船。

有一个函数 Sea.hasShips(topRight, bottomLeft) ,输入参数为右上角和左下角两个点的坐标,当且仅当这两个点所表示的矩形区域(包含边界)内至少有一艘船时,这个函数才返回 true ,否则返回 false 。

给你矩形的右上角 topRight 和左下角 bottomLeft 的坐标,请你返回此矩形内船只的数目。题目保证矩形内 至多只有 10 艘船。

调用函数 hasShips 超过400次 的提交将被判为 错误答案(Wrong Answer) 。同时,任何尝试绕过评测系统的行为都将被取消比赛资格。

示例 1

复制代码
输入:
ships = [[1,1],[2,2],[3,3],[5,5]], topRight = [4,4], bottomLeft = [0,0]
输出:3
解释:在 [0,0] 到 [4,4] 的范围内总共有 3 艘船。

示例 2

复制代码
输入:ans = [[1,1],[2,2],[3,3]], topRight = [1000,1000], bottomLeft = [0,0]
输出:3

提示:

  • ships 数组只用于评测系统内部初始化。你必须"蒙着眼睛"解决这个问题。你无法得知 ships 的信息,所以只能通过调用 hasShips 接口来求解。
  • 0 <= bottomLeft[0] <= topRight[0] <= 1000
  • 0 <= bottomLeft[1] <= topRight[1] <= 1000
  • topRight != bottomLeft

解题思路

解法一:分治四分(主提交版本)

若矩形无船返回 0;若矩形缩成一个点返回 1(hasShips 已确认有船);

否则把矩形沿中线切成四个子矩形,递归统计。每次递归前先用 hasShips 剪枝。

调用次数 O(船数 * log(点数)),最多 10 艘船,远小于 400。

复杂度:时间 O(4^depth),空间 O(depth)。

解法二:二分横纵切分(对照)

只在长边上二分:若宽 > 高则纵向一分为二,否则横向一分为二,递归直至单点。

调用次数更少,逻辑等价。

复杂度:时间 O(log 面积),空间 O(depth)。

代码实现

java 复制代码
public class Solution {

    public int countShips(Sea sea, int[] topRight, int[] bottomLeft) {
        // 四个角都在同一坐标 -> 单点矩形
        if (topRight[0] == bottomLeft[0] && topRight[1] == bottomLeft[1]) {
            return 1;
        }
        int x1 = topRight[0], y1 = topRight[1];
        int x0 = bottomLeft[0], y0 = bottomLeft[1];
        int mx = (x0 + x1) / 2;
        int my = (y0 + y1) / 2;
        int sum = 0;
        // 左下 [x0..mx, y0..my]
        if (has(sea, new int[]{mx, my}, new int[]{x0, y0})) {
            sum += countShips(sea, new int[]{mx, my}, new int[]{x0, y0});
        }
        // 右下 [mx+1..x1, y0..my]
        if (mx + 1 <= x1 && has(sea, new int[]{x1, my}, new int[]{mx + 1, y0})) {
            sum += countShips(sea, new int[]{x1, my}, new int[]{mx + 1, y0});
        }
        // 左上 [x0..mx, my+1..y1]
        if (my + 1 <= y1 && has(sea, new int[]{mx, y1}, new int[]{x0, my + 1})) {
            sum += countShips(sea, new int[]{mx, y1}, new int[]{x0, my + 1});
        }
        // 右上 [mx+1..x1, my+1..y1]
        if (mx + 1 <= x1 && my + 1 <= y1 && has(sea, new int[]{x1, y1}, new int[]{mx + 1, my + 1})) {
            sum += countShips(sea, new int[]{x1, y1}, new int[]{mx + 1, my + 1});
        }
        return sum;
    }

    private boolean has(Sea sea, int[] topRight, int[] bottomLeft) {
        if (topRight[0] < bottomLeft[0] || topRight[1] < bottomLeft[1]) {
            return false;
        }
        return sea.hasShips(topRight, bottomLeft);
    }

    public int countShipsBinary(Sea sea, int[] topRight, int[] bottomLeft) {
        int x1 = topRight[0], y1 = topRight[1];
        int x0 = bottomLeft[0], y0 = bottomLeft[1];
        if (x1 == x0 && y1 == y0) {
            return 1;
        }
        if (x1 - x0 >= y1 - y0) {
            // 纵向切
            int mx = (x0 + x1) / 2;
            int sum = 0;
            if (has(sea, new int[]{mx, y1}, new int[]{x0, y0})) {
                sum += countShipsBinary(sea, new int[]{mx, y1}, new int[]{x0, y0});
            }
            if (mx + 1 <= x1 && has(sea, new int[]{x1, y1}, new int[]{mx + 1, y0})) {
                sum += countShipsBinary(sea, new int[]{x1, y1}, new int[]{mx + 1, y0});
            }
            return sum;
        } else {
            int my = (y0 + y1) / 2;
            int sum = 0;
            if (has(sea, new int[]{x1, my}, new int[]{x0, y0})) {
                sum += countShipsBinary(sea, new int[]{x1, my}, new int[]{x0, y0});
            }
            if (my + 1 <= y1 && has(sea, new int[]{x1, y1}, new int[]{x0, my + 1})) {
                sum += countShipsBinary(sea, new int[]{x1, y1}, new int[]{x0, my + 1});
            }
            return sum;
        }
    }

    public static void main(String[] args) {
        // 模拟:船在 [1,1],[2,2],[3,3],[5,5]
        final java.util.Set<String> ships = new java.util.HashSet<>();
        ships.add("1,1");
        ships.add("2,2");
        ships.add("3,3");
        ships.add("5,5");
        final int[] calls = {0};
        Sea sea = new Sea() {
            @Override
            public boolean hasShips(int[] topRight, int[] bottomLeft) {
                calls[0]++;
                for (int x = bottomLeft[0]; x <= topRight[0]; x++) {
                    for (int y = bottomLeft[1]; y <= topRight[1]; y++) {
                        if (ships.contains(x + "," + y)) {
                            return true;
                        }
                    }
                }
                return false;
            }
        };
        Solution s = new Solution();
        int r1 = s.countShips(sea, new int[]{4, 4}, new int[]{0, 0});
        System.out.println("四分法结果=" + r1 + "(期望 3)");
        calls[0] = 0;
        int r2 = s.countShipsBinary(sea, new int[]{1000, 1000}, new int[]{0, 0});
        System.out.println("二分切法结果=" + r2 + "(期望 4),调用次数=" + calls[0]);
    }
}

interface Sea {
    boolean hasShips(int[] topRight, int[] bottomLeft);
}

复杂度分析

复杂度:时间 O(4^depth),空间 O(depth)。

复杂度:时间 O(log 面积),空间 O(depth)。


1275. Find Winner on a Tic Tac Toe Game(找出井字棋的获胜者)

题目描述

井字棋 是由两个玩家 A 和 B 在 3 x 3 的棋盘上进行的游戏。井字棋游戏的规则如下:

  • 玩家轮流将棋子放在空方格 (' ') 上。
  • 第一个玩家 A 总是用 'X' 作为棋子,而第二个玩家 B 总是用 'O' 作为棋子。
  • 'X' 和 'O' 只能放在空方格中,而不能放在已经被占用的方格上。
  • 只要有 3 个相同的(非空)棋子排成一条直线(行、列、对角线)时,游戏结束。
  • 如果所有方块都放满棋子(不为空),游戏也会结束。
  • 游戏结束后,棋子无法再进行任何移动。
    给你一个数组 moves,其中 moves[i] = [rowi, coli] 表示第 i 次移动在 grid[rowi][coli]。如果游戏存在获胜者(A 或 B),就返回该游戏的获胜者;如果游戏以平局结束,则返回 "Draw";如果仍会有行动(游戏未结束),则返回 "Pending"。
    你可以假设 moves 都 有效(遵循 井字棋 规则),网格最初是空的,A 将先行动。

示例 1

复制代码
输入:moves = [[0,0],[2,0],[1,1],[2,1],[2,2]]
输出:"A"
解释:"A" 获胜,他总是先走。

示例 2

复制代码
输入:moves = [[0,0],[1,1],[0,1],[0,2],[1,0],[2,0]]
输出:"B"
解释:"B" 获胜。

示例 3

复制代码
输入:moves = [[0,0],[1,1],[2,0],[1,0],[1,2],[2,1],[0,1],[0,2],[2,2]]
输出:"Draw"
解释:由于没有办法再行动,游戏以平局结束。

提示:

  • 1 <= moves.length <= 9
  • moves[i].length == 2
  • 0 <= moves[i][j] <= 2
  • moves 里没有重复的元素。
  • moves 遵循井字棋的规则。

解题思路

解法一:模拟 + 校验线(主提交版本)

按 moves 轮流落子,每次落子后检查过该格子的行/列/两条对角线是否三连。

复杂度:时间 O(moves),空间 O(3x3)。

解法二:最终棋盘判定(对照)

先按 moves 生成完整棋盘,再统一检查 8 条线是否三连;无三连时按步数判 Draw/Pending。

复杂度:时间 O(moves + 8),空间 O(3x3)。

代码实现

java 复制代码
public class Solution {

    public String tictactoe(int[][] moves) {
        char[][] board = new char[3][3];
        for (int i = 0; i < moves.length; i++) {
            int r = moves[i][0], c = moves[i][1];
            board[r][c] = (i % 2 == 0) ? 'X' : 'O';
            if (win(board, r, c)) {
                return (i % 2 == 0) ? "A" : "B";
            }
        }
        return moves.length == 9 ? "Draw" : "Pending";
    }

    private boolean win(char[][] board, int r, int c) {
        char ch = board[r][c];
        // 行
        if (board[r][0] == ch && board[r][1] == ch && board[r][2] == ch) {
            return true;
        }
        // 列
        if (board[0][c] == ch && board[1][c] == ch && board[2][c] == ch) {
            return true;
        }
        // 主对角线
        if (r == c && board[0][0] == ch && board[1][1] == ch && board[2][2] == ch) {
            return true;
        }
        // 副对角线
        if (r + c == 2 && board[0][2] == ch && board[1][1] == ch && board[2][0] == ch) {
            return true;
        }
        return false;
    }

    public String tictactoeFinal(int[][] moves) {
        char[][] board = new char[3][3];
        for (int i = 0; i < moves.length; i++) {
            board[moves[i][0]][moves[i][1]] = (i % 2 == 0) ? 'X' : 'O';
        }
        // 检查 8 条线
        String winner = null;
        for (int i = 0; i < 3; i++) {
            if (board[i][0] != 0 && board[i][0] == board[i][1] && board[i][1] == board[i][2]) {
                winner = board[i][0] == 'X' ? "A" : "B";
            }
            if (board[0][i] != 0 && board[0][i] == board[1][i] && board[1][i] == board[2][i]) {
                winner = board[0][i] == 'X' ? "A" : "B";
            }
        }
        if (board[0][0] != 0 && board[0][0] == board[1][1] && board[1][1] == board[2][2]) {
            winner = board[0][0] == 'X' ? "A" : "B";
        }
        if (board[0][2] != 0 && board[0][2] == board[1][1] && board[1][1] == board[2][0]) {
            winner = board[0][2] == 'X' ? "A" : "B";
        }
        if (winner != null) {
            return winner;
        }
        return moves.length == 9 ? "Draw" : "Pending";
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.tictactoe(new int[][]{{0, 0}, {2, 0}, {1, 1}, {2, 1}, {2, 2}}));
        System.out.println(s.tictactoeFinal(new int[][]{{0, 0}, {2, 0}, {1, 1}, {2, 1}, {2, 2}}));
        System.out.println(s.tictactoe(new int[][]{{0, 0}, {1, 1}, {0, 1}, {0, 2}, {1, 0}, {2, 0}}));
        System.out.println(s.tictactoeFinal(new int[][]{{0, 0}, {1, 1}, {0, 1}, {0, 2}, {1, 0}, {2, 0}}));
        int[][] full = {{0, 0}, {1, 1}, {2, 0}, {1, 0}, {1, 2}, {2, 1}, {0, 1}, {0, 2}, {2, 2}};
        System.out.println(s.tictactoe(full));
        System.out.println(s.tictactoeFinal(full));
        System.out.println(s.tictactoe(new int[][]{{0, 0}, {1, 1}}));
        System.out.println(s.tictactoeFinal(new int[][]{{0, 0}, {1, 1}}));
    }
}

复杂度分析

复杂度:时间 O(moves),空间 O(3x3)。

复杂度:时间 O(moves + 8),空间 O(3x3)。


1276. Number of Burgers with No Waste of Ingredients(不浪费原料的汉堡制作方案)

题目描述

圣诞活动预热开始啦,汉堡店推出了全新的汉堡套餐。为了避免浪费原料,请你帮他们制定合适的制作计划。

给你两个整数 tomatoSlices 和 cheeseSlices,分别表示番茄片和奶酪片的数目。不同汉堡的原料搭配如下:

  • 巨无霸汉堡:4 片番茄和 1 片奶酪
  • 小皇堡:2 片番茄和 1 片奶酪
    请你以 [total_jumbo, total_small](巨无霸汉堡总数,小皇堡总数)的格式返回恰当的制作方案,使得剩下的番茄片 tomatoSlices 和奶酪片 cheeseSlices 的数量都是 0。
    如果无法使剩下的番茄片 tomatoSlices 和奶酪片 cheeseSlices 的数量为 0,就请返回 []。

示例 1

复制代码
输入:tomatoSlices = 16, cheeseSlices = 7
输出:[1,6]
解释:制作 1 个巨无霸汉堡和 6 个小皇堡需要 4*1 + 2*6 = 16 片番茄和 1 + 6 = 7 片奶酪。不会剩下原料。

示例 2

复制代码
输入:tomatoSlices = 17, cheeseSlices = 4
输出:[]
解释:只制作小皇堡和巨无霸汉堡无法用光全部原料。

示例 3

复制代码
输入:tomatoSlices = 4, cheeseSlices = 17
输出:[]
解释:制作 1 个巨无霸汉堡会剩下 16 片奶酪,制作 2 个小皇堡会剩下 15 片奶酪。

示例 4

复制代码
输入:tomatoSlices = 0, cheeseSlices = 0
输出:[0,0]

示例 5

复制代码
输入:tomatoSlices = 2, cheeseSlices = 1
输出:[0,1]

提示:

  • 0 <= tomatoSlices <= 10^7
  • 0 <= cheeseSlices <= 10^7

解题思路

解法一:鸡兔同笼解方程(主提交版本)

设巨无霸 x 个、小皇堡 y 个,则 4x+2y=tomatoSlices,x+y=cheeseSlices。

解得 x=(tomatoSlices-2cheeseSlices)/2,y=cheeseSlices-x。
只需校验 x、y 均为非负整数即可。复杂度:时间 O(1),空间 O(1)。
解法二:枚举小皇堡数量(对照)
枚举 y 从 0 到 cheeseSlices:若 4
(cheeseSlices-y)+2*y == tomatoSlices 即找到一组解

(由方程唯一性可知至多一组解,可直接返回)。复杂度:时间 O(cheeseSlices),空间 O(1)。

代码实现

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

public class Solution {

    public List<Integer> numOfBurgers(int tomatoSlices, int cheeseSlices) {
        List<Integer> res = new ArrayList<>();
        if (tomatoSlices % 2 != 0) {
            return res; // 番茄片必须是偶数(4x+2y 恒为偶数)
        }
        int total = tomatoSlices / 2; // 2x + y = tomatoSlices/2
        int jumbo = total - cheeseSlices;       // x = (t/2 - c)
        int small = 2 * cheeseSlices - total;   // y = 2c - t/2
        if (jumbo < 0 || small < 0) {
            return res;
        }
        // 反向验证(防止溢出误判)
        if (4L * jumbo + 2L * small != tomatoSlices || jumbo + small != cheeseSlices) {
            return res;
        }
        res.add(jumbo);
        res.add(small);
        return res;
    }

    public List<Integer> numOfBurgersEnum(int tomatoSlices, int cheeseSlices) {
        List<Integer> res = new ArrayList<>();
        for (int small = 0; small <= cheeseSlices; small++) {
            int jumbo = cheeseSlices - small;
            if (4L * jumbo + 2L * small == tomatoSlices) {
                res.add(jumbo);
                res.add(small);
                return res;
            }
        }
        return res;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.numOfBurgers(16, 7));        // [1,6]
        System.out.println(s.numOfBurgers(17, 4));        // []
        System.out.println(s.numOfBurgers(4, 17));        // []
        System.out.println(s.numOfBurgers(0, 0));         // [0,0]
        System.out.println(s.numOfBurgers(2, 1));         // [0,1]
        System.out.println("--- 枚举版 ---");
        System.out.println(s.numOfBurgersEnum(16, 7));    // [1,6]
        System.out.println(s.numOfBurgersEnum(17, 4));    // []
        System.out.println(s.numOfBurgersEnum(0, 0));     // [0,0]
        System.out.println(s.numOfBurgersEnum(2, 1));     // [0,1]
    }
}

复杂度分析

只需校验 x、y 均为非负整数即可。复杂度:时间 O(1),空间 O(1)。

(由方程唯一性可知至多一组解,可直接返回)。复杂度:时间 O(cheeseSlices),空间 O(1)。


1277. Count Square Submatrices with All Ones(统计全为 1 的正方形子矩阵)

题目描述

给你一个 m * n 的矩阵,矩阵中的元素不是 0 就是 1,请你统计并返回其中完全由 1 组成的 正方形 子矩阵的个数。

示例 1

复制代码
输入:matrix =
[
[0,1,1,1],
[1,1,1,1],
[0,1,1,1]
]
输出:15
解释:
边长为 1 的正方形有 10 个。
边长为 2 的正方形有 4 个。
边长为 3 的正方形有 1 个。
正方形的总数 = 10 + 4 + 1 = 15.

示例 2

复制代码
输入:matrix =
[
[1,0,1],
[1,1,0],
[1,1,0]
]
输出:7
解释:
边长为 1 的正方形有 6 个。
边长为 2 的正方形有 1 个。
正方形的总数 = 6 + 1 = 7.

提示:

  • 1 <= arr.length <= 300
  • 1 <= arr[0].length <= 300
  • 0 <= arr[i][j] <= 1

解题思路

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

令 dpij 表示以 (i,j) 为右下角的全 1 正方形最大边长。

若 matrixij==1,则 dpij = min(dpi-1j, dpij-1, dpi-1j-1) + 1,

否则 dpij = 0。以每个位置为右下角的正方形个数恰好等于 dpij,

累计所有 dpij 即为答案。复杂度:时间 O(mn),空间 O(m n)(可滚动优化为 O(n))。

解法二:暴力枚举边长(对照)

枚举每个左上角 (r,c) 与边长 k,检查该 k×k 区域是否全 1。

用每行前缀和 O(1) 校验一个 k×k 方块,累计满足条件的正方形个数。

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

代码实现

java 复制代码
public class Solution {

    public int countSquares(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        int[][] dp = new int[m][n];
        int res = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == 1) {
                    dp[i][j] = 1;
                    if (i > 0 && j > 0) {
                        dp[i][j] = Math.min(dp[i - 1][j],
                                Math.min(dp[i][j - 1], dp[i - 1][j - 1])) + 1;
                    }
                    res += dp[i][j];
                }
            }
        }
        return res;
    }

    public int countSquaresBrute(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        // 行前缀和:rowSum[i][j] 表示第 i 行前 j 个元素之和
        int[][] rowSum = new int[m][n + 1];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                rowSum[i][j + 1] = rowSum[i][j] + matrix[i][j];
            }
        }
        int res = 0;
        int maxSide = Math.min(m, n);
        for (int k = 1; k <= maxSide; k++) {
            for (int r = 0; r + k <= m; r++) {
                for (int c = 0; c + k <= n; c++) {
                    if (isAllOne(rowSum, r, c, k)) {
                        res++;
                    }
                }
            }
        }
        return res;
    }

    private boolean isAllOne(int[][] rowSum, int r, int c, int k) {
        for (int i = r; i < r + k; i++) {
            if (rowSum[i][c + k] - rowSum[i][c] != k) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] m1 = {{0, 1, 1, 1}, {1, 1, 1, 1}, {0, 1, 1, 1}};
        System.out.println(s.countSquares(m1));        // 15
        System.out.println(s.countSquaresBrute(m1));   // 15
        int[][] m2 = {{1, 0, 1}, {1, 1, 0}, {1, 1, 0}};
        System.out.println(s.countSquares(m2));        // 7
        System.out.println(s.countSquaresBrute(m2));   // 7
        int[][] m3 = {{1}};
        System.out.println(s.countSquares(m3));        // 1
        System.out.println(s.countSquaresBrute(m3));   // 1
        int[][] m4 = {{0}};
        System.out.println(s.countSquares(m4));        // 0
        System.out.println(s.countSquaresBrute(m4));   // 0
    }
}

复杂度分析

累计所有 dpij 即为答案。复杂度:时间 O(mn),空间 O(m n)(可滚动优化为 O(n))。

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


1278. Palindrome Partitioning III(分割回文串 III)

题目描述

给你一个由小写字母组成的字符串 s,和一个整数 k。

请你按下面的要求分割字符串:

  • 首先,你可以将 s 中的部分字符修改为其他的小写英文字母。
  • 接着,你需要把 s 分割成 k 个非空且不相交的子串,并且每个子串都是回文串。
    请返回以这种方式分割字符串所需修改的最少字符数。

示例 1

复制代码
输入:s = "abc", k = 2
输出:1
解释:你可以把字符串分割成 "ab" 和 "c",并修改 "ab" 中的 1 个字符,将它变成回文串。

示例 2

复制代码
输入:s = "aabbc", k = 3
输出:0
解释:你可以把字符串分割成 "aa"、"bb" 和 "c",它们都是回文串。

示例 3

复制代码
输入:s = "leetcode", k = 8
输出:0

提示:

  • 1 <= k <= s.length <= 100
  • s 中只含有小写英文字母。

解题思路

解法一:动态规划 + 预处理(主提交版本)

costij:把子串 si...j 变成回文所需最少修改次数 = 半个区间内不配对字符数。

dppi:把 s0...i 分成 p 段的最小修改次数。

转移:dppi = min(dpp-1j-1 + costji),j 取 p-1...i。

复杂度:时间 O(kn^2),空间 O(n^2+n k)。

解法二:记忆化搜索(对照)

dfs(p, i) 表示把 si... 分成 p 段的最小修改次数,配合 cost 预处理递归记忆化。

复杂度:时间 O(kn^2),空间 O(nk+n^2)。

代码实现

java 复制代码
public class Solution {

    private int[][] cost;
    private int[][] memo;
    private String s;

    public int palindromePartition(String s, int k) {
        int n = s.length();
        // cost[i][j]:s[i..j] 变为回文串的最小修改次数
        int[][] cost = new int[n][n];
        for (int len = 2; len <= n; len++) {
            for (int i = 0; i + len - 1 < n; i++) {
                int j = i + len - 1;
                cost[i][j] = cost[i + 1][j - 1] + (s.charAt(i) == s.charAt(j) ? 0 : 1);
            }
        }
        // dp[p][i]:s[0..i] 分成 p 段的最小修改次数
        int[][] dp = new int[k + 1][n];
        for (int i = 0; i < n; i++) {
            dp[1][i] = cost[0][i];
        }
        for (int p = 2; p <= k; p++) {
            for (int i = p - 1; i < n; i++) {
                int best = Integer.MAX_VALUE;
                for (int j = p - 1; j <= i; j++) {
                    best = Math.min(best, dp[p - 1][j - 1] + cost[j][i]);
                }
                dp[p][i] = best;
            }
        }
        return dp[k][n - 1];
    }

    public int palindromePartitionMemo(String s, int k) {
        int n = s.length();
        this.s = s;
        cost = new int[n][n];
        for (int len = 2; len <= n; len++) {
            for (int i = 0; i + len - 1 < n; i++) {
                int j = i + len - 1;
                cost[i][j] = cost[i + 1][j - 1] + (s.charAt(i) == s.charAt(j) ? 0 : 1);
            }
        }
        memo = new int[k + 1][n + 1];
        for (int i = 0; i <= k; i++) {
            for (int j = 0; j <= n; j++) {
                memo[i][j] = -1;
            }
        }
        return dfs(k, 0);
    }

    // 把 s[idx..] 分成 p 段的最小修改次数
    private int dfs(int p, int idx) {
        int n = s.length();
        if (p == 0) {
            return idx == n ? 0 : Integer.MAX_VALUE / 2;
        }
        if (n - idx < p) {
            return Integer.MAX_VALUE / 2;
        }
        if (memo[p][idx] != -1) {
            return memo[p][idx];
        }
        if (p == 1) {
            memo[p][idx] = cost[idx][n - 1];
            return memo[p][idx];
        }
        int best = Integer.MAX_VALUE;
        for (int j = idx; j < n; j++) {
            int rest = dfs(p - 1, j + 1);
            if (rest < Integer.MAX_VALUE / 2) {
                best = Math.min(best, cost[idx][j] + rest);
            }
        }
        memo[p][idx] = best;
        return best;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.palindromePartition("abc", 2));        // 1
        System.out.println(s.palindromePartitionMemo("abc", 2));    // 1
        System.out.println(s.palindromePartition("aabbc", 3));      // 0
        System.out.println(s.palindromePartitionMemo("aabbc", 3));  // 0
        System.out.println(s.palindromePartition("leetcode", 8));   // 0
        System.out.println(s.palindromePartitionMemo("leetcode", 8));// 0
        System.out.println(s.palindromePartition("abc", 1));        // 1
        System.out.println(s.palindromePartitionMemo("abc", 1));    // 1
    }
}

复杂度分析

复杂度:时间 O(kn^2),空间 O(n^2+n k)。

复杂度:时间 O(kn^2),空间 O(nk+n^2)。


1279. Traffic Light Controlled Intersection(红绿灯路口)🔒 多线程

题目描述

这是两条路的交叉路口。第一条路是 A 路,车辆可沿 1 号方向由北向南行驶,也可沿 2 号方向由南向北行驶。第二条路是 B 路,车辆可沿 3 号方向由西向东行驶,也可沿 4 号方向由东向西行驶。

每条路在路口前都有一个红绿灯。红绿灯可以亮起红灯或绿灯。

  • 绿灯表示两个方向的车辆都可通过路口。
  • 红灯表示两个方向的车辆都不可以通过路口,必须等待绿灯亮起。
    两条路上的红绿灯不可以同时为绿灯。这意味着,当 A 路上的绿灯亮起时,B 路上的红灯会亮起;当 B 路上的绿灯亮起时,A 路上的红灯会亮起.
    开始时,A 路上的绿灯亮起,B 路上的红灯亮起。当一条路上的绿灯亮起时,所有车辆都可以从任意两个方向通过路口,直到另一条路上的绿灯亮起。不同路上的车辆不可以同时通过路口。
    给这个路口设计一个没有死锁的红绿灯控制系统。
    实现函数 void carArrived(carId, roadId, direction, turnGreen, crossCar) :
  • carId 为到达车辆的编号。
  • roadId 为车辆所在道路的编号。
  • direction 为车辆的行进方向。
  • turnGreen 是一个函数,调用此函数会使当前道路上的绿灯亮起。
  • crossCar 是一个函数,调用此函数会允许车辆通过路口。
    当你的答案避免了车辆在路口出现死锁,此答案会被认定为正确的。当路口已经亮起绿灯时仍打开绿灯,此答案会被认定为错误的。

示例 1

复制代码
输入: cars = [1,3,5,2,4], directions = [2,1,2,4,3], arrivalTimes = [10,20,30,40,50]
输出: [
"Car 1 Has Passed Road A In Direction 2",    // A 路上的红绿灯为绿色,1 号车可通过路口。
"Car 3 Has Passed Road A In Direction 1",    // 红绿灯仍为绿色,3 号车通过路口。
"Car 5 Has Passed Road A In Direction 2",    // 红绿灯仍为绿色,5 号车通过路口。
"Traffic Light On Road B Is Green",          // 2 号车在 B 路请求绿灯。
"Car 2 Has Passed Road B In Direction 4",    // B 路上的绿灯现已亮起,2 号车通过路口。
"Car 4 Has Passed Road B In Direction 3"     // 红绿灯仍为绿色,4 号车通过路口。
]

示例 2

复制代码
输入: cars = [1,2,3,4,5], directions = [2,4,3,3,1], arrivalTimes = [10,20,30,40,40]
输出: [
"Car 1 Has Passed Road A In Direction 2",    // A 路上的红绿灯为绿色,1 号车可通过路口。
"Traffic Light On Road B Is Green",          // 2 号车在 B 路请求绿灯。
"Car 2 Has Passed Road B In Direction 4",    // B 路上的绿灯现已亮起,2 号车通过路口。
"Car 3 Has Passed Road B In Direction 3",    // B 路上的绿灯现已亮起,3 号车通过路口。
"Traffic Light On Road A Is Green",          // 5 号车在 A 路请求绿灯。
"Car 5 Has Passed Road A In Direction 1",    // A 路上的绿灯现已亮起,5 号车通过路口。
"Traffic Light On Road B Is Green",          // 4 号车在 B 路请求绿灯。4 号车在路口等灯,直到 5 号车通过路口,B 路的绿灯亮起。
"Car 4 Has Passed Road B In Direction 3"     // B 路上的绿灯现已亮起,4 号车通过路口。
]
解释: 这是一个无死锁的方案。注意,在 A 路上的绿灯亮起、5 号车通过前让 4 号车通过,也是一个正确且可被接受的方案。

提示:

  • 1 <= cars.length <= 20
  • cars.length = directions.length
  • cars.length = arrivalTimes.length
  • cars 中的所有值都是唯一的。
  • 1 <= directions[i] <= 4
  • arrivalTimes 是非递减的。

解题思路

解法一:互斥锁 + 道路状态(主提交版本)

维护一个锁保护的"当前绿灯所在道路"变量,roads 只有 A(1) 与 B(2)。

每次 carArrived:加锁后若 car 所在道路不是当前绿灯道路,则调用 turnGreen 切换并更新状态;

随后调用 crossCar,解锁。锁保证任意时刻只有一辆车在临界区,天然无死锁,且不会重复开绿灯。

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

解法二:按道路分段同步(对照)

用 roadId 计算该车所属道路段号,借助一个"本车到达时道路状态"记录,

相同的简化思路:分别在 A/B 两个 "信号量" 上按绿灯状态放行。

由于本环境难直接并发模拟,此实现以等价的 lock-free 标记 + 互斥在 main 中串行演示两种实现一致。

代码实现

java 复制代码
public class Solution {

    private int currentRoad; // 当前绿灯所在道路:1=A,2=B
    private final Object lock = new Object();

    public Solution() {
        currentRoad = 1; // 开始时 A 路绿灯
    }

    public void carArrived(
            int carId,
            int roadId,
            int direction,
            Runnable turnGreen,
            Runnable crossCar
    ) {
        synchronized (lock) {
            if (roadId != currentRoad) {
                turnGreen.run();
                currentRoad = roadId;
            }
            crossCar.run();
        }
    }

    public static class TrafficLight {
        private int road = 1; // A
        public synchronized void carArrived(int carId, int roadId, int direction,
                                            Runnable turnGreen, Runnable crossCar) {
            if (roadId != road) {
                turnGreen.run();
                road = roadId;
            }
            crossCar.run();
        }
    }

    public static void main(String[] args) {
        StringBuilder log = new StringBuilder();
        Solution s = new Solution();
        // 汽车按到达顺序逐一到达(等效串行调度)
        int[] cars = {1, 3, 5, 2, 4};
        int[] roads = {1, 1, 1, 2, 2};
        for (int i = 0; i < cars.length; i++) {
            final int carId = cars[i];
            final int roadId = roads[i];
            s.carArrived(carId, roadId, 0,
                    () -> log.append("Traffic Light On Road ").append(roadId == 1 ? "A" : "B").append(" Is Green\n"),
                    () -> log.append("Car ").append(carId).append(" Has Passed Road ").append(roadId == 1 ? "A" : "B").append("\n"));
        }
        System.out.print(log.toString());

        // 对照实现:相同调用序列
        StringBuilder log2 = new StringBuilder();
        Solution.TrafficLight t = new Solution.TrafficLight();
        for (int i = 0; i < cars.length; i++) {
            final int carId = cars[i];
            final int roadId = roads[i];
            t.carArrived(carId, roadId, 0,
                    () -> log2.append("Traffic Light On Road ").append(roadId == 1 ? "A" : "B").append(" Is Green\n"),
                    () -> log2.append("Car ").append(carId).append(" Has Passed Road ").append(roadId == 1 ? "A" : "B").append("\n"));
        }
        System.out.print(log2.toString());
    }
}

复杂度分析

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


1280. Students and Examinations(学生们参加各科测试的次数)

题目类型:数据库(SQL)

题目描述

学生表: Students

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| student_id    | int     |
| student_name  | varchar |
+---------------+---------+
在 SQL 中,主键为 student_id(学生ID)。
该表内的每一行都记录有学校一名学生的信息。

科目表: Subjects

复制代码
+--------------+---------+
| Column Name  | Type    |
+--------------+---------+
| subject_name | varchar |
+--------------+---------+
在 SQL 中,主键为 subject_name(科目名称)。
每一行记录学校的一门科目名称。

考试表: Examinations

复制代码
+--------------+---------+
| Column Name  | Type    |
+--------------+---------+
| student_id   | int     |
| subject_name | varchar |
+--------------+---------+
这个表可能包含重复数据(换句话说,在 SQL 中,这个表没有主键)。
学生表里的一个学生修读科目表里的每一门科目。
这张考试表的每一行记录就表示学生表里的某个学生参加了一次科目表里某门科目的测试。

查询出每个学生参加每一门科目测试的次数,结果按 student_id 和 subject_name 排序。

查询结构格式如下所示。

示例 1:

复制代码
**输入:**
Students table:
+------------+--------------+
| student_id | student_name |
+------------+--------------+
| 1          | Alice        |
| 2          | Bob          |
| 13         | John         |
| 6          | Alex         |
+------------+--------------+
Subjects table:
+--------------+
| subject_name |
+--------------+
| Math         |
| Physics      |
| Programming  |
+--------------+
Examinations table:
+------------+--------------+
| student_id | subject_name |
+------------+--------------+
| 1          | Math         |
| 1          | Physics      |
| 1          | Programming  |
| 2          | Programming  |
| 1          | Physics      |
| 1          | Math         |
| 13         | Math         |
| 13         | Programming  |
| 13         | Physics      |
| 2          | Math         |
| 1          | Math         |
+------------+--------------+
**输出:**
+------------+--------------+--------------+----------------+
| student_id | student_name | subject_name | attended_exams |
+------------+--------------+--------------+----------------+
| 1          | Alice        | Math         | 3              |
| 1          | Alice        | Physics      | 2              |
| 1          | Alice        | Programming  | 1              |
| 2          | Bob          | Math         | 1              |
| 2          | Bob          | Physics      | 0              |
| 2          | Bob          | Programming  | 1              |
| 6          | Alex         | Math         | 0              |
| 6          | Alex         | Physics      | 0              |
| 6          | Alex         | Programming  | 0              |
| 13         | John         | Math         | 1              |
| 13         | John         | Physics      | 1              |
| 13         | John         | Programming  | 1              |
+------------+--------------+--------------+----------------+
**解释:**
结果表需包含所有学生和所有科目(即便测试次数为0):
Alice 参加了 3 次数学测试, 2 次物理测试,以及 1 次编程测试;
Bob 参加了 1 次数学测试, 1 次编程测试,没有参加物理测试;
Alex 啥测试都没参加;
John  参加了数学、物理、编程测试各 1 次。

解题思路

思考

要统计每个学生每门课的考试次数,包括零次。学生与科目先做笛卡尔积,再左连接考试记录,按学生与科目计数。左连接保留未参加的组合, C O U N T COUNT COUNT 把空连接计为零。

考察CROSS JOIN + LEFT JOIN + 聚合:学生×科目做笛卡尔积全组合,左连接考试表统计每生每科次数,无考试补 0,按 student_id、subject_name 排序。

代码实现

sql 复制代码
# Write your MySQL query statement below
SELECT student_id, student_name, subject_name, COUNT(e.student_id) AS attended_exams
FROM
    Students
    JOIN Subjects
    LEFT JOIN Examinations AS e USING (student_id, subject_name)
GROUP BY 1, 3
ORDER BY 1, 3;

复杂度分析

本题为数据库查询题,不涉及传统算法的时间/空间复杂度,执行效率由数据库优化器决定;建议对查询涉及的过滤列与连接列建立索引以加速。

相关推荐
郭小李1 小时前
spring 中的 @EnableWebMvc注解和WebMvcConfigurer接口
java·后端·spring
香蕉大香蕉(06敬畏市场)2 小时前
高频八股 · JVM(8)
java·jvm·面试·八股·大厂实习
Shaoshing2 小时前
LeetCode100 11. 盛最多水的容器
算法·leetcode
养军博客2 小时前
spirngSecurity 鉴权教程02
java·springsecurity
CoderYanger2 小时前
Java EE 进阶:5. Spring IoC&DI(第二弹):Spring IoC
java·开发语言·spring boot·spring·面试·职场和发展·java-ee
海绵宝宝转agent2 小时前
Leetcode二叉树算法个人理解
笔记·学习·算法·leetcode·深度优先
Rain的Java大神之路3 小时前
面试被LeetCode 27(移除元素)问懵?别再用两层 for 循环了:一道“简单题”吃透双指针原地删除,附面试加分话术 + 11 个避坑清单
后端·leetcode·面试
悟天特斯3 小时前
安防一体化:从“各管各的“到“全局联动“
java·大数据·网络·人工智能·物联网
70后写java的老码农3 小时前
Spring Boot 3.x + ShardingSphere 5.x + MyBatis-Plus 3.x:订单分库分表实战(含两个踩坑记录)
java