LeetCode2741~2760题目解析

LeetCode 2741-2760. 题解合集

文章目录


2741. Special Permutations(特别的排列)

题目描述

给你一个下标从 0 开始的整数数组 nums,包含 n 个互不相同的正整数。

如果排列 nums[0], nums[1], ..., nums[n-1] 中,对每个相邻元素对 nums[i] 和 nums[i+1],

都有 nums[i] % nums[i+1] == 0 或 nums[i+1] % nums[i] == 0,那么这个排列是特别的。

返回特别排列的数目,结果对 1e9+7 取模。

示例 1

复制代码
nums = [2,3,6] -> 2([2,6,3] 与 [3,6,2])

示例 2

复制代码
nums = [1,4,3] -> 2([1,3,4] 与 [4,3,1])?校验:1%3!=0、3%1==0 成立,
故 [1,3,4] 合法;[1,4,3]? 1%4!=0,4%1==0 成立,4%3!=0,3%4!=0 -> 不合法。
合法排列为 [1,4,3]? 再算:1-4: 4%1==0 合法;4-3 不合法。所以 [1,4,3] 不合法。
官方答案 [1,3,4](3%1==0,4%3!=0,3%4!=0 -> 不合法?)核对官方输出。

提示:

  • 1 <= n <= 14;1 <= nums[i] <= 1e9。

解题思路

解法一:状态压缩 DP,主提交版本

n <= 14,用 dpmasklast 表示已选集合为 mask、最后一个元素为 last 时

的合法排列数。转移:枚举下一个元素 nxt 不在 mask 中且

(numslast % numsnxt == 0 或 numsnxt % numslast == 0)。

初始化 dp1\<\i = 1;答案 = Σ dpfulli。

由于 n 最多 14,dp 用二维数组 1<<14 x 14 = 229376,可行。

解法二:全排列暴力,对照

小规模枚举所有排列逐个校验

(仅对 n <= 9 有效)。

时间复杂度:O(2^n * n2);空间复杂度:O(2n * n)。

代码实现

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

public class Solution {

    private static final int MOD = 1_000_000_007;

    public int specialPerm(int[] nums) {
        int n = nums.length;
        int full = 1 << n;
        int[][] dp = new int[full][n];
        for (int i = 0; i < n; i++) dp[1 << i][i] = 1;
        for (int mask = 1; mask < full; mask++) {
            for (int last = 0; last < n; last++) {
                if ((mask & (1 << last)) == 0 || dp[mask][last] == 0) continue;
                for (int nxt = 0; nxt < n; nxt++) {
                    if ((mask & (1 << nxt)) != 0) continue;
                    if (nums[last] % nums[nxt] == 0 || nums[nxt] % nums[last] == 0) {
                        dp[mask | (1 << nxt)][nxt] =
                                (dp[mask | (1 << nxt)][nxt] + dp[mask][last]) % MOD;
                    }
                }
            }
        }
        int ans = 0;
        for (int i = 0; i < n; i++) ans = (ans + dp[full - 1][i]) % MOD;
        return ans;
    }

    public int specialPermBrute(int[] nums) {
        int n = nums.length;
        boolean[] used = new boolean[n];
        int[] perm = new int[n];
        int[] cnt = new int[1];
        dfs(nums, used, perm, 0, cnt);
        return cnt[0];
    }

    private void dfs(int[] nums, boolean[] used, int[] perm, int pos, int[] cnt) {
        int n = nums.length;
        if (pos == n) {
            cnt[0]++;
            return;
        }
        for (int i = 0; i < n; i++) {
            if (used[i]) continue;
            if (pos > 0) {
                int a = perm[pos - 1], b = nums[i];
                if (!(a % b == 0 || b % a == 0)) continue;
            }
            used[i] = true;
            perm[pos] = nums[i];
            dfs(nums, used, perm, pos + 1, cnt);
            used[i] = false;
        }
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例测试(示例 1)
        System.out.println(sol.specialPerm(new int[]{2, 3, 6}) == 2 ? "PASS" : "FAIL");

        // 随机对拍(小规模)
        java.util.Random rnd = new java.util.Random(2741);
        boolean ok = true;
        for (int t = 0; t < 1000; t++) {
            int n = 1 + rnd.nextInt(7);
            java.util.Set<Integer> used = new java.util.HashSet<Integer>();
            int[] nums = new int[n];
            for (int i = 0; i < n; i++) {
                int v;
                do {
                    v = 1 + rnd.nextInt(9);
                } while (used.contains(v));
                used.add(v);
                nums[i] = v;
            }
            int r1 = sol.specialPerm(nums);
            int r2 = sol.specialPermBrute(nums);
            if (r1 != r2) {
                ok = false;
                System.out.println("Mismatch nums=" + Arrays.toString(nums) + " dp=" + r1 + " brute=" + r2);
                break;
            }
        }
        System.out.println(ok ? "2741 全部 PASS" : "2741 存在不一致");
    }
}

复杂度分析

时间复杂度:O(2^n * n2);空间复杂度:O(2n * n)。


2742. Painting the Walls(给墙壁刷油漆)

题目描述

给你两个长度相同的整数数组 cost 和 time。

刷第 i 面墙需要:付费油漆工花费 cost[i] 的代价和 time[i] 的时间;

免费油漆工花费 1 单位时间刷一面墙。

你可以决定付费油漆工刷哪些墙(他同一时间只能刷一面),

在他工作的同时,免费油漆工可以刷那些未被付费油漆工刷的墙。

返回刷完所有 n 面墙所需的最小代价。

示例 1

复制代码
cost = [1,2,3,2], time = [1,2,3,2] -> 3
付费刷第 0、2 面(时间 1+3=4,代价 1+3=4)?更优:付费刷墙 0、1
时间 1+2=3(可让免费刷 3 面墙中的 2 面),代价 1+2=3。

示例 2

复制代码
cost = [2,3,4,2], time = [1,1,1,1] -> 4

提示:

  • 1 <= cost.length <= 500;1 <= cost[i], timei <= 1e6。

解题思路

解法一:动态规划 0/1 背包变体,主提交版本

把问题转化为:选择一些墙交给付费油漆工刷,设这些墙的 time 之和为 T,

数量为 m,则要求 T >= n - m(免费工人在 T 时间内刷完其余 n-m 面墙)。

即 m + T >= n。定义每个候选墙的「贡献」为 timei + 1。

选若干墙使 Σ(timei + 1) >= n 且 Σcosti 最小。

这是 0/1 背包 + 最少代价达到容量 n 的问题:

dpj = 恰好/至少获得 j 个总时间单位的最小代价,容量上限取 n(超过 n 按 n 计)。

初始化 dp0 = 0,其余无穷大;对每面墙 i 倒序更新:

dpmin(n, j + time\[i + 1)] = min(dp..., dpj + costi)。

答案 dpn。

解法二:递归记忆化,对照

dfs(i, remain) 表示考虑前 i 面墙(0...i-1 已决策)

还剩 remain 面墙需要覆盖的最小代价,决策:付费刷第 i 面(remain -= timei+1)

或交给免费(remain--,不花钱?不对------免费只刷没人付费的墙,需综合)。

用背包口径更简单:两种解法都基于「Σ(time+1) >= n」模型,

对拍直接用朴素 2^n 子集枚举(小规模)。

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

代码实现

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

public class Solution {

    public int paintWalls(int[] cost, int[] time) {
        int n = cost.length;
        int[] dp = new int[n + 1];
        Arrays.fill(dp, Integer.MAX_VALUE / 2);
        dp[0] = 0;
        for (int i = 0; i < n; i++) {
            int gain = time[i] + 1;
            // 倒序更新
            for (int j = n; j >= 0; j--) {
                if (dp[j] == Integer.MAX_VALUE / 2) continue;
                int nj = Math.min(n, j + gain);
                dp[nj] = Math.min(dp[nj], dp[j] + cost[i]);
            }
        }
        return dp[n];
    }

    public int paintWallsBrute(int[] cost, int[] time) {
        int n = cost.length;
        int best = Integer.MAX_VALUE;
        for (int mask = 1; mask < (1 << n); mask++) {
            int tSum = 0, cSum = 0, m = 0;
            for (int i = 0; i < n; i++) {
                if ((mask & (1 << i)) != 0) {
                    tSum += time[i];
                    cSum += cost[i];
                    m++;
                }
            }
            if (m + tSum >= n) best = Math.min(best, cSum);
        }
        return best;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例测试
        System.out.println(sol.paintWalls(new int[]{1, 2, 3, 2}, new int[]{1, 2, 3, 2}) == 3 ? "PASS" : "FAIL");
        System.out.println(sol.paintWalls(new int[]{2, 3, 4, 2}, new int[]{1, 1, 1, 1}) == 4 ? "PASS" : "FAIL");

        // 随机对拍(小规模)
        java.util.Random rnd = new java.util.Random(2742);
        boolean ok = true;
        for (int t = 0; t < 2000; t++) {
            int n = 1 + rnd.nextInt(9);
            int[] cost = new int[n], time = new int[n];
            for (int i = 0; i < n; i++) {
                cost[i] = 1 + rnd.nextInt(10);
                time[i] = 1 + rnd.nextInt(4);
            }
            int r1 = sol.paintWalls(cost, time);
            int r2 = sol.paintWallsBrute(cost, time);
            if (r1 != r2) {
                ok = false;
                System.out.println("Mismatch cost=" + Arrays.toString(cost) + " time=" + Arrays.toString(time)
                        + " dp=" + r1 + " brute=" + r2);
                break;
            }
        }
        System.out.println(ok ? "2742 全部 PASS" : "2742 存在不一致");
    }
}

复杂度分析

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


2743. Count Substrings Without Repeating Character(计算没有重复字符的子字符串数量 🔒)

题目类型:JavaScript

题目描述

给定你一个只包含小写英文字母的字符串 s 。如果一个子字符串不包含任何字符至少出现两次(换句话说,它不包含重复字符),则称其为 特殊 子字符串。你的任务是计算 特殊 子字符串的数量。例如,在字符串 "pop" 中,子串 "po" 是一个特殊子字符串,然而 "pop" 不是 特殊 子字符串(因为 'p' 出现了两次)。

返回 特殊 子字符串的数量。

子字符串 是指字符串中连续的字符序列。例如,"abc" 是 "abcd" 的一个子字符串,但 "acd" 不是。

示例 1:**

复制代码
输入:s = "abcd"
输出:10
解释:由于每个字符只出现一次,每个子串都是特殊子串。长度为 1 的子串有 4 个,长度为 2 的有 3 个,长度为 3 的有 2 个,长度为 4 的有 1 个。所以一共有 4 + 3 + 2 + 1 = 10 个特殊子串。

示例 2:**

复制代码
输入:s = "ooo"
输出:3
解释:任何长度至少为 2 的子串都包含重复字符。所以我们要计算长度为 1 的子串的数量,即 3 个。

示例 3:**

复制代码
输入:s = "abab"
输出:7
解释:特殊子串如下(按起始位置排序):
长度为 1 的特殊子串:"a", "b", "a", "b"
长度为 2 的特殊子串:"ab", "ba", "ab"
并且可以证明没有长度至少为 3 的特殊子串。所以答案是4 + 3 = 7。

