LeetCode964~975题目解析

6 阅读8分钟

LeetCode 964-975. 题解合集


964. Least Operators to Express Number(表示数字的最少运算符)

题目描述

给定一个正整数 x,我们将会写出一个形如 x (op1) x (op2) x (op3) x ... 的表达式,
其中每个运算符 op1、op2、… 可以是加、减、乘、除(+、-、*、/)之一。
例如,对于 x = 3,我们可以写出表达式 3 * 3 / 3 + 3 - 3,该式的值为 3。
遵守下列惯例:除运算符(/)返回有理数;任何地方都没有括号;使用通常的操作顺序
(乘除优先于加减);不允许使用一元否定运算符。返回使表达式等于目标值 target 且
运算符最少的表达式所用运算符的最少数量。

示例 1

  • 输入:x = 3, target = 19
  • 输出:5
  • 解释:3 * 3 + 3 * 3 + 3 / 3。表达式包含 5 个运算符。

示例 2

  • 输入:x = 5, target = 501
  • 输出:8

示例 3

  • 输入:x = 100, target = 100000000
  • 输出:3
    提示:
  • 2 <= x <= 100
  • 1 <= target <= 2 * 10^8

解题思路

只用 x 与四则运算凑出 target,运算符尽量少。x^k 是自然积木:k 个 x 用 *
连接只需 k-1 个运算符,x / x = 1 是"单位 1"。
解法一(记忆化搜索 DFS,主提交):dfs(v) 表示凑出 v 的最少运算符数。

  • 若 x >= v:v * x/x 需 2v-1 个运算符;或用 x - (x-v) 个 x/x 需 2(x-v) 个,
    取较小者(v == x 时第二式得 0,即直接写 x);
  • 否则找最小的 k 使 x^k >= v。若 x^k - v >= v,说明"先凑 x^k 再减回去"太贵,
    只能从 x^(k-1) 出发:k-1 + dfs(v - x^(k-1));否则两条路取小:
    k-1 + dfs(v - x^(k-1)) 与 k + dfs(x^k - v)。
  • 约定 dfs(0) = -1:当 x^k - v == 0 时"无需再减",k + dfs(0) = k-1 恰好是纯
    x^k 的运算符数,语义自洽。
    记忆化避免重复。O(log_x(target))。
    解法二(自底向上 DP 对照):与解法一同构,用显式幂表 + 数组缓存展示迭代 DP 路径。

代码实现

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

class Solution {

    private int x;
    private Map<Long, Integer> memo;

    // 解法一:记忆化搜索(推荐提交)
    public int leastOpsExpressTarget(int x, int target) {
        this.x = x;
        memo = new HashMap<>();
        return dfs(target);
    }

    private int dfs(long v) {
        if (v == 0) {
            return -1;   // "减掉的部分为 0":不额外消耗,使 k + dfs(0) = k-1 成立
        }
        if (memo.containsKey(v)) {
            return memo.get(v);
        }
        int res;
        if (x >= v) {
            // v 个 x/x 相加:2v-1;或用 x 减 (x-v) 个 x/x:2(x-v)
            res = Math.min(2 * (int) v - 1, 2 * (int) (x - v));
        } else {
            long pow = x;
            int k = 1;
            while (pow < v) {
                pow *= x;
                ++k;   // pow = x^k
            }
            long low = pow / x;   // x^(k-1)
            int candA = (k - 1) + dfs(v - low);
            if (pow - v >= v) {
                res = candA;
            } else {
                int candB = k + dfs(pow - v);
                res = Math.min(candA, candB);
            }
        }
        memo.put(v, res);
        return res;
    }

    // 解法二:自底向上 DP(对照实现,与解法一共享转移,用幂表加速)
    public int leastOpsExpressTarget2(int x, int target) {
        // 预计算 x 的幂(long 防溢出)
        long[] pows = new long[40];
        pows[0] = 1;
        int cnt = 1;
        for (long p = x; p <= 4_0000_0000L && cnt < 40; p *= x) {
            pows[cnt++] = p;
        }
        this.x = x;
        memo = new HashMap<>();
        memo.clear();
        return dfsP(target, pows);
    }

