LeetCode1921~1940题目解析

LeetCode 1921-1940. 题解合集

文章目录


1921. Eliminate Maximum Number of Monsters(消灭怪物的最大数量)

题目描述

你正在玩一款电子游戏,在游戏中你需要保护城市免受怪物侵袭。给定一个下标从 0 开始且大小为 n

的整数数组 dist,其中 dist[i] 是第 i 个怪物与城市的初始距离(单位:千米)。

怪物以恒定的速度走向城市。每个怪物的速度都以一个长度为 n 的整数数组 speed 表示,

其中 speed[i] 是第 i 个怪物的速度(单位:千米/分)。

你有一种武器,一旦充满电,就可以消灭一个怪物。但是,武器需要一分钟才能充电。武器在游戏开始时

是充满电的状态,怪物从第 0 分钟时开始移动。

一旦任一怪物到达城市,你就输掉了这场游戏。如果某个怪物恰好在一分钟开始时到达城市(距离表示为 0),

这也会被视为输掉游戏,在你可以使用武器之前,游戏就会结束。

返回在你输掉游戏前可以消灭的怪物的最大数量。如果你可以在所有怪物到达城市前将它们全部消灭,返回 n。

示例 1

  • 输入 :dist = 1,3,4, speed = 1,1,1
  • 输出:3
  • 解释:第 0 分钟消灭第一个怪物,第 1 分钟消灭第二个怪物,第 3 分钟消灭第三个怪物。

示例 2

  • 输入 :dist = 1,1,2,3, speed = 1,1,1,1
  • 输出:1
  • 解释:第 1 分钟开始时第二个怪物距离为 0,你输掉了游戏。

示例 3

  • 输入 :dist = 3,2,4, speed = 5,3,2
  • 输出:1
  • 解释 :第 1 分钟开始时第二个怪物距离为 0,你输掉了游戏。
    提示:
  • - n == dist.length == speed.length
  • - 1 <= n <= 10^5
  • - 1 <= dist[i], speed[i] <= 10^5

解题思路

解法一:贪心 + 排序到达时刻,O(n log n),主提交版本

第 i 个怪物在 t = ceil(dist[i] / speed[i])

分钟到达(到达时距离为 0 也算输)。你在第 0...k-1 分钟可以各消灭一个怪物,因此若想消灭 k 个怪物,

必须存在 k 个怪物的到达时刻严格大于其被消灭的分钟:即把到达时刻升序排列后,第 j 个(0 基)到达时刻

必须 > j。按到达时刻排序后贪心扫描,统计满足 arrive[j] > j 的最长前缀长度。

解法二:模拟每分钟 + 最小堆,对照

小顶堆维护"当前最近到达的怪物时刻",每分钟弹出一个消灭并

计数,若堆顶时刻 <= 当前分钟则说明有怪物已到达,游戏结束。O(n log n),用于交叉验证。

代码实现

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

public class Solution {

    public int eliminateMaximum(int[] dist, int[] speed) {
        int n = dist.length;
        int[] arrive = new int[n];
        for (int i = 0; i < n; i++) {
            arrive[i] = (dist[i] + speed[i] - 1) / speed[i]; // 向上取整到达分钟
        }
        Arrays.sort(arrive);
        int cnt = 0;
        for (int i = 0; i < n; i++) {
            if (arrive[i] <= i) {  // 第 i 分钟开始前该怪物已到(或恰好到)
                break;
            }
            cnt++;
        }
        return cnt;
    }