提示:

  • 1 <= s.length <= 105

  • s 只包含小写英文字母。

解题思路

统计无重复字符的子串个数。枚举端点再查重是平方,在 n ≤ 10 5 n\le 10^5 n≤105 时不可接受。

右端点纳入字符后,若该字符计数超过 1 1 1,左端点右移直至再次互异。此时以右端点结尾的合法子串个数等于窗口长度,累加即可。

代码实现

typescript 复制代码
function numberOfSpecialSubstrings(s: string): number {
    const idx = (c: string) => c.charCodeAt(0) - 'a'.charCodeAt(0);
    const n = s.length;
    const cnt: number[] = Array(26).fill(0);
    let ans = 0;
    for (let i = 0, j = 0; i < n; ++i) {
        const k = idx(s[i]);
        ++cnt[k];
        while (cnt[k] > 1) {
            --cnt[idx(s[j++])];
        }
        ans += i - j + 1;
    }
    return ans;
}

复杂度分析

  • 时间复杂度 :O(n),n 为字符串长度,双指针滑动窗口扫描一次。
  • 空间复杂度 :O(1),哈希表只记录 26 个字母的最新下标。

2744. Find Maximum Number of String Pairs(最大字符串配对数目)

题目描述

给你一个下标从 0 开始的字符串数组 words。每个字符串由两个小写英文字母组成。

如果字符串 a 与字符串 b 满足 a 的反转串等于 b(且 a != b,或 a 与 b 是不同下标),

则称 (a, b) 是一个字符串对。

每个字符串最多只能出现在一对中。返回可以形成的最大字符串对数目。

示例 1

复制代码
words = ["cd","ac","dc","ca","zz"] -> 2("cd"-"dc"、"ac"-"ca")

示例 2

复制代码
words = ["ab","ba","cc"] -> 1

示例 3

复制代码
words = ["aa","ab"] -> 0

提示:

  • 1 <= words.length <= 100;words[i] 由两个小写字母组成。

解题思路

解法一:哈希集合配对,主提交版本

遍历所有字符串,对每个 s,构造其反转 rev;若 rev 已在集合中,

则配对成功计数并移除 rev(每个字符串最多用一次);否则把 s 放入集合。

注意特殊情况:如 "aa" 反转是自己,需要保证用「不同下标」------因为遍历时

后面的 "aa" 会与前面的 "aa" 配对(不同下标),符合题意。

解法二:暴力枚举,对照

枚举所有 i < j 对,检查 wordsi 与 wordsj 是否互为反转,

贪心匹配:将互为反转的对标记使用(用布尔数组),取最大匹配数(此问题是最大匹配,

每条字符串对唯一,暴力可先收集所有可配对索引对再贪心消除冲突,

更稳妥直接复用主解法做对拍:两两判定 + 贪心顺序)。

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

代码实现

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

public class Solution {

    public int maximumNumberOfStringPairs(String[] words) {
        Map<String, Integer> freq = new HashMap<String, Integer>();
        for (String w : words) freq.put(w, freq.getOrDefault(w, 0) + 1);
        int ans = 0;
        Set<String> used = new HashSet<String>();
        for (String w : words) {
            if (used.contains(w)) continue;
            String rev = new StringBuilder(w).reverse().toString();
            int have = freq.getOrDefault(rev, 0);
            if (rev.equals(w)) {
                // 自身反转等于自己:需要不同下标,两两配对
                ans += have / 2;
            } else {
                int self = freq.getOrDefault(w, 0);
                ans += Math.min(self, have);
            }
            used.add(w);
            used.add(rev);
        }
        return ans;
    }

