LeetCode864~875题目解析

3 阅读11分钟

LeetCode 864-875. 题解合集


864. Shortest Path to Get All Keys(获取所有钥匙的最短路径)

题目描述

给你一个二维网格 grid,其中:

  • '.' 代表一个空房间;
  • '#' 代表一堵墙;
  • '@' 是起点;
  • 小写字母代表钥匙;
  • 大写字母代表锁。
    我们从起点开始出发,一次移动是指向四个基本方向之一行走一个单位空间。一些网格区域有锁。
    如果我们有对应的钥匙,我们才能通过锁的区域。开始时我们没有任何钥匙。
    每次经过一个钥匙格,我们都可以把它拾起来,并锁上它对应的门(即该钥匙格标记为已收集)。
    请你返回获取所有钥匙所需要的移动的最少次数。如果无法到达,请返回 -1。

示例 1

  • 输入:grid = ["@.a..","###.#","b.A.B"]
  • 输出:8
  • 解释:目标是获得所有钥匙,而不是打开所有锁。

示例 2

  • 输入:grid = ["@..aA","..B#.","....b"]
  • 输出:6
    提示:
  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 30
  • grid[i][j] 只含有 '.'、'#'、'@'、'a' - 'f' 以及 'A' - 'F'
  • 钥匙的数目范围是 [1, 6]
  • 每个钥匙都对应至少一个锁,每个锁也对应一把钥匙
  • 钥匙和锁的字母互不相同

解题思路

最短路径同时依赖“位置”与“当前已收集的钥匙集合”,把状态扩展为 (x, y, mask) 做 BFS,
其中 mask 为 6 位二进制位表示 a~f 六把钥匙是否已拿到。
解法一(状态压缩 BFS,主提交):先扫描网格找到起点、统计钥匙总数 K(每把钥匙对应
一个位)。BFS 状态 (x, y, mask);从起点出发,四方向移动:

  • 越界或撞墙跳过;
  • 遇到锁(大写字母)时,必须已持有对应钥匙(mask >> (c-'A') & 1 == 1)才能进入;
  • 遇到钥匙(小写字母)时更新 mask,若 mask == (1<<K)-1 立即返回步数。
    用 dist[x][y][mask] 去重,保证每个状态至多扩展一次。
    解法二(优先队列 Dijkstra 视角,对照实现):把移动代价视为 1,用带 (x,y,mask) 状态的
    堆化队列做 BFS,本质上相同;此处通过自定义对象入队记录步数展示另一种状态组织方式。

代码实现

import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;

class Solution {

    private static final int[] DX = {-1, 1, 0, 0};
    private static final int[] DY = {0, 0, -1, 1};