    public int eliminateMaximumHeap(int[] dist, int[] speed) {
        int n = dist.length;
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        for (int i = 0; i < n; i++) {
            pq.offer((dist[i] + speed[i] - 1) / speed[i]);
        }
        int minute = 0;
        while (!pq.isEmpty()) {
            if (pq.peek() <= minute) {   // 该怪物已在第 minute 分钟开始前到达
                break;
            }
            pq.poll();
            minute++;
        }
        return minute;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[] d1 = {1, 3, 4}, sp1 = {1, 1, 1};
        int[] d2 = {1, 1, 2, 3}, sp2 = {1, 1, 1, 1};
        int[] d3 = {3, 2, 4}, sp3 = {5, 3, 2};
        boolean ok = true;
        ok &= s.eliminateMaximum(d1, sp1) == 3 && s.eliminateMaximumHeap(d1, sp1) == 3;
        ok &= s.eliminateMaximum(d2, sp2) == 1 && s.eliminateMaximumHeap(d2, sp2) == 1;
        ok &= s.eliminateMaximum(d3, sp3) == 1 && s.eliminateMaximumHeap(d3, sp3) == 1;
        System.out.println((ok ? "PASS" : "FAIL") + " case1 => " + s.eliminateMaximum(d1, sp1));
        System.out.println((ok ? "PASS" : "FAIL") + " case2 => " + s.eliminateMaximum(d2, sp2));
        System.out.println((ok ? "PASS" : "FAIL") + " case3 => " + s.eliminateMaximum(d3, sp3));
        System.out.println(ok ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1922. Count Good Numbers(统计好数字的数目)

题目描述

我们称一个数字字符串是好数字当它满足(下标从 0 开始):偶数下标处的数字为偶数且奇数下标处的数字

为质数(2,3,5 或 7)。

比方说,"2582" 是好数字,因为偶数下标处的数字(2 和 8)是偶数且奇数下标处的数字

(5 和 2)为质数。但 "3245" 不是好数字,因为 3 在偶数下标处但不是偶数。

给你一个整数 n,请你返回长度为 n 且为好数字的数字字符串总数。由于答案可能会很大,

请你将它对 10^9 + 7 取余后返回。

一个数字字符串是每一位都由 0 到 9 组成的字符串,且可能包含前导 0。

示例 1

  • 输入 :n = 1
  • 输出:5
  • 解释:长度为 1 的好数字包括 "0","2","4","6","8"。

示例 2

  • 输入 :n = 4
  • 输出:400

示例 3

  • 输入 :n = 50
  • 输出 :564908303
    提示:
  • - 1 <= n <= 10^15

解题思路

解法一:快速幂,O(log n),主提交版本

每个位置独立选数字------偶数下标可选 {0,2,4,6,8} 共 5 种,

奇数下标可选 {2,3,5,7} 共 4 种。长度为 n 时偶数位置个数 cntEven = (n + 1) / 2,奇数位置个数

cntOdd = n / 2。答案 = 5^cntEven * 4^cntOdd mod (10^9+7),用快速幂按指数二分计算。

解法二:递推迭代,对照

按位递推 res = res * 5 / * 4,n 可达 10^15 不能逐位,因此把解法一

的快速幂展开为"倍增式"循环(对每一位以 2 为步长处理),本质相同,写法用于交叉验证。

代码实现

java 复制代码
public class Solution {

    private static final int MOD = 1_000_000_007;

    public int countGoodNumbers(long n) {
        long even = (n + 1) / 2;  // 偶数下标数量
        long odd = n / 2;          // 奇数下标数量
        long res = modPow(5, even) * modPow(4, odd) % MOD;
        return (int) res;
    }

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

    public int countGoodNumbersIter(long n) {
        long even = (n + 1) / 2;  // 5^even
        long odd = n / 2;          // 4^odd
        long res = 1;
        long baseEven = 5, baseOdd = 4;
        // 两个底数各自的指数同时做二进制拆分,合并相乘
        while (even > 0 || odd > 0) {
            if ((even & 1) == 1) {
                res = res * baseEven % MOD;
            }
            if ((odd & 1) == 1) {
                res = res * baseOdd % MOD;
            }
            baseEven = baseEven * baseEven % MOD;
            baseOdd = baseOdd * baseOdd % MOD;
            even >>= 1;
            odd >>= 1;
        }
        return (int) res;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        long[] ns = {1, 4, 50, 2, 3};
        int[] exp = {5, 400, 564908303, 20, 100};
        boolean all = true;
        for (int i = 0; i < ns.length; i++) {
            int a = s.countGoodNumbers(ns[i]);
            int b = s.countGoodNumbersIter(ns[i]);
            boolean ok = a == exp[i] && b == exp[i];
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " (n=" + ns[i] + ") => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1923. Longest Common Subpath(最长公共子路径)

题目描述

一个国家由 n 个编号为 0 到 n - 1 的城市组成。在这个国家里,每两个城市之间都有一条道路连接。

总共有 m 个编号为 0 到 m - 1 的朋友想在这个国家旅游。他们每一个人的路径都会包含一些城市。

每条路径都由一个整数数组表示,每个整数数组表示一个朋友按顺序访问过的城市序列。同一个城市在一条

路径中可能重复出现,但同一个城市在一条路径中不会连续出现。

给你一个整数 n 和二维数组 paths,其中 paths[i] 是一个整数数组,表示第 i 个朋友走过的路径,

请你返回每一个朋友都走过的最长公共子路径的长度,如果不存在公共子路径,请你返回 0。

一个子路径指的是一条路径中连续的城市序列。

示例 1

  • 输入 :n = 5, paths = \[0,1,2,3,4,2,3,4,4,0,1,2,3]
  • 输出:2
  • 解释:最长公共子路径为 2,3。

示例 2

  • 输入 :n = 3, paths = \[0,1,2]
  • 输出:0
  • 解释:三条路径没有公共子路径。

示例 3

  • 输入 :n = 5, paths = \[0,1,2,3,4,4,3,2,1,0]
  • 输出:1
  • 解释 :最长公共子路径为 0,1,2,3 和 4。它们长度都为 1。
    提示:
  • - 1 <= n <= 10^5
  • - m == paths.length
  • - 2 <= m <= 10^5
  • - sum(paths[i].length) <= 10^5
  • - 0 <= paths[i][j] < n
    • paths[i] 中同一个城市不会连续重复出现

解题思路

解法一:二分长度 + 滚动哈希去重,O(S log S),主提交版本

检查"是否存在长度为 len 的公共子路径"

是否可行可用二分(答案具有单调性:若 len 可行则更短也可行)。判定时对第一条路径枚举所有长度为 len

的子段计算哈希放入集合(用双哈希 64 位或单 64 位滚动哈希 + 长度绑定避免碰撞,这里用两个 64 位

乘加组合),其余每条路径只保留其长度为 len 的子段哈希中"同时出现在之前所有路径集合里"的哈希,

最后集合为空则不存在。注意每条路径内部哈希要去重(同一路径可能重复出现同一子段),

用每路径内 Set 收集再与全局集合求交。

解法二:暴力枚举起点 + KMP 逐步缩小,对照

先取最短路径为基准,枚举其每个长度为 len 的子段

检查是否为所有路径的子串(用 String/List indexOf 或朴素匹配)。正确但 O(S^2) 仅用于小数据交叉验证。

代码实现

java 复制代码
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class Solution {

    public int longestCommonSubpath(int n, int[][] paths) {
        int lo = 0, hi = Integer.MAX_VALUE;
        for (int[] p : paths) {
            hi = Math.min(hi, p.length);
        }
        while (lo < hi) {
            int mid = (lo + hi + 1) >>> 1;
            if (check(mid, paths)) {
                lo = mid;
            } else {
                hi = mid - 1;
            }
        }
        return lo;
    }

    private boolean check(int len, int[][] paths) {
        if (len == 0) {
            return true;
        }
        final long MOD = (1L << 61) - 1;  // 大质数
        final long BASE = 1000003L;
        // 第一条路径的所有长度为 len 的子段哈希
        Set<Long> common = hashesOf(paths[0], len, BASE, MOD);
        // 其余路径与 common 求交
        for (int i = 1; i < paths.length; i++) {
            Set<Long> cur = hashesOf(paths[i], len, BASE, MOD);
            Set<Long> next = new HashSet<>();
            for (long h : cur) {
                if (common.contains(h)) {
                    next.add(h);
                }
            }
            if (next.isEmpty()) {
                return false;
            }
            common = next;
        }
        return !common.isEmpty();
    }

    // 返回数组所有长度为 len 连续子段的 61 位滚动哈希集合(内部去重)
    private Set<Long> hashesOf(int[] path, int len, long BASE, long MOD) {
        Set<Long> set = new HashSet<>();
        int m = path.length;
        if (m < len) {
            return set;
        }
        // 预计算 base^len mod MOD
        long pow = 1;
        for (int i = 0; i < len; i++) {
            pow = pow * BASE % MOD;
        }
        long h = 0;
        for (int i = 0; i < len; i++) {
            h = (h * BASE + path[i] + 1) % MOD;
        }
        set.add(h);
        for (int i = len; i < m; i++) {
            h = (h * BASE + path[i] + 1) % MOD;
            h = ((h - (path[i - len] + 1) * pow % MOD) + MOD) % MOD;
            set.add(h);
        }
        return set;
    }

    public int longestCommonSubpathBrute(int n, int[][] paths) {
        // 取最短路径
        int minIdx = 0;
        for (int i = 1; i < paths.length; i++) {
            if (paths[i].length < paths[minIdx].length) {
                minIdx = i;
            }
        }
        int best = 0;
        int m = paths[minIdx].length;
        for (int l = 0; l < m; l++) {
            for (int len = 1; l + len <= m; len++) {
                if (isCommon(paths, minIdx, l, len)) {
                    best = Math.max(best, len);
                }
            }
        }
        return best;
    }

    private boolean isCommon(int[][] paths, int baseIdx, int start, int len) {
        int[] base = paths[baseIdx];
        outer:
        for (int i = 0; i < paths.length; i++) {
            int[] p = paths[i];
            boolean found = false;
            for (int j = 0; j + len <= p.length; j++) {
                boolean eq = true;
                for (int t = 0; t < len; t++) {
                    if (p[j + t] != base[start + t]) {
                        eq = false;
                        break;
                    }
                }
                if (eq) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] p1 = {{0, 1, 2, 3, 4}, {2, 3, 4}, {4, 0, 1, 2, 3}};
        int[][] p2 = {{0}, {1}, {2}};
        int[][] p3 = {{0, 1, 2, 3, 4}, {4, 3, 2, 1, 0}};
        boolean ok = true;
        int a1 = s.longestCommonSubpath(5, p1);
        int b1 = s.longestCommonSubpathBrute(5, p1);
        ok &= a1 == 2 && b1 == 2;
        int a2 = s.longestCommonSubpath(3, p2);
        int b2 = s.longestCommonSubpathBrute(3, p2);
        ok &= a2 == 0 && b2 == 0;
        int a3 = s.longestCommonSubpath(5, p3);
        int b3 = s.longestCommonSubpathBrute(5, p3);
        ok &= a3 == 1 && b3 == 1;
        System.out.println((ok ? "PASS" : "FAIL") + " case1 => " + a1 + " / " + b1);
        System.out.println((ok ? "PASS" : "FAIL") + " case2 => " + a2 + " / " + b2);
        System.out.println((ok ? "PASS" : "FAIL") + " case3 => " + a3 + " / " + b3);
        System.out.println(ok ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1924. Erect the Fence II(安装栅栏 II)

题目描述

给你一个二维整数数组 trees,其中 trees[i] = [xi, yi] 表示花园中第 i 棵树的坐标。

你需要用最少的原材料给花园安装一个圆形的栅栏,使花园中所有的树都被围在栅栏内部(在栅栏边界上的树

也算在内)。正式地说,你需要求出栅栏的圆心坐标 (x,y) 和半径 r,使花园中所有的树都在圆的内部

或边界上,并且让半径 r 最小。

请用一个长度为 3 的数组 [x,y,r] 来返回圆心坐标和半径。如果答案与正确答案的误差不超过 10^-5,

则该答案将被视为正确答案通过。

示例 1

  • 输入 :trees = \[1,1,2,2,2,0,2,4,3,3,4,2]
  • 输出:2.00000,2.00000,2.00000
  • 解释:栅栏的圆心应当在 (2, 2) 处,半径为 2。

示例 2

  • 输入 :trees = \[1,2,2,2,4,2]
  • 输出:2.50000,2.00000,1.50000
  • 解释 :栅栏的圆心应当在 (2.5, 2) 处,半径为 1.5。
    提示:
  • - 1 <= trees.length <= 3000
  • - trees[i].length == 2
  • - 0 <= xi, yi <= 3000

解题思路

解法一:Welzl 随机增量法求最小圆覆盖,期望 O(n),主提交版本

利用经典结论:最小覆盖圆由

不超过 3 个点确定。先把点随机打乱,维护当前圆(圆心 O、半径 r)。逐点加入:若点在圆外,则该点

必在边界上,递归调用"过边界点求最小圆":先以该点为圆心 r=0,再扫其余点,若某点也不在圆内,

则以这两点为直径做圆;仍不满足的第三个点,用三点定圆。整过程用 shuffle 保证期望线性。

三点定圆用向量叉积 + 垂径中点法计算。

解法二:暴力三分搜索圆心/半径上界枚举,对照

圆心一定在点集包围盒内;对半径做"用固定圆心尝试

覆盖全部点"的可行性二分并不单调,因此改用直接推导答案的朴素做法不可取------这里用基于"枚举两点直径

或三点外接圆"的暴力:枚举所有两点组合与三点组合,对每个候选圆检查覆盖全部点(O(n^4) 在小数据),

取半径最小者,用于交叉验证。

代码实现

java 复制代码
public class Solution {

    public double[] outerTrees(int[][] trees) {
        // 随机打乱
        java.util.Random rand = new java.util.Random(1234567L);
        int n = trees.length;
        int[][] pts = trees.clone();
        for (int i = n - 1; i > 0; i--) {
            int j = rand.nextInt(i + 1);
            int[] t = pts[i]; pts[i] = pts[j]; pts[j] = t;
        }
        double cx = 0, cy = 0, r = -1;  // r=-1 表示未初始化
        // 初始化圆为前两个点确定的圆(若 n==1 则是单点)
        if (n == 1) {
            return new double[]{pts[0][0], pts[0][1], 0};
        }
        circleFrom2(pts[0], pts[1]);
        // 用增量法:先以两点定圆逐步扩展
        double[] c2 = circleFrom2(pts[0], pts[1]);
        cx = c2[0]; cy = c2[1]; r = c2[2];
        for (int i = 2; i < n; i++) {
            if (!in(pts[i], cx, cy, r)) {
                // pts[i] 在圆外 -> 该点在最终圆边界上,重建
                double[] c = circleWithBoundary(pts, i, cx, cy, r);
                cx = c[0]; cy = c[1]; r = c[2];
            }
        }
        return new double[]{cx, cy, r};
    }

    // 以 points[k] 为边界点,对前 k+1 个点重建最小圆(k 之前点可跨过已确定的边界)
    private double[] circleWithBoundary(int[][] pts, int k, double cx, double cy, double r) {
        // 先以 pts[k] 为圆心、半径 0
        double ox = pts[k][0], oy = pts[k][1], or = 0;
        for (int i = 0; i < k; i++) {
            if (!in(pts[i], ox, oy, or)) {
                // pts[i] 也在边界上:以 pts[k]、pts[i] 为直径
                double[] c = circleFrom2(pts[k], pts[i]);
                ox = c[0]; oy = c[1]; or = c[2];
                for (int j = 0; j < i; j++) {
                    if (!in(pts[j], ox, oy, or)) {
                        // 三点定圆
                        double[] c3 = circleFrom3(pts[k], pts[i], pts[j]);
                        ox = c3[0]; oy = c3[1]; or = c3[2];
                    }
                }
            }
        }
        return new double[]{ox, oy, or};
    }

    private boolean in(int[] p, double cx, double cy, double r) {
        double dx = p[0] - cx, dy = p[1] - cy;
        return dx * dx + dy * dy <= r * r + 1e-9;
    }

    // 以两点为直径的圆
    private double[] circleFrom2(int[] a, int[] b) {
        double cx = (a[0] + b[0]) / 2.0;
        double cy = (a[1] + b[1]) / 2.0;
        double r = Math.hypot(a[0] - b[0], a[1] - b[1]) / 2.0;
        return new double[]{cx, cy, r};
    }

    // 三点外接圆;三点共线时退化为最长边中点直径
    private double[] circleFrom3(int[] a, int[] b, int[] c) {
        double ax = a[0], ay = a[1], bx = b[0], by = b[1], cx = c[0], cy = c[1];
        double d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
        if (Math.abs(d) < 1e-12) {  // 共线
            // 选两两距离最大的一对作直径
            double d1 = dist2(a, b), d2 = dist2(a, c), d3 = dist2(b, c);
            if (d1 >= d2 && d1 >= d3) return circleFrom2(a, b);
            if (d2 >= d3) return circleFrom2(a, c);
            return circleFrom2(b, c);
        }
        double ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay)
                + (cx * cx + cy * cy) * (ay - by)) / d;
        double uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx)
                + (cx * cx + cy * cy) * (bx - ax)) / d;
        double rr = Math.hypot(ax - ux, ay - uy);
        return new double[]{ux, uy, rr};
    }

    private double dist2(int[] a, int[] b) {
        double dx = a[0] - b[0], dy = a[1] - b[1];
        return dx * dx + dy * dy;
    }

    public double[] outerTreesBrute(int[][] trees) {
        int n = trees.length;
        double bestR = Double.MAX_VALUE;
        double bestCx = 0, bestCy = 0;
        // 枚举两点直径
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                double[] c = circleFrom2(trees[i], trees[j]);
                if (coversAll(trees, c[0], c[1], c[2])) {
                    if (c[2] < bestR) {
                        bestR = c[2]; bestCx = c[0]; bestCy = c[1];
                    }
                }
            }
        }
        // 枚举三点外接圆
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                for (int k = j + 1; k < n; k++) {
                    double[] c = circleFrom3(trees[i], trees[j], trees[k]);
                    if (coversAll(trees, c[0], c[1], c[2])) {
                        if (c[2] < bestR) {
                            bestR = c[2]; bestCx = c[0]; bestCy = c[1];
                        }
                    }
                }
            }
        }
        return new double[]{bestCx, bestCy, bestR};
    }

    private boolean coversAll(int[][] pts, double cx, double cy, double r) {
        for (int[] p : pts) {
            if (!in(p, cx, cy, r)) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] t1 = {{1, 1}, {2, 2}, {2, 0}, {2, 4}, {3, 3}, {4, 2}};
        int[][] t2 = {{1, 2}, {2, 2}, {4, 2}};
        double[] r1 = s.outerTrees(t1);
        double[] b1 = s.outerTreesBrute(t1);
        double[] r2 = s.outerTrees(t2);
        double[] b2 = s.outerTreesBrute(t2);
        boolean ok1 = Math.abs(r1[0] - 2) < 1e-5 && Math.abs(r1[1] - 2) < 1e-5 && Math.abs(r1[2] - 2) < 1e-5;
        boolean ok1b = Math.abs(b1[0] - 2) < 1e-5 && Math.abs(b1[1] - 2) < 1e-5 && Math.abs(b1[2] - 2) < 1e-5;
        boolean ok2 = Math.abs(r2[0] - 2.5) < 1e-5 && Math.abs(r2[1] - 2) < 1e-5 && Math.abs(r2[2] - 1.5) < 1e-5;
        boolean ok2b = Math.abs(b2[0] - 2.5) < 1e-5 && Math.abs(b2[1] - 2) < 1e-5 && Math.abs(b2[2] - 1.5) < 1e-5;
        System.out.println((ok1 && ok1b ? "PASS" : "FAIL") + " case1 => " + java.util.Arrays.toString(r1) + " / " + java.util.Arrays.toString(b1));
        System.out.println((ok2 && ok2b ? "PASS" : "FAIL") + " case2 => " + java.util.Arrays.toString(r2) + " / " + java.util.Arrays.toString(b2));
        System.out.println((ok1 && ok1b && ok2 && ok2b) ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1925. Count Square Sum Triples(统计平方和三元组的数目)

题目描述

一个平方和三元组 (a,b,c) 指的是满足 a^2 + b^2 = c^2 的整数三元组 a,b 和 c。

给你一个整数 n,请你返回满足 1 <= a, b, c <= n 的平方和三元组的数目。

示例 1

  • 输入 :n = 5
  • 输出:2
  • 解释:平方和三元组为 (3,4,5) 和 (4,3,5)。

示例 2

  • 输入 :n = 10
  • 输出:4
  • 解释 :平方和三元组为 (3,4,5),(4,3,5),(6,8,10) 和 (8,6,10)。
    提示:
  • - 1 <= n <= 250

解题思路

解法一:平方集合 + 枚举 a、b,O(n^2),主提交版本

把 1..n 的平方预先存进哈希集合。

枚举 a, b in [1, n],若 c2 = a^2 + b^2 是某个 <= n^2 的完全平方数 c^2 且 c <= n,

则 (a, b, c) 计数 +1。因为 (a,b) 有序,所以 (3,4,5) 与 (4,3,5) 算两个,恰好符合题意。

解法二:穷举 c、a 校验,对照

枚举 c 与 a,令 b2 = c^2 - a^2,若 b2 是完全平方且

b <= n 且 b >= 1 则计数。两种枚举角度互相验证。

代码实现

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

public class Solution {

    public int countTriples(int n) {
        Set<Integer> squares = new HashSet<>();
        for (int i = 1; i <= n; i++) {
            squares.add(i * i);
        }
        int ans = 0;
        for (int a = 1; a <= n; a++) {
            for (int b = 1; b <= n; b++) {
                int c2 = a * a + b * b;
                if (c2 <= n * n && squares.contains(c2)) {
                    ans++;
                }
            }
        }
        return ans;
    }

    public int countTriplesEnumC(int n) {
        int ans = 0;
        for (int c = 1; c <= n; c++) {
            for (int a = 1; a < c; a++) {
                int b2 = c * c - a * a;
                int b = (int) Math.sqrt(b2);
                if (b * b == b2 && b >= 1 && b <= n && a < b) {
                    ans += 2; // (a,b) 与 (b,a) 都满足
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[] ns = {5, 10, 1, 12, 250};
        // 期望值用两种方法交叉验证(互相一致即通过;2 4 处给出已知值)
        boolean all = true;
        for (int n : ns) {
            int a = s.countTriples(n);
            int b = s.countTriplesEnumC(n);
            boolean ok = a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " n=" + n + " => " + a + " / " + b);
        }
        // 官方例子校验
        boolean ex = s.countTriples(5) == 2 && s.countTriples(10) == 4;
        all &= ex;
        System.out.println((ex ? "PASS" : "FAIL") + " official ex => 2, 4");
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1926. Nearest Exit from Entrance in Maze(迷宫中离入口最近的出口)

题目描述

给你一个 m x n 的迷宫矩阵 maze(下标从 0 开始),矩阵中有空格子(用 '.' 表示)和墙

(用 '+' 表示)。同时给你迷宫的入口 entrance,用 entrance = [entrancerow, entrancecol]

表示你一开始所在格子的行和列。

每一步操作,你可以往上,下,左或者右移动一个格子。你不能进入墙所在的格子,你也不能离开迷宫。

你的目标是找到离 entrance 最近的出口。出口的含义是 maze 边界上的空格子。entrance 格子

不算出口。

请你返回从 entrance 到最近出口的最短路径的步数,如果不存在这样的路径,请你返回 -1。

示例 1

  • 输入 :maze = \["+","+",".","+",".",".",".","+","+","+","+","."], entrance = 1,2
  • 输出:1
  • 解释:总共有 3 个出口,分别位于 (1,0),(0,2) 和 (2,3)。最近的出口是 (0,2),距离为 1 步。

示例 2

  • 输入 :maze = \["+","+","+",".",".",".","+","+","+"], entrance = 1,0
  • 输出:2
  • 解释:迷宫中只有 1 个出口,在 (1,2) 处。(1,0) 不算出口,因为它是入口格子。距离为 2 步。

示例 3

  • 输入 :maze = \[".","+"], entrance = 0,0
  • 输出:-1
  • 解释 :这个迷宫中没有出口。
    提示:
  • - maze.length == m
  • - maze[i].length == n
  • - 1 <= m, n <= 100
  • - entrance.length == 2
  • - 0 <= entrancerow < m
  • - 0 <= entrancecol < n
    • entrance 一定是空格子

解题思路

解法一:BFS 层序搜索,O(mn),主提交版本

从入口开始 BFS。队列中加入每层的格子,步数随层递增;

每从队列取出一个格子,若是边界且不是入口,则其所在层数即最近出口距离。BFS 天然保证第一次遇到的

出口就是最短距离。注意把已访问格子置为 '+' 避免重复入队。

解法二:双端 BFS / 多起点 Dijkstra 变体,对照

这里改用一个双向队列做"层内扩展",同样按层遍历,

逐层计算当前层所有可达格子并检查边界出口,与解法一逻辑等价但写法不同,用于交叉验证。

代码实现

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

public class Solution {

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

    public int nearestExit(char[][] maze, int[] entrance) {
        int m = maze.length, n = maze[0].length;
        Queue<int[]> q = new ArrayDeque<>();
        q.offer(new int[]{entrance[0], entrance[1]});
        maze[entrance[0]][entrance[1]] = '+';  // 标记入口已访问
        int steps = 0;
        while (!q.isEmpty()) {
            int size = q.size();
            for (int k = 0; k < size; k++) {
                int[] cur = q.poll();
                int r = cur[0], c = cur[1];
                // 出口判定:边界空格子,且不是入口
                boolean isExit = (r == 0 || r == m - 1 || c == 0 || c == n - 1)
                        && !(r == entrance[0] && c == entrance[1]);
                if (isExit) {
                    return steps;
                }
                for (int[] d : dirs) {
                    int nr = r + d[0], nc = c + d[1];
                    if (nr >= 0 && nr < m && nc >= 0 && nc < n && maze[nr][nc] == '.') {
                        maze[nr][nc] = '+';
                        q.offer(new int[]{nr, nc});
                    }
                }
            }
            steps++;
        }
        return -1;
    }

    public int nearestExitSimple(char[][] maze, int[] entrance) {
        int m = maze.length, n = maze[0].length;
        Queue<int[]> q = new ArrayDeque<>();
        int[][] dist = new int[m][n];
        for (int i = 0; i < m; i++) {
            java.util.Arrays.fill(dist[i], -1);
        }
        dist[entrance[0]][entrance[1]] = 0;
        q.offer(new int[]{entrance[0], entrance[1]});
        while (!q.isEmpty()) {
            int[] cur = q.poll();
            int r = cur[0], c = cur[1];
            if ((r == 0 || r == m - 1 || c == 0 || c == n - 1)
                    && !(r == entrance[0] && c == entrance[1])) {
                return dist[r][c];
            }
            for (int[] d : dirs) {
                int nr = r + d[0], nc = c + d[1];
                if (nr >= 0 && nr < m && nc >= 0 && nc < n && maze[nr][nc] == '.'
                        && dist[nr][nc] == -1) {
                    dist[nr][nc] = dist[r][c] + 1;
                    q.offer(new int[]{nr, nc});
                }
            }
        }
        return -1;
    }

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

    public static void main(String[] args) {
        Solution s = new Solution();
        char[][] m1 = {{'+', '+', '.', '+'}, {'.', '.', '.', '+'}, {'+', '+', '+', '.'}};
        char[][] m2 = {{'+', '+', '+'}, {'.', '.', '.'}, {'+', '+', '+'}};
        char[][] m3 = {{'.', '+'}};
        boolean ok = true;
        int a1 = s.nearestExit(deepCopy(m1), new int[]{1, 2});
        int b1 = s.nearestExitSimple(deepCopy(m1), new int[]{1, 2});
        ok &= a1 == 1 && b1 == 1;
        int a2 = s.nearestExit(deepCopy(m2), new int[]{1, 0});
        int b2 = s.nearestExitSimple(deepCopy(m2), new int[]{1, 0});
        ok &= a2 == 2 && b2 == 2;
        int a3 = s.nearestExit(deepCopy(m3), new int[]{0, 0});
        int b3 = s.nearestExitSimple(deepCopy(m3), new int[]{0, 0});
        ok &= a3 == -1 && b3 == -1;
        System.out.println((ok ? "PASS" : "FAIL") + " case1 => " + a1 + " / " + b1);
        System.out.println((ok ? "PASS" : "FAIL") + " case2 => " + a2 + " / " + b2);
        System.out.println((ok ? "PASS" : "FAIL") + " case3 => " + a3 + " / " + b3);
        System.out.println(ok ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1927. Sum Game(求和游戏)

题目描述

Alice 和 Bob 玩一个游戏,两人轮流行动,Alice 先手。

给你一个偶数长度的字符串 num,每一个字符为数字字符或者 '?'。每一次操作中,如果 num 中至少

有一个 '?',那么玩家可以执行以下操作:

  • 选择一个下标 i 满足 num[i] == '?'。
  • 将 num[i] 用 '0' 到 '9' 之间的一个数字字符替代。
    当 num 中没有 '?' 时,游戏结束。
    Bob 获胜的条件是 num 中前一半数字的和等于后一半数字的和。Alice 获胜的条件是前一半的和与后一半的
    和不相等。
    比方说,游戏结束时 num = "243801",那么 Bob 获胜,因为 2+4+3 = 8+0+1。
    如果游戏结束时 num = "243803",那么 Alice 获胜,因为 2+4+3 != 8+0+3。
    在 Alice 和 Bob 都采取最优策略的前提下,如果 Alice 获胜,请返回 true,如果 Bob 获胜,请返回 false。

示例 1

  • 输入 :num = "5023"
  • 输出 :false
  • 解释 :num 中没有 '?',没法进行任何操作。前一半的和等于后一半的和:5 + 0 = 2 + 3。

示例 2

  • 输入 :num = "25??"
  • 输出 :true
  • 解释 :Alice 可以将两个 '?' 中的一个替换为 '9',Bob 无论如何都无法使前一半的和等于后一半的和。

示例 3

  • 输入 :num = "?3295???"
  • 输出 :false
  • 解释 :Bob 总是能赢。一种可能的结果是 Alice 将第一个 '?' 用 '9' 替换,之后 Bob 补齐其余 '?',
    最终 Bob 获胜,因为 9 + 3 + 2 + 9 = 5 + 9 + 2 + 7。
    提示:
  • - 2 <= num.length <= 10^5
    • num.length 是偶数
    • num 只包含数字字符和 '?'

解题思路

解法一:贪心数学判定,O(n),主提交版本

设前半 ? 数量为 lq、后半 ? 数量为 rq。若 (lq + rq)

为奇数,Alice 总能取胜(她可以下一次改变 ?,让 Bob 无法恰好补平差值)------更精确的结论:

当 ? 总数 cnt 为奇数时 Alice 必胜;当 cnt 为偶数时,设 d = 前半已知数字和 - 后半已知数字和,

则只有当 d == 9 * (rq - lq) / 2 时 Bob 才能赢,否则 Alice 赢。直观解释:每个 '?' 由一对玩家

轮流填,Alice/Bob 可以把各自的 ? 尽量推向对自己有利的极端(9 或 0),若双方 ? 数量相同则差值固定,

当且仅当差值已经等于"后半 ? 需要补的量"时 Bob 才刚好追平。

解法二:博弈搜索回溯,对照,仅小数据

? 数量少时用递归搜索双方最优决策:Alice 最大化最终

"前后半是否不等"(返回 1 表示 Alice 胜),Bob 最小化。O(10^cnt) 仅用于交叉验证解法一。

代码实现

java 复制代码
public class Solution {

    public boolean sumGame(String num) {
        int n = num.length();
        int half = n / 2;
        int leftQ = 0, rightQ = 0;
        long leftSum = 0, rightSum = 0;
        for (int i = 0; i < half; i++) {
            char c = num.charAt(i);
            if (c == '?') leftQ++;
            else leftSum += c - '0';
        }
        for (int i = half; i < n; i++) {
            char c = num.charAt(i);
            if (c == '?') rightQ++;
            else rightSum += c - '0';
        }
        int totalQ = leftQ + rightQ;
        if ((totalQ & 1) == 1) {
            // 奇数个 '?' -> Alice 必胜
            return true;
        }
        // 偶数个 '?':Bob 能赢当且仅当 已知差 == 9 * (rightQ - leftQ) / 2
        long diff = leftSum - rightSum;   // 前半 - 后半
        long need = 9L * (rightQ - leftQ) / 2;
        return diff != need;
    }

    public boolean sumGameBrute(String num) {
        char[] cs = num.toCharArray();
        // Alice 先手,最大化"前后半不等" => 返回 true 表示 Alice 胜
        return dfs(cs, true);
    }

    private boolean dfs(char[] cs, boolean aliceTurn) {
        int n = cs.length;
        // 找第一个 '?'
        int q = -1;
        for (int i = 0; i < n; i++) {
            if (cs[i] == '?') {
                q = i;
                break;
            }
        }
        if (q == -1) {
            // 游戏结束,判定 Bob 是否赢(前后半相等)
            return !halfEqual(cs);
        }
        if (aliceTurn) {
            // Alice 走:选任一 ? 填 0..9,只要任意一种能赢则赢
            for (int i = 0; i < n; i++) {
                if (cs[i] == '?') {
                    for (int d = 0; d <= 9; d++) {
                        cs[i] = (char) ('0' + d);
                        if (dfs(cs, false)) {
                            cs[i] = '?';
                            return true;
                        }
                        cs[i] = '?';
                    }
                }
            }
            return false;
        } else {
            // Bob 走:Bob 想要前后半相等(返回 false 表示 Alice 输)
            for (int i = 0; i < n; i++) {
                if (cs[i] == '?') {
                    for (int d = 0; d <= 9; d++) {
                        cs[i] = (char) ('0' + d);
                        if (!dfs(cs, true)) {
                            cs[i] = '?';
                            return false;
                        }
                        cs[i] = '?';
                    }
                }
            }
            return true;
        }
    }

    private boolean halfEqual(char[] cs) {
        int n = cs.length, half = n / 2;
        long l = 0, r = 0;
        for (int i = 0; i < half; i++) l += cs[i] - '0';
        for (int i = half; i < n; i++) r += cs[i] - '0';
        return l == r;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        String[] cases = {"5023", "25??", "?3295???", "?", "??", "1?2?3?4?"};
        // 期望:5023 -> false; 25?? -> true; ?3295??? -> false; "?" -> true(Alice先手填非0即不等,总能赢? 实际填0则相等...仔细分析:单个?Alice填数字后结束,Alice要不等,填 1..9 即可,所以 true)
        // "??" -> 两?偶数个,差 need=9*(1-1)/2=0,已知差0==0 -> Bob能赢 -> false
        // "1?2?3?4?" -> 4个?偶数,左右各2个?,已知左=1+2=3 右=3+4=7,need=9*(2-2)/2=0,diff=-4!=0 -> Alice -> true
        boolean[] exp = {false, true, false, true, false, true};
        boolean all = true;
        for (int i = 0; i < cases.length; i++) {
            if (cases[i].indexOf('?') >= 0 && cases[i].length() > 4) {
                // 暴力只跑小数据
                continue;
            }
            boolean a = s.sumGame(cases[i]);
            boolean ok = a == exp[i];
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a);
        }
        // 小数据暴力与数学对拍
        boolean bruteOk = true;
        String[] small = {"?", "??", "0?", "?0", "1?1"};
        for (String st : small) {
            boolean a = s.sumGame(st);
            boolean b = s.sumGameBrute(st);
            if (a != b) {
                bruteOk = false;
                System.out.println("MISMATCH " + st + " => " + a + " vs " + b);
            }
        }
        all &= bruteOk;
        System.out.println(bruteOk ? "PASS brute vs math" : "FAIL brute vs math");
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1928. Minimum Cost to Reach Destination in Time(规定时间内到达终点的最小花费)

题目描述

一个国家有 n 个城市,城市编号为 0 到 n - 1,题目保证所有城市都由双向道路连接在一起。

道路由二维整数数组 edges 表示,其中 edges[i] = [xi, yi, timei] 表示城市 xi 和 yi 之间

有一条双向道路,耗费时间为 timei 分钟。两个城市之间可能会有多条耗费时间不同的道路,

但是不会有道路两头连接着同一座城市。

每次经过一个城市时,你需要付通行费。通行费用一个长度为 n 且下标从 0 开始的整数数组 passingFees

表示,其中 passingFees[j] 是你经过城市 j 需要支付的费用。

一开始,你在城市 0,你想要在 maxTime 分钟以内(包含 maxTime 分钟)到达城市 n - 1。

旅行的费用为你经过的所有城市通行费之和(包括起点和终点城市的通行费)。

给你 maxTime、edges 和 passingFees,请你返回完成旅行的最小费用,如果无法在 maxTime 分钟

以内完成旅行,请你返回 -1。

示例 1

  • 输入 :maxTime = 30, edges = \[0,1,10,1,2,10,2,5,10,0,3,1,3,4,10,4,5,15],
    passingFees = [5,1,2,20,20,3]
  • 输出:11
  • 解释:最优路径为 0 -> 1 -> 2 -> 5,总共需要耗费 30 分钟,需要支付 11 的通行费。

示例 2

  • 输入 :maxTime = 29, edges = \[0,1,10,1,2,10,2,5,10,0,3,1,3,4,10,4,5,15],
    passingFees = [5,1,2,20,20,3]
  • 输出:48
  • 解释:最优路径为 0 -> 3 -> 4 -> 5,总共需要耗费 26 分钟,需要支付 48 的通行费。

示例 3

  • 输入 :maxTime = 25, edges = \[0,1,10,1,2,10,2,5,10,0,3,1,3,4,10,4,5,15],
    passingFees = [5,1,2,20,20,3]
  • 输出:-1
  • 解释 :无法在 25 分钟以内从城市 0 到达城市 5。
    提示:
  • - 1 <= n <= 1000
  • - 0 <= edges.length <= 1000
  • - edges[i].length == 3
  • - 0 <= xi, yi < n
  • - 1 <= timei <= 1000
  • - 1 <= maxTime <= 1000
  • - passingFees.length == n
  • - 1 <= passingFees[j] <= 1000
    • 图中两个节点之间可能有多条路径
    • 图中不含有自环

解题思路

解法一:DP + Dijkstra 式优先队列,O(T * E log V),主提交版本

dist[t][v] 表示"恰好用 t 分钟

到达城市 v 的最小费用"不方便,改用 Dijkstra 在"时间-城市"状态图上做最短路:状态 (time, city),

代价为累计费用,用优先队列按费用升序扩展。每次弹出状态 (cost, time, city),若 time > maxTime 剪枝;

若 city == n-1 直接返回 cost(首次到达终点的即最优,因为按费用出队)。扩展每条边 (city, nxt, w):

新状态 (cost + passingFeesnxt, time + w, nxt)。为避免大量冗余状态,用 best[time][city] 记录

到达该 (time, city) 的最小费用,仅当更优才入队。

解法二:DP 递推,对照

dp[t][v] = 用至多 t 分钟到达 v 的最小费用。按时间从 0 到 maxTime 递推:

dp[t][v] = min(dp[t-1][v], min over edge (u,v,w): dp[t-w][u] + passingFees[v]),答案取

min(dp[t][n-1])。O(maxTime * E),用于交叉验证。

代码实现

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

public class Solution {

    public int minCost(int maxTime, int[][] edges, int[] passingFees) {
        int n = passingFees.length;
        List<int[]>[] g = new ArrayList[n];
        for (int i = 0; i < n; i++) {
            g[i] = new ArrayList<>();
        }
        for (int[] e : edges) {
            g[e[0]].add(new int[]{e[1], e[2]});
            g[e[1]].add(new int[]{e[0], e[2]});
        }
        // best[t][v]:恰好用 t 分钟到 v 的最小费用
        int[][] best = new int[maxTime + 1][n];
        for (int[] row : best) {
            Arrays.fill(row, Integer.MAX_VALUE);
        }
        PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]); // (cost, time, city)
        pq.offer(new int[]{passingFees[0], 0, 0});
        best[0][0] = passingFees[0];
        while (!pq.isEmpty()) {
            int[] cur = pq.poll();
            int cost = cur[0], time = cur[1], city = cur[2];
            if (cost > best[time][city]) {
                continue;  // 过时状态
            }
            if (city == n - 1) {
                return cost;  // 按费用出队,首次到达即最优
            }
            for (int[] e : g[city]) {
                int nxt = e[0], w = e[1];
                int nt = time + w;
                if (nt > maxTime) {
                    continue;
                }
                int nc = cost + passingFees[nxt];
                if (nc < best[nt][nxt]) {
                    best[nt][nxt] = nc;
                    pq.offer(new int[]{nc, nt, nxt});
                }
            }
        }
        return -1;
    }

    public int minCostDp(int maxTime, int[][] edges, int[] passingFees) {
        int n = passingFees.length;
        // 邻接表
        List<int[]>[] g = new ArrayList[n];
        for (int i = 0; i < n; i++) {
            g[i] = new ArrayList<>();
        }
        for (int[] e : edges) {
            g[e[0]].add(new int[]{e[1], e[2]});
            g[e[1]].add(new int[]{e[0], e[2]});
        }
        long INF = Long.MAX_VALUE / 4;
        long[][] dp = new long[maxTime + 1][n];
        for (long[] row : dp) {
            Arrays.fill(row, INF);
        }
        dp[0][0] = passingFees[0];
        for (int t = 1; t <= maxTime; t++) {
            for (int v = 0; v < n; v++) {
                dp[t][v] = dp[t - 1][v];           // 原地等待(不产生新费用,等价于"至多 t 分钟")
                for (int[] e : g[v]) {
                    int u = e[0], w = e[1];
                    if (t >= w && dp[t - w][u] < INF) {
                        dp[t][v] = Math.min(dp[t][v], dp[t - w][u] + passingFees[v]);
                    }
                }
            }
        }
        return dp[maxTime][n - 1] >= INF ? -1 : (int) dp[maxTime][n - 1];
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] edges = {{0, 1, 10}, {1, 2, 10}, {2, 5, 10}, {0, 3, 1}, {3, 4, 10}, {4, 5, 15}};
        int[] fees = {5, 1, 2, 20, 20, 3};
        boolean ok = true;
        int a1 = s.minCost(30, edges, fees);
        int b1 = s.minCostDp(30, edges, fees);
        ok &= a1 == 11 && b1 == 11;
        int a2 = s.minCost(29, edges, fees);
        int b2 = s.minCostDp(29, edges, fees);
        ok &= a2 == 48 && b2 == 48;
        int a3 = s.minCost(25, edges, fees);
        int b3 = s.minCostDp(25, edges, fees);
        ok &= a3 == -1 && b3 == -1;
        System.out.println((ok ? "PASS" : "FAIL") + " case1 => " + a1 + " / " + b1);
        System.out.println((ok ? "PASS" : "FAIL") + " case2 => " + a2 + " / " + b2);
        System.out.println((ok ? "PASS" : "FAIL") + " case3 => " + a3 + " / " + b3);
        System.out.println(ok ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1929. Concatenation of Array(数组串联)

题目描述

给你一个长度为 n 的整数数组 nums。请你构建一个长度为 2n 的答案数组 ans,数组下标从 0

开始计数,对于所有 0 <= i < n 满足 ans[i] == nums[i] 且 ans[i + n] == nums[i]。

具体地,ans 由两个 nums 数组串联形成。请你返回构建好的数组 ans。

示例 1

  • 输入 :nums = 1,2,1
  • 输出:1,2,1,1,2,1
  • 解释 :数组 ans 按下述方式形成:ans = `nums[0]`,`nums[1]`,`nums[2]`,`nums[0]`,`nums[1]`,`nums[2]` = 1,2,1,1,2,1。

示例 2

  • 输入 :nums = 1,3,2,1
  • 输出:1,3,2,1,1,3,2,1
  • 解释 :数组 ans 按下述方式形成:ans = 1,3,2,1,1,3,2,1。
    提示:
  • - n == nums.length
  • - 1 <= n <= 1000
  • - 1 <= nums[i] <= 1000

解题思路

解法一:System.arraycopy,O(n),主提交版本

申请 2n 数组,前 n 位复制 nums,后 n 位再复制一遍。

解法二:双循环逐位填充,对照

用两次循环分别填 ans[i] 与 ans[i+n],写法更直观,便于验证。

代码实现

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

public class Solution {

    public int[] getConcatenation(int[] nums) {
        int n = nums.length;
        int[] ans = new int[2 * n];
        System.arraycopy(nums, 0, ans, 0, n);
        System.arraycopy(nums, 0, ans, n, n);
        return ans;
    }

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

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

复杂度分析


1930. Unique Length-3 Palindromic Subsequences(长度为 3 的不同回文子序列)

题目描述

给你一个字符串 s,返回 s 中长度为 3 的不同回文子序列的个数。

即便存在多种方法来构建相同的子序列,但相同的子序列只计数一次。

回文是正着读和反着读一样的字符串。

子序列是由原字符串删除其中部分字符(也可以不删除)且不改变剩余字符之间相对顺序形成的一个新字符串。

例如,"ace" 是 "abcde" 的一个子序列。

示例 1

  • 输入 :s = "aabca"
  • 输出:3
  • 解释 :长度为 3 的 3 个回文子序列分别是:"aba"、"aaa"、"aca"。

示例 2

  • 输入 :s = "adc"
  • 输出:0
  • 解释 :"adc" 不存在长度为 3 的回文子序列。

示例 3

  • 输入 :s = "bbcbaba"
  • 输出:4
  • 解释 :长度为 3 的 4 个回文子序列分别是:"bbb"、"bcb"、"bab"、"aba"。
    提示:
  • - 3 <= s.length <= 10^5
    • s 仅由小写英文字母组成

解题思路

解法一:首末字符枚举 + 中间存在性,O(26n),主提交版本

长度为 3 的回文子序列形如 x _ y,

其中必须 x == y,中间可以是任意字符。对每个字母 c,找到它第一次出现与最后一次出现的位置

first[c]、last[c];若两者之间(开区间)至少还有一个位置,则 c 可以作为回文的两端,

中间字符可以是两端之间出现的任一字母。因此对每个字母 c,统计区间 (first[c], last[c]) 内

出现的不同字符数即可(用前缀和或位掩码)。总计数 = 各 c 的该区间内不同字符数之和。

解法二:位掩码前缀统计,对照

维护 pre[i] 为前 i 个字符出现字母的位掩码(26 位),

对每个字母 c 的 (first, last) 区间,区间内字母集合 = pre[last] XOR pre[first+1] 的二进制 1 个数

(注意 pre 记录的是字符集合,异或得到 (first, last] 区间新出现的字母,实际上是出现的字母集合差)。

用 Integer.bitCount 统计,写法更紧凑,用于交叉验证。

代码实现

java 复制代码
public class Solution {

    public int countPalindromicSubsequence(String s) {
        int n = s.length();
        int[] first = new int[26];
        int[] last = new int[26];
        java.util.Arrays.fill(first, -1);
        for (int i = 0; i < n; i++) {
            int c = s.charAt(i) - 'a';
            if (first[c] == -1) {
                first[c] = i;
            }
            last[c] = i;
        }
        // prefix[i][c] = s[0..i-1] 中字母 c 是否出现
        boolean[][] pre = new boolean[n + 1][26];
        for (int i = 1; i <= n; i++) {
            for (int c = 0; c < 26; c++) {
                pre[i][c] = pre[i - 1][c];
            }
            pre[i][s.charAt(i - 1) - 'a'] = true;
        }
        int ans = 0;
        for (int c = 0; c < 26; c++) {
            if (first[c] == -1 || first[c] + 1 >= last[c]) {
                continue;   // 没有可放中间字符的空间
            }
            int l = first[c] + 1, r = last[c];   // 开区间 (first, last)
            for (int mid = 0; mid < 26; mid++) {
                // 中间字符 mid 在 (first, last) 内出现过?
                boolean appeared = pre[r][mid] && !pre[l - 1][mid];
                if (appeared) {
                    ans++;
                }
            }
        }
        return ans;
    }

    public int countPalindromicSubsequenceMask(String s) {
        int n = s.length();
        int[] first = new int[26];
        int[] last = new int[26];
        java.util.Arrays.fill(first, -1);
        for (int i = 0; i < n; i++) {
            int c = s.charAt(i) - 'a';
            if (first[c] == -1) first[c] = i;
            last[c] = i;
        }
        int[] preMask = new int[n + 1];  // 前 i 个字符的字母集合掩码
        for (int i = 1; i <= n; i++) {
            preMask[i] = preMask[i - 1] | (1 << (s.charAt(i - 1) - 'a'));
        }
        int ans = 0;
        for (int c = 0; c < 26; c++) {
            if (first[c] == -1 || first[c] + 1 >= last[c]) {
                continue;
            }
            // 区间 (first, last] 的字母集合(左开右闭)
            int mask = preMask[last[c]] & ~preMask[first[c]];
            ans += Integer.bitCount(mask);
        }
        return ans;
    }

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

复杂度分析


1931. Painting a Grid With Three Different Colors(用三种不同颜色为网格涂色)

题目描述

给你两个整数 m 和 n。构造一个 m x n 的网格,其中每个单元格最开始是白色。

请你用红、绿、蓝三种颜色为每个单元格涂色。所有单元格都需要被涂色。

涂色方案需要满足:不存在相邻两个单元格颜色相同的情况。返回网格涂色的方法数。

因为答案可能非常大,返回对 10^9 + 7 取余的结果。

示例 1

  • 输入 :m = 1, n = 1
  • 输出:3
  • 解释:存在三种可能的涂色方案。

示例 2

  • 输入 :m = 1, n = 2
  • 输出:6
  • 解释:存在六种可能的涂色方案。

示例 3

  • 输入 :m = 5, n = 5
  • 输出 :580986
    提示:
  • - 1 <= m <= 5
  • - 1 <= n <= 1000

解题思路

解法一:按列状态 DP,O(n * 4^m * 合法态数),主提交版本

m <= 5,一列最多 3^m = 243 种

涂色,但合法的列内状态(上下格颜色不同)远少于这个数。预处理所有合法列状态

(把三种颜色编码为 0/1/2,每格占 2 位),以及状态间是否可相邻(逐位比较上下对应格颜色不同)。

DP:dp[j][s] = 前 j 列以状态 s 结尾的方案数,转移 dp[j][s] = sum over t 可接 s 的 dp[j-1][t]。

第一列状态全为 1。答案 = 最后一列所有状态之和 mod。

解法二:公式化枚举行三元组计数,对照,仅 m <= 3

当 m 很小(1/2/3)时,可以直接按行枚举

相邻行颜色组合再乘列方向,但实现复杂;这里改用"构建转移矩阵 + 快速幂"作为对照------

若 n 特别大可对转移矩阵做快速幂,本解法直接使用同样的合法态 DP 但换用矩阵乘法形式,

用于交叉验证解法一。由于矩阵快速幂代码量大,这里用逐列迭代版本(与首解法一样的 DP

但缓存转移表 + 用 int\[\]\[\]),仅实现角度差异。

代码实现

java 复制代码
public class Solution {

    private static final int MOD = 1_000_000_007;

    public int colorTheGrid(int m, int n) {
        // 1) 枚举状态:m 个格每格颜色 0/1/2(2 位/格),枚举全部 4^m 种 2 位组合再过滤合法列。
        //    ⚠️ 不能只枚举 3^m 个(对 m=4/5,3^m < 4^m,高行在 2 位编码下将被漏算永远为颜色 0)
        int total = 1 << (2 * m);   // 4^m
        // 2) 收集合法列状态
        java.util.List<Integer> states = new java.util.ArrayList<>();
        for (int s = 0; s < total; s++) {
            if (isValidCol(s, m)) {
                states.add(s);
            }
        }
        int K = states.size();
        int[] st = new int[K];
        for (int i = 0; i < K; i++) st[i] = states.get(i);
        // 3) 转移表:t 能否接在 s 之后(逐位颜色不同)
        boolean[][] can = new boolean[K][K];
        for (int i = 0; i < K; i++) {
            for (int j = 0; j < K; j++) {
                can[i][j] = compatible(st[i], st[j], m);
            }
        }
        // 4) DP
        long[] dp = new long[K];
        for (int i = 0; i < K; i++) dp[i] = 1;
        for (int col = 1; col < n; col++) {
            long[] ndp = new long[K];
            for (int j = 0; j < K; j++) {
                long sum = 0;
                for (int i = 0; i < K; i++) {
                    if (can[i][j]) {
                        sum += dp[i];
                    }
                }
                ndp[j] = sum % MOD;
            }
            dp = ndp;
        }
        long ans = 0;
        for (long v : dp) ans = (ans + v) % MOD;
        return (int) ans;
    }

    private boolean isValidCol(int s, int m) {
        int prev = -1;
        for (int i = 0; i < m; i++) {
            int c = (s >> (2 * i)) & 3;
            if (c == prev || c > 2) {
                return false;
            }
            prev = c;
        }
        return true;
    }

    private boolean compatible(int s1, int s2, int m) {
        for (int i = 0; i < m; i++) {
            int c1 = (s1 >> (2 * i)) & 3;
            int c2 = (s2 >> (2 * i)) & 3;
            if (c1 == c2) {
                return false;
            }
        }
        return true;
    }

    public int colorTheGridCache(int m, int n) {
        int total = 1 << (2 * m);   // 4^m(同样必须枚举全部 2 位组合)
        java.util.List<Integer> states = new java.util.ArrayList<>();
        for (int s = 0; s < total; s++) {
            if (isValidCol(s, m)) states.add(s);
        }
        int K = states.size();
        // 预计算每个状态的可后继列表
        java.util.List<java.util.List<Integer>> nxt = new java.util.ArrayList<>();
        for (int i = 0; i < K; i++) {
            java.util.List<Integer> list = new java.util.ArrayList<>();
            for (int j = 0; j < K; j++) {
                if (compatible(states.get(i), states.get(j), m)) {
                    list.add(j);
                }
            }
            nxt.add(list);
        }
        long[] dp = new long[K];
        java.util.Arrays.fill(dp, 1);
        for (int col = 1; col < n; col++) {
            long[] ndp = new long[K];
            for (int i = 0; i < K; i++) {
                long v = dp[i];
                if (v == 0) continue;
                for (int j : nxt.get(i)) {
                    ndp[j] = (ndp[j] + v) % MOD;
                }
            }
            dp = ndp;
        }
        long ans = 0;
        for (long v : dp) ans = (ans + v) % MOD;
        return (int) ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] cs = {{1, 1}, {1, 2}, {2, 1}, {2, 2}, {2, 3}, {3, 2}, {3, 3}, {4, 2}, {5, 5}, {5, 1000}};
        // 官方样例:m=1,n=1 -> 3;m=1,n=2 -> 6;m=5,n=5 -> 580986。其余小规模用暴力对照验证
        int[] exp = {3, 6, 6, 0, 0, 0, 0, 0, 580986, 0};
        boolean all = true;
        for (int i = 0; i < cs.length; i++) {
            int a = s.colorTheGrid(cs[i][0], cs[i][1]);
            int b = s.colorTheGridCache(cs[i][0], cs[i][1]);
            boolean ok = a == b && (exp[i] == 0 || a == exp[i]);
            int br = -1;
            if (cs[i][0] * cs[i][1] <= 9) { // 小规模暴力对照
                br = brute(cs[i][0], cs[i][1]);
                ok &= a == br;
            }
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " m=" + cs[i][0] + " n=" + cs[i][1]
                    + " => dp=" + a + " / cache=" + b + (br >= 0 ? " / brute=" + br : ""));
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }

    private static int brute(int m, int n) {
        int cells = m * n;
        int total = 1;
        for (int i = 0; i < cells; i++) total *= 3;
        int cnt = 0;
        for (int s = 0; s < total; s++) {
            int[] g = new int[cells];
            int x = s;
            for (int i = 0; i < cells; i++) {
                g[i] = x % 3;
                x /= 3;
            }
            boolean ok = true;
            for (int r = 0; r < m && ok; r++) {
                for (int c = 0; c < n && ok; c++) {
                    int id = r * n + c;
                    if (c + 1 < n && g[id] == g[id + 1]) ok = false;      // 右邻
                    if (r + 1 < m && g[id] == g[id + n]) ok = false;      // 下邻
                }
            }
            if (ok) cnt++;
        }
        return cnt;
    }
}

复杂度分析


1932. Merge BSTs to Create Single BST(合并多棵二叉搜索树)

题目描述

给你 n 个二叉搜索树的根节点,存储在数组 trees 中(下标从 0 开始),对应 n 棵不同的二叉搜索树。

trees 中的每棵二叉搜索树最多有 3 个节点,且不存在值相同的两个根节点。在一步操作中,将会完成

下述步骤:

  • 选择两个不同的下标 i 和 j,要求满足在 trees[i] 中的某个叶节点的值等于 trees[j] 的根节点的值。
  • 用 trees[j] 替换 trees[i] 中的那个叶节点。
  • 从 trees 中移除 trees[j]。
    如果在执行 n - 1 次操作后,能形成一棵有效的二叉搜索树,则返回结果二叉树的根节点;
    如果无法构造一棵有效的二叉搜索树,返回 null。
    二叉搜索树是一种二叉树,且树中每个节点均满足下述属性:
  • 任意节点的左子树中的值都严格小于此节点的值。
  • 任意节点的右子树中的值都严格大于此节点的值。
    叶节点是不含子节点的节点。

示例 1

  • 输入 :trees = \[2,1,3,2,5,5,4]
  • 输出 :3,2,5,1,`null`,4
  • 解释 :第一步把 2,1 合并到 3,2,5,第二步把 5,4 合并到 3,2,5,1,得到有效 BST。

示例 2

  • 输入 :trees = \[5,3,8,3,2,6]
  • 输出:\[\]
  • 解释 :合并后 5,3,8,2,6 不是有效二叉搜索树,返回 null。

示例 3

  • 输入 :trees = \[5,4,3]
  • 输出:\[\]
  • 解释 :无法执行任何操作。
    提示:
  • - n == trees.length
  • - 1 <= n <= 2 * 10^4
    • 每棵树中节点数目在范围 1, 3 内
    • 输入数据的每个节点可能有子节点但不存在子节点的子节点
    • trees 中不存在两棵树根节点值相同的情况
    • 输入中的所有树都是有效的二叉搜索树
  • - 1 <= TreeNode.val <= 5 * 10^4

解题思路

解法一:直接挂接 + 全树校验,O(n),主提交版本

  • 用 map 记录 根值 -> 树(每棵树按根值唯一标识)。
  • 对每棵树的每个叶节点(无左右孩子),若其值恰好等于另一棵树的根值,则该叶节点可被那棵树替换,
    把被替换的树从 map 中删除并"挂"进去(直接修改树指针)。因为操作要恰好 n-1 次,所以最终 map
    只剩 1 棵树;任一步有多个可选项都不会影响最终结果(题目保证存在唯一可行合并时,任意顺序
    合并结果相同,因为合并是确定性的:每个叶值至多匹配一棵树的根,反之亦然)。
  • 得到唯一根后做一次全树 BST 校验(中序遍历严格递增)并核对总节点数,合法则返回根,否则 null。
    解法二:参考官方解法:先构建 children 映射再组装,对照
    用
    child2parent[value] = (树index, 该节点) 记录哪些叶节点可以被其他树替换;随后同样把那些
    子树挂到对应叶节点下;最后校验所得树是否为 BST 且包含全部 n 棵树的所有节点(节点总数一致)。
    与解法一的组装方式略有不同(解法一直接改指针,解法二记录映射后统一挂接),用于交叉验证。

代码实现

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

public class Solution {

    public TreeNode canMerge(java.util.List<TreeNode> trees) {
        Map<Integer, TreeNode> rootMap = new HashMap<>();
        Set<Integer> allVals = new HashSet<>();
        for (TreeNode t : trees) {
            rootMap.put(t.val, t);
            collectVals(t, allVals);   // 修改前收集全部节点值(避免挂接后重复计数)
        }
        // 对每棵非叶树,其叶节点可以被以该值为根的树替换
        for (TreeNode t : trees) {
            // 树 t 的每个叶节点
            for (TreeNode leaf : leaves(t)) {
                TreeNode sub = rootMap.get(leaf.val);
                if (sub != null && sub != t) {
                    leaf.left = sub.left;      // 用 sub 替换 leaf(直接挂接整棵子树)
                    leaf.right = sub.right;
                    rootMap.remove(sub.val);   // sub 已被合并,从 map 移除
                }
            }
        }
        // 最终应只剩一棵未合并的树
        if (rootMap.size() != 1) {
            return null;
        }
        TreeNode root = rootMap.values().iterator().next();
        // 全树必须是合法 BST
        long[] prev = {Long.MIN_VALUE};
        if (!isBST(root, prev)) {
            return null;
        }
        // 值集合必须与全部原始树节点值集合一致(含全部节点)
        Set<Integer> got = new HashSet<>();
        collectVals(root, got);
        if (!got.equals(allVals)) {
            return null;
        }
        return root;
    }

    private void collectVals(TreeNode node, Set<Integer> set) {
        if (node == null) return;
        set.add(node.val);
        collectVals(node.left, set);
        collectVals(node.right, set);
    }

    private java.util.List<TreeNode> leaves(TreeNode root) {
        java.util.List<TreeNode> res = new java.util.ArrayList<>();
        collectLeaves(root, res);
        return res;
    }

    private void collectLeaves(TreeNode node, java.util.List<TreeNode> res) {
        if (node.left == null && node.right == null) {
            res.add(node);
            return;
        }
        if (node.left != null) collectLeaves(node.left, res);
        if (node.right != null) collectLeaves(node.right, res);
    }

    private boolean isBST(TreeNode node, long[] prev) {
        if (node == null) return true;
        if (!isBST(node.left, prev)) return false;
        if (node.val <= prev[0]) return false;
        prev[0] = node.val;
        return isBST(node.right, prev);
    }

    private int countNodes(TreeNode node) {
        if (node == null) return 0;
        return 1 + countNodes(node.left) + countNodes(node.right);
    }

    public TreeNode canMergeMap(java.util.List<TreeNode> trees) {
        Map<Integer, TreeNode> roots = new HashMap<>();
        Set<Integer> allVals = new HashSet<>();
        for (TreeNode t : trees) {
            roots.put(t.val, t);
            collectVals(t, allVals);
        }
        // leafParent: 叶节点值 -> [父节点]
        Map<Integer, TreeNode> leafParent = new HashMap<>();
        Set<Integer> usedRoots = new HashSet<>();
        for (TreeNode t : trees) {
            for (TreeNode leaf : leaves(t)) {
                if (roots.containsKey(leaf.val) && roots.get(leaf.val) != t) {
                    leafParent.put(leaf.val, t);
                    usedRoots.add(roots.get(leaf.val).val);
                }
            }
        }
        for (TreeNode t : trees) {
            if (usedRoots.contains(t.val)) continue;  // 这棵树作为根被挂在别处
            for (TreeNode leaf : leaves(t)) {
                TreeNode sub = roots.get(leaf.val);
                if (sub != null && sub != t) {
                    leaf.left = sub.left;
                    leaf.right = sub.right;
                }
            }
        }
        // 最终根:未被挂接的树
        TreeNode root = null;
        for (TreeNode t : trees) {
            if (!usedRoots.contains(t.val)) {
                if (root != null) return null;  // 多于一个根
                root = t;
            }
        }
        if (root == null) return null;
        long[] prev = {Long.MIN_VALUE};
        if (!isBST(root, prev)) return null;
        Set<Integer> got = new HashSet<>();
        collectVals(root, got);
        if (!got.equals(allVals)) return null;
        return root;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        boolean all = true;
        // ===== 解法一:直接挂接 =====
        // 用例 1:trees = [[2,1],[3,2,5],[5,4]] -> 可合并为 BST [3,2,5,1,null,4]
        // ⚠️ 注意 [3,2,5] 的节点 2、5 在层序输入中是叶子,1、4 分别位于独立树 [2,1]、[5,4] 中
        java.util.List<TreeNode> l1 = new java.util.ArrayList<>();
        l1.add(new TreeNode(2, new TreeNode(1), null));
        l1.add(new TreeNode(3, new TreeNode(2), new TreeNode(5)));
        l1.add(new TreeNode(5, new TreeNode(4), null));
        TreeNode r1 = s.canMerge(l1);
        boolean ok1 = r1 != null && r1.val == 3 && isBstCheck(r1) && countN(r1) == 5;
        all &= ok1;
        System.out.println((ok1 ? "PASS" : "FAIL") + " case1(sol1) => " + (r1 == null ? "null" : "root=" + r1.val));

        // 用例 2:trees = [[5,3,8],[3,2,6]] -> 合并后 [5,3,8,2,6] 不是 BST,返回 null
        java.util.List<TreeNode> l2 = new java.util.ArrayList<>();
        l2.add(new TreeNode(5, new TreeNode(3), new TreeNode(8)));   // [5,3,8]:3、8 均为叶子
        l2.add(new TreeNode(3, new TreeNode(2), new TreeNode(6)));   // [3,2,6]:将被挂到 5 的左子
        TreeNode r2 = s.canMerge(l2);
        boolean ok2 = r2 == null;
        all &= ok2;
        System.out.println((ok2 ? "PASS" : "FAIL") + " case2(sol1) => null");

        // 用例 3:trees = [[5,4],[3]] -> 无任何叶值与根值匹配,无法操作
        java.util.List<TreeNode> l3 = new java.util.ArrayList<>();
        l3.add(new TreeNode(5, new TreeNode(4), null));
        l3.add(new TreeNode(3, null, null));
        TreeNode r3 = s.canMerge(l3);
        boolean ok3 = r3 == null;
        all &= ok3;
        System.out.println((ok3 ? "PASS" : "FAIL") + " case3(sol1) => null");

        // ===== 解法二:映射挂接(需重建 fresh 树,避免解法一已原地改指针) =====
        java.util.List<TreeNode> l4 = new java.util.ArrayList<>();
        l4.add(new TreeNode(2, new TreeNode(1), null));
        l4.add(new TreeNode(3, new TreeNode(2), new TreeNode(5)));
        l4.add(new TreeNode(5, new TreeNode(4), null));
        TreeNode r4 = s.canMergeMap(l4);
        boolean ok4 = r4 != null && r4.val == 3 && isBstCheck(r4) && countN(r4) == 5;
        all &= ok4;
        System.out.println((ok4 ? "PASS" : "FAIL") + " case4(sol2) => " + (r4 == null ? "null" : "root=" + r4.val));

        java.util.List<TreeNode> l5 = new java.util.ArrayList<>();
        l5.add(new TreeNode(5, new TreeNode(3), new TreeNode(8)));
        l5.add(new TreeNode(3, new TreeNode(2), new TreeNode(6)));
        boolean ok5 = s.canMergeMap(l5) == null;
        all &= ok5;
        System.out.println((ok5 ? "PASS" : "FAIL") + " case5(sol2) => null");

        java.util.List<TreeNode> l6 = new java.util.ArrayList<>();
        l6.add(new TreeNode(5, new TreeNode(4), null));
        l6.add(new TreeNode(3, null, null));
        boolean ok6 = s.canMergeMap(l6) == null;
        all &= ok6;
        System.out.println((ok6 ? "PASS" : "FAIL") + " case6(sol2) => null");

        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }

    private static int countN(TreeNode n) {
        if (n == null) return 0;
        return 1 + countN(n.left) + countN(n.right);
    }

    private static boolean isBstCheck(TreeNode r) {
        long[] prev = {Long.MIN_VALUE};
        return check(r, prev);
    }

    private static boolean check(TreeNode n, long[] prev) {
        if (n == null) return true;
        if (!check(n.left, prev)) return false;
        if (n.val <= prev[0]) return false;
        prev[0] = n.val;
        return check(n.right, prev);
    }

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

复杂度分析


1933. Check if String Is Decomposable Into Value-Equal Substrings

题目描述

(判断字符串是否可分解为值均等的子串)

一个字符串的所有字符都是一样的,被称作等值字符串。

举例,"1111" 和 "33" 就是等值字符串。相比之下,"123" 就不是等值字符串。

规则:给出一个数字字符串 s,将字符串分解成一些等值字符串,如果有且仅有一个等值子字符串长度为 2,

其他的等值子字符串的长度都是 3。

如果能够按照上面的规则分解字符串 s,就返回真,否则返回假。

子串就是原字符串中连续的字符序列。

示例 1

  • 输入 :s = "000111000"
  • 输出 :false
  • 解释 :s 只能被分解为长度为 3 的等值子字符串。

示例 2

  • 输入 :s = "00011111222"
  • 输出 :true
  • 解释 :s 能被分解为 "000","111","11","222"。

示例 3

  • 输入 :s = "01110002223300"
  • 输出 :false
  • 解释 :一个不能被分解的原因是在开头有一个 0。
    提示:
  • - 1 <= s.length <= 1000
    • s 仅包含数字

解题思路

解法一:按连续段长度分情况处理,O(n),主提交版本

把 s 按相同字符切成若干连续段,记录每段长度。

规则等价于:每个连续段长度 len 必须满足 len % 3 == 0 或 len % 3 == 2

(因为段内要么拆成若干个 3,要么恰好用一个 2 + 若干 3,均要求 len % 3 != 1),

并且"恰好有一个长度为 2 的等值子串"意味着这些段中 len % 3 == 2 的段必须恰有一个。

注意:len 可以是 2、5、8...(都拆出 3+3+...+2),所以这里有且仅有一段给出那个 2。

若满足则 true,否则 false。

解法二:贪心模拟切割,对照

从左往右贪心:当前连续相同字符长度 c,若 c % 3 == 2 则此段必须

贡献那个唯一的 2,标记 used2 并跳过;若 c % 3 == 1 则该段不合法直接 false(长度 1、4、7...)。

逐段处理完检查是否恰好用了一次 2。与解法一等价但实现角度不同,用于交叉验证。

代码实现

java 复制代码
public class Solution {

    public boolean isDecomposable(String s) {
        int n = s.length();
        boolean usedTwo = false;
        int i = 0;
        while (i < n) {
            int j = i;
            while (j < n && s.charAt(j) == s.charAt(i)) {
                j++;
            }
            int len = j - i;
            int mod = len % 3;
            if (mod == 1) {
                return false;               // 长度 1/4/7... 无法拆成 2+3k
            }
            if (mod == 2) {
                if (usedTwo) {
                    return false;           // 有两个段需要那个唯一的 2
                }
                usedTwo = true;
            }
            i = j;
        }
        return usedTwo;                      // 必须恰好有一个长度为 2 的段
    }

    public boolean isDecomposableGreedy(String s) {
        int n = s.length();
        boolean usedTwo = false;
        for (int i = 0; i < n; ) {
            int j = i;
            while (j < n && s.charAt(j) == s.charAt(i)) {
                j++;
            }
            int len = j - i;
            // 每个段必须能拆成 3+3+...+2 或 3+3+...
            if (len % 3 == 1) {
                return false;
            }
            if (len % 3 == 2) {
                if (usedTwo) {
                    return false;
                }
                usedTwo = true;
            }
            i = j;
        }
        return usedTwo;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        String[] cs = {"000111000", "00011111222", "01110002223300", "00", "000", "000111", "1", "11", "1111"};
        boolean[] exp = {false, true, false, true, false, false, false, true, false};
        boolean all = true;
        for (int i = 0; i < cs.length; i++) {
            boolean a = s.isDecomposable(cs[i]);
            boolean b = s.isDecomposableGreedy(cs[i]);
            boolean ok = a == exp[i] && b == exp[i];
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1934. Confirmation Rate(确认率)

题目类型:数据库(SQL)

题目描述

表:Signups

复制代码
+----------------+----------+
| Column Name    | Type     |
+----------------+----------+
| user_id        | int      |
| time_stamp     | datetime |
+----------------+----------+

user_id 是该表的主键。每一行都包含 ID 为 user_id 的用户的注册时间信息。

表:Confirmations

复制代码
+----------------+----------+
| Column Name    | Type     |
+----------------+----------+
| user_id        | int      |
| time_stamp     | datetime |
| action         | ENUM     |
+----------------+----------+

(user_id, time_stamp) 是该表的主键。action 是类型为 ('confirmed', 'timeout') 的 ENUM。

用户的确认率 是 'confirmed' 消息的数量除以请求的确认消息的总数。没有请求任何确认消息的用户的确认率为 0。确认率四舍五入到小数点后两位 。编写一个 SQL 查询来查找每个用户的确认率。

示例 1:

复制代码
输入:
Signups 表:                        Confirmations 表:
+---------+---------------------+   +---------+---------------------+-----------+
| user_id | time_stamp          |   | user_id | time_stamp          | action    |
+---------+---------------------+   +---------+---------------------+-----------+
| 3       | 2020-03-21 10:16:13 |   | 3       | 2021-01-06 03:30:46 | timeout   |
| 7       | 2020-01-04 13:57:59 |   | 3       | 2021-07-14 14:00:00 | timeout   |
| 2       | 2020-07-29 23:09:44 |   | 7       | 2021-06-12 11:57:29 | confirmed |
| 6       | 2020-12-09 10:39:37 |   | 7       | 2021-06-13 12:58:28 | confirmed |
+---------+---------------------+   | 7       | 2021-06-14 13:59:27 | confirmed |
                                   | 2       | 2021-01-22 00:00:00 | confirmed |
                                   | 2       | 2021-02-28 23:59:59 | timeout   |
                                   +---------+---------------------+-----------+
输出:
+---------+-------------------+
| user_id | confirmation_rate |
+---------+-------------------+
| 6       | 0.00              |
| 3       | 0.00              |
| 7       | 1.00              |
| 2       | 0.50              |
+---------+-------------------+

解题思路

Signups 与 Confirmations 左连接后按用户分组:

AVG(action = 'confirmed') 在 MySQL 中会把布尔表达式转成 0/1 求平均即确认率;没有请求的用户

AVG 为 NULL,用 IFNULL(..., 0) 兜底为 0,最后 ROUND(..., 2) 保留两位小数。

代码实现

sql 复制代码
SELECT s.user_id,
       ROUND(IFNULL(AVG(c.action = 'confirmed'), 0), 2) AS confirmation_rate
FROM Signups s
LEFT JOIN Confirmations c ON s.user_id = c.user_id
GROUP BY s.user_id;

复杂度分析

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


1935. Maximum Number of Words You Can Type(可以输入的最大单词数)

题目描述

键盘出现了一些故障,有些字母键无法正常工作。而键盘上所有其他键都能够正常工作。

给你一个由若干单词组成的字符串 text,单词间由单个空格组成(不含前导和尾随空格);另有一个字符串

brokenLetters,由所有已损坏的不同字母键组成,返回你可以使用此键盘完全输入的 text 中单词的数目。

示例 1

  • 输入 :text = "hello world", brokenLetters = "ad"
  • 输出:1
  • 解释 :无法输入 "world",因为字母键 'd' 已损坏。

示例 2

  • 输入 :text = "leet code", brokenLetters = "lt"
  • 输出:1
  • 解释 :无法输入 "leet",因为字母键 'l' 和 't' 已损坏。

示例 3

  • 输入 :text = "leet code", brokenLetters = "e"
  • 输出:0
  • 解释 :无法输入任何单词,因为字母键 'e' 已损坏。
    提示:
  • - 1 <= text.length <= 10^4
  • - 0 <= brokenLetters.length <= 26
    • text 由若干用单个空格分隔的单词组成,且不含任何前导和尾随空格
    • 每个单词仅由小写英文字母组成
    • brokenLetters 由互不相同的小写英文字母组成

解题思路

解法一:单词拆分 + 字符集合,O(len),主提交版本

把 brokenLetters 存进布尔数组(26 位),

按空格拆分 text 为单词,统计完全不含坏键的单词数。

解法二:单次扫描按空格切词,对照

不调用 split,用一个循环扫描 text,遇到空格结束当前单词判断,

单词内若出现坏键则标记本词不可输入;最后再处理最后一个单词。与 split 等价但实现不同,用于交叉验证。

代码实现

java 复制代码
public class Solution {

    public int canBeTypedWords(String text, String brokenLetters) {
        boolean[] broken = new boolean[26];
        for (char c : brokenLetters.toCharArray()) {
            broken[c - 'a'] = true;
        }
        int ans = 0;
        for (String w : text.split(" ")) {
            boolean ok = true;
            for (char c : w.toCharArray()) {
                if (broken[c - 'a']) {
                    ok = false;
                    break;
                }
            }
            if (ok) {
                ans++;
            }
        }
        return ans;
    }

    public int canBeTypedWordsScan(String text, String brokenLetters) {
        boolean[] broken = new boolean[26];
        for (char c : brokenLetters.toCharArray()) {
            broken[c - 'a'] = true;
        }
        int ans = 0;
        int n = text.length();
        boolean bad = false;
        for (int i = 0; i <= n; i++) {
            if (i == n || text.charAt(i) == ' ') {
                if (!bad) {
                    ans++;
                }
                bad = false;
                continue;
            }
            if (broken[text.charAt(i) - 'a']) {
                bad = true;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        String[] t = {"hello world", "leet code", "leet code", "a b c d", "abc", "zzz"};
        String[] b = {"ad", "lt", "e", "abcde", "z", ""};
        int[] exp = {1, 1, 0, 0, 1, 1};
        boolean all = true;
        for (int i = 0; i < t.length; i++) {
            int a = s.canBeTypedWords(t[i], b[i]);
            int c = s.canBeTypedWordsScan(t[i], b[i]);
            boolean ok = a == exp[i] && c == exp[i];
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + c);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1936. Add Minimum Number of Rungs(新增的最少台阶数)

题目描述

给你一个严格递增的整数数组 rungs,用于表示梯子上每一台阶的高度。当前你正站在高度为 0 的地板上,

并打算爬到最后一个台阶。

另给你一个整数 dist。每次移动中,你可以到达下一个距离你当前位置(地板或台阶)不超过 dist 高度的

台阶。当然,你也可以在任何正整数高度处插入尚不存在的新台阶。

返回爬到最后一阶时必须添加到梯子上的最少台阶数。

示例 1

  • 输入 :rungs = 1,3,5,10, dist = 2
  • 输出:2
  • 解释 :现在无法到达最后一阶。在高度为 7 和 8 的位置增设新的台阶,以爬上梯子。
    梯子在高度为 1,3,5,7,8,10 的位置上有台阶。

示例 2

  • 输入 :rungs = 3,6,8,10, dist = 3
  • 输出:0
  • 解释:这个梯子无需增设新台阶也可以爬上去。

示例 3

  • 输入 :rungs = 3,4,6,7, dist = 2
  • 输出:1
  • 解释:现在无法从地板到达梯子的第一阶。在高度为 1 的位置增设新的台阶,以爬上梯子。

示例 4

  • 输入 :rungs = 5, dist = 10
  • 输出:0
  • 解释 :这个梯子无需增设新台阶也可以爬上去。
    提示:
  • - 1 <= rungs.length <= 10^5
  • - 1 <= rungs[i] <= 10^9
  • - 1 <= dist <= 10^9
    • rungs 严格递增

解题思路

解法一:贪心差分,O(n),主提交版本

从当前位置 cur(初始 0)推进到每个台阶 next,若

next - cur > dist,则需要在中间补台阶。每补一个台阶最多跨越 dist 高度,所以要补

(next - cur - 1) / dist 个(向上取整再减 1)。补完后把 cur 更新为 next。

解法二:逐段模拟插入位置,对照

维护当前可达高度 reach = cur + dist,若 reach < next,

则在 cur + dist 处插入台阶并累加计数,重复直到 reach >= next。逻辑等价,写法更贴近模拟,

用于交叉验证。

代码实现

java 复制代码
public class Solution {

    public int addRungs(int[] rungs, int dist) {
        long ans = 0;
        long cur = 0;
        for (int next : rungs) {
            if (next - cur > dist) {
                // 需要 (next - cur) 被 dist 分成 ceil 段,中间插入 ceil-1 个台阶
                ans += (next - cur - 1) / dist;
            }
            cur = next;
        }
        return (int) ans;
    }

    public int addRungsSim(int[] rungs, int dist) {
        long ans = 0;
        long cur = 0;
        for (int next : rungs) {
            while (next - cur > dist) {
                cur += dist;   // 在 cur+dist 处补一个台阶并站上去
                ans++;
            }
            cur = next;        // 直接上到真实台阶
        }
        return (int) ans;
    }

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

复杂度分析


1937. Maximum Number of Points with Cost(扣分后的最大得分)

题目描述

给你一个 m x n 的整数矩阵 points(下标从 0 开始)。一开始你的得分为 0,你想最大化从矩阵中

得到的分数。

你的得分方式为:每一行中选取一个格子,选中坐标为 (r, c) 的格子会给你的总得分增加 points[r][c]。

然而,相邻行之间被选中的格子如果隔得太远,你会失去一些得分。对于相邻行 r 和 r + 1

(其中 0 <= r < m - 1),选中格子 (r, c1) 和 (r + 1, c2),你的总得分减少 abs(c1 - c2)。

请你返回你能得到的最大得分。

abs(x) 定义为:如果 x >= 0,那么值为 x;如果 x < 0,那么值为 -x。

提示:

  • - m == points.length
  • - n == points[i].length
  • - 1 <= m, n <= 10^5
  • - 1 <= m * n <= 10^5
  • - 0 <= points[i][j] <= 10^5

解题思路

解法一:前后缀最大值优化 DP,O(mn),主提交版本

设 dp[c] 为上一行选第 c 列时的最大得分。转移:ndp[c] = points[i][c] + max( max over c1 <= c 的 dp[c1] - (c - c1), // 从左向右:dp[c1] + c1 - c max over c1 >= c 的 dp[c1] - (c1 - c) // 从右向左:dp[c1] - c1 + c )。

预处理 leftBest[c] = max(dp[c1] + c1)(左扫)与 rightBest[c] = max(dp[c1] - c1)(右扫),

则 ndp[c] = points[i][c] + max(leftBest[c] - c, rightBest[c] + c)。O(n) 每行,总 O(mn)。

解法二:暴力 O(mn^2),对照

直接按定义 ndp[c] = points[i][c] + max_j(dp[j] - |j - c|),

小数据交叉验证。

代码实现

java 复制代码
public class Solution {

    public long maxPoints(int[][] points) {
        int m = points.length, n = points[0].length;
        long[] dp = new long[n];
        for (int j = 0; j < n; j++) {
            dp[j] = points[0][j];
        }
        for (int i = 1; i < m; i++) {
            long[] left = new long[n];
            long[] right = new long[n];
            long best = Long.MIN_VALUE;
            for (int c = 0; c < n; c++) {
                best = Math.max(best, dp[c] + c);
                left[c] = best;
            }
            best = Long.MIN_VALUE;
            for (int c = n - 1; c >= 0; c--) {
                best = Math.max(best, dp[c] - c);
                right[c] = best;
            }
            long[] ndp = new long[n];
            for (int c = 0; c < n; c++) {
                ndp[c] = points[i][c] + Math.max(left[c] - c, right[c] + c);
            }
            dp = ndp;
        }
        long ans = 0;
        for (long v : dp) {
            ans = Math.max(ans, v);
        }
        return ans;
    }

    public long maxPointsBrute(int[][] points) {
        int m = points.length, n = points[0].length;
        long[] dp = new long[n];
        for (int j = 0; j < n; j++) {
            dp[j] = points[0][j];
        }
        for (int i = 1; i < m; i++) {
            long[] ndp = new long[n];
            for (int c = 0; c < n; c++) {
                long best = Long.MIN_VALUE;
                for (int c1 = 0; c1 < n; c1++) {
                    best = Math.max(best, dp[c1] - Math.abs(c1 - c));
                }
                ndp[c] = points[i][c] + best;
            }
            dp = ndp;
        }
        long ans = 0;
        for (long v : dp) ans = Math.max(ans, v);
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] p1 = {{1, 2, 3}, {1, 5, 1}, {3, 1, 1}};
        int[][] p2 = {{1, 5}, {2, 3}, {4, 2}};
        int[][] p3 = {{0, 0, 0}};
        boolean ok = true;
        long a1 = s.maxPoints(p1);
        long b1 = s.maxPointsBrute(p1);
        ok &= a1 == 9 && b1 == 9;
        long a2 = s.maxPoints(p2);
        long b2 = s.maxPointsBrute(p2);
        ok &= a2 == 11 && b2 == 11;
        long a3 = s.maxPoints(p3);
        long b3 = s.maxPointsBrute(p3);
        ok &= a3 == 0 && b3 == 0;
        System.out.println((ok ? "PASS" : "FAIL") + " case1 => " + a1 + " / " + b1);
        System.out.println((ok ? "PASS" : "FAIL") + " case2 => " + a2 + " / " + b2);
        System.out.println((ok ? "PASS" : "FAIL") + " case3 => " + a3 + " / " + b3);
        System.out.println(ok ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1938. Maximum Genetic Difference Query(查询最大基因差)

题目描述

给你一棵 n 个节点的有根树,节点编号从 0 到 n - 1。每个节点的编号表示这个节点的独一无二的

基因值(也就是说节点 x 的基因值为 x)。两个基因值的基因差是两者的异或和。给你整数数组 parents,

其中 parents[i] 是节点 i 的父节点。如果节点 x 是树的根,那么 parents[x] == -1。

给你查询数组 queries,其中 queries[i] = [nodei, vali]。对于查询 i,请你找到 vali 和 pi

的最大基因差,其中 pi 是节点 nodei 到根之间的任意节点(包含 nodei 和根节点)。

更正式的,你想要最大化 vali XOR pi。

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

示例 1

  • 输入 :parents = -1,0,1,1, queries = \[0,2,3,2,2,5]
  • 输出:2,3,7
  • 解释 :0,2 最大基因差的对应节点为 0,基因差为 2 XOR 0 = 2;
    3,2 对应节点 1,2 XOR 1 = 3;2,5 对应节点 2,5 XOR 2 = 7。

示例 2

  • 输入 :parents = 3,7,-1,2,0,7,0,2, queries = \[4,6,1,15,0,5]
  • 输出:6,14,7
  • 解释 :4,6 对应节点 0,6 XOR 0 = 6;1,15 对应节点 1,15 XOR 1 = 14;
    0,5 对应节点 2,5 XOR 2 = 7。
    提示:
  • - 2 <= parents.length <= 10^5
    • 对于每个不是根节点的 i,有 0 <= parents[i] <= parents.length - 1
  • - parents[root] == -1
  • - 1 <= queries.length <= 10^5
  • - 0 <= nodei <= parents.length - 1
  • - 0 <= vali <= 2 * 10^5

解题思路

解法一:离线 DFS + 二进制字典树回溯,O((n+q) * 17),主提交版本

把查询按 node 挂在树节点上。从根 DFS,进入节点时把其编号(基因值)插入字典树,离开时删除。

这样 DFS 过程中字典树恰好维护"当前节点到根路径上的所有基因值"。遇到挂在该节点的查询

(node, val),就在字典树上贪心求 val XOR 任意路径基因值 的最大值(从高位到低位走相反分支)。

用 cnt[node] 记录字典树节点计数实现可删除;插入/删除都维护计数,查询时只走存在分支。

解法二:暴力枚举路径比对,对照

对每个查询,从 node 向上走到根,沿途枚举所有祖先编号求异或

最大值。O(q * depth) 仅适用于小数据,用于交叉验证。

代码实现

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

public class Solution {

    private static final int MAX_BIT = 17; // val 与节点编号 <= 2*10^5 < 2^18,用 17..0 共 18 位足够

    public int[] maxGeneticDifference(int[] parents, int[][] queries) {
        int n = parents.length;
        List<Integer>[] children = new ArrayList[n];
        for (int i = 0; i < n; i++) {
            children[i] = new ArrayList<>();
        }
        int root = -1;
        for (int i = 0; i < n; i++) {
            if (parents[i] == -1) {
                root = i;
            } else {
                children[parents[i]].add(i);
            }
        }
        // 查询按 node 分组
        Map<Integer, List<int[]>> qByNode = new HashMap<>();
        for (int qi = 0; qi < queries.length; qi++) {
            qByNode.computeIfAbsent(queries[qi][0], k -> new ArrayList<>())
                    .add(new int[]{queries[qi][1], qi});
        }
        Trie trie = new Trie();
        int[] ans = new int[queries.length];
        dfs(root, children, qByNode, trie, ans);
        return ans;
    }

    private void dfs(int u, List<Integer>[] children, Map<Integer, List<int[]>> qByNode,
                     Trie trie, int[] ans) {
        trie.insert(u);
        List<int[]> qs = qByNode.get(u);
        if (qs != null) {
            for (int[] q : qs) {
                ans[q[1]] = trie.maxXor(q[0]);
            }
        }
        for (int v : children[u]) {
            dfs(v, children, qByNode, trie, ans);
        }
        trie.remove(u);   // 回溯:离开子树时删除自己
    }

    static class Trie {
        private static class Node {
            Node[] next = new Node[2];
            int cnt = 0;
        }
        private final Node root = new Node();

        void insert(int x) {
            Node cur = root;
            for (int b = MAX_BIT; b >= 0; b--) {
                int bit = (x >> b) & 1;
                if (cur.next[bit] == null) {
                    cur.next[bit] = new Node();
                }
                cur = cur.next[bit];
                cur.cnt++;
            }
        }

        void remove(int x) {
            Node cur = root;
            for (int b = MAX_BIT; b >= 0; b--) {
                int bit = (x >> b) & 1;
                cur = cur.next[bit];
                cur.cnt--;
            }
        }

        int maxXor(int x) {
            Node cur = root;
            int res = 0;
            for (int b = MAX_BIT; b >= 0; b--) {
                int bit = (x >> b) & 1;
                int want = 1 - bit;   // 优先走相反位
                if (cur.next[want] != null && cur.next[want].cnt > 0) {
                    res |= (1 << b);
                    cur = cur.next[want];
                } else {
                    cur = cur.next[bit];
                }
            }
            return res;
        }
    }

    public int[] maxGeneticDifferenceBrute(int[] parents, int[][] queries) {
        int n = parents.length;
        int[] ans = new int[queries.length];
        for (int qi = 0; qi < queries.length; qi++) {
            int node = queries[qi][0], val = queries[qi][1];
            int best = 0;
            int cur = node;
            while (cur != -1) {
                best = Math.max(best, val ^ cur);
                cur = parents[cur];
            }
            ans[qi] = best;
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[] p1 = {-1, 0, 1, 1};
        int[][] q1 = {{0, 2}, {3, 2}, {2, 5}};
        int[] e1 = {2, 3, 7};
        // p2: parents[0]=3, parents[1]=7, parents[2]=-1, parents[3]=2, parents[4]=0, parents[5]=7, parents[6]=0, parents[7]=2
        int[] p2 = {3, 7, -1, 2, 0, 7, 0, 2};
        int[][] q2 = {{4, 6}, {1, 15}, {0, 5}};
        int[] e2 = {6, 14, 7};
        boolean ok = java.util.Arrays.equals(s.maxGeneticDifference(p1, q1), e1)
                && java.util.Arrays.equals(s.maxGeneticDifferenceBrute(p1, q1), e1)
                && java.util.Arrays.equals(s.maxGeneticDifference(p2, q2), e2)
                && java.util.Arrays.equals(s.maxGeneticDifferenceBrute(p2, q2), e2);
        System.out.println((ok ? "PASS" : "FAIL") + " case1 => " + java.util.Arrays.toString(s.maxGeneticDifference(p1, q1)));
        System.out.println((ok ? "PASS" : "FAIL") + " case2 => " + java.util.Arrays.toString(s.maxGeneticDifference(p2, q2)));
        System.out.println(ok ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1939. Users That Actively Request Confirmation Messages(主动请求确认消息的用户)

题目类型:数据库(SQL)

题目描述

表:Signups

复制代码
+----------------+----------+
| Column Name    | Type     |
+----------------+----------+
| user_id        | int      |
| time_stamp     | datetime |
+----------------+----------+

user_id 是该表的主键。

表:Confirmations

复制代码
+----------------+----------+
| Column Name    | Type     |
+----------------+----------+
| user_id        | int      |
| time_stamp     | datetime |
| action         | ENUM     |
+----------------+----------+

(user_id, time_stamp) 是该表的主键。action 是类型为 ('confirmed', 'timeout') 的 ENUM。

编写 SQL 查询以查找在 24 小时窗口内 两次请求确认消息的用户的 ID。两个正好相隔 24 小时的消息被认为是在窗口内(即时间差 <= 24 小时,包含端点)。

示例:

复制代码
输入:
Confirmations table:
+---------+---------------------+-----------+
| user_id | time_stamp          | action    |
+---------+---------------------+-----------+
| 3       | 2021-01-06 03:30:46 | timeout   |
| 3       | 2021-01-06 03:37:45 | timeout   |
| 7       | 2021-06-12 11:57:29 | confirmed |
| 7       | 2021-06-13 11:57:30 | confirmed |
| 2       | 2021-01-22 00:00:00 | confirmed |
| 2       | 2021-01-23 00:00:00 | timeout   |
| 6       | 2021-10-23 14:14:14 | confirmed |
| 6       | 2021-10-24 14:14:13 | timeout   |
+---------+---------------------+-----------+
输出:
+---------+
| user_id |
+---------+
| 2       |
| 3       |
| 6       |
+---------+

解释:用户 2 恰好相隔 24 小时、用户 3 相隔 6 分 59 秒、用户 6 相隔 23 小时 59 分 59 秒,均在窗口内;用户 7 相隔 24 小时 1 秒,不在窗口内。

解题思路

表自连接:同一 user_id 的两条消息,用 TIMESTAMPDIFF(HOUR, ...)(或 TIME_TO_SEC 差值)判断

两条消息时间差 <= 24 小时即可,DISTINCT 去重后输出。

代码实现

sql 复制代码
SELECT DISTINCT a.user_id
FROM Confirmations a
JOIN Confirmations b
  ON a.user_id = b.user_id
 AND a.time_stamp < b.time_stamp
WHERE TIMESTAMPDIFF(HOUR, a.time_stamp, b.time_stamp) <= 24
ORDER BY a.user_id;

复杂度分析

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


1940. Longest Common Subsequence Between Sorted Arrays(排序数组之间的最长公共子序列)

题目描述

给定一个由整数数组组成的数组 arrays,其中 arrays[i] 是严格递增排序的,返回一个所有数组均包含的

最长公共子序列的整数数组。

子序列是从另一个序列派生出来的序列,删除一些元素或不删除任何元素,而不改变其余元素的顺序。

示例 1

  • 输入 :arrays = \[1,3,4,1,4,7,9]
  • 输出:1,4
  • 解释:这两个数组中的最长子序列是 1,4。

示例 2

  • 输入 :arrays = \[2,3,6,8,1,2,3,5,6,7,10,2,3,4,6,9]
  • 输出:2,3,6
  • 解释:这三个数组中的最长公共子序列是 2,3,6。

示例 3

  • 输入 :arrays = \[1,2,3,4,5,6,7,8]
  • 输出:\[\]
  • 解释 :这两个数组之间没有公共子序列。
    限制条件:
  • 2 <= arrays.length <= 100
  • 1 <= arrays[i].length <= 100
  • 1 <= arrays[i][j] <= 100
  • arrays[i] 是严格递增排序

解题思路

解法一:计数 + 频次筛选,O(total * len),主提交版本

因为每个数组严格递增且值域只有 1...100,

所以公共子序列等价于"每个数组都包含的元素集合"(严格递增有序数组中的公共子序列就是公共元素

的交集按原顺序排列)。用计数数组统计每个值在多少数组中出现,出现次数 == arrays.length 的值

即公共元素,按数值升序即为最长公共子序列(且唯一)。

解法二:哈希集合求交,对照

第一个数组元素放入集合,逐个与后续数组取交集(Intersection),

最后排序输出。逻辑与解法一同源,仅实现方式不同,用于交叉验证。

代码实现

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

public class Solution {

    public List<Integer> longestCommonSubsequence(int[][] arrays) {
        int k = arrays.length;
        int[] cnt = new int[101];
        for (int[] arr : arrays) {
            boolean[] seen = new boolean[101];
            for (int x : arr) {
                if (!seen[x]) {
                    seen[x] = true;
                    cnt[x]++;
                }
            }
        }
        List<Integer> res = new ArrayList<>();
        for (int v = 1; v <= 100; v++) {
            if (cnt[v] == k) {
                res.add(v);
            }
        }
        return res;
    }

    public List<Integer> longestCommonSubsequenceSet(int[][] arrays) {
        boolean[] common = new boolean[101];
        for (int x : arrays[0]) {
            common[x] = true;
        }
        for (int i = 1; i < arrays.length; i++) {
            boolean[] cur = new boolean[101];
            for (int x : arrays[i]) {
                cur[x] = true;
            }
            for (int v = 1; v <= 100; v++) {
                common[v] = common[v] && cur[v];
            }
        }
        List<Integer> res = new ArrayList<>();
        for (int v = 1; v <= 100; v++) {
            if (common[v]) {
                res.add(v);
            }
        }
        return res;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] a1 = {{1, 3, 4}, {1, 4, 7, 9}};
        int[][] a2 = {{2, 3, 6, 8}, {1, 2, 3, 5, 6, 7, 10}, {2, 3, 4, 6, 9}};
        int[][] a3 = {{1, 2, 3, 4, 5}, {6, 7, 8}};
        List<Integer> e1 = Arrays.asList(1, 4);
        List<Integer> e2 = Arrays.asList(2, 3, 6);
        List<Integer> e3 = new ArrayList<>();
        boolean ok = s.longestCommonSubsequence(a1).equals(e1)
                && s.longestCommonSubsequenceSet(a1).equals(e1)
                && s.longestCommonSubsequence(a2).equals(e2)
                && s.longestCommonSubsequenceSet(a2).equals(e2)
                && s.longestCommonSubsequence(a3).equals(e3)
                && s.longestCommonSubsequenceSet(a3).equals(e3);
        System.out.println((ok ? "PASS" : "FAIL") + " case1 => " + s.longestCommonSubsequence(a1));
        System.out.println((ok ? "PASS" : "FAIL") + " case2 => " + s.longestCommonSubsequence(a2));
        System.out.println((ok ? "PASS" : "FAIL") + " case3 => " + s.longestCommonSubsequence(a3));
        System.out.println(ok ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析

相关推荐
Tt有了t1 小时前
设计模式速查
java·python·设计模式
Thomas.Sir1 小时前
第15课:SpringBoot连接池详解:HikariCP配置、调优、监控,4.x连接池新特性
java·spring boot
VimorphTech2 小时前
每批开 4 个线程,为什么限制不住 TTS 的总并发?
java·spring boot·spring
Pniubi2 小时前
Java为什么要分堆和栈
java·开发语言
杨凯凡2 小时前
Spring Boot 缓存一致性怎么保证?@Cacheable、@CacheEvict 与事务提交时机实战
java·spring boot·spring cache·后端开发·缓存一致性
Rain的Java大神之路3 小时前
国家数据集团校招、社招都开了:总部雄安,这些技术岗值得认真看看
java·后端·面试
一去不复返的通信er3 小时前
排序 + 双指针:力扣15三数之和
算法·leetcode
吴名氏.3 小时前
Java转AI Agent教程(第九周)错误矩阵
java·人工智能·矩阵·ai agent