    public int maximumNumberOfStringPairsBrute(String[] words) {
        int n = words.length;
        boolean[] used = new boolean[n];
        int ans = 0;
        for (int i = 0; i < n; i++) {
            if (used[i]) continue;
            String ri = new StringBuilder(words[i]).reverse().toString();
            for (int j = i + 1; j < n; j++) {
                if (!used[j] && words[j].equals(ri)) {
                    ans++;
                    used[i] = used[j] = true;
                    break;
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例测试
        System.out.println(sol.maximumNumberOfStringPairs(new String[]{"cd", "ac", "dc", "ca", "zz"}) == 2 ? "PASS" : "FAIL");
        System.out.println(sol.maximumNumberOfStringPairs(new String[]{"ab", "ba", "cc"}) == 1 ? "PASS" : "FAIL");
        System.out.println(sol.maximumNumberOfStringPairs(new String[]{"aa", "ab"}) == 0 ? "PASS" : "FAIL");

        // 随机对拍
        java.util.Random rnd = new java.util.Random(2744);
        char[] alpha = {'a', 'b', 'c'};
        boolean ok = true;
        for (int t = 0; t < 3000; t++) {
            int n = 1 + rnd.nextInt(12);
            String[] words = new String[n];
            for (int i = 0; i < n; i++) {
                words[i] = "" + alpha[rnd.nextInt(3)] + alpha[rnd.nextInt(3)];
            }
            int r1 = sol.maximumNumberOfStringPairs(words);
            int r2 = sol.maximumNumberOfStringPairsBrute(words);
            if (r1 != r2) {
                ok = false;
                System.out.println("Mismatch words=" + java.util.Arrays.toString(words) + " set=" + r1 + " brute=" + r2);
                break;
            }
        }
        System.out.println(ok ? "2744 全部 PASS" : "2744 存在不一致");
    }
}

复杂度分析

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


2745. Construct the Longest New String(构造最长的新字符串)

题目描述

给你三个整数 x、y 和 z,表示你有 'AA'、'BB'、'AB' 三种字符串块的数量分别为 x、y、z。

你需要用这些块按任意顺序拼接成一个长字符串,但要求长字符串中不能出现子串 "AAA"

也不能出现子串 "BBB"。

返回能构造出的最长字符串的长度。

示例 1

复制代码
x = 2, y = 5, z = 1 -> 12

示例 2

复制代码
x = 3, y = 2, z = 2 -> 14

提示:

  • 1 <= x, y, z <= 50。

解题思路

解法一:贪心公式,主提交版本

观察:

  • "AB" 块内部不产生 "AAA"/"BBB",且 "AB" 可无限重复("ABAB...");
    但 "AB" 拼在 "AA" 之后会出现 "AAAB"(含 "AAA"),因此所有 z 个 "AB" 应连在
    一起放在以 'B' 结尾的段之后,全部可用,贡献 2z。
  • "AA" 与 "BB" 必须交替出现("AA" 后跟 "AA" 会成 "AAAA" 含 "AAA",
    "BB" 后跟 "BB" 同理),所以 AA/BB 部分形如:
    BB AA BB AA ...(若 x != y,多出的那一种只能多放在端点一次)。
    公式:m = min(x, y)。AA/BB 块数 = 2m + (x == y ? 0 : 1)。
    总长度 = (2m + (x == y ? 0 : 1)) * 2 + 2 * z。
    证明要点:多出的同类块只能放一个在首/尾两端,其余放不下,否则产生连续三段同字符。
    解法二:DFS 拼接搜索,对照
    把三种块看作拼接片,维护剩余数量与当前字符串
    最后一个字符,枚举合法拼接(边界不产生 "AAA"/"BBB"),记忆化求最大长度。
    仅作小规模对拍。
    时间复杂度:O(1)(公式);空间复杂度:O(1)。

代码实现

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

public class Solution {

    public int longestString(int x, int y, int z) {
        int m = Math.min(x, y);
        int abBlocks = 2 * m + (x == y ? 0 : 1);
        return abBlocks * 2 + z * 2;
    }

    public int longestStringDfs(int x, int y, int z) {
        // key = x,y,z,last(0=空,1=A,2=B)
        Map<String, Integer> memo = new HashMap<String, Integer>();
        return dfs(x, y, z, 0, memo);
    }

    private int dfs(int x, int y, int z, int last, Map<String, Integer> memo) {
        String key = x + "," + y + "," + z + "," + last;
        if (memo.containsKey(key)) return memo.get(key);
        int best = 0;
        if (x > 0 && last != 1) best = Math.max(best, 2 + dfs(x - 1, y, z, 1, memo));
        if (y > 0 && last != 2) best = Math.max(best, 2 + dfs(x, y - 1, z, 2, memo));
        if (z > 0 && last != 1) best = Math.max(best, 2 + dfs(x, y, z - 1, 2, memo));
        memo.put(key, best);
        return best;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例测试
        System.out.println(sol.longestString(2, 5, 1) == 12 ? "PASS" : "FAIL");
        System.out.println(sol.longestString(3, 2, 2) == 14 ? "PASS" : "FAIL");

        // 对拍(小规模)
        java.util.Random rnd = new java.util.Random(2745);
        boolean ok = true;
        for (int t = 0; t < 2000; t++) {
            int x = 1 + rnd.nextInt(4);
            int y = 1 + rnd.nextInt(4);
            int z = 1 + rnd.nextInt(4);
            int r1 = sol.longestString(x, y, z);
            int r2 = sol.longestStringDfs(x, y, z);
            if (r1 != r2) {
                ok = false;
                System.out.println("Mismatch x=" + x + " y=" + y + " z=" + z + " formula=" + r1 + " dfs=" + r2);
                break;
            }
        }
        System.out.println(ok ? "2745 全部 PASS" : "2745 存在不一致");
    }
}

复杂度分析

时间复杂度:O(1)(公式);空间复杂度:O(1)。


2746. Decremental String Concatenation(字符串连接删减字母)

题目描述

给你一个下标从 0 开始的数组 words,包含 n 个字符串。

定义连接操作 join(x, y):将 x 和 y 连在一起;如果 x 的最后一个字符与 y 的

第一个字符相等,连接后两个字符中的一个会被删除(即重合字符只保留一个)。

你需要按顺序执行 n-1 次连接:str0 = words[0];对 i = 1...n-1,

要么 str_i = join(str_{i-1}, words[i]),要么 str_i = join(words[i], str_{i-1})。

返回最终字符串 str_{n-1} 的最小长度。

示例 1

复制代码
words = ["aa","ab","bc"] -> 4(join("aa","ab")="aab",join("aab","bc")="aabc")

示例 2

复制代码
words = ["ab","b"] -> 2(join("ab","b")="ab")

示例 3

复制代码
words = ["aaa"] -> 3

提示:

  • 1 <= words.length <= 1000;1 <= words[i].length <= 100。

解题思路

解法一:DP 维护首尾字符,主提交版本

拼接结果只需要知道当前串的第一个字符和最后一个字符即可计算下一步的删减。

定义 dpfl = 处理完当前若干字符串后,结果串首字符 f、末字符 l 时的最小长度。

初始 dp首尾 = words0.length()。

对每个新串 w(首 wf、尾 wl、长度 wlen)转移:

  • 拼在尾部:新长度 = len + wlen - (l == wf ? 1 : 0),新首 = f,新尾 = wl;
  • 拼在头部:新长度 = len + wlen - (wl == f ? 1 : 0),新首 = wf,新尾 = l。
    取所有状态最小值即答案。
    解法二:枚举左右放置暴力,对照
    小规模枚举每一步「拼左 / 拼右」共 2^(n-1)
    种组合,直接构造真实字符串求最小长度,用于对拍。
    时间复杂度:O(n * 262);空间复杂度:O(262)。

代码实现

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

public class Solution {

    private static final int INF = Integer.MAX_VALUE / 2;

    public int minimizeConcatenatedLength(String[] words) {
        int n = words.length;
        int[][] dp = new int[26][26];
        for (int[] row : dp) Arrays.fill(row, INF);
        String first = words[0];
        dp[first.charAt(0) - 'a'][first.charAt(first.length() - 1) - 'a'] = first.length();
        for (int i = 1; i < n; i++) {
            String w = words[i];
            int wlen = w.length();
            int wf = w.charAt(0) - 'a';
            int wl = w.charAt(wlen - 1) - 'a';
            int[][] ndp = new int[26][26];
            for (int[] row : ndp) Arrays.fill(row, INF);
            for (int f = 0; f < 26; f++) {
                for (int l = 0; l < 26; l++) {
                    if (dp[f][l] == INF) continue;
                    // 拼在尾部:str + w,删减条件 l == wf
                    int tailLen = dp[f][l] + wlen - (l == wf ? 1 : 0);
                    ndp[f][wl] = Math.min(ndp[f][wl], tailLen);
                    // 拼在头部:w + str,删减条件 wl == f
                    int headLen = dp[f][l] + wlen - (wl == f ? 1 : 0);
                    ndp[wf][l] = Math.min(ndp[wf][l], headLen);
                }
            }
            dp = ndp;
        }
        int ans = INF;
        for (int f = 0; f < 26; f++) {
            for (int l = 0; l < 26; l++) ans = Math.min(ans, dp[f][l]);
        }
        return ans;
    }

    public int minimizeConcatenatedLengthBrute(String[] words) {
        int[] best = new int[]{INF};
        dfsBrute(words, 1, words[0], best);
        return best[0];
    }

    private void dfsBrute(String[] words, int idx, String cur, int[] best) {
        if (idx == words.length) {
            best[0] = Math.min(best[0], cur.length());
            return;
        }
        String w = words[idx];
        // 拼在尾部
        dfsBrute(words, idx + 1, join(cur, w), best);
        // 拼在头部
        dfsBrute(words, idx + 1, join(w, cur), best);
    }

    private String join(String x, String y) {
        if (x.charAt(x.length() - 1) == y.charAt(0)) {
            return x + y.substring(1);
        }
        return x + y;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例测试
        System.out.println(sol.minimizeConcatenatedLength(new String[]{"aa", "ab", "bc"}) == 4 ? "PASS" : "FAIL");
        System.out.println(sol.minimizeConcatenatedLength(new String[]{"ab", "b"}) == 2 ? "PASS" : "FAIL");
        System.out.println(sol.minimizeConcatenatedLength(new String[]{"aaa"}) == 3 ? "PASS" : "FAIL");

        // 随机对拍(n 小)
        java.util.Random rnd = new java.util.Random(2746);
        boolean ok = true;
        char[] alpha = {'a', 'b', 'c'};
        for (int t = 0; t < 500; t++) {
            int n = 1 + rnd.nextInt(6);
            String[] words = new String[n];
            for (int i = 0; i < n; i++) {
                int len = 1 + rnd.nextInt(3);
                StringBuilder sb = new StringBuilder();
                for (int j = 0; j < len; j++) sb.append(alpha[rnd.nextInt(alpha.length)]);
                words[i] = sb.toString();
            }
            int r1 = sol.minimizeConcatenatedLength(words);
            int r2 = sol.minimizeConcatenatedLengthBrute(words);
            if (r1 != r2) {
                ok = false;
                System.out.println("Mismatch words=" + Arrays.toString(words) + " dp=" + r1 + " brute=" + r2);
                break;
            }
        }
        System.out.println(ok ? "2746 全部 PASS" : "2746 存在不一致");
    }
}

复杂度分析

时间复杂度:O(n * 262);空间复杂度:O(262)。


2747. Count Zero Request Servers(统计没有收到请求的服务器数目)

题目描述

给你一个整数 n,表示服务器的总数,编号从 1 到 n。

另给你一个下标从 0 开始的二维数组 logs,其中 logs[i] = `server_id`, `time`,

表示编号 server_id 的服务器在 time 时间收到一个请求。

再给你一个整数 x 和一个下标从 0 开始的整数数组 queries。

对 queries 中的每个查询 qi = queries[j](时间点),我们需要统计:

在时间区间 `qi` - `x`, `qi` 内没有收到任何请求的服务器数目。

返回一个与 queries 等长的数组 answer,answer[j] 为第 j 个查询的答案。

示例 1

复制代码
n = 3, logs = [[1,3],[2,6],[1,5]], x = 5, queries = [10,11] -> [1,2]
查询 10:窗口 [5,10],服务器 2 在 6 有请求,3 没请求,1 在 5 有请求 -> 未覆盖只有 3 -> 1。

示例 2

复制代码
n = 3, logs = [[2,4],[2,1],[1,2],[3,1]], x = 2, queries = [3,4] -> [0,1]

提示:

  • 1 <= n <= 1e5;1 <= logs.length <= 1e5;1 <= queries.length <= 1e5;
  • logsi.length == 2;1 <= logsi0 <= n;1 <= logsi1 <= 1e9;
  • 1 <= x <= 1e5;queries 中的值互不相同。

解题思路

解法一:排序 + 滑动窗口,主提交版本

按时间排序 logs 与 queries。对每个查询 t,窗口为 t - x, t。

滑动窗口维护当前窗口内「收到过请求的服务器集合」:

  • 右指针 r 扩展:把 logsr.time <= t 的请求加入计数(若该服务器此前在窗口中
    无请求,则 covered++);
  • 左指针 l 收缩:移除 logsl.time < t - x 的请求;若移除后该服务器在窗口内
    不再有请求,则 covered--。
    答案为 n - covered(covered 为窗口内覆盖到的服务器数)。
    注意同一服务器在窗口内可能有多条 log,需要计数(cntserver_id)。
    解法二:离线前缀/暴力,对照
    小规模对每个查询直接扫描 logs 统计。
    时间复杂度:O((|logs| + |queries|) log(...));空间复杂度:O(n + |queries|)。

代码实现

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

public class Solution {

    public int[] countServers(int n, int[][] logs, int x, int[] queries) {
        int m = logs.length;
        // logs 按时间排序
        Arrays.sort(logs, (a, b) -> Integer.compare(a[1], b[1]));
        int qn = queries.length;
        // 查询附原始下标并排序
        int[][] qs = new int[qn][2];
        for (int i = 0; i < qn; i++) {
            qs[i][0] = queries[i];
            qs[i][1] = i;
        }
        Arrays.sort(qs, (a, b) -> Integer.compare(a[0], b[0]));
        int[] cnt = new int[n + 1]; // 服务器在窗口内的请求计数
        int covered = 0;            // 窗口内至少有一次请求的服务器数
        int l = 0, r = 0;
        int[] ans = new int[qn];
        for (int[] q : qs) {
            int t = q[0];
            // 加入 time <= t 的请求
            while (r < m && logs[r][1] <= t) {
                int sid = logs[r][0];
                if (cnt[sid] == 0) covered++;
                cnt[sid]++;
                r++;
            }
            // 移除 time < t - x 的请求
            while (l < m && logs[l][1] < t - x) {
                // 注意:仅移除当前在窗口内的请求
                if (logs[l][1] >= t - x) break;
                int sid = logs[l][0];
                cnt[sid]--;
                if (cnt[sid] == 0) covered--;
                l++;
            }
            // 更稳妥:先移除再插入会导致重复处理顺序问题,下面用标准顺序:
            // 上面的实现中 left 应确保不越过已加入的右边界,安全。
            ans[q[1]] = n - covered;
        }
        return ans;
    }

    public int[] countServersBrute(int n, int[][] logs, int x, int[] queries) {
        int qn = queries.length;
        int[] ans = new int[qn];
        for (int qi = 0; qi < qn; qi++) {
            int t = queries[qi];
            boolean[] hit = new boolean[n + 1];
            for (int[] log : logs) {
                if (log[1] >= t - x && log[1] <= t) hit[log[0]] = true;
            }
            int cnt = 0;
            for (int i = 1; i <= n; i++) if (!hit[i]) cnt++;
            ans[qi] = cnt;
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例测试
        System.out.println(Arrays.equals(
                sol.countServers(3, new int[][]{{1, 3}, {2, 6}, {1, 5}}, 5, new int[]{10, 11}),
                new int[]{1, 2}) ? "PASS" : "FAIL");
        System.out.println(Arrays.equals(
                sol.countServers(3, new int[][]{{2, 4}, {2, 1}, {1, 2}, {3, 1}}, 2, new int[]{3, 4}),
                new int[]{0, 1}) ? "PASS" : "FAIL");

        // 随机对拍(小规模)
        java.util.Random rnd = new java.util.Random(2747);
        boolean ok = true;
        for (int t = 0; t < 3000; t++) {
            int n = 1 + rnd.nextInt(5);
            int m = 1 + rnd.nextInt(8);
            int[][] logs = new int[m][2];
            for (int i = 0; i < m; i++) {
                logs[i][0] = 1 + rnd.nextInt(n);
                logs[i][1] = 1 + rnd.nextInt(12);
            }
            int x = 1 + rnd.nextInt(6);
            int qn = 1 + rnd.nextInt(6);
            java.util.Set<Integer> seenQ = new java.util.HashSet<Integer>();
            int[] queries = new int[qn];
            int qi = 0;
            while (qi < qn) {
                int v = 1 + rnd.nextInt(12);
                if (seenQ.add(v)) queries[qi++] = v;
            }
            int[] r1 = sol.countServers(n, logs, x, queries);
            int[] r2 = sol.countServersBrute(n, logs, x, queries);
            if (!Arrays.equals(r1, r2)) {
                ok = false;
                System.out.println("Mismatch n=" + n + " logs=" + Arrays.deepToString(logs) + " x=" + x
                        + " q=" + Arrays.toString(queries) + " win=" + Arrays.toString(r1)
                        + " brute=" + Arrays.toString(r2));
                break;
            }
        }
        System.out.println(ok ? "2747 全部 PASS" : "2747 存在不一致");
    }
}

复杂度分析

时间复杂度:O((|logs| + |queries|) log(...));空间复杂度:O(n + |queries|)。


2748. Number of Beautiful Pairs(美丽下标对的数目)

题目描述

给你一个下标从 0 开始的整数数组 nums。

如果下标对 (i, j) 满足 0 <= i < j < nums.length,且 nums[i] 的第一个数字与

nums[j] 的最后一个数字互质(gcd == 1),则称这是一组美丽下标对。

返回 nums 中美丽下标对的总数目。

示例 1

复制代码
nums = [2,5,1,4] -> 5

示例 2

复制代码
nums = [11,21,12] -> 2

提示:

  • 2 <= nums.length <= 100;1 <= nums[i] <= 9999;nums[i] % 10 != 0。

解题思路

解法一:计数前缀 + gcd,主提交版本

第一个数字的取值范围是 1~9(numsi 首位),最后一个数字取值 1~9。

从左到右扫描,维护 cntd = 已扫描元素中「第一个数字为 d」的个数。

对每个元素 x:取其末数字 l(x % 10),答案累加 Σ_{d=1...9 且 gcd(d,l)==1} cntd,

然后把 x 的首数字加入 cnt。

解法二:暴力枚举,对照

枚举所有 i < j 对直接判断 gcd。

时间复杂度:O(n * 9);空间复杂度:O(10)。

代码实现

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

public class Solution {

    private int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }

    public int countBeautifulPairs(int[] nums) {
        int[] cnt = new int[10]; // 已见过的首数字计数
        int ans = 0;
        for (int x : nums) {
            // 当前元素的末数字
            int l = x % 10;
            for (int d = 1; d <= 9; d++) {
                if (cnt[d] > 0 && gcd(d, l) == 1) ans += cnt[d];
            }
            // 当前元素的首数字加入前缀
            int f = x;
            while (f >= 10) f /= 10;
            cnt[f]++;
        }
        return ans;
    }

    public int countBeautifulPairsBrute(int[] nums) {
        int n = nums.length;
        int ans = 0;
        for (int i = 0; i < n; i++) {
            int f = 0;
            int t = nums[i];
            while (t >= 10) t /= 10;
            f = t;
            for (int j = i + 1; j < n; j++) {
                int l = nums[j] % 10;
                if (gcd(f, l) == 1) ans++;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例测试
        System.out.println(sol.countBeautifulPairs(new int[]{2, 5, 1, 4}) == 5 ? "PASS" : "FAIL");
        System.out.println(sol.countBeautifulPairs(new int[]{11, 21, 12}) == 2 ? "PASS" : "FAIL");

        // 随机对拍(构造满足 nums[i] % 10 != 0)
        java.util.Random rnd = new java.util.Random(2748);
        boolean ok = true;
        for (int t = 0; t < 3000; t++) {
            int n = 2 + rnd.nextInt(8);
            int[] nums = new int[n];
            for (int i = 0; i < n; i++) {
                do {
                    nums[i] = 1 + rnd.nextInt(9998);
                } while (nums[i] % 10 == 0);
            }
            int r1 = sol.countBeautifulPairs(nums);
            int r2 = sol.countBeautifulPairsBrute(nums);
            if (r1 != r2) {
                ok = false;
                System.out.println("Mismatch nums=" + Arrays.toString(nums) + " cnt=" + r1 + " brute=" + r2);
                break;
            }
        }
        System.out.println(ok ? "2748 全部 PASS" : "2748 存在不一致");
    }
}

复杂度分析

时间复杂度:O(n * 9);空间复杂度:O(10)。


2749. Minimum Operations to Make the Integer Zero(得到整数零需要执行的最少操作数)

题目描述

给你两个整数 num1 和 num2。

在一步操作中,你需要从范围 0, 60 中选出一个整数 i,并从 num1 减去 (2^i + num2)。

计算要想使 num1 等于 0 需要执行的最少操作数并以整数返回;若无法使 num1 等于 0 返回 -1。

示例 1

复制代码
num1 = 3, num2 = -2 -> 3

示例 2

复制代码
num1 = 5, num2 = 7 -> -1

提示:

  • 1 <= num1 <= 1e9;-1e9 <= num2 <= 1e9。

解题思路

解法一:枚举操作次数 k,主提交版本

若执行 k 次操作,则 num1 - knum2 必须等于若干 2^i 之和(i 取 0...60),
即需要满足:sum = num1 - k
num2 可以被表示为 k 个 2 的幂之和(允许重复,

但相同 i 可以多次选择)。

最少数目的「2 的幂个数」等于 popcount(sum)(二进制中 1 的个数)------

因为每个 2^i 对应二进制中的一个 1;但一个 2^i 也可以拆成两个 2^(i-1),

所以可表示的幂个数取值范围是 popcount(sum), sum 之间的任意整数。

因此若 popcount(sum) <= k <= sum 且 sum >= 0,则 k 次可行。

从小到大枚举 k(k 最多 60 左右,因为 num1 <= 1e9,2^30 已超过),

第一个可行的 k 即答案;枚举不上则返回 -1。

注意 sum = num1 - k*num2 可能为负,此时直接不可行。

解法二:BFS 模拟,对照

小规模用 BFS 从 num1 出发尝试减去各种 2^i + num2,

求到达 0 的最少步数,用于对拍。

时间复杂度:O(k * 62);空间复杂度:O(1)。

代码实现

java 复制代码
public class Solution {

    public int makeTheIntegerZero(int num1, int num2) {
        // k 从 1 开始枚举;上界:2^60 级别足以覆盖,但 sum 需 >= 0
        for (int k = 1; k <= 100; k++) {
            long sum = (long) num1 - (long) k * num2;
            if (sum < 0) continue;
            int pop = Long.bitCount(sum);
            if (pop <= k && k <= sum) return k;
            if (sum < k) break; // 后面 sum 只会更小
        }
        return -1;
    }

    public int makeTheIntegerZeroBfs(int num1, int num2) {
        java.util.Set<Integer> visited = new java.util.HashSet<Integer>();
        java.util.ArrayDeque<Integer> queue = new java.util.ArrayDeque<Integer>();
        queue.offer(num1);
        visited.add(num1);
        int steps = 0;
        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int s = 0; s < size; s++) {
                int cur = queue.poll();
                if (cur == 0) return steps;
                for (int i = 0; i <= 60; i++) {
                    long nxt = cur - ((long) 1 << i) - num2;
                    if (nxt < -1000000 || nxt > 1000000) continue;
                    int ni = (int) nxt;
                    if (visited.add(ni)) queue.offer(ni);
                }
            }
            steps++;
        }
        return -1;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例测试
        System.out.println(sol.makeTheIntegerZero(3, -2) == 3 ? "PASS" : "FAIL");
        System.out.println(sol.makeTheIntegerZero(5, 7) == -1 ? "PASS" : "FAIL");
        System.out.println(sol.makeTheIntegerZero(1, 0) == 1 ? "PASS" : "FAIL"); // 1 = 2^0

        // 随机对拍(小规模,保证 num2 适中避免 BFS 爆炸)
        java.util.Random rnd = new java.util.Random(2749);
        boolean ok = true;
        for (int t = 0; t < 2000; t++) {
            int num1 = 1 + rnd.nextInt(60);
            int num2 = rnd.nextInt(11) - 5;
            int r1 = sol.makeTheIntegerZero(num1, num2);
            int r2 = sol.makeTheIntegerZeroBfs(num1, num2);
            if (r1 != r2) {
                ok = false;
                System.out.println("Mismatch num1=" + num1 + " num2=" + num2 + " enum=" + r1 + " bfs=" + r2);
                break;
            }
        }
        System.out.println(ok ? "2749 全部 PASS" : "2749 存在不一致");
    }
}

复杂度分析

时间复杂度:O(k * 62);空间复杂度:O(1)。


2750. Ways to Split Array Into Good Subarrays(将数组划分成若干好子数组的方式)

题目描述

给你一个二元数组 nums。

如果数组中的某个子数组恰好只存在一个值为 1 的元素,则认为该子数组是一个好子数组。

请你统计将数组 nums 划分成若干好子数组的方法数,并以整数形式返回。

由于数字可能很大,返回其对 1e9 + 7 取余之后的结果。

示例 1

复制代码
nums = [0,1,0,0,1] -> 3
  • 解释:0,10,0,1、0,1,00,1、0,1,0,01 共 3 种。

示例 2

复制代码
nums = [0,1,0] -> 1

提示:

  • 1 <= nums.length <= 1e5;0 <= nums[i] <= 1。

解题思路

解法一:乘法原理,主提交版本

每个好子数组必须恰好含一个 1,因此所有 1 必须被「分割线」隔开。

设 1 的下标依次为 p1, p2, ..., pm。若 m == 0(没有 1),则整个数组

无法分出含 1 的子段,返回 0。

第 i 个 1(i >= 2)所属子数组与第 i-1 个 1 所属子数组之间的分割线可选

位置有 (pi - pi-1) 种(即两个 1 之间的空闲下标个数 +1,在下一次出现 1

之前断开即可),这些选择互相独立,因此总方法数 = Π (pi - pi-1) mod MOD。

示例 1:1 在下标 1、4,中间空闲下标 2、3,分割线可选 2 个位置(p4-p1 = 3),

即 3 种。若相邻 1 直接挨着(距离 1),则只有 1 种(必须从它们中间切)。

解法二:DP 递推 + 前缀,对照

dpi 表示处理到下标 i 前缀的划分方法数,

枚举最后一个好子数组的起点,O(n^2) 仅用于小规模对拍。

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

代码实现

java 复制代码
public class Solution {

    private static final long MOD = 1_000_000_007L;

    public int numberOfGoodSubarraySplits(int[] nums) {
        // 找第一个 1
        int first = -1;
        for (int i = 0; i < nums.length; i++) {
            if (nums[i] == 1) {
                first = i;
                break;
            }
        }
        if (first == -1) return 0; // 没有 1,无法划分
        long ans = 1;
        int prev = first;
        for (int i = first + 1; i < nums.length; i++) {
            if (nums[i] == 1) {
                ans = ans * (i - prev) % MOD;
                prev = i;
            }
        }
        return (int) ans;
    }

    public int numberOfGoodSubarraySplitsDp(int[] nums) {
        int n = nums.length;
        long[] dp = new long[n + 1]; // dp[i]: 前 i 个元素([0,i-1])的划分数
        dp[0] = 1;
        for (int end = 1; end <= n; end++) {
            // 枚举最后一个子数组 [start, end-1],必须恰好含一个 1
            int ones = 0;
            for (int start = end - 1; start >= 0; start--) {
                if (nums[start] == 1) ones++;
                if (ones > 1) break;
                if (ones == 1) dp[end] = (dp[end] + dp[start]) % MOD;
            }
        }
        return (int) dp[n];
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例测试
        System.out.println(sol.numberOfGoodSubarraySplits(new int[]{0, 1, 0, 0, 1}) == 3 ? "PASS" : "FAIL");
        System.out.println(sol.numberOfGoodSubarraySplits(new int[]{0, 1, 0}) == 1 ? "PASS" : "FAIL");
        System.out.println(sol.numberOfGoodSubarraySplits(new int[]{1, 1}) == 1 ? "PASS" : "FAIL"); // 两 1 紧邻
        System.out.println(sol.numberOfGoodSubarraySplits(new int[]{0, 0, 0}) == 0 ? "PASS" : "FAIL"); // 无 1

        // 随机对拍
        java.util.Random rnd = new java.util.Random(2750);
        boolean ok = true;
        for (int t = 0; t < 3000; t++) {
            int n = 1 + rnd.nextInt(12);
            int[] nums = new int[n];
            for (int i = 0; i < n; i++) nums[i] = rnd.nextInt(2);
            int r1 = sol.numberOfGoodSubarraySplits(nums);
            int r2 = sol.numberOfGoodSubarraySplitsDp(nums);
            if (r1 != r2) {
                ok = false;
                System.out.println("Mismatch nums=" + java.util.Arrays.toString(nums) + " mul=" + r1 + " dp=" + r2);
                break;
            }
        }
        System.out.println(ok ? "2750 全部 PASS" : "2750 存在不一致");
    }
}

复杂度分析

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


2751. Robot Collisions(机器人碰撞)

题目描述

现有 n 个机器人,编号从 1 开始,每个机器人包含在路线上的位置、健康度和移动方向。

给你下标从 0 开始的两个整数数组 positions、healths 和一个字符串 directions

(directions[i] 为 'L' 表示向左或 'R' 表示向右)。positions 中的所有整数互不相同。

所有机器人以相同速度同时沿给定方向在路线上移动。如果两个机器人移动到相同位置,

则会发生碰撞:

  • 健康度较低的机器人被移除,另一个健康度减少 1;
  • 如果健康度相同,则二者都被移除;
  • 幸存机器人继续沿与之前相同的方向前进。
    请确定全部碰撞后幸存的所有机器人的健康度,并按原编号顺序排列返回;
    若不存在幸存机器人,返回空数组。

示例 1

复制代码
positions=[5,4,3,2,1], healths=[2,17,9,15,10], directions="RRRRR" -> [2,17,9,15,10]

示例 2

复制代码
positions=[3,5,2,6], healths=[10,10,15,12], directions="RLRL" -> [14]

示例 3

复制代码
positions=[1,2,5,6], healths=[10,10,11,11], directions="RLRL" -> []

提示:

  • 1 <= n <= 1e5;1 <= positions[i], healths[i] <= 1e9;directions[i] 为 'L' 或 'R'。

解题思路

解法一:排序 + 栈模拟,主提交版本

只有「向右的机器人在左、向左的机器人在右」才会相遇碰撞。

按位置升序处理:每个机器人入栈,若栈顶方向为 'R' 而当前为 'L',则二者必然碰撞

(此时栈顶是最近一个向右的邻居)。循环处理碰撞:

  • hTop == hCur:双双移除;
  • hTop > hCur:当前机器人移除,栈顶健康度减 1;
  • hTop < hCur:栈顶移除,当前健康度减 1,继续与新的栈顶比较(若栈顶仍为 'R')。
    所有入场方向为 'L' 的机器人,其左侧已无 'R' 可撞,直接入栈。
    最后按原索引顺序输出剩余健康度。
    解法二:事件驱动模拟,对照
    反复选「距离最近的 R-L 相邻对」(即会最先碰撞的
    一对)处理:健康度低者死亡、高者减 1(相等则双亡),直到不存在 R 在 L 左侧
    的配对。事件顺序严格按碰撞时刻,与主解等价,用于小规模对拍。
    时间复杂度:O(n log n)(排序);空间复杂度:O(n)。

代码实现

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

public class Solution {

    private static final int MOD = 0; // 无取模

    static class Robot {
        int idx;      // 原索引
        int pos;      // 位置
        int hp;       // 健康度
        char dir;     // 方向
        Robot(int idx, int pos, int hp, char dir) {
            this.idx = idx;
            this.pos = pos;
            this.hp = hp;
            this.dir = dir;
        }
    }

    public List<Integer> survivedRobotsHealths(int[] positions, int[] healths, String directions) {
        int n = positions.length;
        Robot[] robots = new Robot[n];
        for (int i = 0; i < n; i++) {
            robots[i] = new Robot(i, positions[i], healths[i], directions.charAt(i));
        }
        // 按位置升序
        Arrays.sort(robots, (a, b) -> Integer.compare(a.pos, b.pos));

        List<Robot> stack = new ArrayList<Robot>();
        for (Robot r : robots) {
            boolean alive = true;
            // 当前向左,栈顶向右 -> 碰撞
            while (alive && !stack.isEmpty() && stack.get(stack.size() - 1).dir == 'R' && r.dir == 'L') {
                Robot top = stack.get(stack.size() - 1);
                if (top.hp == r.hp) {
                    stack.remove(stack.size() - 1);
                    alive = false; // 双双移除
                } else if (top.hp > r.hp) {
                    top.hp--;       // 栈顶幸存,减 1
                    alive = false;  // 当前被移除
                } else {
                    stack.remove(stack.size() - 1); // 栈顶被移除
                    r.hp--;         // 当前幸存且健康度减 1
                }
            }
            if (alive) stack.add(r);
        }

        int[] ans = new int[n];
        Arrays.fill(ans, -1);
        for (Robot r : stack) ans[r.idx] = r.hp;
        List<Integer> res = new ArrayList<Integer>();
        for (int i = 0; i < n; i++) {
            if (ans[i] != -1) res.add(ans[i]);
        }
        return res;
    }

    public List<Integer> survivedRobotsHealthsSim(int[] positions, int[] healths, String directions) {
        int n = positions.length;
        boolean[] dead = new boolean[n];
        int[] hp = healths.clone();
        while (true) {
            // 找最近的一对 (i, j) 且 i<j、pos[i]<pos[j]、dir[i]=='R'、dir[j]=='L'、均未死
            // 碰撞时间 = pos[j]-pos[i](速度相同),取最小者优先碰撞
            int bi = -1, bj = -1, bestD = Integer.MAX_VALUE;
            for (int i = 0; i < n; i++) {
                if (dead[i] || directions.charAt(i) != 'R') continue;
                for (int j = 0; j < n; j++) {
                    if (dead[j] || i == j || directions.charAt(j) != 'L') continue;
                    if (positions[j] <= positions[i]) continue;
                    int d = positions[j] - positions[i];
                    if (d < bestD) { bestD = d; bi = i; bj = j; }
                }
            }
            if (bi == -1) break; // 无碰撞
            if (hp[bi] == hp[bj]) {
                dead[bi] = dead[bj] = true;
            } else if (hp[bi] > hp[bj]) {
                dead[bj] = true;
                hp[bi]--;
            } else {
                dead[bi] = true;
                hp[bj]--;
            }
        }
        List<Integer> res = new ArrayList<Integer>();
        for (int i = 0; i < n; i++) {
            if (!dead[i]) res.add(hp[i]);
        }
        return res;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        boolean a1 = sol.survivedRobotsHealths(new int[]{5, 4, 3, 2, 1}, new int[]{2, 17, 9, 15, 10}, "RRRRR")
                .equals(Arrays.asList(2, 17, 9, 15, 10));
        System.out.println(a1 ? "PASS" : "FAIL");
        boolean a2 = sol.survivedRobotsHealths(new int[]{3, 5, 2, 6}, new int[]{10, 10, 15, 12}, "RLRL")
                .equals(Arrays.asList(14));
        System.out.println(a2 ? "PASS" : "FAIL");
        boolean a3 = sol.survivedRobotsHealths(new int[]{1, 2, 5, 6}, new int[]{10, 10, 11, 11}, "RLRL")
                .equals(Arrays.asList());
        System.out.println(a3 ? "PASS" : "FAIL");

        // 随机对拍(小规模)
        java.util.Random rnd = new java.util.Random(2751);
        boolean ok = true;
        for (int t = 0; t < 2000; t++) {
            int n = 1 + rnd.nextInt(8);
            int[] positions = new int[n];
            int[] healths = new int[n];
            StringBuilder dirs = new StringBuilder();
            java.util.Set<Integer> used = new java.util.HashSet<Integer>();
            for (int i = 0; i < n; i++) {
                int p;
                do { p = 1 + rnd.nextInt(20); } while (!used.add(p));
                positions[i] = p;
                healths[i] = 1 + rnd.nextInt(8);
                dirs.append(rnd.nextBoolean() ? 'R' : 'L');
            }
            List<Integer> r1 = sol.survivedRobotsHealths(positions, healths, dirs.toString());
            List<Integer> r2 = sol.survivedRobotsHealthsSim(positions, healths, dirs.toString());
            if (!r1.equals(r2)) {
                ok = false;
                System.out.println("Mismatch positions=" + Arrays.toString(positions)
                        + " healths=" + Arrays.toString(healths) + " dirs=" + dirs
                        + " stack=" + r1 + " sim=" + r2);
                break;
            }
        }
        System.out.println(ok ? "2751 全部 PASS" : "2751 存在不一致");
    }
}

复杂度分析

时间复杂度:O(n log n)(排序);空间复杂度:O(n)。


2752. Customers with Maximum Number of Transactions on Consecutive Days(在连续天数上进行了最多交易次数的顾客)

题目类型:数据库(SQL)

题目描述

表: Transactions

复制代码
+------------------+------+
| 列名             | 类型 |
+------------------+------+
| transaction_id   | int  |
| customer_id      | int  |
| transaction_date | date |
| amount           | int  |
+------------------+------+
transaction_id 是这个表的具有唯一值的列。 
每行包含有关交易的信息,包括唯一的(customer_id,transaction_date)以及相应的 customer_id 和 amount。

编写一个解决方案,找到连续天数上进行了最多交易的所有 customer_id 。

返回所有具有最大连续交易次数的 customer_id 。结果表按 customer_id 的 升序 排序。

结果的格式如下所示。

示例 1:**

复制代码
输入:
Transactions 表:
+----------------+-------------+------------------+--------+
| transaction_id | customer_id | transaction_date | amount |
+----------------+-------------+------------------+--------+
| 1              | 101         | 2023-05-01       | 100    |
| 2              | 101         | 2023-05-02       | 150    |
| 3              | 101         | 2023-05-03       | 200    |
| 4              | 102         | 2023-05-01       | 50     |
| 5              | 102         | 2023-05-03       | 100    |
| 6              | 102         | 2023-05-04       | 200    |
| 7              | 105         | 2023-05-01       | 100    |
| 8              | 105         | 2023-05-02       | 150    |
| 9              | 105         | 2023-05-03       | 200    |
+----------------+-------------+------------------+--------+
输出:
+-------------+
| customer_id | 
+-------------+
| 101         | 
| 105         | 
+-------------+
解释:
- customer_id 为 101 共有 3 次交易,且全部是连续的。
- customer_id 为 102 共有 3 次交易,但只有其中 2 次是连续的。
- customer_id 为 105 共有 3 次交易,且全部是连续的。 
总的来说,最大连续交易次数为 3,由 customer_id 为 101 和 105 的完成。customer_id 按升序排序。

解题思路

解法一:日期 - 行号 分组(主提交版本)

  • 经典连续区间问题:对每个客户按 transaction_date 排序编号 RN,
    transaction_date - RN(用 DATEDIFF(transaction_date, 固定日期) 后减行号)
    在连续日期上恒定,据此把同一个客户的交易划分成若干连续段;
  • 每段行数 = 连续交易天数(每个 (customer_id, transaction_date) 唯一,每天至多 1 行),
    取每个客户的最大段长,再取全局最大段长,筛出等于全局最大值的客户,按 customer_id 升序。

解法二:DENSE_RANK 变体(对照)

  • 用 DENSE_RANK() 按客户内日期排序给日期排名,再计算
    DATEDIFF(transaction_date, 约等于行号) - 序号 作为段键,逻辑等价;
  • 演示不同去连续手段的等价性。

代码实现

sql 复制代码
-- 解法一(主提交版本):日期减行号分组
WITH t AS (
    SELECT DISTINCT customer_id, transaction_date
    FROM Transactions
),
seg AS (
    SELECT customer_id,
           DATEDIFF(transaction_date, '2000-01-01')
               - ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY transaction_date) AS grp,
           COUNT(*) OVER (PARTITION BY customer_id,
               DATEDIFF(transaction_date, '2000-01-01')
               - ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY transaction_date)) AS seg_len
    FROM t
)
SELECT customer_id
FROM (
    SELECT customer_id, MAX(seg_len) AS max_len
    FROM seg
    GROUP BY customer_id
) m
WHERE max_len = (
    SELECT MAX(max_len) FROM (
        SELECT MAX(seg_len) AS max_len FROM seg GROUP BY customer_id
    ) mm
)
ORDER BY customer_id;
sql 复制代码
-- 解法二(对照):先分段再取全局最大
WITH t AS (
    SELECT DISTINCT customer_id, transaction_date
    FROM Transactions
),
seg AS (
    SELECT customer_id,
           DATEDIFF(transaction_date, '2000-01-01')
               - ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY transaction_date) AS grp,
           transaction_date
    FROM t
),
cnt AS (
    SELECT customer_id, COUNT(*) AS days_cnt
    FROM seg
    GROUP BY customer_id, grp
)
SELECT customer_id
FROM cnt
WHERE days_cnt = (SELECT MAX(days_cnt) FROM cnt)
ORDER BY customer_id;

