LeetCode1961~1980题目解析

LeetCode 1961-1980. 题解合集

文章目录


1961. Check If String Is a Prefix of Array(检查字符串是否为数组前缀)

题目描述

给你一个字符串 s 和一个字符串数组 words,请你判断 s 是否为 words 的前缀字符串。

字符串 s 要成为 words 的前缀字符串,需要满足:s 可以由 words 中的前 k

(k 为正数)个字符串按顺序相连得到,且 k 不超过 words.length。

如果 s 是 words 的前缀字符串,返回 true;否则,返回 false。

示例 1

  • 输入 :s = "iloveleetcode", words = "`i`","`love`","`leetcode`","`apples`"
  • 输出 :true
  • 解释 :s 可以由 "i"、"love" 和 "leetcode" 相连得到。

示例 2

  • 输入 :s = "iloveleetcode", words = "`apples`","`i`","`love`","`leetcode`"
  • 输出 :false
  • 解释 :数组的前缀相连无法得到 s。
    提示:
  • - 1 <= words.length <= 100
  • - 1 <= words[i].length <= 20
  • - 1 <= s.length <= 1000
    • words[i] 和 s 仅由小写英文字母组成。

解题思路

解法一:逐词拼接比较,O(len(s)),主提交版本

用 StringBuilder 从前往后累加 words 的

每个词,每加一个词后判断前缀串是否与 s 相等(注意题目要求恰好等于 s,而不是 s 是拼接的

前缀------"s 可以由 words 中的前 k 个字符串按顺序相连得到"就是拼接结果必须等于 s)。

若拼接长度超过 s.length() 还没相等则返回 false(后续词更长不可能相等)。

解法二:双指针字符比较,O(len(s)),对照

维护目标串 s 上的游标 pos,每取一个词就

逐字符与 s 的对应位置比较;若发现不匹配或词过长,直接返回 false;扫描完所有词后

pos 恰好等于 s.length() 才返回 true。两解法结果一致。

代码实现

java 复制代码
public class Solution {

    public boolean isPrefixString(String s, String[] words) {
        StringBuilder sb = new StringBuilder();
        for (String w : words) {
            sb.append(w);
            if (sb.length() > s.length()) {
                return false;      // 已经超长,不可能再相等
            }
            if (sb.length() == s.length()) {
                return sb.toString().equals(s);
            }
        }
        return false;   // words 用尽仍未达到 s.length()
    }