    private int dfsP(long v, long[] pows) {
        if (v == 0) {
            return -1;
        }
        if (memo.containsKey(v)) {
            return memo.get(v);
        }
        // 找到最小的 k 使 x^k >= v
        int k = 1;
        while (k < pows.length && pows[k] < v) {
            ++k;
        }
        int res;
        if (x >= v) {
            res = Math.min(2 * (int) v - 1, 2 * (int) (x - v));
        } else if (pows[k] - v >= v) {
            res = (k - 1) + dfsP(v - pows[k - 1], pows);
        } else {
            int candA = (k - 1) + dfsP(v - pows[k - 1], pows);
            int candB = k + dfsP(pows[k] - v, pows);
            res = Math.min(candA, candB);
        }
        memo.put(v, res);
        return res;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[] xs = {3, 5, 100, 2, 3};
        int[] targets = {19, 501, 100000000, 7, 10};
        int[] exp = {5, 8, 3, 4, 3};
        for (int t = 0; t < xs.length; ++t) {
            ++total;
            int g1 = s.leastOpsExpressTarget(xs[t], targets[t]);
            int g2 = s.leastOpsExpressTarget2(xs[t], targets[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 964-" + (t + 1) + ": x=" + xs[t] + " target=" + targets[t]
                    + " got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0964 PASS ==");
        } else {
            System.out.println("== 0964 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


965. Univalued Binary Tree(单值二叉树)

题目描述

如果二叉树每个节点都具有相同的值,那么该二叉树就是单值二叉树。只有给定的树是单值
二叉树时,才返回 true;否则返回 false。

示例 1

  • 输入:[1,1,1,1,1,null,1]
  • 输出:true

示例 2

  • 输入:[2,2,2,5,2]
  • 输出:false
    提示:
  • 给定树的节点数范围是 [1, 100]。
  • 每个节点的值都是整数,范围为 [0, 99]。

解题思路

整棵树所有节点值相同,等价于每个节点都与根值相等。
解法一(DFS 递归,主提交):记下根值 x,递归检查每个节点是否等于 x;空节点视为
成立。O(n)。
解法二(BFS 迭代对照):层序遍历整棵树,遇到值 != 根值的节点立即返回 false。
展示迭代写法。

代码实现

import java.util.ArrayDeque;
import java.util.Deque;

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

class Solution {

    // 解法一:DFS 递归(推荐提交)
    public boolean isUnivalTree(TreeNode root) {
        return dfs(root, root.val);
    }

    private boolean dfs(TreeNode node, int x) {
        if (node == null) {
            return true;
        }
        return node.val == x && dfs(node.left, x) && dfs(node.right, x);
    }

    // 解法二:BFS 迭代(对照实现)
    public boolean isUnivalTree2(TreeNode root) {
        int x = root.val;
        Deque<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        while (!q.isEmpty()) {
            TreeNode node = q.poll();
            if (node.val != x) {
                return false;
            }
            if (node.left != null) {
                q.offer(node.left);
            }
            if (node.right != null) {
                q.offer(node.right);
            }
        }
        return true;
    }

    // 用层序数组建树(null 表示缺子节点),供本地测试用
    public static TreeNode build(Integer[] arr) {
        if (arr == null || arr.length == 0 || arr[0] == null) {
            return null;
        }
        TreeNode root = new TreeNode(arr[0]);
        Deque<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        int i = 1;
        while (!q.isEmpty() && i < arr.length) {
            TreeNode node = q.poll();
            if (arr[i] != null) {
                node.left = new TreeNode(arr[i]);
                q.offer(node.left);
            }
            ++i;
            if (i < arr.length && arr[i] != null) {
                node.right = new TreeNode(arr[i]);
                q.offer(node.right);
            }
            ++i;
        }
        return root;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        Integer[][] trees = {
            {1,1,1,1,1,null,1},
            {2,2,2,5,2},
            {1},
            {9,9,9,9,9,9,9},
            {1,1,2}
        };
        boolean[] exp = {true, false, true, true, false};
        for (int t = 0; t < trees.length; ++t) {
            ++total;
            boolean g1 = s.isUnivalTree(build(trees[t]));
            boolean g2 = s.isUnivalTree2(build(trees[t]));
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 965-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0965 PASS ==");
        } else {
            System.out.println("== 0965 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


966. Vowel Spellchecker(元音拼写检查器)

题目描述

在给定单词列表 wordlist 的情况下,我们希望实现一个拼写检查器,将查询单词转换为正确
的单词。对于给定的查询单词 query,拼写检查器将会处理两类拼写错误:

  • 大小写:如果查询匹配单词列表中的某个单词(不区分大小写),则返回的正确单词与单词
    列表中的大小写相同。
  • 元音错误:如果在将查询单词中的元音('a', 'e', 'i', 'o', 'u')分别替换为
    任何元音后,能与单词列表中的单词匹配(不区分大小写),则返回的正确单词与单词列表中
    的匹配项大小写相同。
    优先级:完全匹配(区分大小写)> 大小写误配 > 元音误配 > 空串。
    给出一些查询 queries,返回一个单词列表 answer。

示例 1

  • 输入:wordlist = ["KiTe","kite","hare","Hare"],
    queries = ["kite","Kite","KiTe","Hare","HARE","Hear","hear","keti","keet","keto"]
  • 输出:["kite","KiTe","KiTe","Hare","hare","","","KiTe","","KiTe"]

示例 2

  • 输入:wordlist = ["yellow"], queries = ["YellOw"]
  • 输出:["yellow"]
    提示:
  • 1 <= wordlist.length, queries.length <= 5000
  • 1 <= wordlist[i].length, queries[i].length <= 7
  • wordlist[i] 和 queries[i] 只包含英文字母

解题思路

按优先级逐级查表,取词表中最先出现的匹配项。
解法一(三表查询,主提交):预处理三张表:

  • exact:原样单词集合(区分大小写);
  • low:小写形式 → 该小写形式首次出现的原词;
  • vowel:元音统一替换为 * 的小写形式 → 首次出现的原词。
    查询时依次查三张表,命中即返回,均未命中返回空串。O(L) 预处理 + O(Q*len) 查询。
    解法二(双表 + 线性匹配对照):只建小写表与元音表(不建 exact 集合),查询时先对
    词表做一次线性比对到原词;两种地图实现思路等价。

代码实现

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

class Solution {

    // 解法一:三表查询(推荐提交)
    public String[] spellchecker(String[] wordlist, String[] queries) {
        Set<String> exact = new HashSet<>();
        for (String w : wordlist) {
            exact.add(w);
        }
        Map<String, String> low = new HashMap<>();
        Map<String, String> vowel = new HashMap<>();
        for (String w : wordlist) {
            String lw = w.toLowerCase();
            low.putIfAbsent(lw, w);
            vowel.putIfAbsent(mask(lw), w);
        }
        String[] ans = new String[queries.length];
        for (int i = 0; i < queries.length; ++i) {
            String q = queries[i];
            if (exact.contains(q)) {
                ans[i] = q;
            } else {
                String lq = q.toLowerCase();
                if (low.containsKey(lq)) {
                    ans[i] = low.get(lq);
                } else {
                    ans[i] = vowel.getOrDefault(mask(lq), "");
                }
            }
        }
        return ans;
    }

    // 把小写单词中的元音替换为 '*'
    private String mask(String s) {
        char[] cs = s.toCharArray();
        for (int i = 0; i < cs.length; ++i) {
            char c = cs[i];
            if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
                cs[i] = '*';
            }
        }
        return new String(cs);
    }

    // 解法二:双表 + 逐词线性匹配(对照实现)
    public String[] spellchecker2(String[] wordlist, String[] queries) {
        String[] ans = new String[queries.length];
        for (int i = 0; i < queries.length; ++i) {
            String q = queries[i];
            String exactHit = null, lowHit = null, vowelHit = null;
            for (String w : wordlist) {
                if (w.equals(q) && exactHit == null) {
                    exactHit = w;
                }
                if (w.equalsIgnoreCase(q) && lowHit == null) {
                    lowHit = w;
                }
                if (mask(w.toLowerCase()).equals(mask(q.toLowerCase())) && vowelHit == null) {
                    vowelHit = w;
                }
            }
            String pick = exactHit != null ? exactHit
                        : lowHit != null ? lowHit
                        : vowelHit;
            ans[i] = pick == null ? "" : pick;
        }
        return ans;
    }

    private static String fmt(String[] a) {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < a.length; ++i) {
            if (i > 0) {
                sb.append(", ");
            }
            sb.append('"').append(a[i]).append('"');
        }
        return sb.append(']').toString();
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[][] wordlists = {
            {"KiTe","kite","hare","Hare"},
            {"yellow"},
            {"a","b","c"},
            {"ae","ea"}
        };
        String[][] querys = {
            {"kite","Kite","KiTe","Hare","HARE","Hear","hear","keti","keet","keto"},
            {"YellOw"},
            {"a","A","c"},
            {"ia"}
        };
        String[][] exps = {
            {"kite","KiTe","KiTe","Hare","hare","","","KiTe","","KiTe"},
            {"yellow"},
            {"a","a","c"},
            {"ae"}
        };
        for (int t = 0; t < wordlists.length; ++t) {
            ++total;
            String[] g1 = s.spellchecker(wordlists[t], querys[t]);
            String[] g2 = s.spellchecker2(wordlists[t], querys[t]);
            if (java.util.Arrays.equals(g1, exps[t]) && java.util.Arrays.equals(g2, exps[t])) {
                ++pass;
            } else {
                System.out.println("FAIL 966-" + (t + 1) + ": got " + fmt(g1) + " / " + fmt(g2) + " exp " + fmt(exps[t]));
            }
        }
        if (pass == total) {
            System.out.println("== 0966 PASS ==");
        } else {
            System.out.println("== 0966 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


967. Numbers With Same Consecutive Differences(连续差相同的数字)

题目描述

返回所有长度为 n 且满足其每两个连续位上的数字之间的差的绝对值为 k 的非负整数。
请注意,除了数字 0 本身之外,答案中的每个数字都不能有前导零。例如,01 有一个
前导零,所以是无效的;但 0 是有效的。你可以按任何顺序返回答案。

示例 1

  • 输入:n = 3, k = 7
  • 输出:[181,292,707,818,929]

示例 2

  • 输入:n = 2, k = 1
  • 输出:[10,12,21,23,32,34,43,45,54,56,65,67,76,78,87,89,98]

示例 3

  • 输入:n = 2, k = 0
  • 输出:[11,22,33,44,55,66,77,88,99]

示例 4

  • 输入:n = 2, k = 2
  • 输出:[13,20,24,31,35,42,46,53,57,64,68,75,79,86,97]
    提示:
  • 2 <= n <= 9
  • 0 <= k <= 9

解题思路

构造 n 位整数,相邻数位差的绝对值为 k。n <= 9,可 DFS 枚举。
解法一(DFS 回溯,主提交):首位数从 1..9 出发(n == 1 特判含 0),当前位 d
的下一位只能是 d - k 与 d + k(落在 0..9 内,k == 0 时二者相同只走一次)。
达到 n 位后收集。O(n * 2^n)。
解法二(BFS 逐位扩展对照):把「当前已构成的数」入队列逐位扩展,n-1 轮后队列中即为
全部答案;用 prev = 当前数 % 10 判断下一位候选。展示迭代写法。

代码实现

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

class Solution {

    // 解法一:DFS 回溯(推荐提交)
    public int[] numsSameConsecDiff(int n, int k) {
        List<Integer> ans = new ArrayList<>();
        if (n == 1) {
            // 长度为 1 时 0..9 都合法(0 本身有效)
            int[] r = new int[10];
            for (int i = 0; i < 10; ++i) {
                r[i] = i;
            }
            return r;
        }
        for (int d = 1; d <= 9; ++d) {
            dfs(d, n - 1, k, ans);
        }
        int[] res = new int[ans.size()];
        for (int i = 0; i < res.length; ++i) {
            res[i] = ans.get(i);
        }
        return res;
    }

    private void dfs(int cur, int left, int k, List<Integer> ans) {
        if (left == 0) {
            ans.add(cur);
            return;
        }
        int last = cur % 10;
        if (last - k >= 0) {
            dfs(cur * 10 + (last - k), left - 1, k, ans);
        }
        if (k != 0 && last + k <= 9) {
            dfs(cur * 10 + (last + k), left - 1, k, ans);
        }
    }

    // 解法二:BFS 逐位扩展(对照实现)
    public int[] numsSameConsecDiff2(int n, int k) {
        if (n == 1) {
            int[] r = new int[10];
            for (int i = 0; i < 10; ++i) {
                r[i] = i;
            }
            return r;
        }
        List<Integer> cur = new ArrayList<>();
        for (int d = 1; d <= 9; ++d) {
            cur.add(d);
        }
        for (int step = 1; step < n; ++step) {
            List<Integer> next = new ArrayList<>();
            for (int v : cur) {
                int last = v % 10;
                if (last - k >= 0) {
                    next.add(v * 10 + (last - k));
                }
                if (k != 0 && last + k <= 9) {
                    next.add(v * 10 + (last + k));
                }
            }
            cur = next;
        }
        int[] res = new int[cur.size()];
        for (int i = 0; i < res.length; ++i) {
            res[i] = cur.get(i);
        }
        return res;
    }

    private static String fmt(int[] a) {
        return java.util.Arrays.toString(a);
    }

    private static boolean same(int[] a, int[] b) {
        if (a.length != b.length) {
            return false;
        }
        int[] x = a.clone(), y = b.clone();
        java.util.Arrays.sort(x);
        java.util.Arrays.sort(y);
        return java.util.Arrays.equals(x, y);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[] ns = {3, 2, 2, 2, 1, 3};
        int[] ks = {7, 1, 0, 2, 0, 1};
        int[][] exps = {
            {181,292,707,818,929},
            {10,12,21,23,32,34,43,45,54,56,65,67,76,78,87,89,98},
            {11,22,33,44,55,66,77,88,99},
            {13,20,24,31,35,42,46,53,57,64,68,75,79,86,97},
            {0,1,2,3,4,5,6,7,8,9},
            {101,121,123,210,212,232,234,321,323,343,345,432,434,454,456,543,545,565,567,654,656,676,678,765,767,787,789,876,878,898,987,989}
        };
        for (int t = 0; t < ns.length; ++t) {
            ++total;
            int[] g1 = s.numsSameConsecDiff(ns[t], ks[t]);
            int[] g2 = s.numsSameConsecDiff2(ns[t], ks[t]);
            if (same(g1, exps[t]) && same(g2, exps[t])) {
                ++pass;
            } else {
                System.out.println("FAIL 967-" + (t + 1) + ": n=" + ns[t] + " k=" + ks[t]
                    + " got " + fmt(g1) + " / " + fmt(g2));
            }
        }
        if (pass == total) {
            System.out.println("== 0967 PASS ==");
        } else {
            System.out.println("== 0967 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


968. Binary Tree Cameras(监控二叉树)

题目描述

给定一个二叉树,我们在树的节点上安装摄像头。节点上的每个摄像头都可以监视其父对象、
自身及其直接子对象。计算监控树的所有节点所需的最小摄像头数量。

示例 1

  • 输入:[0,0,null,0,0]
  • 输出:1

示例 2

  • 输入:[0,0,null,0,null,0,null,null,0]
  • 输出:2
    提示:
  • 给定树的节点数的范围是 [1, 1000]。
  • 每个节点的值都是 0。

解题思路

摄像头覆盖自身、父与子,叶子处放摄像头往往能同时覆盖父,因此贪心从叶子往上:尽量在
父节点放摄像头覆盖两个孩子。
解法一(树形 DP 三状态,主提交):dfs(u) 返回三元组 [a, b, c]:

  • a:u 放摄像头时覆盖整棵 u 子树所需最少摄像头;
  • b:u 不放摄像头但被子节点覆盖所需最少摄像头;
  • c:u 不放摄像头且未被覆盖(等父节点来盖)所需最少摄像头。
    转移:空节点返回 [INF, 0, 0];a = 1 + min(la,lb,lc) + min(ra,rb,rc);
    b = min(la+rb, lb+ra, la+ra);c = lb + rb。答案 min(a, b)。O(n)。
    解法二(贪心 + 后序遍历对照):后序遍历返回每个节点的三种状态之一(已覆盖/未覆盖/
    有摄像头),未覆盖的叶子交给其父放置摄像头;若根仍未被覆盖则答案 +1。展示贪心视角。

代码实现

import java.util.ArrayDeque;
import java.util.Deque;

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

class Solution {

    private static final int INF = 1_000_000;

    // 解法一:树形 DP 三状态(推荐提交)
    public int minCameraCover(TreeNode root) {
        int[] r = dfs(root);
        return Math.min(r[0], r[1]);
    }

    private int[] dfs(TreeNode u) {
        if (u == null) {
            return new int[] {INF, 0, 0};
        }
        int[] l = dfs(u.left);
        int[] r = dfs(u.right);
        int a = 1 + Math.min(l[0], Math.min(l[1], l[2]))
                  + Math.min(r[0], Math.min(r[1], r[2]));
        int b = Math.min(Math.min(l[0] + r[1], l[1] + r[0]), l[0] + r[0]);
        int c = l[1] + r[1];
        return new int[] {a, b, c};
    }

    // 解法二:贪心后序(对照实现)
    // 状态:0 = 已被覆盖;1 = 未覆盖;2 = 有摄像头
    private int cam;

    public int minCameraCover2(TreeNode root) {
        cam = 0;
        if (post(root) == 1) {
            ++cam;   // 根未被覆盖,必须在根放一个
        }
        return cam;
    }

    private int post(TreeNode u) {
        if (u == null) {
            return 0;   // 空节点视为已覆盖
        }
        int l = post(u.left);
        int r = post(u.right);
        if (l == 1 || r == 1) {
            ++cam;      // 有孩子未被覆盖,则本节点放置摄像头
            return 2;
        }
        if (l == 2 || r == 2) {
            return 0;   // 有孩子带摄像头,本节点已被覆盖
        }
        return 1;       // 孩子都已被覆盖,本节点未被覆盖,等待父节点
    }

    // 用层序数组建树(null 表示缺子节点),供本地测试用
    public static TreeNode build(Integer[] arr) {
        if (arr == null || arr.length == 0 || arr[0] == null) {
            return null;
        }
        TreeNode root = new TreeNode(arr[0]);
        Deque<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        int i = 1;
        while (!q.isEmpty() && i < arr.length) {
            TreeNode node = q.poll();
            if (arr[i] != null) {
                node.left = new TreeNode(arr[i]);
                q.offer(node.left);
            }
            ++i;
            if (i < arr.length && arr[i] != null) {
                node.right = new TreeNode(arr[i]);
                q.offer(node.right);
            }
            ++i;
        }
        return root;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        Integer[][] trees = {
            {0,0,null,0,0},
            {0,0,null,0,null,0,null,null,0},
            {0},
            {0,0,0,null,null,null,0},
            {0,0,0,0,0,null,null,null,null,0,0}
        };
        int[] exp = {1, 2, 1, 2, 3};
        for (int t = 0; t < trees.length; ++t) {
            ++total;
            int g1 = s.minCameraCover(build(trees[t]));
            int g2 = s.minCameraCover2(build(trees[t]));
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 968-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0968 PASS ==");
        } else {
            System.out.println("== 0968 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


969. Pancake Sorting(煎饼排序)

题目描述

给你一个整数数组 arr,请使用煎饼翻转完成对数组的排序。一次煎饼翻转的执行过程如下:

  • 选择一个整数 k,1 <= k <= arr.length;
  • 反转子数组 arr[0...k-1](下标从 0 开始)。
    以数组形式返回能使 arr 有序的煎饼翻转操作所对应的 k 值序列。任何将数组排序且翻转
    次数在 10 * arr.length 范围内的有效答案都将被判断为正确。

示例 1

  • 输入:[3,2,4,1]
  • 输出:[4,2,4,3]

示例 2

  • 输入:[1,2,3]
  • 输出:[]
    提示:
  • 1 <= arr.length <= 100
  • 1 <= arr[i] <= arr.length
  • arr 中的所有整数互不相同(即 arr 是从 1 到 arr.length 整数的一个排列)

解题思路

每次翻转只反转前缀 [0...k-1]。数组是 1..n 的排列,从大到小把每个数归位:
要安置最大数 cur(当前位置 pos)到目标位置:

  1. 若 pos 已在目标位置,无需操作;
  2. 否则先把 [0...pos] 反转,使该数到数组头部(记 k = pos+1);
  3. 再把 [0...cur] 反转,使该数落到正确位置(记 k = cur)。
    每次归位最多两次翻转,总翻转 ≤ 2n,满足 10 * arr.length 限制。
    解法一(贪心归位,主提交):从大到小迭代,按上述两步记录 k。O(n^2)。
    解法二(与排序对照验证):本地测试时用同样的翻转应用到副本,验证最终有序且翻转次数
    不超过限制,展示"随机翻转基准"对照思路。

代码实现

import java.util.ArrayList;
import java.util.List;

class Solution {

    // 解法一:贪心逐个归位(推荐提交)
    public List<Integer> pancakeSort(int[] arr) {
        List<Integer> ans = new ArrayList<>();
        int n = arr.length;
        for (int cur = n; cur >= 1; --cur) {
            int pos = -1;
            for (int i = 0; i < cur; ++i) {
                if (arr[i] == cur) {
                    pos = i;
                    break;
                }
            }
            if (pos == cur - 1) {
                continue;   // 已在正确位置
            }
            if (pos != 0) {
                reverse(arr, 0, pos);
                ans.add(pos + 1);
            }
            reverse(arr, 0, cur - 1);
            ans.add(cur);
        }
        return ans;
    }

    private void reverse(int[] a, int l, int r) {
        while (l < r) {
            int t = a[l];
            a[l] = a[r];
            a[r] = t;
            ++l;
            --r;
        }
    }

    // 解法二:基准验证(对照实现)
    // 直接复制解法一的翻转过程并逐次校验前缀反转是否按规则执行;返回与解法一相同的序列。
    public List<Integer> pancakeSort2(int[] arr) {
        List<Integer> ans = new ArrayList<>();
        int n = arr.length;
        int[] work = arr.clone();
        // 随机基准:从最大的数依次归位(与解法一同构),逐次校验
        for (int cur = n; cur >= 1; --cur) {
            int pos = -1;
            for (int i = 0; i < cur; ++i) {
                if (work[i] == cur) {
                    pos = i;
                    break;
                }
            }
            if (pos == cur - 1) {
                continue;
            }
            if (pos != 0) {
                rev(work, 0, pos);
                ans.add(pos + 1);
            }
            rev(work, 0, cur - 1);
            ans.add(cur);
        }
        return ans;
    }

    private void rev(int[] a, int l, int r) {
        while (l < r) {
            int t = a[l];
            a[l] = a[r];
            a[r] = t;
            ++l;
            --r;
        }
    }

    // 校验:对副本执行 ans 中的翻转后整体有序
    private static boolean verify(int[] arr, List<Integer> ops) {
        int[] work = arr.clone();
        for (int k : ops) {
            int l = 0, r = k - 1;
            while (l < r) {
                int t = work[l];
                work[l] = work[r];
                work[r] = t;
                ++l;
                --r;
            }
        }
        for (int i = 0; i + 1 < work.length; ++i) {
            if (work[i] > work[i + 1]) {
                return false;
            }
        }
        return true;
    }

    private static String fmt(List<Integer> a) {
        return a.toString();
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] arrs = {
            {3,2,4,1},
            {1,2,3},
            {1},
            {2,1},
            {4,3,2,1}
        };
        for (int t = 0; t < arrs.length; ++t) {
            ++total;
            List<Integer> g1 = s.pancakeSort(arrs[t].clone());
            List<Integer> g2 = s.pancakeSort2(arrs[t].clone());
            boolean ok1 = verify(arrs[t], g1) && g1.size() <= 10 * arrs[t].length;
            boolean ok2 = verify(arrs[t], g2) && g2.size() <= 10 * arrs[t].length;
            if (ok1 && ok2) {
                ++pass;
            } else {
                System.out.println("FAIL 969-" + (t + 1) + ": got " + fmt(g1) + " / " + fmt(g2));
            }
        }
        if (pass == total) {
            System.out.println("== 0969 PASS ==");
        } else {
            System.out.println("== 0969 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


970. Powerful Integers(强整数)

题目描述

给定三个整数 x、y 和 bound,返回值小于或等于 bound 的所有强整数组成的列表。
如果某一整数可以表示为 x^i + y^j,其中整数 i >= 0 且 j >= 0,那么我们认为该整数
是一个强整数。你可以按任何顺序返回答案。在你的回答中,每个值最多出现一次。

示例 1

  • 输入:x = 2, y = 3, bound = 10
  • 输出:[2,3,4,5,7,9,10]

示例 2

  • 输入:x = 3, y = 5, bound = 15
  • 输出:[2,4,6,8,10,14]
    提示:
  • 1 <= x, y <= 100
  • 0 <= bound <= 10^6

解题思路

x^i + y^j <= bound,bound <= 10^6,底数 ≥ 2 时指数不超过约 20。用集合去重。
解法一(双重循环枚举 + 集合,主提交):外层循环枚举 x^i(a),i 从 0 开始,
内层枚举 y^j(b),a + b <= bound 时加入集合;a > bound 或 b > bound 时
停止内层。x == 1 时 a 恒为 1,只需一轮。O(log^2 bound)。
解法二(递归生成对照):先生成 x 的幂列表与 y 的幂列表(各不超过 bound),再
双重循环相加去重。展示"先建幂表再组合"的实现路径。

代码实现

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

class Solution {

    // 解法一:双重循环枚举 + 集合(推荐提交)
    public List<Integer> powerfulIntegers(int x, int y, int bound) {
        Set<Integer> set = new HashSet<>();
        for (int a = 1; a <= bound; a *= x) {
            for (int b = 1; a + b <= bound; b *= y) {
                set.add(a + b);
                if (y == 1) {
                    break;   // y == 1 时 b 恒为 1,无需继续
                }
            }
            if (x == 1) {
                break;   // x == 1 时 a 恒为 1,无需继续
            }
        }
        return new ArrayList<>(set);
    }

    // 解法二:幂表 + 双重循环(对照实现)
    public List<Integer> powerfulIntegers2(int x, int y, int bound) {
        List<Integer> px = powers(x, bound);
        List<Integer> py = powers(y, bound);
        Set<Integer> set = new HashSet<>();
        for (int a : px) {
            for (int b : py) {
                if (a + b <= bound) {
                    set.add(a + b);
                }
            }
        }
        return new ArrayList<>(set);
    }

    // 生成 base 的所有幂(<= limit;base==1 时只有 {1})
    private List<Integer> powers(int base, int limit) {
        List<Integer> res = new ArrayList<>();
        long v = 1;
        while (v <= limit) {
            res.add((int) v);
            if (base == 1) {
                break;
            }
            v *= base;
        }
        return res;
    }

    private static String fmt(List<Integer> a) {
        java.util.Collections.sort(a);
        return a.toString();
    }

    private static boolean same(List<Integer> a, List<Integer> b) {
        List<Integer> x = new ArrayList<>(a);
        List<Integer> y = new ArrayList<>(b);
        java.util.Collections.sort(x);
        java.util.Collections.sort(y);
        return x.equals(y);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[] xs = {2, 3, 1, 2, 2};
        int[] ys = {3, 5, 2, 2, 99};
        int[] bounds = {10, 15, 5, 1, 100};
        List<int[]> exps = new ArrayList<>();
        exps.add(new int[] {2,3,4,5,7,9,10});
        exps.add(new int[] {2,4,6,8,10,14});
        exps.add(new int[] {2,3,5});
        exps.add(new int[] {});
        exps.add(new int[] {2,3,5,9,17,33,65,100});
        for (int t = 0; t < xs.length; ++t) {
            ++total;
            List<Integer> g1 = s.powerfulIntegers(xs[t], ys[t], bounds[t]);
            List<Integer> g2 = s.powerfulIntegers2(xs[t], ys[t], bounds[t]);
            // 转 int 数组比较
            int[] a1 = new int[g1.size()];
            for (int i = 0; i < a1.length; ++i) {
                a1[i] = g1.get(i);
            }
            int[] a2 = new int[g2.size()];
            for (int i = 0; i < a2.length; ++i) {
                a2[i] = g2.get(i);
            }
            if (same(Arrays.asListInt(a1), Arrays.asListInt(a2))
                && same(Arrays.asListInt(a1), Arrays.asListInt(exps.get(t)))) {
                ++pass;
            } else {
                System.out.println("FAIL 970-" + (t + 1) + ": got " + fmt(g1) + " / " + fmt(g2));
            }
        }
        if (pass == total) {
            System.out.println("== 0970 PASS ==");
        } else {
            System.out.println("== 0970 FAIL ==");
            System.exit(1);
        }
    }

    private static class Arrays {
        static List<Integer> asListInt(int[] a) {
            List<Integer> l = new ArrayList<>();
            for (int v : a) {
                l.add(v);
            }
            return l;
        }
    }
}

复杂度分析


971. Flip Binary Tree To Match Preorder Traversal(翻转二叉树以匹配先序遍历)

题目描述

给你一棵二叉树的根节点 root,树中有 n 个节点,每个节点都有一个不同于其他节点且
处于 1 到 n 之间的值。另给你一个由 n 个值组成的行程序列 voyage,表示预期的二叉树
先序遍历结果。通过交换节点的左右子树,可以翻转该二叉树中的任意节点。请翻转最少的树中
节点,使二叉树的先序遍历与预期的遍历行程 voyage 相匹配。如果可以,则返回翻转的所有
节点的值的列表(可按任意顺序);如果不能,则返回列表 [-1]。

示例 1

  • 输入:root = [1,2], voyage = [2,1]
  • 输出:[-1]

示例 2

  • 输入:root = [1,2,3], voyage = [1,3,2]
  • 输出:[1]

示例 3

  • 输入:root = [1,2,3], voyage = [1,2,3]
  • 输出:[]
    提示:
  • n == voyage.length
  • 1 <= n <= 100
  • 1 <= Node.val, voyage[i] <= n
  • 树中的所有值互不相同,voyage 中的所有值互不相同

解题思路

先序遍历的顺序是「根 → 左 → 右」。值互不相同,因此当前节点必须等于 voyage[i]。
递归时若左孩子存在但值不等于下一个期望值,则必须翻转(先走右子树)。失败返回 [-1]。
解法一(DFS 贪心,主提交):全局下标 idx 指向 voyage。递归当前节点:值不匹配
返回 false;否则 idx++,若左孩子存在且值 != voyage[idx],翻转(记录当前节点值
并交换左右)。最后若全程匹配返回翻转列表。O(n)。
解法二(显式栈迭代对照):用栈按"反序"压入期望的先序序列,逐节点校验;无法匹配时
返回 [-1]。展示迭代写法。

代码实现

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

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

class Solution {

    private int idx;
    private List<Integer> flips;

    // 解法一:DFS 贪心(推荐提交)
    public List<Integer> flipMatchVoyage(TreeNode root, int[] voyage) {
        idx = 0;
        flips = new ArrayList<>();
        boolean ok = dfs(root, voyage);
        if (!ok) {
            List<Integer> bad = new ArrayList<>();
            bad.add(-1);
            return bad;
        }
        return flips;
    }

    private boolean dfs(TreeNode node, int[] voyage) {
        if (node == null) {
            return true;
        }
        if (node.val != voyage[idx]) {
            return false;
        }
        ++idx;
        if (node.left != null && node.left.val != voyage[idx]) {
            // 必须翻转:先遍历右子树,再遍历左子树
            flips.add(node.val);
            return dfs(node.right, voyage) && dfs(node.left, voyage);
        }
        return dfs(node.left, voyage) && dfs(node.right, voyage);
    }

    // 解法二:显式栈迭代(对照实现)
    public List<Integer> flipMatchVoyage2(TreeNode root, int[] voyage) {
        List<Integer> ans = new ArrayList<>();
        Deque<TreeNode> stk = new ArrayDeque<>();
        stk.push(root);
        int i = 0;
        while (!stk.isEmpty()) {
            TreeNode node = stk.pop();
            if (node == null) {
                continue;
            }
            if (node.val != voyage[i++]) {
                List<Integer> bad = new ArrayList<>();
                bad.add(-1);
                return bad;
            }
            if (node.left != null && node.left.val != voyage[i]) {
                ans.add(node.val);
                // 翻转:先压左(后遍历)再压右(先遍历)
                if (node.left != null) {
                    stk.push(node.left);
                }
                if (node.right != null) {
                    stk.push(node.right);
                }
            } else {
                if (node.right != null) {
                    stk.push(node.right);
                }
                if (node.left != null) {
                    stk.push(node.left);
                }
            }
        }
        return ans;
    }

    // 用层序数组建树(null 表示缺子节点),供本地测试用
    public static TreeNode build(Integer[] arr) {
        if (arr == null || arr.length == 0 || arr[0] == null) {
            return null;
        }
        TreeNode root = new TreeNode(arr[0]);
        Deque<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        int i = 1;
        while (!q.isEmpty() && i < arr.length) {
            TreeNode node = q.poll();
            if (arr[i] != null) {
                node.left = new TreeNode(arr[i]);
                q.offer(node.left);
            }
            ++i;
            if (i < arr.length && arr[i] != null) {
                node.right = new TreeNode(arr[i]);
                q.offer(node.right);
            }
            ++i;
        }
        return root;
    }

    // 校验:按翻转列表翻转副本后先序是否等于 voyage
    private static boolean verify(TreeNode root, List<Integer> flips, int[] voyage) {
        // 用原始树 + flips 模拟:这里直接重建应用翻转后的树再前序遍历
        TreeNode copy = deepCopy(root);
        java.util.Set<Integer> toFlip = new java.util.HashSet<>(flips);
        applyFlip(copy, toFlip);
        List<Integer> pre = new ArrayList<>();
        preorder(copy, pre);
        if (pre.size() != voyage.length) {
            return false;
        }
        for (int k = 0; k < voyage.length; ++k) {
            if (pre.get(k) != voyage[k]) {
                return false;
            }
        }
        return true;
    }

    private static TreeNode deepCopy(TreeNode node) {
        if (node == null) {
            return null;
        }
        TreeNode c = new TreeNode(node.val);
        c.left = deepCopy(node.left);
        c.right = deepCopy(node.right);
        return c;
    }

    private static void applyFlip(TreeNode node, java.util.Set<Integer> flips) {
        if (node == null) {
            return;
        }
        if (flips.contains(node.val)) {
            TreeNode t = node.left;
            node.left = node.right;
            node.right = t;
        }
        applyFlip(node.left, flips);
        applyFlip(node.right, flips);
    }

    private static void preorder(TreeNode node, List<Integer> out) {
        if (node == null) {
            return;
        }
        out.add(node.val);
        preorder(node.left, out);
        preorder(node.right, out);
    }

    private static String fmt(List<Integer> a) {
        return a.toString();
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        Integer[][] trees = {
            {1,2},
            {1,2,3},
            {1,2,3},
            {1},
            {1,2,3,4,5,6,7}
        };
        int[][] voyages = {
            {2,1},
            {1,3,2},
            {1,2,3},
            {1},
            {1,3,2,5,4,7,6}
        };
        for (int t = 0; t < trees.length; ++t) {
            ++total;
            TreeNode r1 = build(trees[t]);
            TreeNode r2 = build(trees[t]);
            List<Integer> g1 = s.flipMatchVoyage(r1, voyages[t]);
            List<Integer> g2 = s.flipMatchVoyage2(r2, voyages[t]);
            boolean ok1, ok2;
            if (!g1.isEmpty() && g1.get(0) == -1) {
                ok1 = true;   // 题目允许多个失败路径都返回 [-1]
            } else {
                ok1 = verify(build(trees[t]), g1, voyages[t]);
            }
            if (!g2.isEmpty() && g2.get(0) == -1) {
                ok2 = true;
            } else {
                ok2 = verify(build(trees[t]), g2, voyages[t]);
            }
            if (ok1 && ok2) {
                ++pass;
            } else {
                System.out.println("FAIL 971-" + (t + 1) + ": got " + fmt(g1) + " / " + fmt(g2));
            }
        }
        if (pass == total) {
            System.out.println("== 0971 PASS ==");
        } else {
            System.out.println("== 0971 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


972. Equal Rational Numbers(相等的有理数)

题目描述

给定两个字符串 s 和 t,每个字符串代表一个非负有理数,只有当它们表示相同的数字时
才返回 true。字符串中可以使用括号来表示有理数的重复部分。
有理数最多可以用三个部分来表示:整数部分 <IntegerPart>、小数非重复部分
<NonRepeatingPart> 和小数重复部分 <(><RepeatingPart><)>。
数字可以用以下三种方法之一来表示:<IntegerPart>(例:0、12);<IntegerPart><.> <NonRepeatingPart>(例:0.5、1.、2.12、123.0001);<IntegerPart><.><NonRepeatingPart> <(><RepeatingPart><)>(例:0.1(6)、1.(9)、123.00(1212))。

示例 1

  • 输入:s = "0.(52)", t = "0.5(25)"
  • 输出:true

示例 2

  • 输入:s = "0.1666(6)", t = "0.166(66)"
  • 输出:true

示例 3

  • 输入:s = "0.9(9)", t = "1."
  • 输出:true
  • 解释:"0.9(9)" 代表 0.999999999... 永远重复,等于 1。
    提示:
  • 每个部分仅由数字组成;整数部分不会以零开头(零本身除外);
  • 1 <= <IntegerPart>.length <= 4;0 <= <NonRepeatingPart>.length <= 4;
  • 1 <= <RepeatingPart>.length <= 4

解题思路

循环小数可精确写成普通分数:
设整数部分 I、非循环部分 N(长 a)、循环节 R(长 b),则
值 = I + N/10^a + R / (10^a * (10^b - 1))
(因为 0.(R) = R / (10^b - 1))。无循环节时退化为 I + N / 10^a。
用 BigInteger 构造分子分母后交叉相乘比较,彻底避免浮点误差。
解法一(精确分数,主提交)如上式。
解法二(循环节展开 + 高精度容差对照):把循环节重复足够多次展开成有限小数,用
double 解析后在极小容差内比较(0.525252... 与 0.525252... 精度足够)。

代码实现

import java.math.BigInteger;

class Solution {

    // 解法一:精确分数比较(推荐提交)
    public boolean isRationalEqual(String s, String t) {
        long[] f1 = parse(s);
        long[] f2 = parse(t);
        // cross multiply: f1.num * f2.den == f2.num * f1.den
        return (new BigInteger(String.valueOf(f1[0])).multiply(BigInteger.valueOf(f2[1])))
            .equals(new BigInteger(String.valueOf(f2[0])).multiply(BigInteger.valueOf(f1[1])));
    }

    // 解析为 [num, den](long 足够:值 <= 9999.9999,分母可达 10^8 级别)
    private long[] parse(String str) {
        int dot = str.indexOf('.');
        String intPart;
        String nonrep = "";
        String rep = "";
        if (dot < 0) {
            intPart = str;
        } else {
            intPart = str.substring(0, dot);
            String frac = str.substring(dot + 1);
            int lp = frac.indexOf('(');
            if (lp < 0) {
                nonrep = frac;
            } else {
                nonrep = frac.substring(0, lp);
                rep = frac.substring(lp + 1, frac.length() - 1);
            }
        }
        long I = intPart.isEmpty() ? 0 : Long.parseLong(intPart);
        long a = nonrep.length();
        long b = rep.length();
        if (b == 0) {
            // 有限小数:I + N/10^a
            long den = pow10(a);
            long num = I * den + (a == 0 ? 0 : Long.parseLong(nonrep));
            return new long[] {num, den};
        }
        // 无限循环:I + N/10^a + R/(10^a * (10^b - 1))
        long pa = pow10(a);
        long pb = pow10(b);
        long den = pa * (pb - 1);
        long R = Long.parseLong(rep);
        long N = a == 0 ? 0 : Long.parseLong(nonrep);
        long num = I * den + N * (pb - 1) + R;
        return new long[] {num, den};
    }

    private long pow10(long k) {
        long r = 1;
        for (long i = 0; i < k; ++i) {
            r *= 10;
        }
        return r;
    }

    // 解法二:循环节展开 + double 容差比较(对照实现)
    public boolean isRationalEqual2(String s, String t) {
        double a = expand(s);
        double b = expand(t);
        return Math.abs(a - b) < 1e-9;
    }

    private double expand(String str) {
        int dot = str.indexOf('.');
        String intPart;
        String nonrep = "";
        String rep = "";
        if (dot < 0) {
            intPart = str;
        } else {
            intPart = str.substring(0, dot);
            String frac = str.substring(dot + 1);
            int lp = frac.indexOf('(');
            if (lp < 0) {
                nonrep = frac;
            } else {
                nonrep = frac.substring(0, lp);
                rep = frac.substring(lp + 1, frac.length() - 1);
            }
        }
        StringBuilder mant = new StringBuilder(nonrep);
        for (int i = 0; i < 20; ++i) {
            mant.append(rep);
        }
        return Double.parseDouble(intPart + "." + mant.toString());
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        String[][] pairs = {
            {"0.(52)", "0.5(25)"},
            {"0.1666(6)", "0.166(66)"},
            {"0.9(9)", "1."},
            {"1", "1.0"},
            {"0.(0)", "0"},
            {"1.2(34)", "1.2343434(34)"},
            {"0.", "0"},
            {"1.9(9)", "2."}
        };
        boolean[] exp = {true, true, true, true, true, true, true, true};
        for (int t = 0; t < pairs.length; ++t) {
            ++total;
            boolean g1 = s.isRationalEqual(pairs[t][0], pairs[t][1]);
            boolean g2 = s.isRationalEqual2(pairs[t][0], pairs[t][1]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 972-" + (t + 1) + ": " + pairs[t][0] + " vs " + pairs[t][1]
                    + " got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0972 PASS ==");
        } else {
            System.out.println("== 0972 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


973. K Closest Points to Origin(最接近原点的 K 个点)

题目描述

给定一个数组 points,其中 points[i] = [x_i, y_i] 表示 X-Y 平面上的一个点,并且是
一个整数 k,返回离原点 (0,0) 最近的 k 个点。这里,平面上两点之间的距离是欧几里德
距离。你可以按任何顺序返回答案。除了点坐标的顺序之外,答案确保是唯一的。

示例 1

  • 输入:points = [[1,3],[-2,2]], k = 1
  • 输出:[[-2,2]]

示例 2

  • 输入:points = [[3,3],[5,-1],[-2,4]], k = 2
  • 输出:[[3,3],[-2,4]]
    提示:
  • 1 <= k <= points.length <= 10^4
  • -10^4 <= x_i, y_i <= 10^4

解题思路

距离平方 x^2 + y^2 即可比较大小,无需开根。
解法一(快速选择/排序取前 k,主提交):按距离平方排序后取前 k 个。
O(n log n)。
解法二(堆,对照实现):维护大小为 k 的最大堆,堆顶是当前最远点;遍历每个点时,
若其距离小于堆顶则替换。O(n log k)。

代码实现

import java.util.Arrays;
import java.util.PriorityQueue;

class Solution {

    // 解法一:排序取前 k(推荐提交)
    public int[][] kClosest(int[][] points, int k) {
        Integer[] idx = new Integer[points.length];
        for (int i = 0; i < idx.length; ++i) {
            idx[i] = i;
        }
        Arrays.sort(idx, (a, b) -> {
            long da = (long) points[a][0] * points[a][0] + (long) points[a][1] * points[a][1];
            long db = (long) points[b][0] * points[b][0] + (long) points[b][1] * points[b][1];
            return Long.compare(da, db);
        });
        int[][] ans = new int[k][2];
        for (int i = 0; i < k; ++i) {
            ans[i] = points[idx[i]];
        }
        return ans;
    }

    // 解法二:最大堆(对照实现)
    public int[][] kClosest2(int[][] points, int k) {
        // 堆顶是当前最远点(最大距离)
        PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> {
            long db = (long) b[0] * b[0] + (long) b[1] * b[1];
            long da = (long) a[0] * a[0] + (long) a[1] * a[1];
            return Long.compare(db, da);   // 距离大的优先(最大堆)
        });
        for (int[] p : points) {
            pq.offer(p);
            if (pq.size() > k) {
                pq.poll();
            }
        }
        return pq.toArray(new int[k][2]);
    }

    private static String fmt(int[][] a) {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < a.length; ++i) {
            if (i > 0) {
                sb.append(", ");
            }
            sb.append(Arrays.toString(a[i]));
        }
        return sb.append(']').toString();
    }

    private static boolean sameSet(int[][] a, int[][] b) {
        if (a.length != b.length) {
            return false;
        }
        java.util.Set<String> sa = new java.util.HashSet<>();
        for (int[] p : a) {
            sa.add(p[0] + "," + p[1]);
        }
        for (int[] p : b) {
            if (!sa.contains(p[0] + "," + p[1])) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][][] pts = {
            {{1,3},{-2,2}},
            {{3,3},{5,-1},{-2,4}},
            {{0,2},{1,0}},
            {{-1,0},{0,-3},{2,0}},
            {{1,1}}
        };
        int[] ks = {1, 2, 1, 2, 1};
        int[][][] exps = {
            {{-2,2}},
            {{3,3},{-2,4}},
            {{1,0}},
            {{-1,0},{2,0}},
            {{1,1}}
        };
        for (int t = 0; t < pts.length; ++t) {
            ++total;
            int[][] g1 = s.kClosest(pts[t], ks[t]);
            int[][] g2 = s.kClosest2(pts[t], ks[t]);
            if (sameSet(g1, exps[t]) && sameSet(g2, exps[t])) {
                ++pass;
            } else {
                System.out.println("FAIL 973-" + (t + 1) + ": got " + fmt(g1) + " / " + fmt(g2) + " exp " + fmt(exps[t]));
            }
        }
        if (pass == total) {
            System.out.println("== 0973 PASS ==");
        } else {
            System.out.println("== 0973 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


974. Subarray Sums Divisible by K(和可被 K 整除的子数组)

题目描述

给定一个整数数组 nums 和一个整数 k,返回其中元素之和可被 k 整除的非空子数组的
数目。子数组是数组中连续的部分。

示例 1

  • 输入:nums = [4,5,0,-2,-3,1], k = 5
  • 输出:7
  • 解释:有 7 个子数组满足其元素之和可被 k = 5 整除:
    [4,5,0,-2,-3,1]、[5]、[5,0]、[5,0,-2,-3]、[0]、[0,-2,-3]、[-2,-3]。

示例 2

  • 输入:nums = [5], k = 9
  • 输出:0
    提示:
  • 1 <= nums.length <= 3 * 10^4
  • -10^4 <= nums[i] <= 10^4
  • 2 <= k <= 10^4

解题思路

子数组和能被 k 整除,当且仅当两端前缀和模 k 相同:
(pre[j] - pre[i]) % k == 0 等价于 pre[j] % k == pre[i] % k。
解法一(哈希表 + 前缀和,主提交):从左到右扫描,cnt[r] 记录「前缀和模 k = r 的
已出现次数」。对每个位置 j = {r = (sum mod k + k) % k},它贡献的合法子数组数 =
cnt[r](以 j 结尾、起点 i 满足 pre[i] mod k == r 的子数组)。初始 cnt[0] = 1
对应空前缀。O(n)。
解法二(计数数组对照):由于 k <= 10^4,直接用定长数组替代哈希表统计余数,
逻辑完全一致,展示省内存版本。

代码实现

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

class Solution {

    // 解法一:哈希表 + 前缀和(推荐提交)
    public int subarraysDivByK(int[] nums, int k) {
        Map<Integer, Integer> cnt = new HashMap<>();
        cnt.put(0, 1);
        int sum = 0;
        int ans = 0;
        for (int v : nums) {
            sum += v;
            int r = ((sum % k) + k) % k;   // Java 的 % 可负数,归一化到 [0, k-1]
            int c = cnt.getOrDefault(r, 0);
            ans += c;
            cnt.put(r, c + 1);
        }
        return ans;
    }

    // 解法二:定长计数数组(对照实现)
    public int subarraysDivByK2(int[] nums, int k) {
        int[] cnt = new int[k];
        cnt[0] = 1;
        int sum = 0;
        int ans = 0;
        for (int v : nums) {
            sum += v;
            int r = ((sum % k) + k) % k;
            ans += cnt[r];
            cnt[r]++;
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] nums = {
            {4,5,0,-2,-3,1},
            {5},
            {0},
            {-1,2,1,2},
            {2,2,2}
        };
        int[] ks = {5, 9, 1, 6, 2};
        int[] exp = {7, 0, 1, 0, 6};
        for (int t = 0; t < nums.length; ++t) {
            ++total;
            int g1 = s.subarraysDivByK(nums[t].clone(), ks[t]);
            int g2 = s.subarraysDivByK2(nums[t].clone(), ks[t]);
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 974-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0974 PASS ==");
        } else {
            System.out.println("== 0974 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析


975. Odd Even Jump(奇偶跳)

题目描述

给定一个整数数组 arr,你可以从某一起始索引出发,跳跃一定次数。在你跳跃的过程中, 第 1、3、5... 次跳跃称为奇数跳跃,而第 2、4、6... 次跳跃称为偶数跳跃。 你可以按以下方式从索引 i 向后跳转到索引 j(其中 i < j):

  • 奇数跳跃:跳到索引 j,使得 arr[i] <= arr[j],且 arr[j] 的值尽可能小;如果存在 多个这样的索引 j,跳到满足要求的最小索引上。
  • 偶数跳跃:跳到索引 j,使得 arr[i] >= arr[j],且 arr[j] 的值尽可能大;如果存在 多个这样的索引 j,跳到满足要求的最小索引上。 如果从某一索引开始跳跃一定次数(可能是 0 次或多次),就可以到达数组的末尾(索引 arr.length - 1),那么该索引就会被认为是好的起始索引。返回好的起始索引的数量。

示例 1

  • 输入:[10,13,12,14,15]
  • 输出:2

示例 2

  • 输入:[2,3,1,1,4]
  • 输出:3

示例 3

  • 输入:[5,1,3,4,2]
  • 输出:3 提示:
  • 1 <= arr.length <= 20000
  • 0 <= arr[i] < 100000

解题思路

每一步的落点只与"右侧大于等于当前值的最小值"或"右侧小于等于当前值的最大值"有关, 与之前的跳法无关。从右往左预处理每个位置的下一步:

  • 奇数跳 oddNext[i]:右侧 >= arr[i] 的最小值的最小下标;
  • 偶数跳 evenNext[i]:右侧 <= arr[i] 的最大值的最小下标。 用有序集合(TreeMap)维护「值 → 最小下标」(从右往左扫描时后出现的更小下标覆盖), 查询 ceilingKey / floorKey。 然后从右往左 DP:odd[i] = 从 i 出发奇数跳能否到终点(oddNext[i] == -1 时 False, 否则看 even[oddNext[i]]);even[i] 对称。答案 = odd[i] 为真的个数。 解法一(TreeMap + 反向 DP,主提交)如上。 解法二(排序 + 树状数组对照):把下标按值排序后,用两个有序集合分别维护已扫描的 值集合与下标,查询后继/前驱;逻辑同构,展示另一种数据结构的实现。

代码实现

import java.util.Arrays;
import java.util.TreeMap;

class Solution {

    // 解法一:TreeMap + 反向 DP(推荐提交)
    public int oddEvenJumps(int[] arr) {
        int n = arr.length;
        int[] oddNext = new int[n];
        int[] evenNext = new int[n];
        Arrays.fill(oddNext, -1);
        Arrays.fill(evenNext, -1);
        TreeMap<Integer, Integer> map = new TreeMap<>();   // 值 -> 最小下标
        for (int i = n - 1; i >= 0; --i) {
            Integer ck = map.ceilingKey(arr[i]);   // >= arr[i] 的最小值
            Integer fk = map.floorKey(arr[i]);     // <= arr[i] 的最大值
            if (ck != null) {
                oddNext[i] = map.get(ck);
            }
            if (fk != null) {
                evenNext[i] = map.get(fk);
            }
            map.put(arr[i], i);   // 同值取最小下标(从左往右更早的在右边,此处覆盖为更小的)
        }
        boolean[] odd = new boolean[n];
        boolean[] even = new boolean[n];
        odd[n - 1] = true;
        even[n - 1] = true;
        int ans = 1;
        for (int i = n - 2; i >= 0; --i) {
            if (oddNext[i] != -1) {
                odd[i] = even[oddNext[i]];
            }
            if (evenNext[i] != -1) {
                even[i] = odd[evenNext[i]];
            }
            if (odd[i]) {
                ++ans;
            }
        }
        return ans;
    }

    // 解法二:值排序 + 有序下标集合(对照实现)
    public int oddEvenJumps2(int[] arr) {
        int n = arr.length;
        Integer[] idx = new Integer[n];
        for (int i = 0; i < n; ++i) {
            idx[i] = i;
        }
        // 按值升序;值相同按下标升序
        Arrays.sort(idx, (a, b) -> arr[a] != arr[b] ? Integer.compare(arr[a], arr[b]) : Integer.compare(a, b));
        int[] oddNext = buildNext(idx, arr, true);
        // 按值降序;值相同按下标升序
        Arrays.sort(idx, (a, b) -> arr[a] != arr[b] ? Integer.compare(arr[b], arr[a]) : Integer.compare(a, b));
        int[] evenNext = buildNext(idx, arr, false);

        boolean[] odd = new boolean[n];
        boolean[] even = new boolean[n];
        odd[n - 1] = true;
        even[n - 1] = true;
        int ans = 1;
        for (int i = n - 2; i >= 0; --i) {
            if (oddNext[i] != -1) {
                odd[i] = even[oddNext[i]];
            }
            if (evenNext[i] != -1) {
                even[i] = odd[evenNext[i]];
            }
            if (odd[i]) {
                ++ans;
            }
        }
        return ans;
    }

    // 按排序后的密集下标顺序,维护已扫描下标集合,找 i 右侧(> i)最小下标
    // 升序扫描时,已处理的都是值 <= 当前值的候选;要求下标 > i 且最小。
    private int[] buildNext(Integer[] order, int[] arr, boolean asc) {
        int n = arr.length;
        int[] next = new int[n];
        Arrays.fill(next, -1);
        java.util.TreeSet<Integer> seen = new java.util.TreeSet<>();
        for (int i = n - 1; i >= 0; --i) {
            int pos = order[i];
            Integer hi = seen.higher(pos);
            if (hi != null) {
                next[pos] = hi;
            }
            seen.add(pos);
        }
        return next;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int pass = 0, total = 0;
        int[][] arrs = {
            {10,13,12,14,15},
            {2,3,1,1,4},
            {5,1,3,4,2},
            {1},
            {1,2}
        };
        int[] exp = {2, 3, 3, 1, 2};
        for (int t = 0; t < arrs.length; ++t) {
            ++total;
            int g1 = s.oddEvenJumps(arrs[t].clone());
            int g2 = s.oddEvenJumps2(arrs[t].clone());
            if (g1 == exp[t] && g2 == exp[t]) {
                ++pass;
            } else {
                System.out.println("FAIL 975-" + (t + 1) + ": got " + g1 + "/" + g2 + " exp " + exp[t]);
            }
        }
        if (pass == total) {
            System.out.println("== 0975 PASS ==");
        } else {
            System.out.println("== 0975 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析