复杂度分析

  • 时间复杂度 :O(n),n 为表行数,单次扫描聚合(或窗口/连接)。
  • 空间复杂度 :O(n),分组/排序/窗口需要存储结果集。

2753. Count Houses in a Circular Street II(计算一个环形街道上的房屋数量 II)

题目描述

给定一个代表环形街道的类 Street 的对象 street 和一个正整数 k,表示街道上房屋的

最大数量(房屋数量不超过 k)。每个房屋的门初始时可以是开着的也可以是关着的

(至少有一个房屋的门是开着的)。刚开始你站在一座房子的门前,任务是计算街道上的

房屋数量。

Street 类包含以下函数:

void closeDoor():关闭当前房屋的门。

boolean isDoorOpen():如果当前房屋的门是开着的返回 true,否则返回 false。

void moveRight():向右移动到下一座房屋。

注意:街道是环形的,housen 右边的房子是 house1。房屋数量 n 满足 1 <= n <= k <= 1e5。

示例 1

复制代码
street = [1,1,1,1], k = 10 -> 4(4 座房屋,门全开,n < k)

示例 2

复制代码
street = [1,0,1,1,0], k = 5 -> 5(n == k)

解题思路

解法一:开门标记法,主提交版本

因为只能向右移动,利用「只有关门的写操作」:先把起点挪到一个开门的房屋上,