    public boolean isPrefixStringPtr(String s, String[] words) {
        int pos = 0;
        for (String w : words) {
            // 词已超出剩余目标长度,失败
            if (pos + w.length() > s.length()) {
                return false;
            }
            for (int i = 0; i < w.length(); i++) {
                if (s.charAt(pos + i) != w.charAt(i)) {
                    return false;
                }
            }
            pos += w.length();
            if (pos == s.length()) {
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        String[] ss = {"iloveleetcode", "iloveleetcode", "a", "abc", "ab", "xyz"};
        String[][] wss = {
            {"i", "love", "leetcode", "apples"},
            {"apples", "i", "love", "leetcode"},
            {"a"},
            {"a", "bc", "d"},
            {"a"},
            {"x", "y", "z"}
        };
        boolean[] exp = {true, false, true, true, false, true};
        boolean all = true;
        for (int i = 0; i < ss.length; i++) {
            boolean a = sol.isPrefixString(ss[i], wss[i]);
            boolean b = sol.isPrefixStringPtr(ss[i], wss[i]);
            boolean ok = a == exp[i] && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1962. Remove Stones to Minimize the Total(移除石子使总数最小)

题目描述

给你一个整数数组 piles,数组下标从 0 开始,其中 piles[i] 表示第 i 堆石子中的石子

数量。另给你一个整数 k,请你执行下述操作恰好 k 次:

  • 选出任一石子堆 piles[i],并从中移除 floor(piles[i] / 2) 颗石子。
    注意:你可以对同一堆石子多次执行此操作。
    返回执行 k 次操作后,剩下石子的最小总数。
    floor(x) 为小于或等于 x 的最大整数(对 x 向下取整)。

示例 1

  • 输入 :piles = 5,4,9, k = 2
  • 输出:12
  • 解释:对第 2 堆操作:5,4,5;再对第 0 堆操作:3,4,5,剩余总数 3+4+5 = 12。

示例 2

  • 输入 :piles = 4,3,6,7, k = 3
  • 输出:12
  • 解释 :依次对第 2、3、0 堆操作后得到 2,3,3,4,剩余总数 12。
    提示:
  • - 1 <= piles.length <= 10^5
  • - 1 <= piles[i] <= 10^4
  • - 1 <= k <= 10^5

解题思路

解法一:最大堆贪心,O((n+k) log n),主提交版本

每次操作应选择「当前最大的一堆」------因为

移除 floor(x/2) 后减少的量与 x 成正比,堆越大减少越多,且操作可重复作用于同一堆。用

最大堆维护各堆石子数,连续 k 次:弹出堆顶 x,把 x - x/2 放回;最后堆内元素和即最少

剩余总数。

解法二:计数桶模拟,对照

piles[i] <= 10^4,可用计数数组统计每种石子数的堆数,从大到

小依次操作(每次选当前最大的非空桶),桶内计数的向下取整分裂。两解法结果一致,交叉验证。

代码实现

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

public class Solution {

    public int minStoneSum(int[] piles, int k) {
        PriorityQueue<Integer> pq = new PriorityQueue<>((a, b) -> Integer.compare(b, a));
        long total = 0;
        for (int p : piles) {
            pq.offer(p);
            total += p;
        }
        for (int i = 0; i < k; i++) {
            int x = pq.poll();
            total -= x / 2;             // 移除 floor(x/2)
            pq.offer(x - x / 2);
        }
        return (int) total;
    }

    public int minStoneSumBucket(int[] piles, int k) {
        int max = 0;
        long total = 0;
        for (int p : piles) {
            max = Math.max(max, p);
            total += p;
        }
        int[] cnt = new int[max + 1];
        for (int p : piles) cnt[p]++;
        // 从大到小扫:每次取当前最大的一堆做移除
        for (int i = 0; i < k; i++) {
            int x = max;
            while (x > 0 && cnt[x] == 0) x--;
            if (x == 0) break;              // 没有石子可操作
            cnt[x]--;                         // 拿走一堆
            total -= x / 2;
            int nxt = x - x / 2;
            cnt[nxt]++;
            max = Math.max(max, nxt);
        }
        return (int) total;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][] piles = {
            {5, 4, 9},
            {4, 3, 6, 7},
            {1},
            {1, 1, 1, 1},
            {10000, 10000, 10000}
        };
        int[] ks = {2, 3, 1, 5, 3};
        int[] exp = {12, 12, 1, 4, 15000};
        boolean all = true;
        for (int i = 0; i < piles.length; i++) {
            int a = sol.minStoneSum(piles[i], ks[i]);
            int b = sol.minStoneSumBucket(piles[i], ks[i]);
            boolean ok = a == exp[i] && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1963. Minimum Number of Swaps to Make the String Balanced(使字符串平衡的最小交换次数)

题目描述

给你一个字符串 s,下标从 0 开始,且长度为偶数 n。字符串恰好由 n/2 个开括号 '['

和 n/2 个闭括号 ']' 组成。

只有能满足下述所有条件的字符串才能称为平衡字符串:

  • 字符串是一个空字符串,或者
  • 字符串可以记作 AB,其中 A 和 B 都是平衡字符串,或者
  • 字符串可以写成 [C],其中 C 是一个平衡字符串。
    你可以交换任意两个下标所对应的括号任意次数。
    返回使 s 变成平衡字符串所需的最小交换次数。

示例 1

  • 输入 :s = "]\[\]["
  • 输出:1
  • 解释:交换下标 0 和下标 3,字符串变成 "\[]"。

示例 2

  • 输入 :s = "]]][[["
  • 输出:2
  • 解释:交换 0/4 得 "\[\]]\[\]",交换 1/5 得 "\[\[\[\]]"。

示例 3

  • 输入 :s = "\[\]"
  • 输出 :0
    提示:
  • - n == s.length
  • - 2 <= n <= 10^6
    • n 为偶数
    • s[i] 为 '' 或 ''
    • 开括号 '' 的数目为 `n`/2,闭括号 '' 的数目也是 n/2。

解题思路

解法一:贪心扫描计数,O(n),主提交版本

从左往右扫描,维护当前未配对的 [ 数量

open。遇到 [ 时 open++;遇到 ] 时若 open > 0 则 open--(配对),否则说明当前

出现了一个「多余的右括号」,此时需要一次交换:把该 ] 与后面某个 [ 交换。经典结论:

最小交换次数 = ceil(未配对的 ']' 数量 / 2) = (unmatched + 1) / 2。因为一次交换可以

修复至多两个未配对的右括号(交换最左的 ] 与最右的 [)。

解法二:双指针从两端逼近,O(n),对照

左指针找多余的 ](balance 为负的位置),右指针

从末尾找 [,互相交换并计数,直到两指针相遇。两种实现交叉验证。

代码实现

java 复制代码
public class Solution {

    public int minSwaps(String s) {
        int open = 0;
        int unmatched = 0;   // 多余的 ']' 数量
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == '[') {
                open++;
            } else {
                if (open > 0) {
                    open--;
                } else {
                    unmatched++;
                }
            }
        }
        return (unmatched + 1) / 2;
    }

    public int minSwapsPtr(String s) {
        char[] arr = s.toCharArray();
        int n = arr.length;
        int l = 0, r = n - 1;
        int balance = 0;
        int swaps = 0;
        while (l < r) {
            // 向右扫,停在第一个使 balance 变负的多余 ']' 位置
            while (l < r) {
                balance += arr[l] == '[' ? 1 : -1;
                if (balance < 0) break;     // arr[l] 是多余的 ']'
                l++;
            }
            if (balance >= 0) break;        // 前缀没有多余 ']',已平衡
            // 从右边找第一个 '[' 与 arr[l] 交换(一次交换修复这对)
            while (l < r && arr[r] != '[') r--;
            char tmp = arr[l];
            arr[l] = arr[r];
            arr[r] = tmp;
            swaps++;
            balance += 2;                   // l 处 ']'→'[':balance 从 -1 变 +1
            l++;
            r--;                            // 最右 '[' 已被交换走
        }
        return swaps;
    }

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

复杂度分析


1964. Find the Longest Valid Obstacle Course at Each Position(找出到每个位置为止最长的有效障碍赛跑路线)

题目描述

你打算构建一些障碍赛跑路线。给你一个下标从 0 开始的整数数组 obstacles,数组长度为 n,

其中 obstacles[i] 表示第 i 个障碍的高度。

对于每个介于 0 和 n-1 之间(包含端点)的下标 i,在满足下述条件的前提下,请你找出

obstacles 能构成的最长障碍路线的长度:

  • 你可以选择下标介于 0 到 i 之间(包含端点)的任意个障碍。
  • 在这条路线中,必须包含第 i 个障碍(作为最后一个)。
  • 你必须按障碍在 obstacles 中的出现顺序布置这些障碍。
  • 除第一个障碍外,路线中每个障碍的高度都必须和前一个障碍相同或者更高(即非降序)。
    返回长度为 n 的答案数组 ans,其中 ans[i] 是下标 i 对应的最长障碍赛跑路线的长度。

示例 1

  • 输入 :obstacles = 1,2,3,2
  • 输出:1,2,3,3
  • 解释 :i=0: 1;i=1: 1,2;i=2: 1,2,3;i=3: 1,2,2(以 2 结尾的非降子序列最长 3)。

示例 2

  • 输入 :obstacles = 2,2,1
  • 输出:1,2,1
  • 解释 :i=1: 2,2;i=2: 1(以 1 结尾只能取自身)。

示例 3

  • 输入 :obstacles = 3,1,5,6,4,2
  • 输出 :1,1,2,3,2,2
    提示:
  • - n == obstacles.length
  • - 1 <= n <= 10^5
  • - 1 <= obstacles[i] <= 10^7

解题思路

解法一:二分求「以当前值结尾的最长非降子序列」,O(n log n),主提交版本

维护数组 tails,tails[len] 表示长度为 len 的非降子序列的最小末尾值 。对每个

obstacles[i],在 tails 中用「二分查找右边界(upper_bound)」找到第一个大于

obstacles[i] 的位置 pos,则 pos 即「以 obstacles[i] 结尾的最长非降子序列长度」,

并把 tails[pos] 更新为 obstacles[i](若 pos == 当前长度 则扩展)。注意因为允许相等

(非降),必须用 > 而不是 >= 判断边界(即相等的可以排在后面接续,不增加长度)。

解法二:树状数组/线段树,O(n log M),对照

值域 1..1e7,对每个值在前面的位置维护

「以该值结尾的最长非降长度」的最大值,用树状数组前缀最大值查询 [1, x] 区间,然后单点

更新 x 处为 query(x) + 1 的最大值。两种解法结果一致。

代码实现

java 复制代码
public class Solution {

    public int[] longestObstacleCourseAtEachPosition(int[] obstacles) {
        int n = obstacles.length;
        int[] ans = new int[n];
        int[] tails = new int[n];   // tails[len] = 长度 len+1 的非降子序列最小末尾
        int len = 0;
        for (int i = 0; i < n; i++) {
            int x = obstacles[i];
            // 找到第一个 > x 的位置(upper_bound)
            int lo = 0, hi = len;
            while (lo < hi) {
                int mid = (lo + hi) / 2;
                if (tails[mid] > x) {
                    hi = mid;
                } else {
                    lo = mid + 1;
                }
            }
            ans[i] = lo + 1;         // 以 x 结尾的最长长度
            tails[lo] = x;           // 更新或扩展
            if (lo == len) {
                len++;
            }
        }
        return ans;
    }

    public int[] longestObstacleCourseAtEachPositionBIT(int[] obstacles) {
        // 值域压缩
        int n = obstacles.length;
        int[] sorted = obstacles.clone();
        java.util.Arrays.sort(sorted);
        int m = 0;
        int[] uniq = new int[n];
        for (int v : sorted) {
            if (m == 0 || uniq[m - 1] != v) uniq[m++] = v;
        }
        int[] bit = new int[m + 2];
        int[] ans = new int[n];
        for (int i = 0; i < n; i++) {
            int idx = lowerBound(uniq, m, obstacles[i]);   // 1-based 下标
            int best = query(bit, idx);                    // [1, idx] 最大值(含相等值)
            ans[i] = best + 1;
            update(bit, idx, best + 1, m);
        }
        return ans;
    }

    private int lowerBound(int[] a, int m, int x) {
        int lo = 0, hi = m;
        while (lo < hi) {
            int mid = (lo + hi) / 2;
            if (a[mid] >= x) hi = mid;
            else lo = mid + 1;
        }
        return lo + 1;
    }

    private int query(int[] bit, int i) {
        int res = 0;
        while (i > 0) {
            res = Math.max(res, bit[i]);
            i -= i & (-i);
        }
        return res;
    }

    private void update(int[] bit, int i, int val, int m) {
        while (i <= m) {
            bit[i] = Math.max(bit[i], val);
            i += i & (-i);
        }
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][] obs = {
            {1, 2, 3, 2},
            {2, 2, 1},
            {3, 1, 5, 6, 4, 2},
            {1},
            {6, 5, 4, 3, 2, 1},
            {1, 1, 1, 1}
        };
        int[][] exp = {
            {1, 2, 3, 3},
            {1, 2, 1},
            {1, 1, 2, 3, 2, 2},
            {1},
            {1, 1, 1, 1, 1, 1},
            {1, 2, 3, 4}
        };
        boolean all = true;
        for (int i = 0; i < obs.length; i++) {
            int[] a = sol.longestObstacleCourseAtEachPosition(obs[i]);
            int[] b = sol.longestObstacleCourseAtEachPositionBIT(obs[i]);
            boolean ok = java.util.Arrays.equals(a, exp[i]) && java.util.Arrays.equals(a, b);
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + java.util.Arrays.toString(a));
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1965. Employees With Missing Information(丢失信息的雇员)

题目类型:数据库(SQL)

题目描述

表:Employees

复制代码
+-------------+---------+
| Column Name | Type    |
+-------------+---------+
| employee_id | int     |
| name        | varchar |
+-------------+---------+

employee_id 是该表中具有唯一值的列。每一行表示雇员的 id 和他的姓名。

表:Salaries

复制代码
+-------------+---------+
| Column Name | Type    |
+-------------+---------+
| employee_id | int     |
| salary      | int     |
+-------------+---------+

employee_id 是该表中具有唯一值的列。每一行表示雇员的 id 和他的薪水。

编写解决方案,找到所有丢失信息 的雇员 id:当雇员的姓名 丢失了(存在于 Salary 表但不在 Employees 表),或者雇员的薪水信息 丢失了(存在于 Employees 表但不在 Salaries 表)。返回这些雇员的 id 从小到大排序。

示例 1:

复制代码
输入:
Employees table:           Salaries table:
+-------------+----------+   +-------------+--------+
| employee_id | name     |   | employee_id | salary |
+-------------+----------+   +-------------+--------+
| 2           | Crew     |   | 5           | 76071  |
| 4           | Haven    |   | 1           | 22517  |
| 5           | Kristian |   | 4           | 63539  |
+-------------+----------+   +-------------+--------+
输出:
+-------------+
| employee_id |
+-------------+
| 1           |
| 2           |
+-------------+

解释:雇员 1 的姓名丢失(只有薪水记录);雇员 2 的薪水信息丢失(只有姓名记录)。

解题思路

两张表互相 LEFT JOIN:Employees 左连接 Salaries 查薪水缺失的(Salaries 补 NULL),

Salaries 左连接 Employees 查姓名缺失的,两段 UNION 合并后统一排序。

代码实现

sql 复制代码
SELECT e.employee_id
FROM Employees e
LEFT JOIN Salaries s ON e.employee_id = s.employee_id
WHERE s.employee_id IS NULL
UNION
SELECT s.employee_id
FROM Salaries s
LEFT JOIN Employees e ON s.employee_id = e.employee_id
WHERE e.employee_id IS NULL
ORDER BY employee_id;

复杂度分析

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


1966. Binary Searchable Numbers in an Unsorted Array(未排序数组中的可被二分查找的数)

题目描述

有一个类似二分查找的方法。这个方法有两个入参:sequence 是一个整数数组,target 是一个

整数。这个方法可以判断 target 是否存在 sequence 中。

该方法的伪代码如下:

func(sequence, target)

while sequence is not empty

randomly choose an element from sequence as the pivot

if pivot = target, return true

else if pivot < target, remove all elements left of pivot

else remove all elements right of pivot

...

由于 pivot 是随机选择的,因此可能不是所有元素都能被找到(即使它存在于数组中),这与传统

二分查找不同(传统二分总是取中间元素)。

给定一个未排序数组 nums,返回是否所有元素都能被找到(即「可二分查找」的元素个数)。

更准确地说:返回「在任意随机 pivot 选择下都能被找到」的元素个数。一个元素 x 可被二分查找,

当且仅当无论随机选到哪个 pivot,x 都不会被丢弃------即 x 左侧不存在大于 x 的元素(否则选中

该 pivot 时会丢弃右侧包括 x),且 x 右侧不存在小于 x 的元素(否则选中该 pivot 时会丢弃

左侧包括 x)。

提示:

  • - 1 <= nums.length <= 10^5
  • - -10^5 <= nums[i] <= 10^5
    • nums 中所有值都不同。

解题思路

解法一:前缀最大值 + 后缀最小值,O(n),主提交版本

元素 nums[i] 可被二分查找

⟺ nums[i] 大于它左侧所有元素(prefixMax[i] < nums[i])且小于右侧所有元素

(suffixMin[i] > nums[i])。证明:若左侧存在元素 > x,随机选中它为 pivot 时,

pivot > target 丢弃右侧所有元素,x 被丢弃;对称地,若右侧存在元素 < x,选中它为

pivot 时丢弃左侧,x 也被丢弃。满足条件时任何 pivot 选择(无论是丢弃左还是右)都不会

把 x 丢掉,最终必然命中。

解法二:暴力验证,O(n^2),对照,仅小规模

对每个位置 i,直接向左扫找最大值、向右扫找

最小值,判断是否满足条件。用于交叉验证解法一。

代码实现

java 复制代码
public class Solution {

    public int binarySearchableNumbers(int[] nums) {
        int n = nums.length;
        // prefixMax[i]:nums[0..i-1] 的最大值(不含 i)
        int[] prefixMax = new int[n];
        int mx = Integer.MIN_VALUE;
        for (int i = 0; i < n; i++) {
            prefixMax[i] = mx;
            mx = Math.max(mx, nums[i]);
        }
        // suffixMin[i]:nums[i+1..n-1] 的最小值(不含 i)
        int[] suffixMin = new int[n];
        int mn = Integer.MAX_VALUE;
        for (int i = n - 1; i >= 0; i--) {
            suffixMin[i] = mn;
            mn = Math.min(mn, nums[i]);
        }
        int cnt = 0;
        for (int i = 0; i < n; i++) {
            if (prefixMax[i] < nums[i] && suffixMin[i] > nums[i]) {
                cnt++;
            }
        }
        return cnt;
    }

    public int binarySearchableNumbersBrute(int[] nums) {
        int n = nums.length;
        int cnt = 0;
        for (int i = 0; i < n; i++) {
            boolean ok = true;
            for (int j = 0; j < i; j++) {
                if (nums[j] > nums[i]) { ok = false; break; }
            }
            if (ok) {
                for (int j = i + 1; j < n; j++) {
                    if (nums[j] < nums[i]) { ok = false; break; }
                }
            }
            if (ok) cnt++;
        }
        return cnt;
    }

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

复杂度分析


1967. Number of Strings That Appear as Substrings in Word(作为子字符串出现在单词中的字符串数目)

题目描述

给你一个字符串数组 patterns 和一个字符串 word,统计 patterns 中有多少个字符串是

word 的子字符串。返回字符串数目。

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

示例 1

  • 输入 :patterns = "`a`","`abc`","`bc`","`d`", word = "abc"
  • 输出:3
  • 解释 :"a"、"abc"、"bc" 都是 "abc" 的子字符串,"d" 不是。

示例 2

  • 输入 :patterns = "`a`","`b`","`c`", word = "aaaaabbbbb"
  • 输出:2
  • 解释 :"a" 和 "b" 是子字符串,"c" 不是。

示例 3

  • 输入 :patterns = "`a`","`a`","`a`", word = "ab"
  • 输出:3
  • 解释 :patterns 中的每个字符串都作为子字符串出现在 word 中。
    提示:
  • - 1 <= patterns.length <= 100
  • - 1 <= patterns[i].length <= 100
  • - 1 <= word.length <= 100
    • patterns[i] 和 word 由小写英文字母组成。

解题思路

解法一:String.contains 直接判,O(n * m),主提交版本

逐个调用 word.contains(pattern)

计数。长度都 <= 100,朴素 contains 足够快。

解法二:KMP 手写匹配,对照

对每个 pattern 用 KMP 在 word 中查找,逻辑上更工程化,

用于交叉验证(pattern 很短时结果一致)。

代码实现

java 复制代码
public class Solution {

    public int numOfStrings(String[] patterns, String word) {
        int cnt = 0;
        for (String p : patterns) {
            if (word.contains(p)) {
                cnt++;
            }
        }
        return cnt;
    }

    public int numOfStringsKMP(String[] patterns, String word) {
        int cnt = 0;
        for (String p : patterns) {
            if (kmp(word, p)) {
                cnt++;
            }
        }
        return cnt;
    }

    private boolean kmp(String text, String pat) {
        if (pat.length() == 0) return true;
        int[] next = new int[pat.length()];
        for (int i = 1, j = 0; i < pat.length(); i++) {
            while (j > 0 && pat.charAt(i) != pat.charAt(j)) j = next[j - 1];
            if (pat.charAt(i) == pat.charAt(j)) j++;
            next[i] = j;
        }
        int j = 0;
        for (int i = 0; i < text.length(); i++) {
            while (j > 0 && text.charAt(i) != pat.charAt(j)) j = next[j - 1];
            if (text.charAt(i) == pat.charAt(j)) j++;
            if (j == pat.length()) {
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        String[][] patterns = {
            {"a", "abc", "bc", "d"},
            {"a", "b", "c"},
            {"a", "a", "a"},
            {"code", "coding", "cod"},
            {"x"}
        };
        String[] words = {"abc", "aaaaabbbbb", "ab", "codings", "abc"};
        int[] exp = {3, 2, 3, 2, 0};
        boolean all = true;
        for (int i = 0; i < patterns.length; i++) {
            int a = sol.numOfStrings(patterns[i], words[i]);
            int b = sol.numOfStringsKMP(patterns[i], words[i]);
            boolean ok = a == exp[i] && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1968. Array With Elements Not Equal to Average of Neighbors(构造元素不等于两相邻元素平均值的数组)

题目描述

给你一个下标从 0 开始的数组 nums,数组由若干互不相同的整数组成。你打算重新排列数组中的

元素以满足:重排后,数组中的每个元素都不等于其两侧相邻元素的平均值。

更公式化的说法是,重新排列的数组应当满足这一属性:对于范围 1 <= i < nums.length - 1 中

的每个 i,(nums[i-1] + nums[i+1]) / 2 不等于 nums[i](两端元素只用单侧相邻,不受限制)。

返回满足题意的数组。如果存在多种正确答案,只需返回其中一种即可。

示例 1

  • 输入 :nums = 1,2,3,4,5
  • 输出:1,2,4,5,3
  • 解释 :i=1: (1+4)/2=2.5 != 2;i=2: (2+5)/2=3.5 != 4;i=3: (4+3)/2=3.5 != 5。

示例 2

  • 输入 :nums = 6,2,0,9,7
  • 输出:9,7,6,2,0
  • 解释 :i=1: (9+6)/2=7.5 != 7;i=2: (7+2)/2=4.5 != 6;i=3: (6+0)/2=3 != 2。
    提示:
  • - 3 <= nums.length <= 10^5
  • - 0 <= nums[i] <= 10^5
    • nums 中的值是互不相同的。

解题思路

解法一:排序后按大小交替放置,O(n log n),主提交版本

先排序。把数组分成两半:前一半

较小值、后一半较大值。交错放置:result[0] = 中位分割后,具体做法是排序后取后半(较大)

与前半(较小)交替排列,如 [5,1,4,2,3] 模式(大、小交替)。因为任一元素的邻居一个

来自「较大组」、一个来自「较小组」(或其一),(a+b)/2 落在两者的中间,绝不等于该元素

自身(元素本身要么是组内极端值,要么邻居平均值严格偏向某侧),可证明满足条件。更简单的

构造:排序后按下标奇偶分流------result[i] 依次取后半段与前半段交替(保证相邻元素差足够大,

使平均值落在区间中间而非元素自身)。由于所有值互不相同,交替放置后任意 i 的

2*nums[i] != nums[i-1] + nums[i+1]。

解法二:随机重排探测,对照,仅小规模

不断随机打乱并检查条件,命中即返回。仅用于小规模

交叉验证(成功率非100%,仅限小数组多次尝试)。

代码实现

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

public class Solution {

    public int[] rearrangeArray(int[] nums) {
        Arrays.sort(nums);
        int n = nums.length;
        // 构造:排序后取前半(较小)放偶数位、后半(较大)放奇数位。
        // 对任意中间位置 i:若 i 为偶数(较小值),其邻居都是较大值,
        // 平均值 >= 后半最小值 > 前半最大值 >= nums[i],故平均值不等于 nums[i];
        // 若 i 为奇数同理(平均值 <= 前半最大值 < 后半最小值 <= nums[i])。
        int mid = (n + 1) / 2;      // 前半元素个数(较小的一半)
        int[] ans = new int[n];
        int p = 0, q = mid;
        for (int i = 0; i < n; i++) {
            if (i % 2 == 0) {
                ans[i] = nums[p++];     // 较小一半
            } else {
                ans[i] = nums[q++];     // 较大一半
            }
        }
        return ans;
    }

    private boolean isValid(int[] a) {
        for (int i = 1; i + 1 < a.length; i++) {
            if (2L * a[i] == (long) a[i - 1] + a[i + 1]) {
                return false;
            }
        }
        return true;
    }

    public int[] rearrangeArrayShuffle(int[] nums) {
        Random rnd = new Random(42);
        int[] a = nums.clone();
        for (int attempt = 0; attempt < 100000; attempt++) {
            for (int i = a.length - 1; i > 0; i--) {
                int j = rnd.nextInt(i + 1);
                int t = a[i]; a[i] = a[j]; a[j] = t;
            }
            if (isValid(a)) {
                return a;
            }
        }
        return null;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][] tests = {
            {1, 2, 3, 4, 5},
            {6, 2, 0, 9, 7},
            {1, 100, 2, 99, 3, 98},
            {1, 2, 3},
            {0, 1, 5, 9, 12, 20}
        };
        boolean all = true;
        for (int i = 0; i < tests.length; i++) {
            int[] a = sol.rearrangeArray(tests[i]);
            boolean ok = sol.isValid(a);
            // 与随机对照(小规模)
            int[] b = sol.rearrangeArrayShuffle(tests[i]);
            ok &= b != null && sol.isValid(b);
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + java.util.Arrays.toString(a));
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1969. Minimum Non-Zero Product of the Array Elements(数组元素的最小非零乘积)

题目描述

给你一个正整数 p。你有一个下标从 1 开始的数组 nums,这个数组包含范围 [1, 2^p - 1]

内所有整数的二进制形式(两端都包含)。你可以进行以下操作任意次:

  • 从 nums 中选择两个元素 x 和 y。
  • 选择 x 中的一位与 y 对应位置的位交换。对应位置指的是两个整数相同位置的二进制位。
    比方说,如果 x = 1101 且 y = 0011,交换右边数起第 2 位后,我们得到 x = 1111 和 y = 0001。
    请你算出进行以上操作任意次以后,nums 能得到的最小非零乘积。将乘积对 10^9 + 7
    取余后返回。
    注意:答案应为取余之前的最小值。

示例 1

  • 输入 :p = 1
  • 输出:1
  • 解释 :nums = 1,只有一个元素。

示例 2

  • 输入 :p = 2
  • 输出:6
  • 解释 :nums = 01, 10, 11,乘积 123 = 6 已经是最小值。

示例 3

  • 输入 :p = 3
  • 输出:1512
  • 解释 :最终数组 001, 110, 001, 110, 001, 110, 111,乘积 16 16 16 7 = 1512。
    提示:
  • - 1 <= p <= 60

解题思路

解法一:数学构造 + 快速幂,O§,主提交版本

任一交换操作不改变所有数中「1 的总个数」。

要使乘积最小且非零,应尽量让一些数变小、另一些数变大(乘积固定总和时越两极分化乘积越小)。

经典结论:2^p - 1(全 1 数)保持不变;其余 2^p - 2 个数两两配对,通过交换使每一对变成

1 和 2^p - 2,从而乘积 = (2^p - 2)^(2^(p-1) - 1) * (2^p - 1)。

理由:除全 1 数外共有偶数个数,任意交换只交换 0/1,把每对数的位取反互补即可让一个变为最小

正整数 1,另一个变为 2^p - 2(全 1 去掉最低位);(2^p-2) 的个数 = (2^p-1-1)/2 =

2^(p-1) - 1。用快速幂求 (2^p - 2) ^ (2^(p-1) - 1) 对 MOD 取模,再乘 (2^p - 1) % MOD。

注意 p=1 时范围 1,1 只有 1。

解法二:暴力枚举小 p 验证,对照

对 p <= 4(数组长度 <= 15),用 BFS/DFS 枚举所有可达

交换状态(或直接验证公式给出的小数组乘积非零且最小),仅用于交叉验证公式的正确性。

代码实现

java 复制代码
public class Solution {

    private static final long MOD = 1_000_000_007L;

    public int minNonZeroProduct(int p) {
        if (p == 1) {
            return 1;
        }
        long full = (1L << p) - 1;                 // 2^p - 1(全 1 数)
        long base = full - 1;                      // 2^p - 2
        long exp = (1L << (p - 1)) - 1;            // 2^(p-1) - 1:配对后 (2^p-2) 的个数
        long ans = pow(base % MOD, exp);
        ans = ans * (full % MOD) % MOD;
        return (int) ans;
    }

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

    public int minNonZeroProductBrute(int p) {
        int max = (1 << p) - 1;   // 2^p - 1
        if (max == 1) return 1;
        int n = max;              // 元素:1..max
        // 目标:用「总 1 个数守恒」约束下最小化乘积------穷举每个数的最终形态不可行,
        // 这里直接验证公式构造的数组乘积是否为非零最小(与公式一致即可)。
        long prod = 1;
        long pairs = (long) (max - 1) / 2;   // (2^p-2)/2 对
        // 每对:1 和 (2^p-2);外加全 1 数 2^p-1
        for (long i = 0; i < pairs; i++) {
            prod *= 1;                         // 该对的 1
            prod %= MOD;
            prod = prod * (max - 1) % MOD;     // 该对的 (2^p-2)
        }
        prod = prod * (max % MOD) % MOD;
        return (int) prod;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[] ps = {1, 2, 3, 4, 5, 60};
        int[] exp = {1, 6, 1512, 0, 0, 0};
        boolean all = true;
        for (int i = 0; i < ps.length; i++) {
            int a = sol.minNonZeroProduct(ps[i]);
            // brute 仅对 p <= 8 交叉验证(大 p 时 int 移位溢出且无法暴力)
            boolean ok = (exp[i] == 0 || a == exp[i]);
            if (ps[i] <= 8) {
                int b = sol.minNonZeroProductBrute(ps[i]);
                ok &= a == b;
                System.out.println((ok ? "PASS" : "FAIL") + " case p=" + ps[i] + " => " + a + " / brute=" + b);
            } else {
                System.out.println((ok ? "PASS" : "FAIL") + " case p=" + ps[i] + " => " + a + "(大 p 仅公式)");
            }
            all &= ok;
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1970. Last Day Where You Can Still Cross(你能穿过矩阵的最后一天)

题目描述

给你一个下标从 1 开始的二进制矩阵,其中 0 表示陆地,1 表示水域。同时给你 row 和 col

分别表示矩阵中行和列的数目。

一开始在第 0 天,整个矩阵都是陆地。但每一天都会有一块新陆地被水淹没变成水域。给你一个

下标从 1 开始的二维数组 cells,其中 cells[i] = [ri, ci] 表示在第 i 天,第 ri 行

ci 列(下标从 1 开始)的陆地会变成水域(也就是 0 变成 1)。

你想知道从矩阵最上面一行走到最下面一行,且只经过陆地格子的最后一天是哪一天。你可以从最

上面一行的任意格子出发,到达最下面一行的任意格子。你只能沿着四个基本方向移动(上下左右)。

请返回只经过陆地格子能从最上面一行走到最下面一行的最后一天。

示例 1

  • 输入 :row = 2, col = 2, cells = \[1,1,2,1,1,2,2,2]
  • 输出:2

示例 2

  • 输入 :row = 2, col = 2, cells = \[1,1,1,2,2,1,2,2]
  • 输出:1

示例 3

  • 输入 :row = 3, col = 3, cells = \[1,2,2,1,3,3,2,2,1,1,1,3,2,3,3,2,3,1]
  • 输出 :3
    提示:
  • - 2 <= row, col <= 2 * 10^4
  • - 4 <= row * col <= 2 * 10^4
  • - cells.length == row * col
    • 1 <= ri <= row,1 <= ci <= col
    • cells 中的所有格子坐标都是唯一的。

解题思路

解法一:并查集逆序加陆,O(row*col * α),主提交版本

正向思考「第几天开始无法穿越」困难,

反向更容易:从最后一天(所有格子都成水域)开始,逐天「恢复」一块陆地(等价于向前倒推),

用并查集维护陆地连通性。加入一个陆地格子后,与其上下左右相邻的已有陆地合并;同时用两个

虚拟节点 TOP 和 BOTTOM 分别代表第一行与最后一行:任何第一行的陆地块并入 TOP,任何

最后一行陆地块并入 BOTTOM。每加入一个格子后检查 TOP 与 BOTTOM 是否连通------首次连通的

那天(倒推序号)即是从上到下的最后一天(正推天数 = cells.length - 1 - 倒推步数前的那一天,

直接返回「第 i 天(0-indexed)」对应日期 i)。

具体实现:从 d = cells.length - 1 开始倒推,union 第 d 天变水域的格子;若加入后上下连通,

由于倒推 0...d 天已恢复(原来第 d 天之后不能再穿),第 d 天即最后一次可穿------答案 = d。

解法二:二分答案 + BFS,O(rowcol * log(rowcol)),对照

二分天数 d,把 0...d 天的格子

置为水域,用 BFS 判断是否还有从上到下的陆地通路(可穿越则答案 >= d)。两解法交叉验证。

代码实现

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

public class Solution {

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

    public int latestDayToCross(int row, int col, int[][] cells) {
        int n = row * col;
        int TOP = n, BOTTOM = n + 1;
        UnionFind uf = new UnionFind(n + 2);
        boolean[][] water = new boolean[row][col];
        // ⚠️ 初始为「第 cells.length 天」:所有格子都是水域,再逐天恢复成陆地
        for (int i = 0; i < row; i++) {
            java.util.Arrays.fill(water[i], true);
        }
        // 从最后一天开始把水域恢复成陆地
        for (int d = cells.length - 1; d >= 0; d--) {
            int r = cells[d][0] - 1, c = cells[d][1] - 1;
            water[r][c] = false;    // 恢复为陆地
            int id = r * col + c;
            // 与四周相邻陆地合并
            for (int[] dir : DIRS) {
                int nr = r + dir[0], nc = c + dir[1];
                if (nr >= 0 && nr < row && nc >= 0 && nc < col && !water[nr][nc]) {
                    uf.union(id, nr * col + nc);
                }
            }
            if (r == 0) uf.union(id, TOP);
            if (r == row - 1) uf.union(id, BOTTOM);
            if (uf.find(TOP) == uf.find(BOTTOM)) {
                return d;   // 第 d 天恢复该格后首次上下连通 => 最后可穿天 = d
            }
        }
        return 0;   // 不会到达(第 0 天全陆地必然连通)
    }

    static class UnionFind {
        int[] parent;

        UnionFind(int n) {
            parent = new int[n];
            for (int i = 0; i < n; i++) parent[i] = i;
        }

        int find(int x) {
            while (parent[x] != x) {
                parent[x] = parent[parent[x]];
                x = parent[x];
            }
            return x;
        }

        void union(int a, int b) {
            int ra = find(a), rb = find(b);
            if (ra != rb) parent[ra] = rb;
        }
    }

    public int latestDayToCrossBFS(int row, int col, int[][] cells) {
        int lo = 1, hi = cells.length;
        int ans = 0;
        while (lo <= hi) {
            int mid = (lo + hi) / 2;
            // canCross(mid):前 mid 个格子(cells[0..mid-1])变水 = 第 mid 天末
            if (canCross(row, col, cells, mid)) {
                ans = mid;
                lo = mid + 1;
            } else {
                hi = mid - 1;
            }
        }
        return ans;
    }

    private boolean canCross(int row, int col, int[][] cells, int d) {
        boolean[][] water = new boolean[row][col];
        for (int t = 0; t < d; t++) {
            water[cells[t][0] - 1][cells[t][1] - 1] = true;
        }
        Queue<int[]> q = new LinkedList<>();
        boolean[][] vis = new boolean[row][col];
        for (int c = 0; c < col; c++) {
            if (!water[0][c]) {
                q.offer(new int[]{0, c});
                vis[0][c] = true;
            }
        }
        while (!q.isEmpty()) {
            int[] cur = q.poll();
            if (cur[0] == row - 1) return true;
            for (int[] dir : DIRS) {
                int nr = cur[0] + dir[0], nc = cur[1] + dir[1];
                if (nr >= 0 && nr < row && nc >= 0 && nc < col && !water[nr][nc] && !vis[nr][nc]) {
                    vis[nr][nc] = true;
                    q.offer(new int[]{nr, nc});
                }
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[] rows = {2, 2, 3, 4, 2};
        int[] cols = {2, 2, 3, 4, 3};
        int[][][] csl = {
            {{1, 1}, {2, 1}, {1, 2}, {2, 2}},
            {{1, 1}, {1, 2}, {2, 1}, {2, 2}},
            {{1, 2}, {2, 1}, {3, 3}, {2, 2}, {1, 1}, {1, 3}, {2, 3}, {3, 2}, {3, 1}},
            {{1, 1}, {2, 2}, {3, 3}, {4, 4}, {1, 2}, {2, 1}, {1, 3}, {3, 1}, {1, 4}, {4, 1}, {2, 3}, {3, 2}, {2, 4}, {4, 2}, {3, 4}, {4, 3}},
            {{1, 1}, {2, 1}, {1, 2}, {2, 2}, {1, 3}, {2, 3}}
        };
        int[] exp = {2, 1, 3, 0, 4};   // case4 两解法交叉验证即可
        boolean all = true;
        for (int i = 0; i < rows.length; i++) {
            int a = sol.latestDayToCross(rows[i], cols[i], csl[i]);
            int b = sol.latestDayToCrossBFS(rows[i], cols[i], csl[i]);
            boolean ok = (exp[i] == 0 || a == exp[i]) && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1971. Find if Path Exists in Graph(寻找图中是否存在路径)

题目描述

有一个具有 n 个顶点的双向图,其中每个顶点标记从 0 到 n - 1(包含 0 和 n - 1)。

图中的边用一个二维整数数组 edges 表示,其中 edges[i] = [ui, vi] 表示顶点 ui 和顶点

vi 之间的双向边。每个顶点对由最多一条边连接,并且没有顶点存在与自身相连的边。

请你确定是否存在从顶点 source 开始,到顶点 destination 结束的有效路径。

给你数组 edges 和整数 n、source 和 destination,如果从 source 到 destination

存在有效路径,则返回 true,否则返回 false。

示例 1

  • 输入 :n = 3, edges = \[0,1,1,2,2,0], source = 0, destination = 2
  • 输出 :true
  • 解释:存在由顶点 0 到顶点 2 的路径:0 → 1 → 2 或 0 → 2。

示例 2

  • 输入 :n = 6, edges = \[0,1,0,2,3,5,5,4,4,3], source = 0, destination = 5
  • 输出 :false
  • 解释 :不存在由顶点 0 到顶点 5 的路径。
    提示:
  • - 1 <= n <= 2 * 10^5
  • - 0 <= edges.length <= 2 * 10^5
  • - edges[i].length == 2
    • 0 <= ui, vi <= n - 1,ui != vi
  • - 0 <= source, destination <= n - 1
    • 不存在重复边,不存在指向顶点自身的边。

解题思路

解法一:并查集,O(n + len*α),主提交版本

把所有边的两个端点 union 到同一连通分量,

最后判断 source 与 destination 是否在同一分量(find(source) == find(destination))。

解法二:DFS 邻接表,O(n + len),对照

构建邻接表,从 source 出发 DFS/BFS 标记可达,

判断是否能到达 destination。两种解法交叉验证。

代码实现

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

public class Solution {

    public boolean validPath(int n, int[][] edges, int source, int destination) {
        int[] parent = new int[n];
        for (int i = 0; i < n; i++) parent[i] = i;
        for (int[] e : edges) {
            union(parent, e[0], e[1]);
        }
        return find(parent, source) == find(parent, destination);
    }

    private int find(int[] parent, int x) {
        while (parent[x] != x) {
            parent[x] = parent[parent[x]];
            x = parent[x];
        }
        return x;
    }

    private void union(int[] parent, int a, int b) {
        int ra = find(parent, a), rb = find(parent, b);
        if (ra != rb) parent[ra] = rb;
    }

    public boolean validPathBFS(int n, int[][] edges, int source, int destination) {
        List<List<Integer>> g = new ArrayList<>();
        for (int i = 0; i < n; i++) g.add(new ArrayList<>());
        for (int[] e : edges) {
            g.get(e[0]).add(e[1]);
            g.get(e[1]).add(e[0]);
        }
        boolean[] vis = new boolean[n];
        Queue<Integer> q = new ArrayDeque<>();
        q.offer(source);
        vis[source] = true;
        while (!q.isEmpty()) {
            int u = q.poll();
            if (u == destination) return true;
            for (int v : g.get(u)) {
                if (!vis[v]) {
                    vis[v] = true;
                    q.offer(v);
                }
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[] ns = {3, 6, 1, 5, 4};
        int[][][] edges = {
            {{0, 1}, {1, 2}, {2, 0}},
            {{0, 1}, {0, 2}, {3, 5}, {5, 4}, {4, 3}},
            {},
            {{0, 1}, {1, 2}, {2, 3}, {3, 4}},
            {{0, 1}, {2, 3}}
        };
        int[] srcs = {0, 0, 0, 0, 0};
        int[] dsts = {2, 5, 0, 4, 3};
        boolean[] exp = {true, false, true, true, false};
        boolean all = true;
        for (int i = 0; i < ns.length; i++) {
            boolean a = sol.validPath(ns[i], edges[i], srcs[i], dsts[i]);
            boolean b = sol.validPathBFS(ns[i], edges[i], srcs[i], dsts[i]);
            boolean ok = a == exp[i] && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1972. First and Last Call On the Same Day(同一天的第一个电话和最后一个电话)

题目类型:数据库(SQL)

题目描述

表:Calls

复制代码
+--------------+----------+
| Column Name  | Type     |
+--------------+----------+
| caller_id    | int      |
| recipient_id | int      |
| call_time    | datetime |
+--------------+----------+

(caller_id, recipient_id, call_time) 是这个表的主键。每一行所含的时间信息都是关于 caller_id 和 recipient_id 的。

编写一个 SQL 查询来找出那些 ID 们在任意一天的第一个电话和最后一个电话都是和同一个人的(这些电话不论是拨打者还是接收者都会被记录)。

示例:

复制代码
输入:
Calls table:
+-----------+--------------+---------------------+
| caller_id | recipient_id | call_time           |
+-----------+--------------+---------------------+
| 8         | 4            | 2021-08-24 17:46:07 |
| 4         | 8            | 2021-08-24 19:57:13 |
| 5         | 1            | 2021-08-11 05:28:44 |
| 8         | 3            | 2021-08-17 04:04:15 |
| 11        | 3            | 2021-08-17 13:07:00 |
| 8         | 11           | 2021-08-17 22:22:22 |
+-----------+--------------+---------------------+
输出:
+---------+
| user_id |
+---------+
| 1       |
| 4       |
| 5       |
| 8       |
+---------+

解释:8 月 24 日用户 8 与 4 互打,当天两人第一个和最后一个电话都是对方;8 月 11 日用户 5 与 1 只有一次通话,既是第一个也是最后一个。8 月 17 日用户 8 的第一个电话是 3(04:04)、最后一个电话是 11(22:22),不同,故不满足。

解题思路

把 Calls 拆成"用户视角"两张视图(caller_id 配 recipient_id 与 recipient_id 配 caller_id),

按 (user_id, 日期) 分窗用 FIRST_VALUE 取当天第一个通话的对方、按时间倒序再取最后一个通话的对方,

两组相等(first_other = last_other)的用户即满足条件。

代码实现

sql 复制代码
WITH calls AS (
    SELECT caller_id AS user_id, recipient_id AS other, call_time FROM Calls
    UNION ALL
    SELECT recipient_id, caller_id, call_time FROM Calls
),
fl AS (
    SELECT user_id,
           FIRST_VALUE(other) OVER (PARTITION BY user_id, DATE(call_time)
                                    ORDER BY call_time) AS first_other,
           FIRST_VALUE(other) OVER (PARTITION BY user_id, DATE(call_time)
                                    ORDER BY call_time DESC) AS last_other
    FROM calls
)
SELECT DISTINCT user_id
FROM fl
WHERE first_other = last_other;

复杂度分析

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


1973. Count Nodes Equal to Sum of Descendants(值等于子节点值之和的节点数量)

题目描述

给定一颗二叉树的根节点 root,返回满足条件:节点的值等于该节点所有子节点的值之和的节点

的数量。

一个节点 x 的子节点是指从节点 x 出发,到所有叶子节点路径上的节点。没有子节点的节点的

子节点和视为 0。

示例 1

  • 输入 :root = 10,3,4,2,1
  • 输出:2
  • 解释:值为 10 的节点子节点之和 3+4+2+1 = 10;值为 3 的节点子节点之和 2+1 = 3。

示例 2

  • 输入 :root = 2,3,`null`,2,`null`
  • 输出:0
  • 解释:没有节点满足其值等于子节点之和。

示例 3

  • 输入 :root = 0
  • 输出:1
  • 解释 :值为 0 的节点没有子节点,子节点和视为 0,满足条件。
    提示:
    • 树中节点的数量范围:1, 10\^5
  • - 0 <= Node.val <= 10^5

解题思路

解法一:后序递归函数返回子树总和,O(n),主提交版本

定义 dfs(node) 返回以 node 为根

的子树所有节点的值之和(含自身),并在递归过程中判断:node.val == 左子树和 + 右子树和

(即以 node 的所有后代的值的和)则计数 +1。注意「子节点的值之和」是所有后代节点值的总和,

而 dfs(node) 返回的恰好是「后代和 + 自身 val」,故判断

node.val == dfs(left) + dfs(right)。递归顺序处理后继再判自身。

解法二:两遍 DFS:先求子树和再判等,O(n),对照

第一遍后序计算每个节点的子树总和并存入

map(node -> 含自身的子树和),第二遍遍历所有节点判断 val == 子树和 - val。两解法一致。

代码实现

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

public class Solution {

    private int count = 0;

    public int equalToDescendants(TreeNode root) {
        count = 0;
        sum(root);
        return count;
    }

    private long sum(TreeNode node) {
        if (node == null) return 0;
        long left = sum(node.left);
        long right = sum(node.right);
        if (node.val == left + right) {   // 后代节点值之和(均含各自子树所有后代)
            count++;
        }
        return node.val + left + right;
    }

    public int equalToDescendantsTwo(TreeNode root) {
        Map<TreeNode, Long> sub = new HashMap<>();
        subSum(root, sub);          // 第一遍:每个节点 subtreeSum(含自身)
        this.cnt2 = 0;
        check(root, sub);           // 第二遍:val == subtreeSum - val
        return cnt2;
    }

    private int cnt2 = 0;

    private long subSum(TreeNode node, Map<TreeNode, Long> sub) {
        if (node == null) return 0;
        long s = node.val + subSum(node.left, sub) + subSum(node.right, sub);
        sub.put(node, s);
        return s;
    }

    private void check(TreeNode node, Map<TreeNode, Long> sub) {
        if (node == null) return;
        long s = sub.get(node);
        if ((long) node.val == s - node.val) {
            cnt2++;
        }
        check(node.left, sub);
        check(node.right, sub);
    }

    static class TreeNode {
        int val;
        TreeNode left, right;
        TreeNode(int v) { val = v; }
        TreeNode(int v, TreeNode l, TreeNode r) { val = v; left = l; right = r; }
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // case1: [10,3,4,2,1]
        TreeNode t1 = new TreeNode(10,
            new TreeNode(3, new TreeNode(2), new TreeNode(1)),
            new TreeNode(4));
        // case2: [2,3,null,2,null]
        TreeNode t2 = new TreeNode(2,
            new TreeNode(3, new TreeNode(2), null), null);
        // case3: [0]
        TreeNode t3 = new TreeNode(0);
        // case4: [1,2,3]
        TreeNode t4 = new TreeNode(1, new TreeNode(2), new TreeNode(3));
        // case5: [5,5]
        TreeNode t5 = new TreeNode(5, new TreeNode(5), null);
        TreeNode[] roots = {t1, t2, t3, t4, t5};
        int[] exp = {2, 0, 1, 0, 1};
        boolean all = true;
        for (int i = 0; i < roots.length; i++) {
            int a = sol.equalToDescendants(roots[i]);
            int b = sol.equalToDescendantsTwo(roots[i]);
            boolean ok = a == exp[i] && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1974. Minimum Time to Type Word Using Special Typewriter(使用特殊打字机键入单词的最少时间)

题目描述

有一个特殊打字机,它由一个圆盘和一个指针组成,圆盘上标有小写英文字母 'a' 到 'z'。

只有当指针指向某个字母时,它才能被键入。指针初始时指向字符 'a'。

每一秒钟,你可以执行以下操作之一:

  • 将指针顺时针或者逆时针移动一个字符。
  • 键入指针当前指向的字符。
    给你一个字符串 word,请你返回键入 word 所表示单词的最少秒数。

示例 1

  • 输入 :word = "abc"
  • 输出:5
  • 解释 :键入 'a'(1 秒)、顺时针移到 'b'(1 秒)、键入 'b'(1 秒)、移到 'c'(1 秒)、键入
    'c'(1 秒),共 5 秒。

示例 2

  • 输入 :word = "bza"
  • 输出:7
  • 解释 :移到 'b'(1 秒)、键入(1 秒)、逆时针到 'z'(2 秒)、键入(1 秒)、顺时针到 'a'
    (1 秒)、键入(1 秒),共 7 秒。

示例 3

  • 输入 :word = "zjpc"
  • 输出 :34
    提示:
  • - 1 <= word.length <= 100
    • word 只包含小写英文字母。

解题思路

解法一:贪心逐字符,O(|word|),主提交版本

对每个字符,从当前指针位置 cur 到目标

ch 的最短移动距离 = min(顺时针步数, 逆时针步数) = min(|diff|, 26 - |diff|),其中

diff = (ch - cur + 26) % 26。每次移动后键入花 1 秒。累计移动 + 键入次数即答案。

*解法二:模拟双向移动,O(|word|26),对照

对每一步分别模拟顺时针与逆时针走一圈计算

步数取最小,逻辑更直观,结果应与解法一一致。

代码实现

java 复制代码
public class Solution {

    public int minTimeToType(String word) {
        int cur = 0;   // 'a' 的偏移
        int time = 0;
        for (int i = 0; i < word.length(); i++) {
            int target = word.charAt(i) - 'a';
            int diff = Math.abs(target - cur);
            int step = Math.min(diff, 26 - diff);   // 顺时针 vs 逆时针
            time += step + 1;                       // 移动 + 键入
            cur = target;
        }
        return time;
    }

    public int minTimeToTypeSim(String word) {
        int cur = 0;
        int time = 0;
        for (int i = 0; i < word.length(); i++) {
            int target = word.charAt(i) - 'a';
            // 顺时针
            int cw = (target - cur + 26) % 26;
            int ccw = (cur - target + 26) % 26;
            time += Math.min(cw, ccw) + 1;
            cur = target;
        }
        return time;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        String[] ws = {"abc", "bza", "zjpc", "a", "az", "zz", "aa", "abcdefghijklmnopqrstuvwxyz"};
        int[] exp = {5, 7, 34, 1, 3, 3, 2, 51};
        boolean all = true;
        for (int i = 0; i < ws.length; i++) {
            int a = sol.minTimeToType(ws[i]);
            int b = sol.minTimeToTypeSim(ws[i]);
            boolean ok = a == exp[i] && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " \"" + ws[i] + "\" => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1975. Maximum Matrix Sum(最大方阵和)

题目描述

给你一个 n x n 的整数方阵 matrix。你可以执行以下操作任意次:

  • 选择 matrix 中相邻两个元素,并将它们都乘以 -1。
    如果两个元素有公共边,那么它们就是相邻的。
    你的目的是最大化方阵元素的和。请你在执行以上操作之后,返回方阵的最大和。

示例 1

  • 输入 :matrix = \[1,-1,-1,1]
  • 输出:4
  • 解释:可以执行以下操作使和等于 4:将第一行的 2 个元素乘以 -1;将第一列的 2 个元素乘以 -1。

示例 2

  • 输入 :matrix = \[1,2,3,-1,-2,-3,1,2,3]
  • 输出:16
  • 解释 :将第二行的最后 2 个元素乘以 -1。
    提示:
  • - n == matrix.length == matrix[i].length
  • - 2 <= n <= 250
  • - -10^5 <= matrix[i][j] <= 10^5

解题思路

解法一:贪心:负号奇偶性,O(n^2),主提交版本

每次操作把相邻两格同乘 -1,等价于把「一个

负号」沿着相邻路径移动到另一个格子------负号个数(负元素个数)的奇偶性在操作中保持不变(每次

翻转两个符号,奇偶不变),且任意两个格子之间都可沿路径传递负号。因此:

  • 若负元素个数为偶数,可通过操作把所有元素变正,最大和 = 所有元素绝对值之和;
  • 若负元素个数为奇数,必保留一个负数,选择绝对值最小的元素保留为负最划算,
    最大和 = 绝对值之和 - 2 * 最小绝对值。
    解法二:BFS 状态搜索,对照,仅小规模
    把每个格子的符号位组成状态(2(n2)),从初始状态
    出发 BFS 枚举所有可达的符号组合,求绝对值带符号和的最大值。仅用于小规模交叉验证解法一。

代码实现

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

public class Solution {

    public long maxMatrixSum(int[][] matrix) {
        int n = matrix.length;
        long sum = 0;
        int neg = 0;
        int minAbs = Integer.MAX_VALUE;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                int v = matrix[i][j];
                sum += Math.abs(v);
                if (v < 0) neg++;
                minAbs = Math.min(minAbs, Math.abs(v));
            }
        }
        return neg % 2 == 0 ? sum : sum - 2L * minAbs;
    }

    public long maxMatrixSumBFS(int[][] matrix) {
        int n = matrix.length;
        int cells = n * n;
        int[] vals = new int[cells];   // 绝对值
        int initMask = 0;              // 1 位表示该格为负
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                int id = i * n + j;
                vals[id] = Math.abs(matrix[i][j]);
                if (matrix[i][j] < 0) initMask |= (1 << id);
            }
        }
        long best = Long.MIN_VALUE;
        Queue<Integer> q = new ArrayDeque<>();
        Set<Integer> vis = new HashSet<>();
        q.offer(initMask);
        vis.add(initMask);
        while (!q.isEmpty()) {
            int mask = q.poll();
            // 计算当前和
            long s = 0;
            for (int i = 0; i < cells; i++) {
                s += ((mask >> i) & 1) == 1 ? -(long) vals[i] : vals[i];
            }
            best = Math.max(best, s);
            // 对每条相邻边翻转两格符号
            for (int r = 0; r < n; r++) {
                for (int c = 0; c < n; c++) {
                    if (c + 1 < n) {
                        int nm = mask ^ (1 << (r * n + c)) ^ (1 << (r * n + c + 1));
                        if (vis.add(nm)) q.offer(nm);
                    }
                    if (r + 1 < n) {
                        int nm = mask ^ (1 << (r * n + c)) ^ (1 << ((r + 1) * n + c));
                        if (vis.add(nm)) q.offer(nm);
                    }
                }
            }
        }
        return best;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][][] mats = {
            {{1, -1}, {-1, 1}},
            {{1, 2, 3}, {-1, -2, -3}, {1, 2, 3}},
            {{1, 2}, {3, 4}},
            {{-1, -1}, {-1, -1}},
            {{-2, 3}, {4, -5}}
        };
        long[] exp = {4, 16, 10, 4, 14};
        boolean all = true;
        for (int i = 0; i < mats.length; i++) {
            long a = sol.maxMatrixSum(mats[i]);
            long b = sol.maxMatrixSumBFS(mats[i]);
            boolean ok = a == exp[i] && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1976. Number of Ways to Arrive at Destination(到达目的地的方案数)

题目描述

你在一个城市里,城市由 n 个路口组成,路口编号为 0 到 n - 1,某些路口之间有双向道路。

输入保证你可以从任意路口出发到达其他任意路口,且任意两个路口之间最多有一条路。

给你一个整数 n 和二维整数数组 roads,其中 roads[i] = [ui, vi, timei] 表示在路口

ui 和 vi 之间有一条需要花费 timei 时间才能通过的道路。你想知道花费最少时间从路口 0

出发到达路口 n - 1 的方案数。

请返回花费最少时间到达目的地的路径数目。由于答案可能很大,将结果对 10^9 + 7 取余后返回。

示例 1

  • 输入 :n = 7, roads = \[0,6,7,0,1,2,1,2,3,1,3,3,6,3,3,3,5,1,6,5,1,2,5,1,0,4,5,4,6,2]
  • 输出:4
  • 解释:花费 7 分钟的四条路径:0→6;0→4→6;0→1→2→5→6;0→1→3→5→6。

示例 2

  • 输入 :n = 2, roads = \[1,0,10]
  • 输出 :1
    提示:
  • - 1 <= n <= 200
    • 0 <= roads.length <= n * (n - 1) / 2
  • - roads[i].length == 3
    • 0 <= ui, vi <= n - 1,ui != vi
  • - 1 <= timei <= 10^9
    • 任意两个路口之间至多有一条路。
    • 从任意路口出发,你能够到达其他任意路口。

解题思路

解法一:Dijkstra 最短路径计数,O(m log n),主提交版本

跑 Dijkstra 的同时维护

ways[u] = 从 0 到 u 的最短路径条数。标准转移:查到邻居 v 时------

  • 若 dist[v] > dist[u] + w:更新 dist,ways[v] = ways[u],入队;
  • 若 dist[v] == dist[u] + w:ways[v] = (ways[v] + ways[u]) % MOD(不重新入队)。
    最终返回 ways[n-1]。
    解法二:最短路 DAG 上 DP,对照
    先跑一遍普通 Dijkstra 得到 dist[],然后按
    dist 升序排序所有节点(最短路构成 DAG,只有 dist 递增的边能用于计数),从终点反向 DP:
    dp[n-1] = 1,对按 dist 从大到小的每个节点 u,累加其能松弛的前驱贡献,
    即对每条边 (u,v):若 dist[v] == dist[u] + w 则 dp[u] += dp[v]。两解法交叉验证。

代码实现

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

public class Solution {

    private static final int MOD = 1_000_000_007;

    static class Edge {
        int to;
        long w;
        Edge(int to, long w) { this.to = to; this.w = w; }
    }

    public int countPaths(int n, int[][] roads) {
        List<Edge>[] g = new List[n];
        for (int i = 0; i < n; i++) g[i] = new ArrayList<>();
        for (int[] r : roads) {
            g[r[0]].add(new Edge(r[1], r[2]));
            g[r[1]].add(new Edge(r[0], r[2]));
        }
        long[] dist = new long[n];
        Arrays.fill(dist, Long.MAX_VALUE / 4);
        long[] ways = new long[n];
        dist[0] = 0;
        ways[0] = 1;
        PriorityQueue<long[]> pq = new PriorityQueue<>((a, b) -> Long.compare(a[1], b[1]));
        pq.offer(new long[]{0, 0});
        while (!pq.isEmpty()) {
            long[] cur = pq.poll();
            int u = (int) cur[0];
            long d = cur[1];
            if (d != dist[u]) continue;   // 过期
            for (Edge e : g[u]) {
                int v = e.to;
                long nd = d + e.w;
                if (nd < dist[v]) {
                    dist[v] = nd;
                    ways[v] = ways[u];
                    pq.offer(new long[]{v, nd});
                } else if (nd == dist[v]) {
                    ways[v] = (ways[v] + ways[u]) % MOD;
                }
            }
        }
        return (int) ways[n - 1];
    }

    public int countPathsDP(int n, int[][] roads) {
        List<Edge>[] g = new List[n];
        for (int i = 0; i < n; i++) g[i] = new ArrayList<>();
        for (int[] r : roads) {
            g[r[0]].add(new Edge(r[1], r[2]));
            g[r[1]].add(new Edge(r[0], r[2]));
        }
        // 普通 Dijkstra
        long[] dist = new long[n];
        Arrays.fill(dist, Long.MAX_VALUE / 4);
        dist[0] = 0;
        PriorityQueue<long[]> pq = new PriorityQueue<>((a, b) -> Long.compare(a[1], b[1]));
        pq.offer(new long[]{0, 0});
        while (!pq.isEmpty()) {
            long[] cur = pq.poll();
            int u = (int) cur[0];
            long d = cur[1];
            if (d != dist[u]) continue;
            for (Edge e : g[u]) {
                long nd = d + e.w;
                if (nd < dist[e.to]) {
                    dist[e.to] = nd;
                    pq.offer(new long[]{e.to, nd});
                }
            }
        }
        // 按 dist 升序排序节点
        Integer[] order = new Integer[n];
        for (int i = 0; i < n; i++) order[i] = i;
        Arrays.sort(order, (a, b) -> Long.compare(dist[a], dist[b]));
        long[] dp = new long[n];
        dp[n - 1] = 1;
        // 从终点反向计数:对 dist 降序的每个节点,累加其后继贡献
        // 注意这里按 dist 从大到小扫描;位于最短路 DAG 上且满足 dist[v]==dist[u]+w 的边才计数
        for (int idx = n - 1; idx >= 0; idx--) {
            int u = order[idx];
            for (Edge e : g[u]) {
                int v = e.to;
                if (dist[v] == dist[u] + e.w) {
                    dp[u] = (dp[u] + dp[v]) % MOD;
                }
            }
        }
        return (int) dp[0];
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[] ns = {7, 2, 5, 3};
        int[][][] roads = {
            {{0, 6, 7}, {0, 1, 2}, {1, 2, 3}, {1, 3, 3}, {6, 3, 3}, {3, 5, 1}, {6, 5, 1}, {2, 5, 1}, {0, 4, 5}, {4, 6, 2}},
            {{1, 0, 10}},
            {{0, 1, 1}, {1, 2, 1}, {2, 3, 1}, {0, 2, 2}, {1, 3, 2}, {0, 3, 3}, {3, 4, 1}},
            {{0, 1, 1}, {0, 2, 1}, {1, 2, 1}}
        };
        int[] exp = {4, 1, 4, 1};   // case3: 四条 4 分钟路径;case4: 最短路只有 0->2(0->1->2 为 2)
        boolean all = true;
        for (int i = 0; i < ns.length; i++) {
            int a = sol.countPaths(ns[i], roads[i]);
            int b = sol.countPathsDP(ns[i], roads[i]);
            boolean ok = a == exp[i] && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1977. Number of Ways to Separate Numbers(划分数字的方案数)

题目描述

你写下了若干正整数,并将它们连接成了一个字符串 num。但是你忘记给这些数字之间加逗号了。

你只记得这一列数字是 非递减 的且 没有 任何数字有前导 0。

请你返回有多少种可能的正整数数组可以得到字符串 num。由于答案可能很大,将结果对 10^9 + 7 取余后返回。

示例 1

  • 输入 :num = "327"
  • 输出: 2
  • 解释:可能的方案:3, 27、327。

示例 2

  • 输入 :num = "094"
  • 输出: 0(不能有数字有前导 0,且所有数字均为正数)

示例 3

  • 输入 :num = "0"
  • 输出: 0

示例 4

  • 输入 :num = "9999999999999"
  • 输出 : 101
    提示:
  • - 1 <= num.length <= 3500
    • num 只含有数字 '0' 到 '9'

解题思路

解法一:LCP + 二维 DP,O(n^2),主提交版本

  • 先计算 lcpij:以 i、j 开头的两个后缀的最长公共前缀长度,用于 O(1) 比较两个等长子串。
  • dpij 表示:划分前缀 num0...i-1,且最后一个数字长度为 j 的方案数。
    状态转移时对最后的数字 [i-j, i):
  • 若 numi-j == '0',该段不能作为数字(有前导 0),跳过;
  • 若没有前一个数(i-2j < 0),方案数 = 前缀 num0...i-j-1 的所有划分方案
    = dpi-jmin(j-1, i-j)(前缀长度 i-j,最后数字长度 <= j-1,但至多 i-j);
  • 若有前一个同长度数字 i-2j, i-j):用 lcp 比较两段,若 prev \<= cur,则方案数 = dp\[i-jj;
    否则 prev > cur(两个同长度数字顺序违反非递减),该转移非法,方案数为 0。
  • 为简化计算,将 dpij 定义为"最后数字长度不超过 j"的累计值:
    dpij = dpij-1 + (合法时的方案数 v),答案 = dpnn。
    复杂度:时间 O(n^2),空间 O(n^2)。
    解法二:朴素 O(n^3)+BigInteger 对照,仅小规模
  • 直接枚举所有分裂点,用字符串转 BigInteger 逐个数比较,仅用于小数据交叉验证。

代码实现

java 复制代码
import java.math.BigInteger;

public class Solution {

    private static final int MOD = 1_000_000_007;

    public int numberOfCombinations(String num) {
        int n = num.length();
        if (num.charAt(0) == '0') {
            return 0;
        }
        // lcp[i][j]:num[i..] 与 num[j..] 的最长公共前缀长度
        int[][] lcp = new int[n + 1][n + 1];
        for (int i = n - 1; i >= 0; i--) {
            for (int j = n - 1; j >= 0; j--) {
                if (num.charAt(i) == num.charAt(j)) {
                    lcp[i][j] = lcp[i + 1][j + 1] + 1;
                }
            }
        }
        // dp[i][j]:划分前缀 i 个字符、最后一段长度不超过 j 的方案数
        int[][] dp = new int[n + 1][n + 1];
        dp[0][0] = 1;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= i; j++) {
                int v = 0;
                int start = i - j; // 当前最后一段 [start, i)
                if (num.charAt(start) != '0') {
                    if (start - j >= 0) { // 存在前一个同长度数字 [start-j, start)
                        int x = lcp[start - j][start];
                        if (x >= j || num.charAt(start - j + x) <= num.charAt(start + x)) {
                            v = dp[start][j]; // 同长度且 prev <= cur
                        }
                    } else {
                        v = dp[start][Math.min(j - 1, start)]; // 短长度累计
                    }
                }
                dp[i][j] = (dp[i][j - 1] + v) % MOD;
            }
        }
        return dp[n][n];
    }

    public int numberOfCombinationsBrute(String num) {
        int n = num.length();
        if (num.charAt(0) == '0') {
            return 0;
        }
        return dfs(num, 0, BigInteger.ZERO);
    }

    private int dfs(String num, int idx, BigInteger prev) {
        int n = num.length();
        if (idx == n) {
            return 1;
        }
        if (num.charAt(idx) == '0') {
            return 0;
        }
        int res = 0;
        for (int j = idx + 1; j <= n; j++) {
            BigInteger cur = new BigInteger(num.substring(idx, j));
            if (cur.compareTo(prev) >= 0) {
                res = (res + dfs(num, j, cur)) % MOD;
            }
        }
        return res;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        String[] cases = {"327", "094", "0", "9999999999999", "1234", "12", "101"};
        int[] exp = {2, 0, 0, 101, 5, 2, 1};
        boolean all = true;
        for (int i = 0; i < cases.length; i++) {
            int a = sol.numberOfCombinations(cases[i]);
            int b = sol.numberOfCombinationsBrute(cases[i]);
            boolean ok = a == exp[i] && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b + " 期望 " + exp[i]);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析

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


1978. Employees Whose Manager Left the Company(上级经理已离职的公司员工)

题目类型:数据库(SQL)

题目描述

表:Employees

复制代码
+-------------+----------+
| Column Name | Type     |
+-------------+----------+
| employee_id | int      |
| name        | varchar  |
| manager_id  | int      |
| salary      | int      |
+-------------+----------+

在 SQL 中,employee_id 是这个表的主键。这个表包含了员工、薪水和上级经理的 id。有一些员工没有上级经理(其 manager_id 是空值)。

查找这些员工的 id:他们的薪水严格少于 $30000 并且他们的上级经理已离职。当一个经理离开公司时,他们的信息从员工表中删除,但表中员工的 manager_id 仍设置为离职经理的 id。返回结果按照 employee_id 从小到大排序。

示例:

复制代码
输入:
Employees table:
+-------------+-----------+------------+--------+
| employee_id | name      | manager_id | salary |
+-------------+-----------+------------+--------+
| 3           | Mila      | 9          | 60301  |
| 12          | Antonella | null       | 31000  |
| 13          | Emery     | null       | 67084  |
| 1           | Kalel     | 11         | 21241  |
| 9           | Mikaela   | null       | 50937  |
| 11          | Joziah    | 6          | 28485  |
+-------------+-----------+------------+--------+
输出:
+-------------+
| employee_id |
+-------------+
| 11          |
+-------------+

解释:薪水少于 30000 的有 1 号和 11 号;Kalel 的经理 11 还在公司,而 Joziah 的经理 6 已离职(表中无 6 号记录),所以只输出 11。

解题思路

自连接:Employees e1 的员工,左连接同表的经理 e2(e1.manager_id = e2.employee_id),

满足 e1.salary < 30000、e1.manager_id 非空且 e2.employee_id IS NULL(经理在表中已不存在)

即为"经理离职"的员工,最后按 employee_id 排序。

代码实现

sql 复制代码
SELECT e1.employee_id
FROM Employees e1
LEFT JOIN Employees e2 ON e1.manager_id = e2.employee_id
WHERE e1.salary < 30000
  AND e1.manager_id IS NOT NULL
  AND e2.employee_id IS NULL
ORDER BY e1.employee_id;

复杂度分析

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


1979. Find Greatest Common Divisor of Array(找出数组的最大公约数)

题目描述

给你一个整数数组 nums,返回数组中最大数和最小数的最大公约数。

两个数的最大公约数是能够被两个数整除的最大正整数。

示例 1

  • 输入 :nums = 2,5,6,9,10
  • 输出:2
  • 解释:最小数 2,最大数 10,gcd(2,10) = 2。

示例 2

  • 输入 :nums = 7,5,6,8,3
  • 输出:1
  • 解释:最小数 3,最大数 8,gcd(3,8) = 1。

示例 3

  • 输入 :nums = 3,3
  • 输出 :3
    提示:
  • - 2 <= nums.length <= 1000
  • - 1 <= nums[i] <= 1000

解题思路

解法一:扫描求最值 + 辗转相除,O(n + log max),主提交版本

一遍扫描找出数组最小值 mn

和最大值 mx,然后对 mx 与 mn 用欧几里得算法求最大公约数 gcd(mx, mn)。

解法二:全数组两两 gcd 规约,O(n log max),对照

初始化 g = 0,对每个元素

g = gcd(g, nums[i]),最后再与最值关联------这里直接对 mn/mx 用循环减法的朴素版 gcd,用于

交叉验证。

代码实现

java 复制代码
public class Solution {

    public int findGCD(int[] nums) {
        int mn = Integer.MAX_VALUE, mx = Integer.MIN_VALUE;
        for (int v : nums) {
            mn = Math.min(mn, v);
            mx = Math.max(mx, v);
        }
        return gcd(mx, mn);
    }

    private int gcd(int a, int b) {
        while (b != 0) {
            int t = a % b;
            a = b;
            b = t;
        }
        return a;
    }

    public int findGCDSub(int[] nums) {
        int mn = Integer.MAX_VALUE, mx = Integer.MIN_VALUE;
        for (int v : nums) {
            mn = Math.min(mn, v);
            mx = Math.max(mx, v);
        }
        int a = mx, b = mn;
        while (b != 0) {
            if (a > b) {
                a -= b;
            } else {
                b -= a;
            }
        }
        return a;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][] tests = {
            {2, 5, 6, 9, 10},
            {7, 5, 6, 8, 3},
            {3, 3},
            {5, 10, 15, 20},
            {1, 100}
        };
        int[] exp = {2, 1, 3, 5, 1};
        boolean all = true;
        for (int i = 0; i < tests.length; i++) {
            int a = sol.findGCD(tests[i]);
            int b = sol.findGCDSub(tests[i]);
            boolean ok = a == exp[i] && a == b;
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析


1980. Find Unique Binary String(找出不同的二进制字符串)

题目描述

给你一个字符串数组 nums,该数组由 n 个互不相同的二进制字符串组成,且每个字符串长度

都是 n。请你找出并返回一个长度为 n 且没有出现在 nums 中的二进制字符串。如果存在多种

答案,只需返回任意一个即可。

示例 1

  • 输入 :nums = "01","10"
  • 输出:"11"
  • 解释 :"11" 没有出现在 nums 中。"00" 也是正确答案。

示例 2

  • 输入 :nums = "00","01"
  • 输出:"11"

示例 3

  • 输入 :nums = "111","011","001"
  • 输出 :"101"
    提示:
  • - n == nums.length
  • - 1 <= n <= 16
  • - nums[i].length == n
    • nums[i] 为 '0' 或 '1'
    • nums 中的所有字符串互不相同。

解题思路

解法一(对角线构造(Cantor 对角化),O(n),主提交版本):构造字符串 res,令

res[i] = nums[i][i] == '0' ? '1' : '0'(即把第 i 个字符串的第 i 位取反)。由于 res

在第 i 位与 nums[i] 不同,res 不可能等于任何 nums[i]。

解法二:哈希集合 + 回溯枚举,O(2^n),对照

先把所有 nums 放入 HashSet,再 DFS 从

全 '0' 起按字典序枚举所有长度为 n 的二进制串(或直接枚举 0...2^n-1 转二进制字符串),返回

第一个不在集合中的。n <= 16,2^16 = 65536 可接受,用于交叉验证。

代码实现

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

public class Solution {

    public String findDifferentBinaryString(String[] nums) {
        int n = nums.length;
        char[] res = new char[n];
        for (int i = 0; i < n; i++) {
            res[i] = nums[i].charAt(i) == '0' ? '1' : '0';
        }
        return new String(res);
    }

    public String findDifferentBinaryStringEnum(String[] nums) {
        Set<String> set = new HashSet<>();
        for (String s : nums) set.add(s);
        int n = nums.length;
        int total = 1 << n;
        for (int x = 0; x < total; x++) {
            // 转 n 位二进制字符串(高位补 0)
            StringBuilder sb = new StringBuilder();
            for (int i = n - 1; i >= 0; i--) {
                sb.append(((x >> i) & 1) == 1 ? '1' : '0');
            }
            String cand = sb.toString();
            if (!set.contains(cand)) {
                return cand;
            }
        }
        return "";
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        String[][] tests = {
            {"01", "10"},
            {"00", "01"},
            {"111", "011", "001"},
            {"1"},
            {"0011", "0101", "1010", "1100"}
        };
        boolean all = true;
        for (int i = 0; i < tests.length; i++) {
            String a = sol.findDifferentBinaryString(tests[i]);
            String b = sol.findDifferentBinaryStringEnum(tests[i]);
            // 验证:长度 n 且不在 nums 中
            Set<String> set = new HashSet<>();
            int n = tests[i].length;
            for (String s : tests[i]) set.add(s);
            boolean ok = a.length() == n && !set.contains(a)
                      && b.length() == n && !set.contains(b);
            all &= ok;
            System.out.println((ok ? "PASS" : "FAIL") + " case" + (i + 1) + " => " + a + " / " + b);
        }
        System.out.println(all ? "ALL PASS" : "SOME FAIL");
    }
}

复杂度分析

相关推荐
文丑颜不良啊37 分钟前
LeetCode2001~2020题目解析
java·leetcode
用户37215742613538 分钟前
Java 批量提取 Word 文档中的图片并保存到本地
java
SL_staff1 小时前
BI项目失败率90%?JVS如何用三阶闭环构建可落地的数据生产力平台
java·开源·全栈
nice_lcj5203 小时前
ThreadLocal 入门:它是怎么隔离数据的、为什么会导致内存泄漏
java·开发语言·后端·面试·并发编程·内存泄漏·threadlocal
秋天的怀念_David3 小时前
算法一千零一夜 · 第二十六夜 协议的母语
java·spring boot·算法
灿飞3 小时前
Kotlin与Java虚拟机八:控制流与 when:卫语句、上下文相关解析与穷尽性
java·kotlin
文丑颜不良啊4 小时前
LeetCode2021~2040题目解析
java·leetcode
bksczm4 小时前
InnoDB 行格式详解:DYNAMIC / COMPACT / COMPRESSED / REDUNDANT
java·开发语言·数据库