    // 解法一:状态压缩 BFS(推荐提交)
    public int shortestPathAllKeys(String[] grid) {
        int m = grid.length, n = grid[0].length();
        int sx = 0, sy = 0, keyMask = 0;
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                char c = grid[i].charAt(j);
                if (c == '@') {
                    sx = i;
                    sy = j;
                } else if (c >= 'a' && c <= 'f') {
                    keyMask |= 1 << (c - 'a');
                }
            }
        }
        boolean[][][] vis = new boolean[m][n][64];
        Queue<int[]> q = new ArrayDeque<>();   // {x, y, mask}
        q.offer(new int[]{sx, sy, 0});
        vis[sx][sy][0] = true;
        int step = 0;
        while (!q.isEmpty()) {
            int size = q.size();
            for (int round = 0; round < size; ++round) {
                int[] cur = q.poll();
                int x = cur[0], y = cur[1], mask = cur[2];
                if (mask == keyMask) {
                    return step;
                }
                for (int d = 0; d < 4; ++d) {
                    int nx = x + DX[d], ny = y + DY[d];
                    if (nx < 0 || nx >= m || ny < 0 || ny >= n) {
                        continue;
                    }
                    char c = grid[nx].charAt(ny);
                    if (c == '#') {
                        continue;
                    }
                    int nmask = mask;
                    if (c >= 'A' && c <= 'F') {
                        if ((mask & (1 << (c - 'A'))) == 0) {
                            continue;   // 没有对应钥匙不能通过
                        }
                    } else if (c >= 'a' && c <= 'f') {
                        nmask |= 1 << (c - 'a');
                    }
                    if (!vis[nx][ny][nmask]) {
                        vis[nx][ny][nmask] = true;
                        q.offer(new int[]{nx, ny, nmask});
                    }
                }
            }
            ++step;
        }
        return -1;
    }

    // 解法二:带步数的状态队列(对照实现)
    public int shortestPathAllKeys2(String[] grid) {
        int m = grid.length, n = grid[0].length();
        int sx = 0, sy = 0, keyMask = 0;
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                char c = grid[i].charAt(j);
                if (c == '@') {
                    sx = i;
                    sy = j;
                } else if (c >= 'a' && c <= 'f') {
                    keyMask |= 1 << (c - 'a');
                }
            }
        }
        Map<String, Integer> dist = new HashMap<>();
        Queue<int[]> q = new ArrayDeque<>();   // {x, y, mask, dist}
        q.offer(new int[]{sx, sy, 0, 0});
        dist.put(sx + "," + sy + "," + 0, 0);
        int ans = Integer.MAX_VALUE;
        while (!q.isEmpty()) {
            int[] cur = q.poll();
            int x = cur[0], y = cur[1], mask = cur[2], d = cur[3];
            if (mask == keyMask) {
                ans = Math.min(ans, d);
                continue;
            }
            for (int dir = 0; dir < 4; ++dir) {
                int nx = x + DX[dir], ny = y + DY[dir];
                if (nx < 0 || nx >= m || ny < 0 || ny >= n) {
                    continue;
                }
                char c = grid[nx].charAt(ny);
                if (c == '#') {
                    continue;
                }
                int nmask = mask;
                if (c >= 'A' && c <= 'F') {
                    if ((mask & (1 << (c - 'A'))) == 0) {
                        continue;
                    }
                } else if (c >= 'a' && c <= 'f') {
                    nmask |= 1 << (c - 'a');
                }
                String key = nx + "," + ny + "," + nmask;
                Integer old = dist.get(key);
                if (old == null || old > d + 1) {
                    dist.put(key, d + 1);
                    q.offer(new int[]{nx, ny, nmask, d + 1});
                }
            }
        }
        return ans == Integer.MAX_VALUE ? -1 : ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[][] grids = {
            {"@.a..", "###.#", "b.A.B"},   // 官方示例 1
            {"@..aA", "..B#.", "....b"},   // 官方示例 2
            {"@Aa"},                       // 官方示例 3:钥匙被锁挡在墙后,不可达
            {"@a.", "b##", ".c#"},         // 无锁,收集 3 把钥匙需 5 步
            {"@.a..A.b"}                   // 需先拿 a 再过 A 拿 b,共 7 步
        };
        int[] exp = {8, 6, -1, 5, 7};
        for (int t = 0; t < grids.length; ++t) {
            ++total;
            int g1 = s.shortestPathAllKeys(grids[t]);
            int g2 = s.shortestPathAllKeys2(grids[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 864-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0864 PASS ==");
        } else {
            System.out.println("== 0864 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(m * n * 2^K)O(m * n * 2^K)
解法二O(m * n * 2^K)O(m * n * 2^K)

865. Smallest Subtree with all the Deepest Nodes(具有所有最深节点的最小子树)

题目描述

给定一个根为 root 的二叉树,每个节点的深度是它到根的最短距离。
返回包含原树中所有最深节点的最小子树。如果一个节点是整棵树的叶子节点且它的深度最大,
那么它自己就是包含所有最深节点的最小子树。

示例 1

  • 输入:root = [3,5,1,6,2,0,8,null,null,7,4]
  • 输出:[2,7,4]
  • 解释:最深节点为 7 和 4(深度 3),包含它们的最小子树是以 2 为根的子树。

示例 2

  • 输入:root = [1]
  • 输出:[1]

示例 3

  • 输入:root = [0,1,3,null,2]
  • 输出:[2]
  • 解释:节点 2 是唯一的叶子节点,深度 2 最大,所以它自己就是答案。
    提示:
  • 树中节点数量在 [1, 500] 范围内
  • 0 <= Node.val <= 500
  • 每个节点的值都是独一无二的

解题思路

所有最深节点的最近公共祖先就是答案。可以自底向上判定:哪个子树“同时包含所有最深节点”。
解法一(两遍 DFS:最大深度 + 定位 LCA,主提交):第一遍求出整棵树最大深度 maxDepth;
第二遍从根开始找“同时包含 maxDepth 层节点的最深子树”:若当前节点深度为 maxDepth,
它本身就是答案;否则递归左右子树,若左右子树都存在深度 maxDepth 的节点,当前节点就是
答案,否则返回包含最深节点的那个子树。
解法二(递归返回 (深度, 子树),对照实现):每个节点返回 (maxDepth, 该深度对应的子树);
若左右子树最大深度相同且都达到全局最大,则合并返回当前节点;否则返回更深一侧的结果。
只需一遍 DFS。

代码实现

import java.util.ArrayDeque;
import java.util.Queue;

class Solution {

    // 解法一:最大深度 + LCA 定位(推荐提交)
    public TreeNode subtreeWithAllDeepest(TreeNode root) {
        int maxDepth = depth(root);
        return find(root, 1, maxDepth);
    }

    private int depth(TreeNode node) {
        if (node == null) {
            return 0;
        }
        return 1 + Math.max(depth(node.left), depth(node.right));
    }

    // 返回包含所有“深度 == maxDepth”节点的最小子树
    private TreeNode find(TreeNode node, int d, int maxDepth) {
        if (node == null) {
            return null;
        }
        if (d == maxDepth) {
            return node;   // 当前节点就是最深节点
        }
        TreeNode l = find(node.left, d + 1, maxDepth);
        TreeNode r = find(node.right, d + 1, maxDepth);
        if (l != null && r != null) {
            return node;   // 左右都有最深节点,交汇于此
        }
        return l != null ? l : r;
    }

    // 解法二:一遍 DFS 返回 (深度, 子树)(对照实现)
    public TreeNode subtreeWithAllDeepest2(TreeNode root) {
        // 返回二元组:deepest(root) = {最大深度, 覆盖全部最深节点的子树}
        return post(root).node;
    }

    private Result post(TreeNode node) {
        if (node == null) {
            return new Result(0, null);
        }
        Result l = post(node.left);
        Result r = post(node.right);
        if (l.depth == r.depth) {
            return new Result(l.depth + 1, node);   // 两侧同深,当前节点成为 LCA
        }
        if (l.depth > r.depth) {
            return new Result(l.depth + 1, l.node);
        }
        return new Result(r.depth + 1, r.node);
    }

    private static class Result {
        int depth;
        TreeNode node;
        Result(int depth, TreeNode node) {
            this.depth = depth;
            this.node = node;
        }
    }

    // 二叉树节点定义
    static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        TreeNode(int x) {
            val = x;
        }
    }

    // 本地测试辅助:层序建树
    private static TreeNode build(Integer[] vals) {
        if (vals == null || vals.length == 0) {
            return null;
        }
        TreeNode root = new TreeNode(vals[0]);
        Queue<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        int i = 1;
        while (!q.isEmpty() && i < vals.length) {
            TreeNode cur = q.poll();
            if (vals[i] != null) {
                cur.left = new TreeNode(vals[i]);
                q.offer(cur.left);
            }
            ++i;
            if (i < vals.length && vals[i] != null) {
                cur.right = new TreeNode(vals[i]);
                q.offer(cur.right);
            }
            ++i;
        }
        return root;
    }

    // 树结构转层序字符串(便于对比;LinkedList 允许 null)
    private static String dump(TreeNode root) {
        if (root == null) {
            return "[]";
        }
        StringBuilder sb = new StringBuilder("[");
        Queue<TreeNode> q = new java.util.LinkedList<>();
        q.offer(root);
        while (!q.isEmpty()) {
            TreeNode cur = q.poll();
            if (cur == null) {
                sb.append("null,");
                continue;
            }
            sb.append(cur.val).append(",");
            q.offer(cur.left);
            q.offer(cur.right);
        }
        // 去掉尾部 null
        String s = sb.toString();
        while (s.endsWith("null,")) {
            s = s.substring(0, s.length() - 5);
        }
        return s.substring(0, s.length() - 1) + "]";
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        ++total;
        {
            TreeNode root = build(new Integer[]{3, 5, 1, 6, 2, 0, 8, null, null, 7, 4});
            if (dump(s.subtreeWithAllDeepest(root)).equals("[2,7,4]")
                    && dump(s.subtreeWithAllDeepest2(root)).equals("[2,7,4]")) {
                ++pass;
            } else {
                System.out.println("FAIL 865-1: " + dump(s.subtreeWithAllDeepest(root)));
            }
        }
        ++total;
        {
            TreeNode root = build(new Integer[]{1});
            if (dump(s.subtreeWithAllDeepest(root)).equals("[1]")) {
                ++pass;
            } else {
                System.out.println("FAIL 865-2");
            }
        }
        ++total;
        {
            TreeNode root = build(new Integer[]{0, 1, 3, null, 2});
            TreeNode got = s.subtreeWithAllDeepest(root);
            if (got != null && got.val == 2 && s.subtreeWithAllDeepest2(root).val == 2) {
                ++pass;
            } else {
                System.out.println("FAIL 865-3: " + (got == null ? "null" : got.val));
            }
        }
        ++total;
        {
            TreeNode root = build(new Integer[]{1, 2, 3, 4});
            if (dump(s.subtreeWithAllDeepest(root)).equals("[4]")) {
                ++pass;
            } else {
                System.out.println("FAIL 865-4: " + dump(s.subtreeWithAllDeepest(root)));
            }
        }
        if (pass == total) {
            System.out.println("== 0865 PASS ==");
        } else {
            System.out.println("== 0865 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(h)(树高,递归栈)
解法二O(n)O(h)

866. Prime Palindrome(回文质数)

题目描述

返回大于或等于 n 的最小回文质数。
一个整数如果它本身是回文数(正读反读相同),并且是质数,那么它就是回文质数。

示例 1

  • 输入:n = 6
  • 输出:7

示例 2

  • 输入:n = 8
  • 输出:11

示例 3

  • 输入:n = 13
  • 输出:101
    提示:
  • 1 <= n <= 10^8
  • 答案确保是一个小于 2 * 10^8 的 32 位有符号整数

解题思路

直接枚举每个整数判断回文再判断质数可行但较慢;利用“偶数长度的回文数一定被 11 整除
(除 11 本身)”,因此长度大于 1 的回文质数必为奇数位。可从低到高枚举回文数的“上半部分”,
构造出完整回文后检查是否 >= n 且为质数,保证答案按数值升序遍历。
解法一(枚举回文数,主提交):从 1 位开始向上枚举奇数位长度,对每个长度枚举“前一半”
的数字 i(最小为 10^(len-1) 的数量级),镜像构造回文,跳过偶数长度(除 11),逐个判断
质数,第一个 >= n 且为质数的即是答案。
解法二(枚举整数 + 改进质数判断,对照实现):从 n 开始逐个整数试探,若该数是回文且是
质数则返回;通过数字转字符串判回文、质数判断只试到 sqrt。在题目范围内通常足够快,
但最坏情况可能多尝试一些数字,思路更直接。

代码实现

class Solution {

    // 解法一:枚举回文数(推荐提交)
    public int primePalindrome(int n) {
        if (n <= 2) {
            return 2;
        }
        if (n == 3) {
            return 3;
        }
        // 2、3、5、7、11 特判
        if (n <= 5) {
            return 5;
        }
        if (n <= 7) {
            return 7;
        }
        if (n <= 11) {
            return 11;
        }
        // 构造长度大于等于从 n 的位数开始(答案位数 >= n 的位数)
        for (int len = String.valueOf(n).length(); ; ++len) {
            if (len % 2 == 0) {
                // 偶数位回文数(除 11)均为 11 的倍数,跳过
                if (len == 2) {
                    continue;   // 11 已特判
                }
                continue;
            }
            int half = (len + 1) / 2;          // 前半部分长度
            long lo = (long) Math.pow(10, half - 1);
            long hi = (long) Math.pow(10, half);   // 前半部分范围 [lo, hi)
            for (long prefix = lo; prefix < hi; ++prefix) {
                long pal = mirror(prefix);
                if (pal < n) {
                    continue;
                }
                if (isPrime(pal)) {
                    return (int) pal;
                }
            }
        }
    }

    // 由前半部分构造奇数位回文数
    private long mirror(long prefix) {
        String s = Long.toString(prefix);
        String rev = new StringBuilder(s).reverse().toString();
        return Long.parseLong(s + rev.substring(1));
    }

    private boolean isPrime(long x) {
        if (x < 2) {
            return false;
        }
        if (x == 2 || x == 3) {
            return true;
        }
        if (x % 2 == 0 || x % 3 == 0) {
            return false;
        }
        for (long i = 5; i * i <= x; i += 6) {
            if (x % i == 0 || x % (i + 2) == 0) {
                return false;
            }
        }
        return true;
    }

    // 解法二:从 n 逐个枚举(对照实现)
    public int primePalindrome2(int n) {
        if (n <= 2) {
            return 2;
        }
        for (int x = n; ; ++x) {
            // 偶数位回文数跳过(除 11)
            if (x > 11 && String.valueOf(x).length() % 2 == 0) {
                // 跳到下一个奇数位长度起点:10^(digits)
                int digits = String.valueOf(x).length();
                x = (int) Math.pow(10, digits) - 1;   // 循环 ++ 后为 10^digits
                continue;
            }
            if (isPalindrome(x) && isPrime(x)) {
                return x;
            }
        }
    }

    private boolean isPalindrome(int x) {
        String s = Integer.toString(x);
        int l = 0, r = s.length() - 1;
        while (l < r) {
            if (s.charAt(l) != s.charAt(r)) {
                return false;
            }
            ++l;
            --r;
        }
        return true;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[] ns = {6, 8, 13, 1, 2, 3, 11, 100, 9989900};
        int[] exp = {7, 11, 101, 2, 2, 3, 11, 101, 100030001};
        for (int t = 0; t < ns.length; ++t) {
            ++total;
            int g1 = s.primePalindrome(ns[t]);
            int g2 = s.primePalindrome2(ns[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 866-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0866 PASS ==");
        } else {
            System.out.println("== 0866 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(10^(L/2) * sqrt(10^L)),L 为答案位数O(1)
解法二O(答案与 n 的差 * sqrt(答案))O(1)

867. Transpose Matrix(转置矩阵)

题目描述

给你一个二维整数数组 matrix,返回 matrix 的转置矩阵。
矩阵的转置是指将矩阵的主对角线翻转,交换矩阵的行索引与列索引。

示例 1

  • 输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
  • 输出:[[1,4,7],[2,5,8],[3,6,9]]

示例 2

  • 输入:matrix = [[1,2,3],[4,5,6]]
  • 输出:[[1,4],[2,5],[3,6]]
    提示:
  • m == matrix.length
  • n == matrix[i].length
  • 1 <= m, n <= 1000
  • 1 <= m * n <= 10^5
  • -10^9 <= matrix[i][j] <= 10^9

解题思路

转置就是把 matrix[i][j] 放到 res[j][i]。原矩阵是 m×n,转置后是 n×m。
解法一(直接构造,主提交):申请 n x m 的新数组,双重循环赋值。时间 O(mn),空间 O(mn)。
解法二(逐列填充,对照实现):逐列处理原矩阵,把第 j 列的所有元素拷贝为新矩阵第 j
行。逻辑等价,便于理解列优先的视角。

代码实现

class Solution {

    // 解法一:直接构造(推荐提交)
    public int[][] transpose(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        int[][] res = new int[n][m];
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                res[j][i] = matrix[i][j];
            }
        }
        return res;
    }

    // 解法二:逐列填充(对照实现)
    public int[][] transpose2(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        int[][] res = new int[n][m];
        for (int j = 0; j < n; ++j) {
            for (int i = 0; i < m; ++i) {
                res[j][i] = matrix[i][j];
            }
        }
        return res;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][][] mats = {
            {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}},
            {{1, 2, 3}, {4, 5, 6}},
            {{1}},
            {{1, 2}, {3, 4}, {5, 6}}
        };
        int[][][] exps = {
            {{1, 4, 7}, {2, 5, 8}, {3, 6, 9}},
            {{1, 4}, {2, 5}, {3, 6}},
            {{1}},
            {{1, 3, 5}, {2, 4, 6}}
        };
        for (int t = 0; t < mats.length; ++t) {
            ++total;
            if (java.util.Arrays.deepEquals(s.transpose(mats[t]), exps[t])
                    && java.util.Arrays.deepEquals(s.transpose2(mats[t]), exps[t])) {
                ++pass;
            } else {
                System.out.println("FAIL 867-" + (t + 1));
            }
        }
        if (pass == total) {
            System.out.println("== 0867 PASS ==");
        } else {
            System.out.println("== 0867 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(m * n)O(m * n)
解法二O(m * n)O(m * n)

868. Binary Gap(二进制间距)

题目描述

给定一个正整数 n,找到并返回 n 的二进制表示中两个相邻 1 之间的最长距离。如果不存在两个
相邻的 1,则返回 0。
如果只有 0 将两个 1 分隔开(可能不存在 0),则认为这两个 1 彼此相邻。两个 1 之间的距离是
它们二进制表示中位置差值的绝对值。例如,"1001" 中的两个 1 之间的距离为 3。

示例 1

  • 输入:n = 22
  • 输出:2
  • 解释:22 的二进制是 "10110"。
    在 22 的二进制表示中,有三个 1,组成两对相邻的 1。
    第一对相邻的 1 中,两个 1 之间的距离为 2。
    第二对相邻的 1 中,两个 1 之间的距离为 1。
    答案取两个距离之中最大的,也就是 2。

示例 2

  • 输入:n = 8
  • 输出:0
  • 解释:8 的二进制是 "1000"。在 8 的二进制表示中没有相邻的两个 1,所以返回 0。

示例 3

  • 输入:n = 5
  • 输出:2
  • 解释:5 的二进制是 "101",两个相邻的 1 之间的距离为 2。
    提示:
  • 1 <= n <= 10^9

解题思路

相邻两个 1 的间距是它们在二进制串中的位置差。逐位扫描二进制位,记录上一个 1 的位置。
解法一(位扫描,主提交):用 Integer 从最低位到最高位依次检查:若当前位为 1,且之前
出现过 1,则用当前位置与上次位置的差更新答案;再用 n >>= 1 或直接用 position 递增
模拟位权。
解法二(toString 法,对照实现):Integer.toBinaryString(n) 得到二进制串,扫描找相邻 1
的位置差最大值;思路直观,借助字符串 API。

代码实现

class Solution {

    // 解法一:位扫描(推荐提交)
    public int binaryGap(int n) {
        int prev = -1;
        int ans = 0;
        for (int i = 0; i < 31; ++i) {
            if (((n >> i) & 1) == 1) {
                if (prev != -1) {
                    ans = Math.max(ans, i - prev);
                }
                prev = i;
            }
        }
        return ans;
    }

    // 解法二:二进制字符串法(对照实现)
    public int binaryGap2(int n) {
        String s = Integer.toBinaryString(n);
        int prev = -1;
        int ans = 0;
        for (int i = 0; i < s.length(); ++i) {
            if (s.charAt(i) == '1') {
                if (prev != -1) {
                    ans = Math.max(ans, i - prev);
                }
                prev = i;
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[] ns = {22, 8, 5, 1, 6, 0};
        int[] exp = {2, 0, 2, 0, 1, 0};
        for (int t = 0; t < ns.length; ++t) {
            ++total;
            if (s.binaryGap(ns[t]) == exp[t] && s.binaryGap2(ns[t]) == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 868-" + (t + 1) + ": got " + s.binaryGap(ns[t]) + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0868 PASS ==");
        } else {
            System.out.println("== 0868 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(log n)O(1)
解法二O(log n)O(log n)(字符串)

869. Reordered Power of 2(重新排序得到 2 的幂)

题目描述

给定正整数 n,我们按任何顺序(包括原始顺序)将数字重新排序,注意其前导数字不能为零。
如果我们可以通过上述方式得到 2 的幂,返回 true;否则返回 false。

示例 1

  • 输入:n = 1
  • 输出:true

示例 2

  • 输入:n = 10
  • 输出:false

示例 3

  • 输入:n = 46
  • 输出:true
    提示:
  • 1 <= n <= 10^9

解题思路

数字重排只改变数位顺序,不改变各数字的出现次数。因此只需比较 n 的数位频次是否与
某个 2 的幂完全一致。由于 n <= 10^9,只需枚举不超过 30 个 2 的幂。
解法一(频次数组比较,主提交):把 n 转成 10 位长频次数组(每位数字计数),枚举
1 << k(k = 0..30)检查其数位频次是否与 n 相同,相同返回 true,否则 false。
解法二(排序字符串比较,对照实现):把 n 的十进制字符串排序与所有 2 的幂的十进制
字符串排序比对,借助排序避免手写频次数组,思路等价。

代码实现

import java.util.HashSet;
import java.util.Set;

class Solution {

    // 解法一:频次数组比较(推荐提交)
    public boolean reorderedPowerOf2(int n) {
        int[] cnt = count(n);
        for (int k = 0; k < 31; ++k) {
            if (same(cnt, count(1 << k))) {
                return true;
            }
        }
        return false;
    }

    private int[] count(int x) {
        int[] c = new int[10];
        String s = Integer.toString(x);
        for (int i = 0; i < s.length(); ++i) {
            ++c[s.charAt(i) - '0'];
        }
        return c;
    }

    private boolean same(int[] a, int[] b) {
        for (int i = 0; i < 10; ++i) {
            if (a[i] != b[i]) {
                return false;
            }
        }
        return true;
    }

    // 解法二:排序字符串比较(对照实现)
    public boolean reorderedPowerOf2_2(int n) {
        String target = sortedStr(n);
        for (int k = 0; k < 31; ++k) {
            if (target.equals(sortedStr(1 << k))) {
                return true;
            }
        }
        return false;
    }

    private String sortedStr(int x) {
        char[] cs = Integer.toString(x).toCharArray();
        java.util.Arrays.sort(cs);
        return new String(cs);
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[] ns = {1, 10, 46, 24, 679, 111, 1000, 1024};
        boolean[] exp = {true, false, true, false, false, false, false, true};
        for (int t = 0; t < ns.length; ++t) {
            ++total;
            if (s.reorderedPowerOf2(ns[t]) == exp[t]
                    && s.reorderedPowerOf2_2(ns[t]) == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 869-" + (t + 1) + ": got " + s.reorderedPowerOf2(ns[t]) + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0869 PASS ==");
        } else {
            System.out.println("== 0869 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(31 * 10)(数位最多 10 位)O(1)
解法二O(31 * d log d),d 为数位O(d)

870. Advantage Shuffle(优势洗牌)

题目描述

给定两个长度相等的数组 nums1 和 nums2,nums1 相对于 nums2 的优势可以用满足
nums1[i] > nums2[i] 的索引 i 的数目来描述。
返回 nums1 的任意排列,使其相对于 nums2 的优势最大化。

示例 1

  • 输入:nums1 = [2,7,11,15], nums2 = [1,10,4,11]
  • 输出:[2,11,7,15]

示例 2

  • 输入:nums1 = [12,24,8,32], nums2 = [13,25,32,11]
  • 输出:[24,32,8,12]
    提示:
  • 1 <= nums1.length <= 10^5
  • 0 <= nums1[i] <= 10^9
  • 0 <= nums2[i] <= 10^9
  • nums1.length == nums2.length

解题思路

与“田忌赛马”同理:用最小代价去消耗对方最强的牌,用强牌去对抗对方比自己弱的牌。
解法一(排序 + 双指针,主提交):把 nums1 升序排序,nums2 的下标按值升序排序。
用双指针处理已排序的 nums1:对于 nums2 中最小的尚未匹配的元素,若 nums1[l] 能赢它
(nums1[l] > nums2[i])就配对并 l++;否则把 nums1[l] 这张“废牌”配给 nums2 中
当前最大的元素(消耗对方强牌),l++、r--。这样最大化胜场数。
解法二(TreeMap 贪心,对照实现):把 nums1 放进 TreeMap 计数;对每个 nums2[i],
用 higherKey(nums2[i]) 找比它大的最小牌,有则用,没有则用当前最小牌(放弃本局)。
思路直观,O(n log n)。

代码实现

import java.util.Arrays;

class Solution {

    // 解法一:排序 + 双指针(推荐提交)
    public int[] advantageCount(int[] nums1, int[] nums2) {
        int n = nums1.length;
        Arrays.sort(nums1);
        Integer[] idx = new Integer[n];
        for (int i = 0; i < n; ++i) {
            idx[i] = i;
        }
        // nums2 的索引按值升序
        Arrays.sort(idx, (a, b) -> Integer.compare(nums2[a], nums2[b]));
        int[] ans = new int[n];
        int left = 0, right = n - 1;
        for (int i = n - 1; i >= 0; --i) {
            int j = idx[i];    // 当前 nums2 中最大元素的下标
            if (nums1[right] > nums2[j]) {
                ans[j] = nums1[right--];   // 用最大的牌赢过对方最大值
            } else {
                ans[j] = nums1[left++];    // 赢不了则用最小的牌消耗对方最大值
            }
        }
        return ans;
    }

    // 解法二:TreeMap 贪心(对照实现)
    public int[] advantageCount2(int[] nums1, int[] nums2) {
        int n = nums1.length;
        java.util.TreeMap<Integer, Integer> map = new java.util.TreeMap<>();
        for (int x : nums1) {
            map.put(x, map.getOrDefault(x, 0) + 1);
        }
        int[] ans = new int[n];
        for (int i = 0; i < n; ++i) {
            Integer better = map.higherKey(nums2[i]);
            if (better == null) {
                better = map.firstKey();      // 放弃本局,用最小的牌
            }
            ans[i] = better;
            if (map.get(better) == 1) {
                map.remove(better);
            } else {
                map.put(better, map.get(better) - 1);
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        // 期待值不定,改为验证得分(优势数)是否达到最优
        int[][] n1s = {
            {2, 7, 11, 15}, {12, 24, 8, 32}, {1, 1, 1, 2}, {2, 4, 6}, {1, 2, 3}
        };
        int[][] n2s = {
            {1, 10, 4, 11}, {13, 25, 32, 11}, {1, 1, 1, 1}, {3, 1, 2}, {1, 2, 3}
        };
        int[] best = {4, 3, 1, 3, 2};
        for (int t = 0; t < n1s.length; ++t) {
            ++total;
            int score1 = score(s.advantageCount(n1s[t], n2s[t]), n2s[t]);
            int score2 = score(s.advantageCount2(n1s[t], n2s[t]), n2s[t]);
            if (score1 == best[t] && score2 == best[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 870-" + (t + 1) + ": score " + score1 + "/" + score2 + " best " + best[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0870 PASS ==");
        } else {
            System.out.println("== 0870 FAIL ==");
            System.exit(1);
        }
    }

    private static int score(int[] res, int[] nums2) {
        int cnt = 0;
        for (int i = 0; i < res.length; ++i) {
            if (res[i] > nums2[i]) {
                ++cnt;
            }
        }
        return cnt;
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n log n)O(n)
解法二O(n log n)O(n)

871. Minimum Number of Refueling Stops(最低加油次数)

题目描述

汽车从起点出发驶向目的地,该目的地位于出发位置东面 target 英里处。
沿途有加油站,用数组 stations 表示。其中 stations[i] = [position_i, fuel_i] 表示第 i
个加油站位于出发位置东面 position_i 英里处,并且有 fuel_i 升汽油。
假设汽车油箱的容量是无限的,其中最初有 startFuel 升燃料。它每行驶 1 英里就会用掉 1 升
汽油。当汽车到达加油站时,它可能停下来加油,将所有汽油从加油站转移到汽车中。
为了到达目的地,汽车所必要的最低加油次数是多少?如果无法到达目的地,则返回 -1。
注意:如果汽车到达加油站时剩余燃料为 0,它仍然可以在那里加油。如果汽车到达目的地时
剩余燃料为 0,仍然认为它已经到达目的地。

示例 1

  • 输入:target = 1, startFuel = 1, stations = []
  • 输出:0

示例 2

  • 输入:target = 100, startFuel = 1, stations = [[10,100]]
  • 输出:-1

示例 3

  • 输入:target = 100, startFuel = 10, stations = [[10,60],[20,30],[30,30],[60,40]]
  • 输出:2
  • 解释:在 10 和 60 两个加油站各加一次油。
    提示:
  • 1 <= target, startFuel <= 10^9
  • 0 <= stations.length <= 500
  • 1 <= position_i < position_{i+1} < target
  • 1 <= fuel_i < 10^9

解题思路

贪心 + “后悔”思想:把已经经过的加油站的油量放入大根堆,当油不够开到下一站时,从堆中取
出最大油量“补加”,尽可能用最少的次数获得最多的油。
解法一(贪心 + 大根堆,主提交):把终点也当作一个 position = target, fuel = 0 的站点
参与扫描。维护当前位置 pre、当前油量 fuel、大根堆 pq(存已经过的站点的油量)。
依次推进:每到一个站点 pos,fuel -= pos - pre;若 fuel < 0,从 pq 中反复弹出
最大油量加入 fuel 并计数,直到 fuel >= 0;若 pq 空仍不够则返回 -1;把该站油量入堆,
更新 pre。遍历结束返回加油次数。
解法二(动态规划,对照实现):dp[i] 表示加油 i 次能到达的最远距离,初始 dp[0] = startFuel。
遍历每个加油站,从大到小更新 dp[j + 1] = max(dp[j+1], dp[j] + fuel_i)(前提 dp[j] >= position_i),
第一个 dp[i] >= target 的 i 就是答案。O(n^2)。

代码实现

import java.util.PriorityQueue;

class Solution {

    // 解法一:贪心 + 大根堆(推荐提交)
    public int minRefuelStops(int target, int startFuel, int[][] stations) {
        PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> Integer.compare(b, a));
        int ans = 0;
        int pre = 0;
        int fuel = startFuel;
        int n = stations.length;
        for (int i = 0; i <= n; ++i) {
            int pos = i < n ? stations[i][0] : target;
            int dist = pos - pre;
            fuel -= dist;
            while (fuel < 0 && !pq.isEmpty()) {
                fuel += pq.poll();
                ++ans;
            }
            if (fuel < 0) {
                return -1;
            }
            if (i < n) {
                pq.offer(stations[i][1]);
                pre = pos;
            }
        }
        return ans;
    }

    // 解法二:动态规划(对照实现)
    public int minRefuelStops2(int target, int startFuel, int[][] stations) {
        int n = stations.length;
        long[] dp = new long[n + 1];   // dp[i] = 加油 i 次能到的最远距离
        dp[0] = startFuel;
        for (int i = 0; i < n; ++i) {
            for (int j = i; j >= 0; --j) {
                if (dp[j] >= stations[i][0]) {
                    dp[j + 1] = Math.max(dp[j + 1], dp[j] + stations[i][1]);
                }
            }
        }
        for (int i = 0; i <= n; ++i) {
            if (dp[i] >= target) {
                return i;
            }
        }
        return -1;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        ++total;
        if (s.minRefuelStops(1, 1, new int[][]{}) == 0
                && s.minRefuelStops2(1, 1, new int[][]{}) == 0) {
            ++pass;
        } else {
            System.out.println("FAIL 871-1");
        }
        ++total;
        if (s.minRefuelStops(100, 1, new int[][]{{10, 100}}) == -1
                && s.minRefuelStops2(100, 1, new int[][]{{10, 100}}) == -1) {
            ++pass;
        } else {
            System.out.println("FAIL 871-2");
        }
        ++total;
        if (s.minRefuelStops(100, 10, new int[][]{{10, 60}, {20, 30}, {30, 30}, {60, 40}}) == 2
                && s.minRefuelStops2(100, 10, new int[][]{{10, 60}, {20, 30}, {30, 30}, {60, 40}}) == 2) {
            ++pass;
        } else {
            System.out.println("FAIL 871-3: " + s.minRefuelStops(100, 10, new int[][]{{10, 60}, {20, 30}, {30, 30}, {60, 40}}));
        }
        ++total;
        if (s.minRefuelStops(1000, 1, new int[][]{}) == -1
                && s.minRefuelStops2(1000, 1, new int[][]{}) == -1) {
            ++pass;
        } else {
            System.out.println("FAIL 871-4");
        }
        ++total;
        if (s.minRefuelStops(100, 50, new int[][]{{10, 60}, {20, 100}, {30, 30}}) == 1
                && s.minRefuelStops2(100, 50, new int[][]{{10, 60}, {20, 100}, {30, 30}}) == 1) {
            ++pass;
        } else {
            System.out.println("FAIL 871-5: " + s.minRefuelStops(100, 50, new int[][]{{10, 60}, {20, 100}, {30, 30}}));
        }
        if (pass == total) {
            System.out.println("== 0871 PASS ==");
        } else {
            System.out.println("== 0871 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n log n)O(n)
解法二O(n^2)O(n)

872. Leaf-Similar Trees(叶子相似的树)

题目描述

请考虑一棵二叉树上所有的叶子,这些叶子的值按从左到右的顺序排列形成一个叶值序列。
如果有两棵二叉树的叶值序列是相同,那么我们就认为它们是叶相似的。
如果给定的两个根结点分别为 root1 和 root2 的树是叶相似的,则返回 true;否则返回
false。

示例 1

  • 输入:root1 = [3,5,1,6,2,9,8,null,null,7,4], root2 = [3,5,1,6,7,4,2,null,null,null,null,null,null,9,8]
  • 输出:true

示例 2

  • 输入:root1 = [1,2,3], root2 = [1,3,2]
  • 输出:false
    提示:
  • 给定的两棵树结点数在 [1, 200] 范围内
  • 给定的两棵树上的值在 [0, 200] 范围内

解题思路

叶值序列是按“从左到右”的先序(中序亦可,叶子顺序天然一致)收集叶子节点得到的结果。
解法一(DFS 收集两棵树的叶子,主提交):递归遍历,左右孩子都为空即为叶子,按先序顺序
(先左后右)收集;得到两个序列直接相等判断。
解法二(迭代栈 DFS,对照实现):用显式栈模拟先序遍历收集叶子,避免递归栈深度占用;
两棵树的叶子序列分别收集后比较。实现不依赖系统栈,思路与解法一相同。

代码实现

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;

class Solution {

    // 解法一:DFS 递归收集(推荐提交)
    public boolean leafSimilar(TreeNode root1, TreeNode root2) {
        List<Integer> l1 = new ArrayList<>();
        List<Integer> l2 = new ArrayList<>();
        dfs(root1, l1);
        dfs(root2, l2);
        return l1.equals(l2);
    }

    private void dfs(TreeNode node, List<Integer> leaves) {
        if (node == null) {
            return;
        }
        if (node.left == null && node.right == null) {
            leaves.add(node.val);
            return;
        }
        dfs(node.left, leaves);
        dfs(node.right, leaves);
    }

    // 解法二:迭代栈(对照实现)
    public boolean leafSimilar2(TreeNode root1, TreeNode root2) {
        return collect(root1).equals(collect(root2));
    }

    private List<Integer> collect(TreeNode root) {
        List<Integer> leaves = new ArrayList<>();
        if (root == null) {
            return leaves;
        }
        ArrayDeque<TreeNode> stack = new ArrayDeque<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode cur = stack.pop();
            if (cur.left == null && cur.right == null) {
                leaves.add(cur.val);
                continue;
            }
            // 先右后左,保证弹出时先处理左子树
            if (cur.right != null) {
                stack.push(cur.right);
            }
            if (cur.left != null) {
                stack.push(cur.left);
            }
        }
        return leaves;
    }

    // 二叉树节点定义
    static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;
        TreeNode(int x) {
            val = x;
        }
    }

    // 本地测试辅助:层序建树
    private static TreeNode build(Integer[] vals) {
        if (vals == null || vals.length == 0) {
            return null;
        }
        TreeNode root = new TreeNode(vals[0]);
        Queue<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        int i = 1;
        while (!q.isEmpty() && i < vals.length) {
            TreeNode cur = q.poll();
            if (vals[i] != null) {
                cur.left = new TreeNode(vals[i]);
                q.offer(cur.left);
            }
            ++i;
            if (i < vals.length && vals[i] != null) {
                cur.right = new TreeNode(vals[i]);
                q.offer(cur.right);
            }
            ++i;
        }
        return root;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        ++total;
        {
            TreeNode r1 = build(new Integer[]{3, 5, 1, 6, 2, 9, 8, null, null, 7, 4});
            TreeNode r2 = build(new Integer[]{3, 5, 1, 6, 7, 4, 2, null, null, null, null, null, null, 9, 8});
            if (s.leafSimilar(r1, r2) && s.leafSimilar2(r1, r2)) {
                ++pass;
            } else {
                System.out.println("FAIL 872-1");
            }
        }
        ++total;
        {
            TreeNode r1 = build(new Integer[]{1, 2, 3});
            TreeNode r2 = build(new Integer[]{1, 3, 2});
            if (!s.leafSimilar(r1, r2) && !s.leafSimilar2(r1, r2)) {
                ++pass;
            } else {
                System.out.println("FAIL 872-2");
            }
        }
        ++total;
        {
            TreeNode r1 = build(new Integer[]{1});
            TreeNode r2 = build(new Integer[]{1, 2, 3});
            if (s.leafSimilar(r1, r2) == false && s.leafSimilar2(r1, r2) == false) {
                ++pass;
            } else {
                System.out.println("FAIL 872-3");
            }
        }
        ++total;
        {
            TreeNode r1 = build(new Integer[]{1, null, 2});
            TreeNode r2 = build(new Integer[]{1, 2});
            if (s.leafSimilar(r1, r2) && s.leafSimilar2(r1, r2)) {
                ++pass;
            } else {
                System.out.println("FAIL 872-4");
            }
        }
        if (pass == total) {
            System.out.println("== 0872 PASS ==");
        } else {
            System.out.println("== 0872 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n1 + n2)O(h1 + h2)(递归深度)
解法二O(n1 + n2)O(h1 + h2)(显式栈)

873. Length of Longest Fibonacci Subsequence(最长的斐波那契子序列的长度)

题目描述

如果序列 x_1, x_2, ..., x_n 满足下列条件,就说它是斐波那契式的:

  • n >= 3
  • 对于所有 i + 2 <= n,都有 x_i + x_{i+1} == x_{i+2}
    给定一个严格递增的正整数数组形成序列 arr,找到 arr 中最长的斐波那契式的子序列的长度。
    如果不存在,返回 0。
    子序列是通过从另一个序列 arr 中删除任意数量的元素(包括删除 0 个元素)得到的,同时不改变
    剩余元素顺序。例如,[3, 5, 8] 是 [3, 4, 5, 6, 7, 8] 的子序列。

示例 1

  • 输入:arr = [1,2,3,4,5,6,7,8]
  • 输出: 5
  • 解释: 最长的斐波那契式子序列为 [1,2,3,5,8]。

示例 2

  • 输入:arr = [1,3,7,11,12,14,18]
  • 输出: 3
  • 解释: 最长的斐波那契式子序列有 [1,11,12]、[3,11,14] 以及 [7,11,18]。
    提示:
  • 3 <= arr.length <= 1000
  • 1 <= arr[i] < arr[i + 1] <= 10^9

解题思路

斐波那契式子序列每新增一项由前两项决定,可以把状态定义为「以 (arr[i], arr[j]) 结尾的
子序列长度」,其中 i < j 且 arr[j] 是最后一项。
解法一(动态规划 + 值到下标映射,主提交):用 dp[i][j] 表示以 arr[i] 和 arr[j]
为最后两项的最长斐波那契式子序列长度(dp[i][j] >= 2 表示至少两项)。枚举最右侧的
j 与中间的 i,若 k = arr[j] - arr[i] 存在于 arr 且 k < arr[i](值为 k 的下标
p 满足 p < i,即 k 在原序列中更靠左),则 dp[i][j] = dp[p][i] + 1,否则 = 2;
全局取最大,若最大 < 3 则返回 0。实现时用一维滚动以省内存(相邻两项 -> 长度)。
解法二(哈希表逐步扩展,对照实现):枚举每个可能的起始对 (i, j),用哈希表查找
arr[i] + arr[j] 是否存在,存在则继续向后扩展,统计长度。以每个起始对为起点,
O(n^2) 起始对,每对的扩展步数 O(n),最坏 O(n^3) 但实际很快;作为对照思路。

代码实现

import java.util.HashMap;
import java.util.Map;

class Solution {

    // 解法一:动态规划(推荐提交)
    public int lenLongestFibSubseq(int[] arr) {
        int n = arr.length;
        Map<Integer, Integer> pos = new HashMap<>();
        for (int i = 0; i < n; ++i) {
            pos.put(arr[i], i);
        }
        int[][] dp = new int[n][n];   // dp[i][j]: 以 arr[i], arr[j] 结尾(i<j)的最长长度
        int ans = 0;
        for (int j = 0; j < n; ++j) {
            for (int i = 0; i < j; ++i) {
                int need = arr[j] - arr[i];
                Integer p = pos.get(need);
                if (p != null && p < i) {
                    dp[i][j] = dp[p][i] + 1;
                } else {
                    dp[i][j] = 2;
                }
                ans = Math.max(ans, dp[i][j]);
            }
        }
        return ans < 3 ? 0 : ans;
    }

    // 解法二:哈希表逐步扩展(对照实现)
    public int lenLongestFibSubseq2(int[] arr) {
        int n = arr.length;
        Map<Integer, Integer> pos = new HashMap<>();
        for (int i = 0; i < n; ++i) {
            pos.put(arr[i], i);
        }
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            for (int j = i + 1; j < n; ++j) {
                int a = arr[i], b = arr[j];
                int len = 2;
                while (true) {
                    Integer k = pos.get(a + b);
                    if (k == null) {
                        break;
                    }
                    int tmp = b;
                    b = a + b;
                    a = tmp;
                    ++len;
                }
                if (len >= 3) {
                    ans = Math.max(ans, len);
                }
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] arrs = {
            {1, 2, 3, 4, 5, 6, 7, 8},
            {1, 3, 7, 11, 12, 14, 18},
            {1, 2, 3},
            {1, 2},
            {2, 4, 7, 8, 11, 12, 14, 15, 18, 19}
        };
        int[] exp = {5, 3, 3, 0, 4};
        for (int t = 0; t < arrs.length; ++t) {
            ++total;
            int g1 = s.lenLongestFibSubseq(arrs[t]);
            int g2 = s.lenLongestFibSubseq2(arrs[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 873-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0873 PASS ==");
        } else {
            System.out.println("== 0873 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n^2)O(n^2)(或 O(n) 滚动)
解法二O(n^3) 最坏O(n)

874. Walking Robot Simulation(模拟行走机器人)

题目描述

机器人在一个无限大小的 XY 网格平面上行走,从点 (0, 0) 处开始出发,面向北方。该机器人
可以接收以下三种类型的命令 commands:

  • -2:向左转 90 度;
  • -1:向右转 90 度;
  • 1 <= x <= 9:向前移动 x 个单位长度。
    在网格上有一些格子被视为障碍物 obstacles。第 i 个障碍物位于网格点
    obstacles[i] = (x_i, y_i)。
    机器人无法走到障碍物上,它将会停在障碍物的前一个网格方块上,但仍然可以继续尝试进行该路线
    的其余部分。
    返回从原点到机器人所有经过的路径点(坐标为整数)的最大欧氏距离的平方。

示例 1

  • 输入:commands = [4,-1,3], obstacles = []
  • 输出:25
  • 解释:机器人开始位于 (0, 0):
  1. 向北移动 4 个单位,到达 (0, 4)。
  2. 右转。
  3. 向东移动 3 个单位,到达 (3, 4)。
    距离原点最远的是 (3, 4) ,距离为 3^2 + 4^2 = 25。

示例 2

  • 输入:commands = [4,-1,4,-2,4], obstacles = [[2,4]]
  • 输出:65
  • 解释:机器人开始位于 (0, 0):
  1. 向北移动 4 个单位,到达 (0, 4)。
  2. 右转。
  3. 向东移动 1 个单位,然后被位于 (2, 4) 的障碍物阻挡,停在 (1, 4)。
  4. 左转。
  5. 向北走 4 个单位,到达 (1, 8)。
    距离原点最远的是 (1, 8) ,距离为 1^2 + 8^2 = 65。
    提示:
  • 1 <= commands.length <= 10^4
  • commands[i] 是 -2、-1 或者是范围 [1, 9] 内的一个整数
  • 0 <= obstacles.length <= 10^4
  • -3 * 10^4 <= x_i, y_i <= 3 * 10^4
  • 答案保证小于 2^31

解题思路

机器人朝四个方向移动,方向用 0=北、1=东、2=南、3=西 表示;-1 右转相当于
dir = (dir + 1) % 4,-2 左转相当于 dir = (dir + 3) % 4。移动时逐格检查前方是否
有障碍物(障碍物集合用哈希存储坐标编码),有则停在该格前。
解法一(方向数组 + HashSet 障碍判定,主提交):四个方向向量
{{0,1},{1,0},{0,-1},{-1,0}},移动时每步试探下一格,若在障碍集合中则停止本次前进,
否则真正移动并更新最远距离平方。
解法二(坐标编码 String,对照实现):把障碍物编码为 x + "," + y 的字符串放入 Set,
方向切换拆成独立 if 分支;逻辑等价,但字符串比较比 int 编码略慢,仅作对照。

代码实现

import java.util.HashSet;
import java.util.Set;

class Solution {

    private static final int[][] DIRS = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};

    // 解法一:方向数组 + int 编码障碍(推荐提交)
    public int robotSim(int[] commands, int[][] obstacles) {
        Set<Integer> block = new HashSet<>();
        for (int[] o : obstacles) {
            block.add(encode(o[0], o[1]));
        }
        int x = 0, y = 0, dir = 0;   // 0=北 1=东 2=南 3=西
        int ans = 0;
        for (int c : commands) {
            if (c == -1) {
                dir = (dir + 1) % 4;
            } else if (c == -2) {
                dir = (dir + 3) % 4;
            } else {
                int dx = DIRS[dir][0], dy = DIRS[dir][1];
                for (int step = 0; step < c; ++step) {
                    int nx = x + dx, ny = y + dy;
                    if (block.contains(encode(nx, ny))) {
                        break;
                    }
                    x = nx;
                    y = ny;
                }
                ans = Math.max(ans, x * x + y * y);
            }
        }
        return ans;
    }

    private int encode(int x, int y) {
        // 坐标范围 +-30000,偏移后编码到 64 位内唯一
        return (x + 30000) * 100000 + (y + 30000);
    }

    // 解法二:字符串编码障碍(对照实现)
    public int robotSim2(int[] commands, int[][] obstacles) {
        Set<String> block = new HashSet<>();
        for (int[] o : obstacles) {
            block.add(o[0] + "," + o[1]);
        }
        int x = 0, y = 0, dir = 0;
        int ans = 0;
        for (int c : commands) {
            if (c == -2) {
                dir = dir == 0 ? 3 : dir - 1;
            } else if (c == -1) {
                dir = (dir + 1) % 4;
            } else {
                int dx = DIRS[dir][0], dy = DIRS[dir][1];
                for (int step = 0; step < c; ++step) {
                    int nx = x + dx, ny = y + dy;
                    if (block.contains(nx + "," + ny)) {
                        break;
                    }
                    x = nx;
                    y = ny;
                }
                ans = Math.max(ans, x * x + y * y);
            }
        }
        return ans;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        ++total;
        if (s.robotSim(new int[]{4, -1, 3}, new int[][]{}) == 25) {
            ++pass;
        } else {
            System.out.println("FAIL 874-1: " + s.robotSim(new int[]{4, -1, 3}, new int[][]{}));
        }
        ++total;
        if (s.robotSim(new int[]{4, -1, 4, -2, 4}, new int[][]{{2, 4}}) == 65) {
            ++pass;
        } else {
            System.out.println("FAIL 874-2: " + s.robotSim(new int[]{4, -1, 4, -2, 4}, new int[][]{{2, 4}}));
        }
        ++total;
        if (s.robotSim(new int[]{6, -2, 8}, new int[][]{}) == 100) {
            ++pass;
        } else {
            System.out.println("FAIL 874-3: " + s.robotSim(new int[]{6, -2, 8}, new int[][]{}));
        }
        ++total;
        if (s.robotSim2(new int[]{4, -1, 3}, new int[][]{}) == 25
                && s.robotSim2(new int[]{4, -1, 4, -2, 4}, new int[][]{{2, 4}}) == 65) {
            ++pass;
        } else {
            System.out.println("FAIL 874-4");
        }
        if (pass == total) {
            System.out.println("== 0874 PASS ==");
        } else {
            System.out.println("== 0874 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n * m),m 为单次最大步数(9) + O(k) 建集O(k)
解法二O(n * m) + O(k)O(k)

875. Koko Eating Bananas(爱吃香蕉的珂珂)

题目描述

珂珂喜欢吃香蕉。这里有 n 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,
将在 h 小时后回来。
珂珂可以决定她吃香蕉的速度 k(单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉
k 根。如果这堆香蕉少于 k 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的
香蕉。
珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。
返回她可以在 h 小时内吃掉所有香蕉的最小速度 k(k 为整数)。

示例 1

  • 输入:piles = [3,6,7,11], h = 8
  • 输出:4

示例 2

  • 输入:piles = [30,11,23,4,20], h = 5
  • 输出:30

示例 3

  • 输入:piles = [30,11,23,4,20], h = 6
  • 输出:23
    提示:
  • 1 <= piles.length <= 10^4
  • piles.length <= h <= 10^9
  • 1 <= piles[i] <= 10^9

解题思路

吃完全部香蕉所需小时数随速度 k 单调递减,因此可对速度二分查找第一个使总耗时
<= h 的 k。每堆耗时 ceil(piles[i] / k)。
解法一(二分答案,主提交):二分区间 [1, max(piles)]。ok(k) 判断以速度 k 是否能在
h 小时内吃完:对每堆累加 (p + k - 1) / k,注意用 long 防溢出;总耗时 <= h 即合法。
二分取合法区间左边界,即为最小速度。
解法二(暴力从 1 递增,对照实现):从 k = 1 开始逐个测试(每堆沿用上面的耗时公式),
找到第一个合法速度。最坏情况接近 max(piles) 时很慢,仅作正确性对照。

代码实现

import java.util.Arrays;

class Solution {

    // 解法一:二分答案(推荐提交)
    public int minEatingSpeed(int[] piles, int h) {
        int lo = 1, hi = 0;
        for (int p : piles) {
            hi = Math.max(hi, p);
        }
        while (lo < hi) {
            int mid = lo + (hi - lo) / 2;
            if (canFinish(piles, h, mid)) {
                hi = mid;
            } else {
                lo = mid + 1;
            }
        }
        return lo;
    }

    private boolean canFinish(int[] piles, int h, int k) {
        long hours = 0;
        for (int p : piles) {
            hours += (p + k - 1L) / k;
            if (hours > h) {
                return false;
            }
        }
        return true;
    }

    // 解法二:递增枚举(对照实现)
    public int minEatingSpeed2(int[] piles, int h) {
        int k = 1;
        while (true) {
            long hours = 0;
            for (int p : piles) {
                hours += (p + k - 1L) / k;
                if (hours > h) {
                    break;
                }
            }
            if (hours <= h) {
                return k;
            }
            ++k;
        }
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        ++total;
        if (s.minEatingSpeed(new int[]{3, 6, 7, 11}, 8) == 4) {
            ++pass;
        } else {
            System.out.println("FAIL 875-1: " + s.minEatingSpeed(new int[]{3, 6, 7, 11}, 8));
        }
        ++total;
        if (s.minEatingSpeed(new int[]{30, 11, 23, 4, 20}, 5) == 30) {
            ++pass;
        } else {
            System.out.println("FAIL 875-2: " + s.minEatingSpeed(new int[]{30, 11, 23, 4, 20}, 5));
        }
        ++total;
        if (s.minEatingSpeed(new int[]{30, 11, 23, 4, 20}, 6) == 23) {
            ++pass;
        } else {
            System.out.println("FAIL 875-3: " + s.minEatingSpeed(new int[]{30, 11, 23, 4, 20}, 6));
        }
        ++total;
        if (s.minEatingSpeed(new int[]{1, 1, 1}, 3) == 1) {
            ++pass;
        } else {
            System.out.println("FAIL 875-4");
        }
        ++total;
        if (s.minEatingSpeed2(new int[]{3, 6, 7, 11}, 8) == 4
                && s.minEatingSpeed2(new int[]{30, 11, 23, 4, 20}, 6) == 23) {
            ++pass;
        } else {
            System.out.println("FAIL 875-5");
        }
        if (pass == total) {
            System.out.println("== 0875 PASS ==");
        } else {
            System.out.println("== 0875 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n log max(piles))O(1)
解法二O(max(piles) * n)O(1)