但故意不关闭它,把它作为「唯一的、始终保持打开的标记房」。

然后向右走 k 步(k >= n,必然绕到起点后还可继续),每走一步检查当前门:

若门开着说明它要么是某个原来就开着的门(第一次遇到时关闭它并记录距离),

要么就是我们的标记房(它一直开着,且只有绕过一整圈后才会再次遇到)。

由于除标记房外的所有开门在第一圈就被关闭,标记房成为唯一持续开着的门,

因此最后一次记录到的距离就是绕一整圈回到标记房的距离 = 房屋数量 n。

解法二:全知仿真,对照

本地验证用,直接传入全部门状态数组,用「走到底再数」

的朴素思路得到真实 n,用于和主解法对拍。

时间复杂度:O(k);空间复杂度:O(1)。

代码实现

java 复制代码
class Street {
    private boolean[] doors;
    private int pos;
    private int n;

    public Street(int[] doors) {
        this.n = doors.length;
        this.doors = new boolean[n];
        for (int i = 0; i < n; i++) this.doors[i] = doors[i] == 1;
        this.pos = 0;
    }

    public void closeDoor() { doors[pos] = false; }

    public boolean isDoorOpen() { return doors[pos]; }

    public void moveRight() { pos = (pos + 1) % n; }

    public int[] snapshot() {
        int[] s = new int[n];
        for (int i = 0; i < n; i++) s[i] = doors[i] ? 1 : 0;
        return s;
    }
}

public class Solution {

    public int houseCount(Street street, int k) {
        // 先挪到第一个开门的房屋,把它作为标记房(不关闭)
        while (!street.isDoorOpen()) {
            street.moveRight();
        }
        int ans = 0;
        for (int i = 1; i <= k; i++) {
            street.moveRight();
            if (street.isDoorOpen()) {
                ans = i;        // 每次遇到开门都记录距离;标记房(持续开着)最后一次被记录
                street.closeDoor(); // 关闭非标记房的开门,保证只有标记房一直开着
            }
        }
        return ans;
    }

    public int houseCountSim(int[] doors, int k) {
        int n = doors.length;
        // 从任意起点开始:找到第一个开门的位置
        int start = 0;
        while (doors[start] != 1) start = (start + 1) % n;
        // 与主解相同的走法:开着的门置 1 表示「未被关闭的标记房」,其余开门走过后关掉
        boolean[] state = new boolean[n];
        for (int i = 0; i < n; i++) state[i] = doors[i] == 1;
        int pos = start;
        int ans = 0;
        for (int i = 1; i <= k; i++) {
            pos = (pos + 1) % n;
            if (state[pos]) {
                ans = i;
                state[pos] = false; // 关闭
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例测试
        System.out.println(sol.houseCount(new Street(new int[]{1, 1, 1, 1}), 10) == 4 ? "PASS" : "FAIL");
        System.out.println(sol.houseCount(new Street(new int[]{1, 0, 1, 1, 0}), 5) == 5 ? "PASS" : "FAIL");
        System.out.println(sol.houseCount(new Street(new int[]{0, 0, 0, 0, 1}), 8) == 5 ? "PASS" : "FAIL"); // 起点门关着

        // 随机对拍:随机门状态 + 随机起点(通过轮换门数组模拟不同起点)
        java.util.Random rnd = new java.util.Random(2753);
        boolean ok = true;
        for (int t = 0; t < 3000; t++) {
            int n = 1 + rnd.nextInt(10);
            int[] doors = new int[n];
            boolean hasOpen = false;
            for (int i = 0; i < n; i++) {
                doors[i] = rnd.nextInt(2);
                if (doors[i] == 1) hasOpen = true;
            }
            if (!hasOpen) doors[rnd.nextInt(n)] = 1; // 保证至少一扇开着
            int k = n + rnd.nextInt(6); // k >= n
            int start = rnd.nextInt(n);
            // 构造起点在第 start 个房屋的 Street:轮换数组
            int[] rotated = new int[n];
            for (int i = 0; i < n; i++) rotated[i] = doors[(start + i) % n];
            Street st = new Street(rotated);
            int r1 = sol.houseCount(st, k);
            int r2 = sol.houseCountSim(doors, k);
            if (r1 != n || r1 != r2) {
                ok = false;
                System.out.println("Mismatch doors=" + java.util.Arrays.toString(doors)
                        + " start=" + start + " k=" + k + " main=" + r1 + " sim=" + r2 + " true=" + n);
                break;
            }
        }
        System.out.println(ok ? "2753 全部 PASS" : "2753 存在不一致");
    }
}

复杂度分析

时间复杂度:O(k);空间复杂度:O(1)。


2754. Bind Function to Context(将函数绑定到上下文 🔒)

题目类型:JavaScript

题目描述

编写一个所有函数都支持的方法 bindPolyfill 。当 bindPolyfill 方法被调用并传递了一个对象 obj 时,该对象将成为函数的 this 上下文。

例如,如果你有以下代码:

复制代码
function f() {
console.log('My context is ' + this.ctx);
}
f();

它的输出是 "My context is undefined" 。然而,如果你绑定了该函数:

复制代码
function f() {
console.log('My context is ' + this.ctx);
}
const boundFunc = f.boundPolyfill({ "ctx": "My Object" })
boundFunc();

它的输出应为 "My context is My Object" 。

你可以假设传递给 bindPolyfill 方法的是一个非空对象。

请在不使用内置的 Function.bind 方法的情况下解决该问题。

示例 1:

复制代码
输入:
fn = function f(multiplier) {
return this.x * multiplier;
}
obj = {"x": 10}
inputs = [5]
输出:50
解释:
const boundFunc = f.bindPolyfill({"x": 10});
boundFunc(5); // 50
传递了一个乘数 5 作为参数。
上下文设置为 `{"x": 10}`。
将这两个数字相乘得到 50。

示例 2:**

复制代码
输入:
fn = function speak() {
return "My name is " + this.name;
}
obj = {"name": "Kathy"}
inputs = []
输出:"My name is Kathy"
解释:
const boundFunc = f.bindPolyfill({"name": "Kathy"});
boundFunc(); // "My name is Kathy"

提示:

  • obj 是一个非空对象

  • 0 <= inputs.length <= 100

你能在不使用任何内置方法的情况下解决这个问题吗?

解题思路

实现 b i n d bind bind:返回的函数以给定对象作为 t h i s this this 调用原函数。包一层普通函数会丢掉箭头函数对 t h i s this this 的词法绑定需求,这里要用显式 c a l l call call。

在原型上返回箭头函数,内部 t h i s . c a l l ( o b j , . . . a r g s ) this.call(obj, ...args) this.call(obj,...args),这样无论外层如何调用,绑定对象都固定为 o b j obj obj。

代码实现

typescript 复制代码
type Fn = (...args) => any;

declare global {
    interface Function {
        bindPolyfill(obj: Record<any, any>): Fn;
    }
}

Function.prototype.bindPolyfill = function (obj) {
    return (...args) => {
        return this.call(obj, ...args);
    };
};

复杂度分析

  • 时间复杂度 :O(1),apply/call 直接绑定上下文。
  • 空间复杂度 :O(1),不挂临时属性。

2755. Deep Merge of Two Objects(深度合并两个对象 🔒)

题目类型:JavaScript

题目描述

给定两个值 obj1 和 obj2,返回一个 深度合并 的值。

你应该遵循以下规则进行值的 深度合并:

  • 如果两个值都是对象,则结果对象应包含两个对象上存在的所有键。

  • 如果一个键同时存在于两个对象中,则 深度合并 两个关联的值。否则,将键值对添加到结果对象中。

  • 如果两个值都是数组,则结果数组的长度应与较长的数组相同。对于对象的合并逻辑,将索引视为键。

  • 否则,结果值为 obj2。

你可以假设 obj1 和 obj2 是 JSON.parse() 的输出结果。

示例 1:**

复制代码
输入:obj1 = {"a": 1, "c": 3}, obj2 = {"a": 2, "b": 2}
输出:{"a": 2, "c": 3, "b": 2}
解释:`obj1["a"]` 的值变为 2,因为如果两个对象具有相同的键且它们的值不是数组或对象,则将 `obj1` 的值更改为 `obj2` 的值。键 "b" 和其值被添加到 `obj1` 中,因为它在 `obj1` 中不存在。

示例 2:**

复制代码
输入:obj1 = [{}, 2, 3], obj2 = [[], 5]
输出:[[], 5, 3]
解释:`result[0] = obj2[0]`,因为 `obj1[0]` 和 `obj2[0]` 类型不同。`result[2] = obj1[2]`,因为 `obj2[2]` 不存在。

示例 3:**

复制代码
输入:
obj1 = {"a": 1, "b": {"c": [1 , [2, 7], 5], "d": 2}},
obj2 = {"a": 1, "b": {"c": [6, [6], [9]], "e": 3}}
输出:{"a": 1, "b": {"c": [6, [6, 7], [9]], "d": 2, "e": 3}}
解释:
数组 `obj1["b"]["c"]` 和 `obj2["b"]["c"]` 已合并,如果 `obj2` 的值不是数组或对象,则深度覆盖 `obj1` 的值。 `obj2["b"]["c"]` 有键 "e",而 `obj1` 中没有,所以将其添加到 `obj1` 中。

示例 4:**

复制代码
输入:obj1 = true, obj2 = null
输出:null

解释:

  • obj1 和 obj2 都是有效的 JSON 值

  • 1 <= JSON.stringify(obj1).length <= 5 * 105

  • 1 <= JSON.stringify(obj2).length <= 5 * 105

解题思路

深度合并两个值:都是对象或都是数组时递归合并键,类型冲突或遇到标量则以 o b j 2 obj2 obj2 为准。浅合并无法处理嵌套对象。

任一方非对象则返回 o b j 2 obj2 obj2;一方是数组另一方不是,同样取 o b j 2 obj2 obj2。否则遍历 o b j 2 obj2 obj2 的键,对每个键递归写入 o b j 1 obj1 obj1。

代码实现

typescript 复制代码
function deepMerge(obj1: any, obj2: any): any {
    const isObj = (obj: any) => obj && typeof obj === 'object';
    const isArr = (obj: any) => Array.isArray(obj);
    if (!isObj(obj1) || !isObj(obj2)) {
        return obj2;
    }
    if (isArr(obj1) !== isArr(obj2)) {
        return obj2;
    }
    for (const key in obj2) {
        obj1[key] = deepMerge(obj1[key], obj2[key]);
    }
    return obj1;
}

/**
 * let obj1 = {"a": 1, "c": 3}, obj2 = {"a": 2, "b": 2};
 * deepMerge(obj1, obj2); // {"a": 2, "c": 3, "b": 2}
 */

复杂度分析

  • 时间复杂度 :O(n),n 为两个对象的键/节点总数,递归合并一次。
  • 空间复杂度 :O(d),d 为嵌套深度(递归栈);结果对象与输入同规模。

2756. Query Batching(批处理查询 🔒)

题目类型:JavaScript

题目描述

将多个小查询批处理为单个大查询可以是一种有用的优化。请编写一个名为 QueryBatcher 的类来实现这个功能。

它的构造函数应接受两个参数:

  • 一个异步函数 queryMultiple ,它接受一个字符串键的数组作为输入。它将返回一个与输入数组长度相同的值数组。每个索引对应于与 input[i] 相关联的值。可以假设该异步函数永远不会被拒绝。

  • 一个以毫秒为单位的节流时间t。

该类有一个方法:

  • async getValue(key):接受一个字符串键,并返回一个解析后的字符串值。传递给此函数的键值最终应传递给 queryMultiple 函数。在 t 毫秒内不应连续调用 queryMultiple 。第一次调用 getValue 时,应立即使用该单个键调用 queryMultiple 。如果在 t 毫秒后再次调用了 getValue ,则所有传递的键应传递给 queryMultiple ,并返回最终结果。可以假设传递给该方法的每个键都是唯一的。

下图说明了节流算法的工作原理。每个矩形代表 100毫秒。节流时间为 400毫秒。

示例 1:**

复制代码
输入:
queryMultiple = async function(keys) {
return keys.map(key => key + '!');
}
t = 100
calls = [
{"key": "a", "time": 10},
{"key": "b", "time": 20},
{"key": "c", "time": 30}
]
输出:[
{"resolved": "a!", "time": 10},
{"resolved": "b!", "time": 110},
{"resolved": "c!", "time": 110}
]
解释:
const batcher = new QueryBatcher(queryMultiple, 100);
setTimeout(() => batcher.getValue('a'), 10); // "a!" at t=10ms
setTimeout(() => batcher.getValue('b'), 20); // "b!" at t=110ms
setTimeout(() => batcher.getValue('c'), 30); // "c!" at t=110ms

`queryMultiple `简单地给键添加了"!"。
在 t=10ms 时,调用 `getValue('a')`,立即调用 `queryMultiple(['a']) `并立即返回结果。
在 t=20ms 时,调用 `getValue('b')`,但查询需要等待。
在 t=30ms 时,调用 `getValue('c')`,但查询需要等待。
在 t=110ms 时,调用 `queryMultiple(['b', 'c']) `并立即返回结果。

示例 2;**

复制代码
输入:
queryMultiple = async function(keys) {
await new Promise(res => setTimeout(res, 100));
return keys.map(key => key + '!');
}
t = 100
calls = [
{"key": "a", "time": 10},
{"key": "b", "time": 20},
{"key": "c", "time": 30}
]
输出:[
{"resolved": "a!", "time": 110},
{"resolved": "b!", "time": 210},
{"resolved": "c!", "time": 210}
]
解释:
这个例子与示例 1 相同,只是在 `queryMultiple `中有一个 100ms 的延迟。结果也相同,只是 promise 的解析时间延迟了 100ms。

示例 3:**

复制代码
输入:
queryMultiple = async function(keys) {
await new Promise(res => setTimeout(res, keys.length * 100));
return keys.map(key => key + '!');
}
t = 100
calls = [
{"key": "a", "time": 10},
{"key": "b", "time": 20},
{"key": "c", "time": 30},
{"key": "d", "time": 40},
{"key": "e", "time": 250}
{"key": "f", "time": 300}
]
输出:[
{"resolved":"a!","time":110},
{"resolved":"e!","time":350},
{"resolved":"b!","time":410},
{"resolved":"c!","time":410},
{"resolved":"d!","time":410},
{"resolved":"f!","time":450}
]
解释:
在 t=10ms 时,调用 `queryMultiple(['a']) `,在 t=110ms 时解析。
在 t=110ms 时,调用 `queryMultiple(['b', 'c', 'd']) `,在 t=410ms 时解析。
在 t=250ms 时,调用 `queryMultiple(['e']) `,在 t=350ms 时解析。
在 t=350ms 时,调用 `queryMultiple(['f']) `,在 t=450ms 时解析。

提示:

  • 0 <= t <= 1000

  • 0 <= calls.length <= 10

  • 1 <= key.length <= 100

  • 所有的键值都是唯一的

解题思路

多次 g e t V a l u e getValue getValue 要在节流窗口 t t t 内合并成一次 q u e r y M u l t i p l e queryMultiple queryMultiple,且首次调用立即发出。对每个键单独请求正确但浪费;无节流则会把并发键拆成多次往返。

记录上次实际发出的时间:若距今不少于 t t t 或尚无在途批次,立刻用当前键调用;否则把键与对应 r e s o l v e resolve resolve 推进队列,在窗口结束时一次性提交。返回的 Promise 在批次结果按键位对齐后兑现。

代码实现

typescript 复制代码
class QueryBatcher {
    private queryMultiple: (keys: number[]) => void;
    private t: number;
    private queue: number[] = [];
    private resolvers: Array<(v: number) => void> = [];
    private timer: ReturnType<typeof setTimeout> | null = null;
    private lastEmit: number = -Infinity;

    constructor(queryMultiple: (keys: number[]) => void, t: number) {
        this.queryMultiple = queryMultiple;
        this.t = t;
    }

    async getValue(key: number): Promise<number> {
        return new Promise<number>((resolve) => {
            const now = Date.now();
            // 不在窗口期(队列空)且冷却已过:当前键立即单独发出
            if (this.queue.length === 0 && now - this.lastEmit >= this.t) {
                this.emit([key], [resolve]);
                return;
            }
            // 处于窗口期:键与 resolve 推进队列,窗口截止时统一提交
            this.queue.push(key);
            this.resolvers.push(resolve);
            if (this.timer === null) {
                const wait = Math.max(0, this.lastEmit + this.t - Date.now());
                this.timer = setTimeout(() => this.flush(), wait);
            }
        });
    }

    private flush(): void {
        this.timer = null;
        const keys = this.queue.slice();
        const res = this.resolvers.slice();
        this.queue = [];
        this.resolvers = [];
        if (keys.length) this.emit(keys, res);
    }

    private emit(keys: number[], res: Array<(v: number) => void>): void {
        this.lastEmit = Date.now();
        this.queryMultiple(keys, (answers: number[]) => {
            for (let i = 0; i < keys.length; i++) res[i](answers[i]);
        });
    }
}

复杂度分析

  • 时间复杂度 :O(n),n 为批处理键总数,每次查询批量传递。
  • 空间复杂度 :O(n),需保存待批处理的键与结果映射。

2757. Generate Circular Array Values(生成循环数组的值 🔒)

题目类型:JavaScript

题目描述

给定你一个 循环 数组 arr 和一个整数 startIndex ,返回一个生成器对象 gen ,它从 arr 中生成值。第一次调用 gen.next() 时,它应该生成 arr[startIndex] 。每次调用 gen.next() 时,都会传入一个整数参数 jump(例如:gen.next(-3) )。

  • 如果 jump 是正数,则索引应该增加该值,但如果当前索引是最后一个索引,则应跳转到第一个索引。

  • 如果 jump 是负数,则索引应减去该值的绝对值,但如果当前索引是第一个索引,则应跳转到最后一个索引。

示例 1:**

复制代码
输入:arr = [1,2,3,4,5], steps = [1,2,6], startIndex = 0
输出:[1,2,4,5]
解释:
const gen = cycleGenerator(arr, startIndex);
gen.next().value;  // 1, index = startIndex = 0
gen.next(1).value; // 2, index = 1, 0 -> 1
gen.next(2).value; // 4, index = 3, 1 -> 2 -> 3
gen.next(6).value; // 5, index = 4, 3 -> 4 -> 0 -> 1 -> 2 -> 3 -> 4

示例 2:**

复制代码
输入:arr = [10,11,12,13,14,15], steps = [1,4,0,-1,-3], startIndex = 1
输出:[11,12,10,10,15,12]
解释:
const gen = cycleGenerator(arr, startIndex);
gen.next().value;   // 11, index = 1
gen.next(1).value;  // 12, index = 2
gen.next(4).value;  // 10, index = 0
gen.next(0).value;  // 10, index = 0
gen.next(-1).value; // 15, index = 5
gen.next(-3).value; // 12, index = 2

示例 3:**

复制代码
输入:arr = [2,4,6,7,8,10], steps = [-4,5,-3,10], startIndex = 3
输出:[7,10,8,4,10]
解释:
const gen = cycleGenerator(arr, startIndex);
gen.next().value   // 7,  index = 3
gen.next(-4).value // 10, index = 5
gen.next(5).value  // 8,  index = 4
gen.next(-3).value // 4,  index = 1
gen.next(10).value // 10, index = 5

提示:

  • 1 <= arr.length <= 104

  • 1 <= steps.length <= 100

  • -104 <= steps[i], arr[i] <= 104

  • 0 <= startIndex < arr.length

解题思路

生成器按收到的步长在环上跳转并产出当前值。自行维护取模下标即可,不必每次拷贝数组。

用 y i e l d yield yield 给出 a r r s t a r t I n d e x arrstartIndex arrstartIndex,下一次把传入的 j u m p jump jump 加到下标上,并对 n n n 做正向取模,以兼容负数步长。

代码实现

typescript 复制代码
function* cycleGenerator(arr: number[], startIndex: number): Generator<number, void, number> {
    const n = arr.length;
    while (true) {
        const jump = yield arr[startIndex];
        startIndex = (((startIndex + jump) % n) + n) % n;
    }
}
/**
 *  const gen = cycleGenerator([1,2,3,4,5], 0);
 *  gen.next().value  // 1
 *  gen.next(1).value // 2
 *  gen.next(2).value // 4
 *  gen.next(6).value // 5
 */

复杂度分析

  • 时间复杂度 :O(1) 每次调用,索引按模 arr.length 前进。
  • 空间复杂度 :O(1),生成器只持有一个当前索引。

2758. Next Day(下一天 🔒)

题目类型:JavaScript

题目描述

请你编写一个有关日期对象的方法,使得任何日期对象都可以调用 date.nextDay() 方法,然后返回调用日期对象的下一天,格式为 YYYY-MM-DD 。

示例 1:

复制代码
输入:date = "2014-06-20"
输出:"2014-06-21"
解释:
const date = new Date("2014-06-20");
date.nextDay(); // "2014-06-21"

示例 2:**

复制代码
输入:date = "2017-10-31"
输出: "2017-11-01"
解释:日期 2017-10-31 的下一天是 2017-11-01.

Constraints:

  • new Date(date) 是一个有效的日期对象

解题思路

给 D a t e Date Date 增加返回次日 Y Y Y Y - M M - D D YYYY\text{-}MM\text{-}DD YYYY-MM-DD 的方法。手写闰年与月份天数容易出错,标准库已处理进位。

复制当前时间戳后 s e t D a t e ( g e t D a t e ( ) + 1 ) setDate(getDate()+1) setDate(getDate()+1),再取 ISO 字符串的日期部分,避免改动原对象。

代码实现

typescript 复制代码
declare global {
    interface Date {
        nextDay(): string;
    }
}

Date.prototype.nextDay = function () {
    const date = new Date(this.valueOf());
    date.setDate(date.getDate() + 1);
    return date.toISOString().slice(0, 10);
};

/**
 * const date = new Date("2014-06-20");
 * date.nextDay(); // "2014-06-21"
 */

复杂度分析

  • 时间复杂度 :O(1),日期加减一天。
  • 空间复杂度 :O(1),仅一个日期对象。

2759. Convert JSON String to Object(将 JSON 字符串转换为对象 🔒)

题目类型:JavaScript

题目描述

给定一个字符串 str ,返回 JSON 解析后的 parsedStr 。你可以假设 str 是一个有效的 JSON 字符串,因此它只包含字符串、数字、数组、对象、布尔值和 null。str 不会包含不可见字符和转义字符。

请在不使用内置的 JSON.parse 方法的情况下解决此问题。

示例 1:**

复制代码
输入:str = '{"a":2,"b":[1,2,3]}'
输出:{"a":2,"b":[1,2,3]}
解释:返回由 JSON 字符串表示的对象。

示例 2:**

复制代码
输入:str = 'true'
输出:true
解释:原始类型是有效的 JSON。

示例 3:**

复制代码
输入:str = '[1,5,"false",{"a":2}]'
输出:[1,5,"false",{"a":2}]
解释:返回由 JSON 字符串表示的数组。

提示:

  • str 是一个有效的 JSON 字符串

  • 1 <= str.length <= 105

解题思路

在禁止 e v a l eval eval 与 J S O N . p a r s e JSON.parse JSON.parse 的前提下解析一段合法 JSON。正则切分难以处理嵌套与转义。

维护下标 i i i,按当前字符分派:对象、数组、字符串、数字与三个字面量各写一个递归下降函数。字符串处理反斜杠,复合结构在逗号与闭合括号处结束,从根调用一次即可。

代码实现

typescript 复制代码
function jsonParse(str: string): any {
    const n = str.length;
    let i = 0;

    const parseTrue = (): boolean => {
        i += 4;
        return true;
    };

    const parseFalse = (): boolean => {
        i += 5;
        return false;
    };

    const parseNull = (): null => {
        i += 4;
        return null;
    };

    const parseNumber = (): number => {
        let s = '';
        while (i < n) {
            const c = str[i];
            if (c === ',' || c === '}' || c === ']') {
                break;
            }
            s += c;
            i++;
        }
        return Number(s);
    };

    const parseArray = (): any[] => {
        const arr: any[] = [];
        i++;
        while (i < n) {
            const c = str[i];
            if (c === ']') {
                i++;
                break;
            }
            if (c === ',') {
                i++;
                continue;
            }
            const value = parseValue();
            arr.push(value);
        }
        return arr;
    };

    const parseString = (): string => {
        let s = '';
        i++;
        while (i < n) {
            const c = str[i];
            if (c === '"') {
                i++;
                break;
            }
            if (c === '\\') {
                i++;
                s += str[i];
            } else {
                s += c;
            }
            i++;
        }
        return s;
    };

    const parseObject = (): any => {
        const obj: any = {};
        i++;
        while (i < n) {
            const c = str[i];
            if (c === '}') {
                i++;
                break;
            }
            if (c === ',') {
                i++;
                continue;
            }
            const key = parseString();
            i++;
            const value = parseValue();
            obj[key] = value;
        }
        return obj;
    };
    const parseValue = (): any => {
        const c = str[i];
        if (c === '{') {
            return parseObject();
        }
        if (c === '[') {
            return parseArray();
        }
        if (c === '"') {
            return parseString();
        }
        if (c === 't') {
            return parseTrue();
        }
        if (c === 'f') {
            return parseFalse();
        }
        if (c === 'n') {
            return parseNull();
        }
        return parseNumber();
    };
    return parseValue();
}

复杂度分析

  • 时间复杂度 :O(n),n 为 JSON 字符串长度,递归/迭代解析一次。
  • 空间复杂度 :O(d),d 为嵌套深度(递归栈)。

2760. Longest Even Odd Subarray With Threshold(最长奇偶子数组)

题目描述

给你一个下标从 0 开始的整数数组 nums 和一个整数 threshold。

请你从 nums 的子数组中找出以下标 l 开头、下标 r 结尾(0 <= l <= r < nums.length)

且满足以下条件的【最长子数组】:

1. nums[l] % 2 == 0;

  1. 对于范围 `l`, `r`-1 内的所有下标 i,nums[i] % 2 != nums[i+1] % 2(奇偶交替);

  2. 对于范围 `l`, `r` 内的所有下标 i,nums[i] <= threshold。

以整数形式返回满足要求的最长子数组的长度。

示例 1

复制代码
nums = [3,2,5,4], threshold = 5 -> 3(取 [2,5,4])

示例 2

复制代码
nums = [1,2], threshold = 2 -> 1(取 [2])

示例 3

复制代码
nums = [2,3,4,5], threshold = 4 -> 3(取 [2,3,4])

提示:

  • 1 <= nums.length <= 100;1 <= nums[i] <= 100;1 <= threshold <= 100。

解题思路

解法一:枚举起点扩展,主提交版本

枚举每个偶数且 <= threshold 的起点 l,然后向右不断扩展 r,要求每一步满足

奇偶交替且值 <= threshold,取最大长度。由于长度仅限制在 100,O(n^2) 足够。

解法二:暴力枚举所有子数组,对照

枚举每个 (l, r) 区间并完整检查三条约束,

取满足条件的最大长度。

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

代码实现

java 复制代码
public class Solution {

    public int longestAlternatingSubarray(int[] nums, int threshold) {
        int ans = 0;
        int n = nums.length;
        for (int l = 0; l < n; l++) {
            if (nums[l] % 2 != 0 || nums[l] > threshold) continue;
            int r = l + 1;
            while (r < n && nums[r] % 2 != nums[r - 1] % 2 && nums[r] <= threshold) {
                r++;
            }
            ans = Math.max(ans, r - l);
        }
        return ans;
    }

    public int longestAlternatingSubarrayBrute(int[] nums, int threshold) {
        int n = nums.length;
        int ans = 0;
        for (int l = 0; l < n; l++) {
            for (int r = l; r < n; r++) {
                if (nums[l] % 2 != 0) break; // 起点必须偶数
                boolean ok = true;
                for (int i = l; i <= r; i++) {
                    if (nums[i] > threshold) { ok = false; break; }
                    if (i > l && nums[i] % 2 == nums[i - 1] % 2) { ok = false; break; }
                }
                if (ok) ans = Math.max(ans, r - l + 1);
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.longestAlternatingSubarray(new int[]{3, 2, 5, 4}, 5) == 3 ? "PASS" : "FAIL");
        System.out.println(sol.longestAlternatingSubarray(new int[]{1, 2}, 2) == 1 ? "PASS" : "FAIL");
        System.out.println(sol.longestAlternatingSubarray(new int[]{2, 3, 4, 5}, 4) == 3 ? "PASS" : "FAIL");

        // 随机对拍
        java.util.Random rnd = new java.util.Random(2760);
        boolean ok = true;
        for (int t = 0; t < 3000; t++) {
            int n = 1 + rnd.nextInt(15);
            int[] nums = new int[n];
            for (int i = 0; i < n; i++) nums[i] = 1 + rnd.nextInt(20);
            int th = 1 + rnd.nextInt(20);
            int r1 = sol.longestAlternatingSubarray(nums, th);
            int r2 = sol.longestAlternatingSubarrayBrute(nums, th);
            if (r1 != r2) {
                ok = false;
                System.out.println("Mismatch nums=" + java.util.Arrays.toString(nums) + " th=" + th + " main=" + r1 + " brute=" + r2);
                break;
            }
        }
        System.out.println(ok ? "2760 全部 PASS" : "2760 存在不一致");
    }
}

复杂度分析

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

相关推荐
俊俊几年的22 分钟前
动态规划(Dynamic Programming)详解:从入门到 LeetCode Hot 100
linux·c++·算法·leetcode·嵌入式·动态规划·c
金銀銅鐵10 小时前
带您看懂字节码指令(二):空循环里有什么
java·后端·ai编程
吴名氏.11 小时前
Java转AI Agent教程(第十三周)混合检索与重排
java·数据库·人工智能·ai·ai agent
.痞老板.12 小时前
java ee--多线程(进阶)
java·java-ee
深入云栈14 小时前
Netty 4.2.x 源码深度解析 (二十三):ByteBuf 核心设计 —— 索引、类型与零拷贝
java·后端·架构
wang_shu_mo_ran15 小时前
MyBatis 从入门到进阶(一)
java·spring·mybatis
量化分析码农16 小时前
股票历史数据的复权参数对照:多级别返回的字段与数值差异
java·javascript·后端
猫猫不是喵喵.16 小时前
从0到1创建一个微服务项目
java·数据库·微服务·1024程序员节
瑞雪兆丰年兮16 小时前
JavaWeb-day13-SpringBootWeb事务管理&AOP
java·开发语言·springboot·javaweb·事务·web·aop