LeetCode1481~1500题目解析

LeetCode 1481-1500. 题解合集

文章目录


1481. Least Number of Unique Integers after K Removals(不同整数的最少数目)

题目描述

给你一个整数数组 arr 和一个整数 k。现需要从数组中恰好移除 k 个元素,

请找出移除后数组中不同整数的最少数目。

示例 1

复制代码
arr = [5,5,4], k = 1 -> 1

示例 2

复制代码
arr = [4,3,1,1,3,3,2], k = 3 -> 2

约束:

  • 1 <= arr.length <= 10^5;1 <= arr[i] <= 10^9;0 <= k <= arr.length。

解题思路

解法一:频率 + 贪心移除(主提交版本)

要使得剩余不同整数最少,应优先删除出现次数最少的整数。

统计每个数的出现次数并按次数升序排序,依次"整数整删":

若 k 足够删掉当前频次最小的整个整数则删掉(不同整数数 -1);

否则当前整数只删一部分(k 用尽),不同整数数不变,循环结束。

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

解法二:频次桶 + 贪心(对照)

用数组 freqCntf 记录"出现次数为 f 的整数个数"(频次最大 <= n),

从 f = 1 开始从小到大贪心削减,免去排序。

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

代码实现

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

public class Solution {

    public int findLeastNumOfUniqueInts(int[] arr, int k) {
        Map<Integer, Integer> freq = new HashMap<>();
        for (int x : arr) {
            freq.put(x, freq.getOrDefault(x, 0) + 1);
        }
        int m = freq.size();
        int[] cnt = new int[m];
        int idx = 0;
        for (int c : freq.values()) {
            cnt[idx++] = c;
        }
        Arrays.sort(cnt); // 出现次数升序
        int remain = m;   // 剩余不同整数个数
        for (int c : cnt) {
            if (k >= c) {
                k -= c;       // 整个删掉这种整数
                remain--;
            } else {
                break;        // 删不完当前整数,后面次数更大更删不完
            }
        }
        return remain;
    }

    public int findLeastNumOfUniqueIntsBucket(int[] arr, int k) {
        Map<Integer, Integer> freq = new HashMap<>();
        for (int x : arr) {
            freq.put(x, freq.getOrDefault(x, 0) + 1);
        }
        int n = arr.length;
        // freqCnt[f] = 出现次数恰好为 f 的不同整数个数
        int[] freqCnt = new int[n + 1];
        for (int c : freq.values()) {
            freqCnt[c]++;
        }
        int remain = freq.size();
        for (int f = 1; f <= n && k > 0; f++) {
            while (freqCnt[f] > 0 && k >= f) {
                k -= f;
                freqCnt[f]--;
                remain--;
            }
        }
        return remain;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.findLeastNumOfUniqueInts(new int[]{5, 5, 4}, 1)); // 1
        System.out.println(s.findLeastNumOfUniqueInts(new int[]{4, 3, 1, 1, 3, 3, 2}, 3)); // 2
        System.out.println(s.findLeastNumOfUniqueInts(new int[]{2, 1, 1, 3, 3, 3}, 3)); // 1
        System.out.println(s.findLeastNumOfUniqueIntsBucket(new int[]{5, 5, 4}, 1)); // 1
        System.out.println(s.findLeastNumOfUniqueIntsBucket(new int[]{4, 3, 1, 1, 3, 3, 2}, 3)); // 2
        System.out.println(s.findLeastNumOfUniqueIntsBucket(new int[]{2, 1, 1, 3, 3, 3}, 3)); // 1
    }
}

复杂度分析

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

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


1482. Minimum Number of Days to Make m Bouquets(制作 m 束花所需的最少天数)

题目描述

给你一个整数数组 bloomDay,以及两个整数 m 和 k。

现需要制作 m 束花。制作花束时,需要使用花园中相邻的 k 朵花。

花园中有 n 朵花,第 i 朵花会在 bloomDay[i] 时盛开,恰好可以用于一束花中。

请你返回从花园中摘 m 束花需要等待的最少的天数。如果不能摘到 m 束花则返回 -1。

示例 1

复制代码
bloomDay = [1,10,3,10,2], m = 3, k = 1 -> 3

示例 2

复制代码
bloomDay = [1,10,3,10,2], m = 3, k = 2 -> -1

示例 3

复制代码
bloomDay = [7,7,7,7,12,7,7], m = 2, k = 3 -> 12

示例 4

复制代码
bloomDay = [1000000000,1000000000], m = 1, k = 1 -> 1000000000

示例 5

复制代码
bloomDay = [1,10,2,9,3,8,4,7,5,6], m = 4, k = 2 -> 9

约束:

  • 1 <= n <= 10^5;1 <= bloomDay[i] <= 10^9;1 <= m <= 10^6;1 <= k <= n。

解题思路

解法一:二分答案(主提交版本)

等待天数 d 单调:d 越大能做的花束越多。因此对 d 在 min, max 上二分,

check(d) 扫描 bloomDay,统计连续"盛开(<= d)"段的长度,每段贡献 len / k 束花。

若总束数 >= m,说明 d 可行,收缩上界;否则放大下界。

特判:n < m * k 时直接返回 -1(花不够)。

复杂度:O(n log(max-min)) 时间,O(1) 空间。

解法二:二分 + 跳跃式 check(对照)

同样二分天数,但 check 中遇到没开的花直接跳到下一位置统计。

复杂度:O(n log(max-min)) 时间,O(1) 空间。

代码实现

java 复制代码
public class Solution {

    public int minDays(int[] bloomDay, int m, int k) {
        int n = bloomDay.length;
        if ((long) n < (long) m * k) {
            return -1;
        }
        int lo = Integer.MAX_VALUE, hi = Integer.MIN_VALUE;
        for (int d : bloomDay) {
            lo = Math.min(lo, d);
            hi = Math.max(hi, d);
        }
        while (lo < hi) {
            int mid = lo + (hi - lo) / 2;
            if (canMake(bloomDay, m, k, mid)) {
                hi = mid;
            } else {
                lo = mid + 1;
            }
        }
        return lo;
    }

    private boolean canMake(int[] bloomDay, int m, int k, int day) {
        int bouquets = 0, cnt = 0;
        for (int d : bloomDay) {
            if (d <= day) {
                cnt++; // 盛开,延续当前相邻段
                if (cnt == k) {
                    bouquets++;
                    cnt = 0;
                }
            } else {
                cnt = 0; // 未开,段断开
            }
        }
        return bouquets >= m;
    }

    public int minDaysJump(int[] bloomDay, int m, int k) {
        int n = bloomDay.length;
        if ((long) n < (long) m * k) {
            return -1;
        }
        int lo = Integer.MAX_VALUE, hi = Integer.MIN_VALUE;
        for (int d : bloomDay) {
            lo = Math.min(lo, d);
            hi = Math.max(hi, d);
        }
        while (lo < hi) {
            int mid = lo + (hi - lo) / 2;
            if (canMakeJump(bloomDay, m, k, mid)) {
                hi = mid;
            } else {
                lo = mid + 1;
            }
        }
        return lo;
    }

    private boolean canMakeJump(int[] bloomDay, int m, int k, int day) {
        int n = bloomDay.length;
        int bouquets = 0;
        int i = 0;
        while (i < n) {
            if (bloomDay[i] > day) {
                i++;               // 这一朵没开,跳过
                continue;
            }
            int len = 0;
            while (i < n && bloomDay[i] <= day) { // 统计连续盛开段
                len++;
                i++;
            }
            bouquets += len / k;
            if (bouquets >= m) {
                return true;
            }
        }
        return bouquets >= m;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.minDays(new int[]{1, 10, 3, 10, 2}, 3, 1)); // 3
        System.out.println(s.minDays(new int[]{1, 10, 3, 10, 2}, 3, 2)); // -1
        System.out.println(s.minDays(new int[]{7, 7, 7, 7, 12, 7, 7}, 2, 3)); // 12
        System.out.println(s.minDays(new int[]{1000000000, 1000000000}, 1, 1)); // 1000000000
        System.out.println(s.minDays(new int[]{1, 10, 2, 9, 3, 8, 4, 7, 5, 6}, 4, 2)); // 9
        System.out.println(s.minDaysJump(new int[]{1, 10, 3, 10, 2}, 3, 1)); // 3
        System.out.println(s.minDaysJump(new int[]{1, 10, 3, 10, 2}, 3, 2)); // -1
        System.out.println(s.minDaysJump(new int[]{7, 7, 7, 7, 12, 7, 7}, 2, 3)); // 12
        System.out.println(s.minDaysJump(new int[]{1, 10, 2, 9, 3, 8, 4, 7, 5, 6}, 4, 2)); // 9
    }
}

复杂度分析

复杂度:O(n log(max-min)) 时间,O(1) 空间。


1483. Kth Ancestor of a Tree Node(树节点的第 K 个祖先)

题目描述

给你一棵树,树上有 n 个节点,按从 0 到 n-1 编号。树以父节点数组的形式给出,

其中 parent[i] 是节点 i 的父节点。树的根节点是编号为 0 的节点。

树节点的第 k 个祖先节点是从该节点到根节点路径上的第 k 个节点。

实现 TreeAncestor 类:

  • TreeAncestor(int n, int[] parent) 对树和父数组中的节点数初始化对象;
  • int getKthAncestor(int node, int k) 返回节点 node 的第 k 个祖先节点,
    如果不存在这样的祖先节点,返回 -1。

示例

复制代码
n = 7, parent = [-1,0,0,1,1,2,2]
getKthAncestor(3,1) = 1;getKthAncestor(5,2) = 0;getKthAncestor(6,3) = -1

约束:

  • 1 <= k <= n <= 5 * 10^4;parent[0] == -1;至多查询 5 * 10^4 次。

解题思路

解法一:倍增法 / Binary Lifting(主提交版本)

预处理 upji = 节点 i 向上跳 2^j 步到达的祖先(-1 表示越过根)。

查询时把 k 拆成二进制位,逐位向上跳;任意一次跳到 -1 立即返回 -1。

单次查询 O(log n),预处理 O(n log n)。

解法二:朴素上跳(对照)

只存 parent 数组,查询时循环 k 次向上跳。单次查询最坏 O(k)。

复杂度:预处理 O(n),单次查询 O(k)(最坏 O(n))。

代码实现

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

public class Solution {

    public static class TreeAncestor {
        private static final int LOG = 17; // 2^16 = 65536 > 5*10^4
        private int[][] up;

        public TreeAncestor(int n, int[] parent) {
            up = new int[LOG][n];
            for (int j = 0; j < LOG; j++) {
                Arrays.fill(up[j], -1);
            }
            for (int i = 0; i < n; i++) {
                up[0][i] = parent[i];
            }
            for (int j = 1; j < LOG; j++) {
                for (int i = 0; i < n; i++) {
                    int mid = up[j - 1][i];
                    up[j][i] = (mid == -1) ? -1 : up[j - 1][mid];
                }
            }
        }

        public int getKthAncestor(int node, int k) {
            int cur = node;
            for (int j = 0; j < LOG && cur != -1; j++) {
                if (((k >> j) & 1) == 1) {
                    cur = up[j][cur];
                }
            }
            return cur;
        }
    }

    public static class TreeAncestorNaive {
        private int[] parent;

        public TreeAncestorNaive(int n, int[] parent) {
            this.parent = parent;
        }

        public int getKthAncestor(int node, int k) {
            int cur = node;
            for (int i = 0; i < k; i++) {
                cur = parent[cur];
                if (cur == -1) {
                    return -1;
                }
            }
            return cur;
        }
    }

    public static void main(String[] args) {
        TreeAncestor t = new TreeAncestor(7, new int[]{-1, 0, 0, 1, 1, 2, 2});
        System.out.println(t.getKthAncestor(3, 1)); // 1
        System.out.println(t.getKthAncestor(5, 2)); // 0
        System.out.println(t.getKthAncestor(6, 3)); // -1

        TreeAncestorNaive tn = new TreeAncestorNaive(7, new int[]{-1, 0, 0, 1, 1, 2, 2});
        System.out.println(tn.getKthAncestor(3, 1)); // 1
        System.out.println(tn.getKthAncestor(5, 2)); // 0
        System.out.println(tn.getKthAncestor(6, 3)); // -1
    }
}

复杂度分析

复杂度:预处理 O(n),单次查询 O(k)(最坏 O(n))。


1484. Group Sold Products By The Date(按日期分组销售产品)

题目类型:数据库(SQL)

题目描述

表 Activities:

复制代码
+-------------+---------+
| 列名         | 类型    |
+-------------+---------+
| sell_date   | date    |
| product     | varchar |
+-------------+---------+
该表没有主键(具有唯一值的列)。它可能包含重复项。
此表的每一行都包含产品名称和在市场上销售的日期。

编写解决方案找出每个日期、销售的不同产品的数量及其名称。

每个日期的销售产品名称应按词典序排列。

返回按 sell_date 排序的结果表。

结果表结果格式如下例所示。

示例 1:

复制代码
`**输入:**
Activities` 表:
+------------+-------------+
| sell_date  | product     |
+------------+-------------+
| 2020-05-30 | Headphone   |
| 2020-06-01 | Pencil      |
| 2020-06-02 | Mask        |
| 2020-05-30 | Basketball  |
| 2020-06-01 | Bible       |
| 2020-06-02 | Mask        |
| 2020-05-30 | T-Shirt     |
+------------+-------------+
**输出:**
+------------+----------+------------------------------+
| sell_date  | num_sold | products                     |
+------------+----------+------------------------------+
| 2020-05-30 | 3        | Basketball,Headphone,T-shirt |
| 2020-06-01 | 2        | Bible,Pencil                 |
| 2020-06-02 | 1        | Mask                         |
+------------+----------+------------------------------+
**解释:**
对于2020-05-30,出售的物品是 (Headphone, Basketball, T-shirt),按词典序排列,并用逗号 ',' 分隔。
对于2020-06-01,出售的物品是 (Pencil, Bible),按词典序排列,并用逗号分隔。
对于2020-06-02,出售的物品是 (Mask),只需返回该物品名。

解题思路

思考

按销售日期统计去重后的商品种数,并把名称按字典序拼成串。GROUP BY sell_date 后用 COUNT(DISTINCT product) 与 GROUP_CONCAT(DISTINCT product) 即可。

考察GROUP BY + GROUP_CONCAT 排序:按日期分组统计不同产品数,产品名用逗号拼接且字典序。

代码实现

sql 复制代码
SELECT
    sell_date,
    COUNT(DISTINCT product) AS num_sold,
    GROUP_CONCAT(DISTINCT product) AS products
FROM Activities
GROUP BY sell_date
ORDER BY sell_date;

复杂度分析

本题为数据库查询题,不涉及传统算法的时间/空间复杂度,执行效率由数据库优化器决定;建议对查询涉及的过滤列与连接列建立索引以加速。


1485. Clone Binary Tree With Random Pointer(克隆含随机指针的二叉树)

题目描述

给你一个二叉树,树中每个节点都含有一个附加的随机指针,

该指针可以指向树中的任何节点或者指向空(null)。

请返回该树的深拷贝。

该树以 Node 类的形式给出,而你需要以 NodeCopy 类的形式返回克隆得到的树。

NodeCopy 类和 Node 类定义一致。

示例 1

复制代码
root = [[1,null],null,[4,3],[7,0]] -> 相同结构的拷贝

示例 2

复制代码
root = [[1,4],null,[1,0],null,[1,5],[1,5]](随机指针可指向自身)

约束:

  • 节点数目范围 0, 1000;每个节点的值范围 1, 10\^6。

解题思路

解法一:DFS + 哈希映射(主提交版本)

第一遍 DFS 按树结构创建 NodeCopy 节点,并用 HashMap 记录 原节点 -> 拷贝节点;

第二遍 DFS 填充每个拷贝节点的 left/right/random 指针。

也可以一遍 DFS 同时完成:创建节点时先递归创建左右子树并填指针,

再通过映射找到 random 指向的拷贝。

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

解法二:递归一次完成(对照)

clone(node) 若映射中已存在直接返回;新建拷贝节点后递归克隆左右子树,

从而保证 random 指向的节点最终都出现在映射中,再统一修正 random。

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

代码实现

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

class Node {
    int val;
    Node left;
    Node right;
    Node random;

    Node(int val) {
        this.val = val;
    }
}

class NodeCopy {
    int val;
    NodeCopy left;
    NodeCopy right;
    NodeCopy random;

    NodeCopy(int val) {
        this.val = val;
    }
}

public class Solution {

    public NodeCopy copyRandomBinaryTree(Node root) {
        Map<Node, NodeCopy> map = new HashMap<>();
        cloneTree(root, map);                 // 第一遍:只建树结构并映射
        fill(root, map);                      // 第二遍:填左右与 random
        return root == null ? null : map.get(root);
    }

    private NodeCopy cloneTree(Node node, Map<Node, NodeCopy> map) {
        if (node == null) {
            return null;
        }
        NodeCopy c = new NodeCopy(node.val);
        map.put(node, c);
        c.left = cloneTree(node.left, map);
        c.right = cloneTree(node.right, map);
        return c;
    }

    private void fill(Node node, Map<Node, NodeCopy> map) {
        if (node == null) {
            return;
        }
        NodeCopy c = map.get(node);
        if (node.left != null) {
            c.left = map.get(node.left);
        }
        if (node.right != null) {
            c.right = map.get(node.right);
        }
        if (node.random != null) {
            c.random = map.get(node.random);
        }
        fill(node.left, map);
        fill(node.right, map);
    }

    private Map<Node, NodeCopy> memo = new HashMap<>();

    public NodeCopy copyRandomBinaryTreeOnePass(Node root) {
        memo.clear();
        return cloneOne(root);
    }

    private NodeCopy cloneOne(Node node) {
        if (node == null) {
            return null;
        }
        if (memo.containsKey(node)) {
            return memo.get(node);
        }
        NodeCopy c = new NodeCopy(node.val);
        memo.put(node, c);          // 先注册,避免成环死循环(虽然树无环,但规范起见)
        c.left = cloneOne(node.left);
        c.right = cloneOne(node.right);
        c.random = node.random == null ? null : cloneOne(node.random);
        return c;
    }

    public static void main(String[] args) {
        // 构造示例 1:[[1,null],null,[4,3],[7,0]]
        Node root = new Node(1);
        Node four = new Node(4);
        Node seven = new Node(7);
        root.right = four;   // 节点 0 的右子是节点 2
        four.left = seven;   // 节点 2 的左子是节点 3
        root.random = null;  // 节点 1 的 random 为 null
        four.random = seven; // 节点 4 的 random 指向 7
        seven.random = root; // 节点 7 的 random 指向 1

        Solution s = new Solution();
        NodeCopy c = s.copyRandomBinaryTree(root);
        System.out.println("root -> " + c.val);              // 1
        System.out.println("root.right -> " + c.right.val);  // 4
        System.out.println("root.right.left -> " + c.right.left.val); // 7
        System.out.println("root.right.random.val -> " + c.right.random.val); // 7
        System.out.println("root.right.left.random.val -> " + c.right.left.random.val); // 1
        System.out.println("copied random points to copy: " + (c.right.random == c.right.left)); // true

        NodeCopy d = s.copyRandomBinaryTreeOnePass(root);
        System.out.println("onepass root.right.random.val -> " + d.right.random.val); // 7
        System.out.println("onepass copied random points to copy: " + (d.right.random == d.right.left)); // true
    }
}

复杂度分析

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


1486. XOR Operation in an Array(数组异或操作)

题目描述

给你两个整数 n 和 start。

数组 nums 定义为:nums[i] = start + 2*i(下标从 0 开始)且 n == nums.length。

请返回 nums 中所有元素按位异或(XOR)后得到的结果。

示例 1

复制代码
n = 5, start = 0 -> 8

示例 2

复制代码
n = 4, start = 3 -> 8

示例 3

复制代码
n = 1, start = 7 -> 7

示例 4

复制代码
n = 10, start = 5 -> 2

约束:

  • 1 <= n <= 1000;0 <= start <= 1000。

解题思路

解法一:模拟迭代(主提交版本)

直接按公式逐项异或:xor( start + 2i )。
复杂度:O(n) 时间,O(1) 空间。
解法二:数学 O(1) 公式(对照)
利用前缀异或 xor(0...x) 的周期性质:
xorOfRange(x) = {x, 1, x+1, 0}(按 x%4 分组)。
原式 = xor_{i=0}^{n-1} (start + 2i)。
若 start 为偶数,start + 2i = 2
(start/2 + i),整体左移一位:

= 2 * xorOfRange(start/2 + n - 1) ^ xorOfRange(start/2 - 1);

若 start 为奇数,首项 start 为奇数:先把最低位单独看,

n 为奇数时结果或上 1,然后高位部分等价于 (start+1)/2 起的偶数序列处理。

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

代码实现

java 复制代码
public class Solution {

    public int xorOperation(int n, int start) {
        int res = 0;
        for (int i = 0; i < n; i++) {
            res ^= start + 2 * i;
        }
        return res;
    }

    public int xorOperationMath(int n, int start) {
        // xor(0..x) 前缀异或函数:周期 4
        // 0^1^...^x 的结果
        // 第0位:
        // 先把 start 拆为 start = 2*s0 或 2*s0+1
        int s0 = start >> 1;         // start / 2
        boolean oddStart = (start & 1) == 1;
        // 基础部分:2 * ( xor(0..s0+n-1) ^ xor(0..s0-1) )
        int base = 2 * (xorRange(s0 + n - 1) ^ xorRange(s0 - 1));
        // 若 start 为奇数,每一项最低位为 1,共 n 项;n 为奇数则最低位合计为 1
        if (oddStart && (n & 1) == 1) {
            return base | 1;
        }
        return base;
    }

    private int xorRange(int x) {
        if (x < 0) {
            return 0;
        }
        switch (x & 3) {
            case 0: return x;
            case 1: return 1;
            case 2: return x + 1;
            default: return 0; // x % 4 == 3
        }
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.xorOperation(5, 0)); // 8
        System.out.println(s.xorOperation(4, 3)); // 8
        System.out.println(s.xorOperation(1, 7)); // 7
        System.out.println(s.xorOperation(10, 5)); // 2
        System.out.println(s.xorOperationMath(5, 0)); // 8
        System.out.println(s.xorOperationMath(4, 3)); // 8
        System.out.println(s.xorOperationMath(1, 7)); // 7
        System.out.println(s.xorOperationMath(10, 5)); // 2
    }
}

复杂度分析

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

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


1487. Making File Names Unique(保证文件名唯一)

题目描述

给你一个长度为 n 的字符串数组 names。你将会在文件系统中创建 n 个文件夹:

在第 i 分钟,新建名为 namesi 的文件夹。

由于两个文件不能共享相同的文件名,因此如果新建文件夹使用的文件名已经被占用,

系统会以 (k) 的形式为新文件夹的文件名添加后缀,其中 k 是能保证文件名唯一的

最小正整数。

返回长度为 n 的字符串数组 ans,其中 ans[i] 是创建第 i 个文件夹时系统分配给

该文件夹的实际名称。

示例 1

复制代码
["pes","fifa","gta","pes(2019)"]
-> ["pes","fifa","gta","pes(2019)"]

示例 2

复制代码
["gta","gta(1)","gta","avalon"]
-> ["gta","gta(1)","gta(2)","avalon"]

示例 3

复制代码
["onepiece","onepiece(1)","onepiece(2)","onepiece(3)","onepiece"]
-> ["onepiece","onepiece(1)","onepiece(2)","onepiece(3)","onepiece(4)"]

示例 4

复制代码
["wano","wano","wano","wano"] -> ["wano","wano(1)","wano(2)","wano(3)"]

示例 5

复制代码
["kaido","kaido(1)","kaido","kaido(1)"]
-> ["kaido","kaido(1)","kaido(2)","kaido(1)(1)"]

约束:

  • 1 <= names.length <= 5 * 10^4;namesi 由小写字母、数字和/或圆括号组成。

解题思路

解法一:哈希表记录"下一个可用编号"(主提交版本)

用 map 保存「基础名 -> 下一个应尝试的编号 k」。

处理 namesi 时:若 map 中没有该名字,直接使用并 put(name, 1);

否则从 map.get(name) 开始递增找候选 name(k),直到候选名不在已用集合中,

同时更新沿途名字的 next 计数(供将来复用)。

复杂度:均摊 O(n) 时间(每个名字至多产生一次冲突链),O(n) 空间。

解法二:已用集合 + 线性探测(对照)

维护 Set used 和 Map<String,Integer> nextCounter;

对每个名字线性探测 k,把探测过的候选也记入 nextCounter 避免重复扫描。

复杂度:均摊 O(n) 时间,O(n) 空间。

代码实现

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

public class Solution {

    public String[] getFolderNames(String[] names) {
        int n = names.length;
        String[] ans = new String[n];
        Map<String, Integer> next = new HashMap<>(); // 名字 -> 下一个可尝试的编号
        Set<String> used = new HashSet<>();
        for (int i = 0; i < n; i++) {
            String name = names[i];
            if (!used.contains(name)) {
                ans[i] = name;
                used.add(name);
                next.put(name, next.getOrDefault(name, 0) + 1);
            } else {
                int k = next.getOrDefault(name, 1);
                String cand = name + "(" + k + ")";
                while (used.contains(cand)) {
                    k++;
                    cand = name + "(" + k + ")";
                }
                ans[i] = cand;
                used.add(cand);
                next.put(name, k + 1); // 下次从 k+1 继续探测
            }
        }
        return ans;
    }

    public String[] getFolderNamesSet(String[] names) {
        int n = names.length;
        String[] ans = new String[n];
        Set<String> used = new HashSet<>();
        Map<String, Integer> counter = new HashMap<>();
        for (int i = 0; i < n; i++) {
            String name = names[i];
            if (used.add(name)) { // 首次出现
                ans[i] = name;
                continue;
            }
            int k = counter.getOrDefault(name, 1);
            String cand = name + "(" + k + ")";
            while (used.contains(cand)) {
                k++;
                cand = name + "(" + k + ")";
            }
            used.add(cand);
            counter.put(name, k);
            ans[i] = cand;
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(Arrays.toString(s.getFolderNames(
                new String[]{"pes", "fifa", "gta", "pes(2019)"})));
        System.out.println(Arrays.toString(s.getFolderNames(
                new String[]{"gta", "gta(1)", "gta", "avalon"})));
        System.out.println(Arrays.toString(s.getFolderNames(
                new String[]{"onepiece", "onepiece(1)", "onepiece(2)", "onepiece(3)", "onepiece"})));
        System.out.println(Arrays.toString(s.getFolderNames(
                new String[]{"wano", "wano", "wano", "wano"})));
        System.out.println(Arrays.toString(s.getFolderNames(
                new String[]{"kaido", "kaido(1)", "kaido", "kaido(1)"})));
        System.out.println(Arrays.toString(s.getFolderNamesSet(
                new String[]{"gta", "gta(1)", "gta", "avalon"})));
        System.out.println(Arrays.toString(s.getFolderNamesSet(
                new String[]{"kaido", "kaido(1)", "kaido", "kaido(1)"})));
    }
}

复杂度分析

复杂度:均摊 O(n) 时间(每个名字至多产生一次冲突链),O(n) 空间。

复杂度:均摊 O(n) 时间,O(n) 空间。


1488. Avoid Flood in The City(避免洪水泛滥)

题目描述

你的国家有 10^9 个湖泊,所有湖泊一开始都是空的。

当第 n 个湖泊下雨前是空的,那么它就会装满水;如果下雨前是满的,这个湖泊会发生洪水。

给你一个整数数组 rains:

  • rainsi > 0 表示第 i 天时,第 rainsi 个湖泊会下雨;
  • rainsi == 0 表示第 i 天没有湖泊会下雨,你必须选择一个湖泊并抽干这个湖泊的水。
    请返回一个数组 ans:rainsi > 0 时 ans[i] == -1;rainsi == 0 时 ans[i] 为抽干的湖泊。
    如果有多种可行解返回任意一个;如果没办法阻止洪水返回空数组。

示例 1

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

示例 2

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

示例 3

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

约束:

  • 1 <= rains.length <= 10^5;0 <= rainsi <= 10^9。

解题思路

解法一:贪心 + 有序集合(主提交版本)

依次处理每一天:

  • 下雨日 lake:若该湖已满(出现在 lastlake 记录中),则必须在其后的某个晴天
    抽干它。做法:把晴天下标放入有序集合 TreeSet;当湖第二次下雨时,在 TreeSet 中
    找大于等于 lastlake 的第一个晴天(即最近可用的晴天)将其抽干,并更新答案;
    找不到则说明无法避免洪水,返回空。
  • 晴天:把下标记入 TreeSet 备用。
    关键:一定要用"最近一次该湖下雨之后"的第一个晴天,留给更紧急的湖更多机会。
    复杂度:O(n log n) 时间,O(n) 空间。
    解法二:优先队列模拟(对照)
    预先为每个晴天收集"之后最早会再次下雨的湖":逆向遍历 rains,
    记录每湖下次下雨日 nextlake,用最小堆维护"晴天可抽干的最紧急湖"。
    复杂度:O(n log n) 时间,O(n) 空间。

代码实现

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

public class Solution {

    public int[] avoidFlood(int[] rains) {
        int n = rains.length;
        int[] ans = new int[n];
        Arrays.fill(ans, -1);
        Map<Integer, Integer> last = new HashMap<>(); // 湖 -> 最近一次下雨的下标
        TreeSet<Integer> dryDays = new TreeSet<>();   // 可抽水的晴天下标(未用)
        for (int i = 0; i < n; i++) {
            int lake = rains[i];
            if (lake > 0) {
                if (last.containsKey(lake)) {
                    // 该湖已经满,必须在其后抽干
                    Integer d = dryDays.ceiling(last.get(lake));
                    if (d == null) {
                        return new int[0]; // 无可用晴天,洪水无法避免
                    }
                    ans[d] = lake;         // 第 d 天抽干该湖
                    dryDays.remove(d);
                }
                last.put(lake, i);
            } else {
                dryDays.add(i);
            }
        }
        // 多余的晴天随便抽 1 号湖(或任何湖),保证 ans 中晴天位置非 -1
        for (int d : dryDays) {
            ans[d] = 1;
        }
        return ans;
    }

    public int[] avoidFloodHeap(int[] rains) {
        int n = rains.length;
        int[] ans = new int[n];
        Arrays.fill(ans, -1);
        Map<Integer, List<Integer>> pos = new HashMap<>(); // 湖 -> 下雨下标列表(升序)
        for (int i = 0; i < n; i++) {
            if (rains[i] > 0) {
                if (!pos.containsKey(rains[i])) {
                    pos.put(rains[i], new ArrayList<>());
                }
                pos.get(rains[i]).add(i);
            }
        }
        // 每个湖下次下雨指针
        Map<Integer, Integer> ptr = new HashMap<>();
        // 存储当前满的湖(湖号 -> 该湖下次下雨时间),Java 8 无 Pair,用 long 编码:
        // high 32 位是下次下雨时间,low 32 位是湖号
        java.util.PriorityQueue<Long> pq = new java.util.PriorityQueue<>();
        java.util.Set<Integer> full = new java.util.HashSet<>();
        for (int i = 0; i < n; i++) {
            int lake = rains[i];
            if (lake > 0) {
                if (!full.add(lake)) {
                    return new int[0]; // 满的湖再次下雨,且之前没有可抽干机会
                }
                int p = ptr.getOrDefault(lake, 0) + 1; // 推进到下一次下雨
                ptr.put(lake, p);
                List<Integer> list = pos.get(lake);
                if (p < list.size()) {
                    pq.offer(((long) list.get(p) << 32) | (lake & 0xffffffffL));
                }
            } else {
                if (!pq.isEmpty()) {
                    long top = pq.poll();
                    int nextDay = (int) (top >>> 32);
                    int victim = (int) top;
                    if (nextDay > i) {
                        ans[i] = victim;
                        full.remove(victim);
                    } else {
                        // 该湖下次下雨已过?按构造不可能(带序号单调),兜底:
                        // 视为无需操作,重新放一个 1
                        ans[i] = 1;
                    }
                } else {
                    ans[i] = 1;
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(Arrays.toString(s.avoidFlood(new int[]{1, 2, 3, 4}))); // [-1,-1,-1,-1]
        System.out.println(Arrays.toString(s.avoidFlood(new int[]{1, 2, 0, 0, 2, 1}))); // [-1,-1,2,1,-1,-1]
        System.out.println(Arrays.toString(s.avoidFlood(new int[]{1, 2, 0, 1, 2}))); // []
        System.out.println(Arrays.toString(s.avoidFlood(new int[]{1, 0, 2, 0, 2, 1}))); // 可行
        System.out.println(Arrays.toString(s.avoidFloodHeap(new int[]{1, 2, 3, 4}))); // [-1,-1,-1,-1]
        System.out.println(Arrays.toString(s.avoidFloodHeap(new int[]{1, 2, 0, 0, 2, 1}))); // [-1,-1,2,1,-1,-1]
        System.out.println(Arrays.toString(s.avoidFloodHeap(new int[]{1, 2, 0, 1, 2}))); // []
        System.out.println(Arrays.toString(s.avoidFloodHeap(new int[]{1, 0, 2, 0, 2, 1}))); // 可行
    }
}

复杂度分析

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


1489. Find Critical and Pseudo-Critical Edges in Minimum Spanning Tree(找到最小生成树里的关键边和伪关键边)

题目描述

给你一个 n 个点的带权无向连通图,节点编号为 0 到 n-1,同时还有一个数组 edges,

其中 edges[i] = `from_i`, `to_i`, `weight_i` 表示在 from_i 和 to_i 节点之间有一条带权无向边。

最小生成树 (MST) 是给定图中边的一个子集,它连接了所有节点且没有环,而且这些边的权值和最小。

请你找到给定图中最小生成树的所有关键边和伪关键边。

如果从图中删去某条边,会导致最小生成树的权值和增加,那么它是一条关键边。

伪关键边则是可能会出现在某些最小生成树中但不会出现在所有最小生成树中的边。

示例 1

复制代码
n = 5, edges = [[0,1,1],[1,2,1],[2,3,2],[0,3,2],[0,4,3],[3,4,3],[1,4,6]]
-> [[0,1],[2,3,4,5]]

示例 2

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

约束:

  • 2 <= n <= 100;1 <= edges.length <= min(200, n*(n-1)/2);weight_i <= 1000。

解题思路

解法一:Kruskal + 删边/强制选边三重计算(主提交版本)

  1. 对边按权重排序,先求出原图 MST 权值 base。
  2. 对每条边 i:
  • 若「禁用边 i 后的 MST 权值」> base(或图不连通),说明删掉它 MST 会变大
    或无法建成 -> 关键边;
  • 否则若「强制选边 i 后得到的 MST 权值」== base,说明存在某棵含边 i 的
    最小生成树 -> 伪关键边。
    复杂度:O(E^2 * n)(并查集近似 O(1)),E <= 200,可行。
    解法二:一次 Kruskal 的等价类划分(对照)
    按相同权重分批处理:先求该批内压缩后的连通分量,再做一次 Kruskal 判断每批内部
    可替换性。实现较繁琐,这里采用"对每条边跑两次 Kruskal"的朴素判定版:
    复杂度:O(E^2 * n),思路直观、正确性明确。

代码实现

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

public class Solution {

    private static class DSU {
        int[] p;

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

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

        boolean union(int a, int b) {
            int ra = find(a), rb = find(b);
            if (ra == rb) {
                return false;
            }
            p[ra] = rb;
            return true;
        }
    }

    private static class Edge {
        int u, v, w, id;

        Edge(int u, int v, int w, int id) {
            this.u = u;
            this.v = v;
            this.w = w;
            this.id = id;
        }
    }

    public List<List<Integer>> findCriticalAndPseudoCriticalEdges(int n, int[][] edges) {
        int m = edges.length;
        Edge[] es = new Edge[m];
        for (int i = 0; i < m; i++) {
            es[i] = new Edge(edges[i][0], edges[i][1], edges[i][2], i);
        }
        Arrays.sort(es, (a, b) -> a.w - b.w);

        int base = kruskal(n, es, -1, -1);
        List<Integer> critical = new ArrayList<>();
        List<Integer> pseudo = new ArrayList<>();
        for (int i = 0; i < m; i++) {
            // 禁用边 i:若 MST 权值变大或无法连通 -> 关键边
            int without = kruskal(n, es, i, -1);
            if (without > base) {
                critical.add(i);
                continue;
            }
            // 强制选边 i:若仍能得到 base 权值的 MST -> 伪关键边
            int with = kruskal(n, es, -1, i);
            if (with == base) {
                pseudo.add(i);
            }
        }
        List<List<Integer>> ans = new ArrayList<>();
        ans.add(critical);
        ans.add(pseudo);
        return ans;
    }

    private int kruskal(int n, Edge[] es, int skipId, int forceId) {
        DSU dsu = new DSU(n);
        int cost = 0, cnt = 0;
        if (forceId != -1) {
            Edge e = es[forceId];
            dsu.union(e.u, e.v);
            cost += e.w;
            cnt++;
        }
        for (Edge e : es) {
            if (e.id == skipId || e.id == forceId) {
                continue;
            }
            if (dsu.union(e.u, e.v)) {
                cost += e.w;
                cnt++;
            }
        }
        return cnt == n - 1 ? cost : Integer.MAX_VALUE;
    }

    public List<List<Integer>> findCriticalAndPseudoCriticalEdgesNaive(int n, int[][] edges) {
        int m = edges.length;
        int[][] sorted = new int[m][4]; // {w, u, v, id}
        for (int i = 0; i < m; i++) {
            sorted[i][0] = edges[i][2];
            sorted[i][1] = edges[i][0];
            sorted[i][2] = edges[i][1];
            sorted[i][3] = i;
        }
        Arrays.sort(sorted, (a, b) -> a[0] - b[0]);
        int base = mstWeight(n, sorted, -1, -1);
        List<Integer> critical = new ArrayList<>();
        List<Integer> pseudo = new ArrayList<>();
        for (int i = 0; i < m; i++) {
            if (mstWeight(n, sorted, i, -1) > base) {
                critical.add(i);
            } else if (mstWeight(n, sorted, -1, i) == base) {
                pseudo.add(i);
            }
        }
        List<List<Integer>> ans = new ArrayList<>();
        ans.add(critical);
        ans.add(pseudo);
        return ans;
    }

    private int mstWeight(int n, int[][] sorted, int skipId, int forceId) {
        Finder f = new Finder(n);
        int cost = 0, cnt = 0;
        if (forceId != -1) {
            f.union(sorted[forceId][1], sorted[forceId][2]);
            cost += sorted[forceId][0];
            cnt++;
        }
        for (int[] e : sorted) {
            if (e[3] == skipId || e[3] == forceId) {
                continue;
            }
            if (f.union(e[1], e[2])) {
                cost += e[0];
                cnt++;
            }
        }
        return cnt == n - 1 ? cost : Integer.MAX_VALUE;
    }

    private static class Finder {
        int[] p;

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

        int find(int x) {
            return p[x] == x ? x : (p[x] = find(p[x]));
        }

        boolean union(int a, int b) {
            int ra = find(a), rb = find(b);
            if (ra == rb) {
                return false;
            }
            p[ra] = rb;
            return true;
        }
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1
        int[][] e1 = {{0, 1, 1}, {1, 2, 1}, {2, 3, 2}, {0, 3, 2}, {0, 4, 3}, {3, 4, 3}, {1, 4, 6}};
        System.out.println(s.findCriticalAndPseudoCriticalEdges(5, e1)); // [[0, 1], [2, 3, 4, 5]]
        // 示例 2
        int[][] e2 = {{0, 1, 1}, {1, 2, 1}, {2, 3, 1}, {0, 3, 1}};
        System.out.println(s.findCriticalAndPseudoCriticalEdges(4, e2)); // [[], [0, 1, 2, 3]]
        System.out.println(s.findCriticalAndPseudoCriticalEdgesNaive(5, e1));
        System.out.println(s.findCriticalAndPseudoCriticalEdgesNaive(4, e2));
    }
}

复杂度分析

复杂度:O(E^2 * n)(并查集近似 O(1)),E <= 200,可行。

复杂度:O(E^2 * n),思路直观、正确性明确。


1490. Clone N-ary Tree(克隆 N 叉树)

题目描述

给定一棵 N 叉树的根节点 root,返回该树的深拷贝(克隆)。

N 叉树的每个节点都包含一个值(int)和子节点的列表(List[Node])。

输入/输出序列用层序遍历表示,每组子节点用 null 分隔。

示例 1

复制代码
root = [1,null,3,2,4,null,5,6] -> 结构相同的拷贝

约束:

  • 树深度 <= 1000;节点个数 0, 10\^4。

解题思路

解法一:DFS 递归深拷贝(主提交版本)

clone(node) = 新 Node(node.val),对每个孩子递归克隆后加入 children 列表。

N 叉树无环(真实树),无需映射表。

复杂度:O(n) 时间,O(h) 递归栈空间(h 为树高)。

解法二:BFS 层序深拷贝(对照)

用队列同时保存「原节点 -> 拷贝节点」对,逐层生成拷贝并拼接子节点。

复杂度:O(n) 时间,O(n) 空间(队列 + 映射表)。

代码实现

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

class NodeN {
    public int val;
    public List<NodeN> children;

    public NodeN() {
        children = new ArrayList<NodeN>();
    }

    public NodeN(int _val) {
        val = _val;
        children = new ArrayList<NodeN>();
    }
}

public class Solution {

    public NodeN cloneTree(NodeN root) {
        if (root == null) {
            return null;
        }
        NodeN copy = new NodeN(root.val);
        for (NodeN child : root.children) {
            copy.children.add(cloneTree(child));
        }
        return copy;
    }

    public NodeN cloneTreeBfs(NodeN root) {
        if (root == null) {
            return null;
        }
        NodeN copyRoot = new NodeN(root.val);
        Queue<NodeN> srcQ = new ArrayDeque<>();
        Queue<NodeN> dstQ = new ArrayDeque<>();
        srcQ.offer(root);
        dstQ.offer(copyRoot);
        while (!srcQ.isEmpty()) {
            NodeN src = srcQ.poll();
            NodeN dst = dstQ.poll();
            for (NodeN child : src.children) {
                NodeN childCopy = new NodeN(child.val);
                dst.children.add(childCopy);
                srcQ.offer(child);
                dstQ.offer(childCopy);
            }
        }
        return copyRoot;
    }

    public static void main(String[] args) {
        // 构造 [1,null,3,2,4,null,5,6]
        NodeN root = new NodeN(1);
        NodeN n3 = new NodeN(3);
        NodeN n2 = new NodeN(2);
        NodeN n4 = new NodeN(4);
        root.children.add(n3);
        root.children.add(n2);
        root.children.add(n4);
        n3.children.add(new NodeN(5));
        n3.children.add(new NodeN(6));

        Solution s = new Solution();
        NodeN c = s.cloneTree(root);
        System.out.println("root val = " + c.val); // 1
        System.out.println("children size = " + c.children.size()); // 3
        System.out.println("first child val = " + c.children.get(0).val); // 3
        System.out.println("3's children = "
                + c.children.get(0).children.get(0).val + "," + c.children.get(0).children.get(1).val); // 5,6

        NodeN d = s.cloneTreeBfs(root);
        System.out.println("bfs root val = " + d.val); // 1
        System.out.println("bfs first grandchild = " + d.children.get(0).children.get(1).val); // 6
    }
}

复杂度分析

复杂度:O(n) 时间,O(h) 递归栈空间(h 为树高)。

复杂度:O(n) 时间,O(n) 空间(队列 + 映射表)。


1491. Average Salary Excluding the Minimum and Maximum Salary(去掉最低工资和最高工资后的工资平均值)

题目描述

给你一个整数数组 salary,数组里每个数都是唯一的,其中 salaryi 是第 i 个员工的工资。

请你返回去掉最低工资和最高工资以后,剩下员工工资的平均值。

示例 1

复制代码
salary = [4000,3000,1000,2000] -> 2500.0

示例 2

复制代码
salary = [1000,2000,3000] -> 2000.0

示例 3

复制代码
salary = [6000,5000,4000,3000,2000,1000] -> 3500.0

示例 4

复制代码
salary = [8000,9000,2000,3000,6000,1000] -> 4750.0

约束:

  • 3 <= salary.length <= 100;10^3 <= salaryi <= 10^6;salaryi 唯一。

解题思路

解法一:一趟扫描找极值(主提交版本)

求和同时记录最小、最大值,返回 (sum - min - max) / (n - 2)。

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

解法二:排序取中间(对照)

排序后对下标 1...n-2 求和取平均。

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

代码实现

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

public class Solution {

    public double average(int[] salary) {
        double sum = 0;
        int min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;
        for (int x : salary) {
            sum += x;
            min = Math.min(min, x);
            max = Math.max(max, x);
        }
        return (sum - min - max) / (salary.length - 2);
    }

    public double averageSort(int[] salary) {
        Arrays.sort(salary);
        long sum = 0;
        for (int i = 1; i < salary.length - 1; i++) {
            sum += salary[i];
        }
        return (double) sum / (salary.length - 2);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.average(new int[]{4000, 3000, 1000, 2000})); // 2500.0
        System.out.println(s.average(new int[]{1000, 2000, 3000})); // 2000.0
        System.out.println(s.average(new int[]{6000, 5000, 4000, 3000, 2000, 1000})); // 3500.0
        System.out.println(s.average(new int[]{8000, 9000, 2000, 3000, 6000, 1000})); // 4750.0
        System.out.println(s.averageSort(new int[]{4000, 3000, 1000, 2000})); // 2500.0
        System.out.println(s.averageSort(new int[]{1000, 2000, 3000})); // 2000.0
        System.out.println(s.averageSort(new int[]{6000, 5000, 4000, 3000, 2000, 1000})); // 3500.0
        System.out.println(s.averageSort(new int[]{8000, 9000, 2000, 3000, 6000, 1000})); // 4750.0
    }
}

复杂度分析

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

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


1492. The kth Factor of n(n 的第 k 个因子)

题目描述

给你两个正整数 n 和 k。

如果正整数 i 满足 n % i == 0,那么我们就说正整数 i 是整数 n 的因子。

考虑整数 n 的所有因子,将它们升序排列。请你返回第 k 个因子。

如果 n 的因子数少于 k,请你返回 -1。

示例 1

复制代码
n = 12, k = 3 -> 3(因子 [1,2,3,4,6,12])

示例 2

复制代码
n = 7, k = 2 -> 7(因子 [1,7])

示例 3

复制代码
n = 4, k = 4 -> -1(因子 [1,2,4] 只有 3 个)

约束:

  • 1 <= k <= n <= 1000。

解题思路

解法一:O(sqrt(n)) 双端收集(主提交版本)

枚举 i 从 1 到 sqrt(n),若 i 是 n 的因子:

  • i 本身是一个因子(递增收集);
  • n / i 是另一个配对因子(压栈,倒序取)。
    最后把栈中"大于 sqrt(n)"的半段按升序接上,取第 k 个。
    复杂度:O(sqrt(n)) 时间,O(sqrt(n)) 空间。
    解法二:直接枚举 O(n)(对照)
    从 1 到 n 枚举因子,遇到就计数,到 k 时返回;结束后返回 -1。
    复杂度:O(n) 时间,O(1) 空间。

代码实现

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

public class Solution {

    public int kthFactor(int n, int k) {
        List<Integer> small = new ArrayList<>(); // 小于等于 sqrt(n) 的因子(升序)
        List<Integer> big = new ArrayList<>();   // 大于 sqrt(n) 的配对因子(压入时倒序)
        for (int i = 1; i * i <= n; i++) {
            if (n % i == 0) {
                small.add(i);
                if (i != n / i) {
                    big.add(0, n / i); // 头插法保持升序(big 数量少,O(1) 均摊)
                }
            }
        }
        int pos = 0;
        for (int x : small) {
            pos++;
            if (pos == k) {
                return x;
            }
        }
        for (int x : big) {
            pos++;
            if (pos == k) {
                return x;
            }
        }
        return -1;
    }

    public int kthFactorLinear(int n, int k) {
        int cnt = 0;
        for (int i = 1; i <= n; i++) {
            if (n % i == 0) {
                cnt++;
                if (cnt == k) {
                    return i;
                }
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.kthFactor(12, 3)); // 3
        System.out.println(s.kthFactor(7, 2)); // 7
        System.out.println(s.kthFactor(4, 4)); // -1
        System.out.println(s.kthFactor(1000, 3)); // 4([1,2,4,...])
        System.out.println(s.kthFactorLinear(12, 3)); // 3
        System.out.println(s.kthFactorLinear(7, 2)); // 7
        System.out.println(s.kthFactorLinear(4, 4)); // -1
    }
}

复杂度分析

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

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


1493. Longest Subarray of 1's After Deleting One Element(删掉一个元素以后全为 1 的最长子数组)

题目描述

给你一个二进制数组 nums,你需要从中删除一个元素。

请你返回删除操作后,数组中仅包含 1 的最长非空子数组的长度。

如果不存在这样的子数组,请返回 0。

示例 1

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

示例 2

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

示例 3

复制代码
nums = [1,1,1] -> 2(必须删除一个元素)

约束:

  • 1 <= nums.length <= 10^5;nums[i] 的值为 0 或 1。

解题思路

解法一:滑动窗口(至多一个 0)(主提交版本)

维护窗口 left, right,窗口内 0 的个数不超过 1。

右指针扩展,遇到 0 时若窗口内已有一个 0,则左指针右移直到移出(或越过)一个 0。

用窗口长度 - 1(删掉那个 0)更新答案。若窗口中没有 0(全 1),则必须删一个 1,

此时"最长全 1 子段"为 len - 1,同样成立。

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

解法二:零元素前后缀拼接(对照)

分别求每个位置左边连续 1 的个数(左侧段)和右边连续 1 的个数,

对每个 0,删除它后可衔接 lefti-1 + righti+1;全 1 数组答案是 n - 1。

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

代码实现

java 复制代码
public class Solution {

    public int longestSubarray(int[] nums) {
        int n = nums.length;
        int left = 0, zeroCnt = 0, best = 0;
        for (int right = 0; right < n; right++) {
            if (nums[right] == 0) {
                zeroCnt++;
            }
            while (zeroCnt > 1) { // 窗口内最多允许 1 个 0
                if (nums[left] == 0) {
                    zeroCnt--;
                }
                left++;
            }
            // 窗口 [left, right],删除 1 个元素后全 1 的最长段 = right - left
            best = Math.max(best, right - left);
        }
        return best;
    }

    public int longestSubarrayPrefix(int[] nums) {
        int n = nums.length;
        int[] pre = new int[n]; // pre[i]:位置 i 及左边连续 1 的个数
        int[] suf = new int[n]; // suf[i]:位置 i 及右边连续 1 的个数
        pre[0] = nums[0];
        for (int i = 1; i < n; i++) {
            pre[i] = nums[i] == 1 ? pre[i - 1] + 1 : 0;
        }
        suf[n - 1] = nums[n - 1];
        for (int i = n - 2; i >= 0; i--) {
            suf[i] = nums[i] == 1 ? suf[i + 1] + 1 : 0;
        }
        int best = 0;
        for (int i = 0; i < n; i++) {
            int leftCnt = i > 0 ? pre[i - 1] : 0;
            int rightCnt = i < n - 1 ? suf[i + 1] : 0;
            best = Math.max(best, leftCnt + rightCnt);
        }
        return best;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.longestSubarray(new int[]{1, 1, 0, 1})); // 3
        System.out.println(s.longestSubarray(new int[]{0, 1, 1, 1, 0, 1, 1, 0, 1})); // 5
        System.out.println(s.longestSubarray(new int[]{1, 1, 1})); // 2
        System.out.println(s.longestSubarray(new int[]{0, 0, 0})); // 0
        System.out.println(s.longestSubarray(new int[]{1, 0, 0, 1, 1, 0, 1})); // 3
        System.out.println(s.longestSubarrayPrefix(new int[]{1, 1, 0, 1})); // 3
        System.out.println(s.longestSubarrayPrefix(new int[]{0, 1, 1, 1, 0, 1, 1, 0, 1})); // 5
        System.out.println(s.longestSubarrayPrefix(new int[]{1, 1, 1})); // 2
        System.out.println(s.longestSubarrayPrefix(new int[]{0, 0, 0})); // 0
    }
}

复杂度分析

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

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


1494. Parallel Courses II(并行课程 II)

题目描述

给你一个整数 n 表示某所大学里课程的数目,编号为 1 到 n,

数组 relations 中 relationsi = xi, yi 表示先修课关系(xi 必须在 yi 之前上)。

同时你还有一个整数 k。在一个学期中,你最多可以同时上 k 门课,

前提是这些课的先修课在之前的学期里已经上过了。

请你返回上完所有课最少需要多少个学期。题目保证一定存在一种上完所有课的方式。

示例 1

复制代码
n = 4, relations = [[2,1],[3,1],[1,4]], k = 2 -> 3

示例 2

复制代码
n = 5, relations = [[2,1],[3,1],[4,1],[1,5]], k = 2 -> 4

示例 3

复制代码
n = 11, relations = [], k = 2 -> 6

约束:

  • 1 <= n <= 15;1 <= k <= n;relations 构成有向无环图。

解题思路

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

预计算每门课的必修前置掩码 prei。dpmask = 上完 mask 这些课所需的最少学期。

枚举 mask 的可达课程集合(前置全部 < mask 且未在 mask 中):avi = 可选课程中

满足 prei 属于 mask 的课。为节省枚举,只枚举 avi 的子集里作为"本期要上的课",

可选数量超过 k 时优先选择所有可选课(贪心成立:同一学期多上不劣),

若 avi 内课程数 > k 可以认为上满 k 门最优------但更稳妥做法是枚举 avi 的所有子集,

由于 n <= 15 且子集枚举 3^n 可以接受,这里采用「枚举 avi 全部子集」,

并做位宽裁剪:子集大小 > k 剪掉。

复杂度:O(3^n) 时间,O(2^n) 空间(n <= 15 完全可行)。

解法二:BFS 求最短路(对照)

把 mask 视为节点,每次从当前 mask 出发扩展「再上一个学期」能到达的新 mask(同样枚举

可选课子集,含位宽限制 k),BFS 求到 (1<<n)-1 的最少层数。

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

代码实现

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

public class Solution {

    public int minNumberOfSemesters(int n, int[][] relations, int k) {
        int[] pre = new int[n]; // 每门课的前置掩码(0 基)
        for (int[] r : relations) {
            int x = r[0] - 1, y = r[1] - 1;
            pre[y] |= 1 << x;
        }
        int full = 1 << n;
        int[] dp = new int[full];
        Arrays.fill(dp, Integer.MAX_VALUE / 2);
        dp[0] = 0;
        int[] bitcnt = new int[full];
        for (int mask = 0; mask < full; mask++) {
            bitcnt[mask] = Integer.bitCount(mask);
        }
        for (int mask = 0; mask < full; mask++) {
            if (dp[mask] == Integer.MAX_VALUE / 2) {
                continue;
            }
            // 当前可学课程:未上 && 前置全部已上
            int avi = 0;
            for (int i = 0; i < n; i++) {
                if ((mask & (1 << i)) == 0 && (pre[i] & mask) == pre[i]) {
                    avi |= 1 << i;
                }
            }
            // 枚举 avi 的每个子集作为本学期课程计划,剪掉超过 k 门的
            for (int sub = avi; sub > 0; sub = (sub - 1) & avi) {
                if (bitcnt[sub] > k) {
                    continue;
                }
                dp[mask | sub] = Math.min(dp[mask | sub], dp[mask] + 1);
            }
        }
        return dp[full - 1];
    }

    public int minNumberOfSemestersBfs(int n, int[][] relations, int k) {
        int[] pre = new int[n];
        for (int[] r : relations) {
            int x = r[0] - 1, y = r[1] - 1;
            pre[y] |= 1 << x;
        }
        int full = 1 << n;
        int target = full - 1;
        boolean[] vis = new boolean[full];
        int[] dist = new int[full];
        Queue<Integer> q = new ArrayDeque<>();
        q.offer(0);
        vis[0] = true;
        while (!q.isEmpty()) {
            int mask = q.poll();
            if (mask == target) {
                return dist[mask];
            }
            int avi = 0;
            for (int i = 0; i < n; i++) {
                if ((mask & (1 << i)) == 0 && (pre[i] & mask) == pre[i]) {
                    avi |= 1 << i;
                }
            }
            for (int sub = avi; sub > 0; sub = (sub - 1) & avi) {
                if (Integer.bitCount(sub) > k) {
                    continue;
                }
                int nm = mask | sub;
                if (!vis[nm]) {
                    vis[nm] = true;
                    dist[nm] = dist[mask] + 1;
                    q.offer(nm);
                }
            }
        }
        return -1; // 题目保证可达,理论到不了这里
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.minNumberOfSemesters(4, new int[][]{{2, 1}, {3, 1}, {1, 4}}, 2)); // 3
        System.out.println(s.minNumberOfSemesters(5, new int[][]{{2, 1}, {3, 1}, {4, 1}, {1, 5}}, 2)); // 4
        System.out.println(s.minNumberOfSemesters(11, new int[][]{}, 2)); // 6
        System.out.println(s.minNumberOfSemestersBfs(4, new int[][]{{2, 1}, {3, 1}, {1, 4}}, 2)); // 3
        System.out.println(s.minNumberOfSemestersBfs(5, new int[][]{{2, 1}, {3, 1}, {4, 1}, {1, 5}}, 2)); // 4
        System.out.println(s.minNumberOfSemestersBfs(11, new int[][]{}, 2)); // 6
    }
}

复杂度分析

复杂度:O(3^n) 时间,O(2^n) 空间(n <= 15 完全可行)。

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


1495. Friendly Movies Streamed Last Month(上月播放的儿童适宜电影 🔒)

题目类型:数据库(SQL)

题目描述

表: TVProgram

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| program_date  | date    |
| content_id    | int     |
| channel       | varchar |
+---------------+---------+
(program_date, content_id) 是该表的主键(具有唯一值的列的组合)。
该表包含电视上的节目信息。
content_id 是电视一些频道上的节目的 id。

表: Content

复制代码
+------------------+---------+
| Column Name      | Type    |
+------------------+---------+
| content_id       | varchar |
| title            | varchar |
| Kids_content     | enum    |
| content_type     | varchar |
+------------------+---------+
content_id 是该表主键(具有唯一值的列)。
Kids_content 是枚举类型, 取值为('Y', 'N'), 其中:
'Y' 表示儿童适宜内容, 而'N'表示儿童不宜内容。
content_type 表示内容的类型, 比如电影, 电视剧等。

编写解决方案, 报告在 2020 年 6 月份播放的儿童适宜电影的去重电影名.

返回的结果表单 没有顺序要求 .

返回结果的格式如下例所示.

示例 1:

复制代码
`**输入:**
TVProgram` 表:
+--------------------+--------------+-------------+
| program_date       | content_id   | channel     |
+--------------------+--------------+-------------+
| 2020-06-10 08:00   | 1            | LC-Channel  |
| 2020-05-11 12:00   | 2            | LC-Channel  |
| 2020-05-12 12:00   | 3            | LC-Channel  |
| 2020-05-13 14:00   | 4            | Disney Ch   |
| 2020-06-18 14:00   | 4            | Disney Ch   |
| 2020-07-15 16:00   | 5            | Disney Ch   |
+--------------------+--------------+-------------+
`Content` 表:
+------------+----------------+---------------+---------------+
| content_id | title          | Kids_content  | content_type  |
+------------+----------------+---------------+---------------+
| 1          | Leetcode Movie | N             | Movies        |
| 2          | Alg. for Kids  | Y             | Series        |
| 3          | Database Sols  | N             | Series        |
| 4          | Aladdin        | Y             | Movies        |
| 5          | Cinderella     | Y             | Movies        |
+------------+----------------+---------------+---------------+
`**输出:**`
+--------------+
| title        |
+--------------+
| Aladdin      |
+--------------+
`**解释:**`
"Leetcode Movie" 是儿童不宜的电影.
"Alg. for Kids" 不是电影.
"Database Sols" 不是电影
"Aladin" 是电影, 儿童适宜, 并且在 2020 年 6 月份播放.
"Cinderella" 不在 2020 年 6 月份播放.

解题思路

思考

节目表与内容表通过 content_id 连接,筛出 2020 2020 2020 年 6 6 6 月、儿童适宜且类型为电影的标题,DISTINCT 去重。

考察JOIN + 日期区间过滤:限定 2020-06 月份的节目,连接内容表取儿童适宜(Y)的电影去重。

代码实现

sql 复制代码
# Write your MySQL query statement below
SELECT DISTINCT title
FROM
    TVProgram
    JOIN Content USING (content_id)
WHERE
    DATE_FORMAT(program_date, '%Y%m') = '202006'
    AND kids_content = 'Y'
    AND content_type = 'Movies';

复杂度分析

本题为数据库查询题,不涉及传统算法的时间/空间复杂度,执行效率由数据库优化器决定;建议对查询涉及的过滤列与连接列建立索引以加速。


1496. Path Crossing(判断路径是否相交)

题目描述

给你一个字符串 path,其中 path[i] 的值可以是 'N'、'S'、'E' 或者 'W',

分别表示向北、向南、向东、向西移动一个单位。

你从二维平面上的原点 (0, 0) 处开始出发,按 path 所指示的路径行走。

如果路径在任何位置上与自身相交,也就是走到之前已经走过的位置,请返回 true;

否则,返回 false。

示例 1

复制代码
path = "NES" -> false

示例 2

复制代码
path = "NESWW" -> true(路径经过原点两次)

约束:

  • 1 <= path.length <= 10^4。

解题思路

解法一:哈希集合记录坐标(主提交版本)

用字符串 "x,y" 或编码后的整数存入 HashSet,初始加入 (0,0);

每走一步若新坐标已存在则返回 true。

坐标范围有限(|x|,|y| <= 10^4),可用 x * 20001 + y 编码成 int 存入集合。

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

解法二:二维布尔网格(对照)

用偏移量把坐标映射到 0, 2*10\^4 的方形网格,布尔数组标记访问。
复杂度:O(n) 时间,O(4* 10^8)?? 空间过大------改为稀疏编码的朴素版本:

用字符串坐标放入集合,逻辑等价但实现直观。

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

代码实现

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

public class Solution {

    public boolean isPathCrossing(String path) {
        Set<Integer> seen = new HashSet<>();
        int x = 0, y = 0;
        seen.add(encode(x, y));
        for (char c : path.toCharArray()) {
            switch (c) {
                case 'N': y++; break;
                case 'S': y--; break;
                case 'E': x++; break;
                case 'W': x--; break;
                default: break;
            }
            if (!seen.add(encode(x, y))) {
                return true;
            }
        }
        return false;
    }

    private int encode(int x, int y) {
        return x * 40005 + y; // 40005 > 2 * 20001,保证编码唯一
    }

    public boolean isPathCrossingString(String path) {
        Set<String> seen = new HashSet<>();
        int x = 0, y = 0;
        seen.add(x + "," + y);
        for (char c : path.toCharArray()) {
            if (c == 'N') y++;
            else if (c == 'S') y--;
            else if (c == 'E') x++;
            else x--;
            if (!seen.add(x + "," + y)) {
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.isPathCrossing("NES")); // false
        System.out.println(s.isPathCrossing("NESWW")); // true
        System.out.println(s.isPathCrossing("SN")); // true(走回原点)
        System.out.println(s.isPathCrossingString("NES")); // false
        System.out.println(s.isPathCrossingString("NESWW")); // true
        System.out.println(s.isPathCrossingString("SN")); // true
    }
}

复杂度分析

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

复杂度:O(n) 时间,O(4*10^8)?? 空间过大------改为稀疏编码的朴素版本:


1497. Check If Array Pairs Are Divisible by k(检查数组对是否可以被 k 整除)

题目描述

给你一个整数数组 arr 和一个整数 k,其中数组长度是偶数,值为 n。

现在需要把数组恰好分成 n / 2 对,以使每对数字的和都能够被 k 整除。

如果存在这样的分法,请返回 true;否则,返回 false。

示例 1

复制代码
arr = [1,2,3,4,5,10,6,7,8,9], k = 5 -> true

示例 2

复制代码
arr = [1,2,3,4,5,6], k = 7 -> true

示例 3

复制代码
arr = [1,2,3,4,5,6], k = 10 -> false

约束:

  • n 为偶数;1 <= n <= 105;-109 <= arr[i] <= 10^9;1 <= k <= 10^5。

解题思路

解法一:余数配对(主提交版本)

(a + b) % k == 0 等价于 (a % k + b % k) % k == 0。

统计每种余数 r 的个数;r == 0 的个数必须是偶数;

对 r in 1...k-1,cntr 必须等于 cntk - r。

注意负数:Java 中 % 可能为负,统一转 (x % k + k) % k。

复杂度:O(n + k) 时间,O(k) 空间。

解法二:排序 + 双指针(对照)

对数组排序后用双指针配对,但需要维护余数配对关系,实现繁琐。

这里提供基于 HashMap 计数的等价版(通用读法),

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

代码实现

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

public class Solution {

    public boolean canArrange(int[] arr, int k) {
        int[] cnt = new int[k];
        for (int x : arr) {
            int r = ((x % k) + k) % k; // 负数取模修正
            cnt[r]++;
        }
        if (cnt[0] % 2 != 0) {
            return false;
        }
        for (int r = 1; r <= k / 2; r++) {
            if (r == k - r) { // k 为偶数时的中间余数
                if (cnt[r] % 2 != 0) {
                    return false;
                }
            } else if (cnt[r] != cnt[k - r]) {
                return false;
            }
        }
        return true;
    }

    public boolean canArrangeMap(int[] arr, int k) {
        Map<Integer, Integer> cnt = new HashMap<>();
        for (int x : arr) {
            int r = ((x % k) + k) % k;
            cnt.put(r, cnt.getOrDefault(r, 0) + 1);
        }
        for (Map.Entry<Integer, Integer> e : cnt.entrySet()) {
            int r = e.getKey();
            int c = e.getValue();
            if (r == 0) {
                if (c % 2 != 0) {
                    return false;
                }
            } else {
                int need = k - r;
                Integer other = cnt.get(need);
                if (other == null || other != c) {
                    // 注意:当 need == r 时要自己与自己配对
                    if (need != r || c % 2 != 0) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.canArrange(new int[]{1, 2, 3, 4, 5, 10, 6, 7, 8, 9}, 5)); // true
        System.out.println(s.canArrange(new int[]{1, 2, 3, 4, 5, 6}, 7)); // true
        System.out.println(s.canArrange(new int[]{1, 2, 3, 4, 5, 6}, 10)); // false
        System.out.println(s.canArrange(new int[]{-1, -1, -1, 1, 1, 1}, 2)); // true
        System.out.println(s.canArrange(new int[]{-4, -7, 5, 2, 9, 1, 10, 4, -8, -3}, 3)); // true
        System.out.println(s.canArrangeMap(new int[]{1, 2, 3, 4, 5, 10, 6, 7, 8, 9}, 5)); // true
        System.out.println(s.canArrangeMap(new int[]{1, 2, 3, 4, 5, 6}, 7)); // true
        System.out.println(s.canArrangeMap(new int[]{1, 2, 3, 4, 5, 6}, 10)); // false
        System.out.println(s.canArrangeMap(new int[]{-1, -1, -1, 1, 1, 1}, 2)); // true
    }
}

复杂度分析

复杂度:O(n + k) 时间,O(k) 空间。

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


1498. Number of Subsequences That Satisfy the Given Sum Condition(满足条件的子序列数目)

题目描述

给你一个整数数组 nums 和一个整数 target。

请你统计并返回 nums 中能满足其最小元素与最大元素的和小于或等于 target 的

非空子序列的数目。由于答案可能很大,请将结果对 10^9 + 7 取余后返回。

示例 1

复制代码
nums = [3,5,6,7], target = 9 -> 4

示例 2

复制代码
nums = [3,3,6,8], target = 10 -> 6

示例 3

复制代码
nums = [2,3,3,4,6,7], target = 12 -> 61

约束:

  • 1 <= nums.length <= 10^5;1 <= nums[i] <= 10^6;1 <= target <= 10^6。

解题思路

解法一:排序 + 双指针 + 预计算 2 的幂(主提交版本)

子序列是否满足只取决于最小与最大元素,因此可以先排序。

固定最小元素 numsi,最大的可配对下标 j 满足 numsi + numsj <= target

(j 从右往左随 i 增大单调不增,双指针维护)。

元素 i 作为的是子序列的最小值,则其余元素只能从 (i+1...j) 中任意挑选,

贡献 2^(j-i) 个子序列(非空也满足,因为 i 本身已算在里面)。

预先计算 pow2 表,累加取模。

复杂度:O(n log n + n) 时间(排序 + 扫描),O(n) 空间。

解法二:二分查找右边界(对照)

同样是排序后固定左端点 i,用二分查找最后一个 j 使 numsi+numsj <= target,

贡献 2^(j-i)。复杂度 O(n log n)。实现直观,作为对照。

代码实现

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

public class Solution {

    private static final int MOD = 1_000_000_007;

    public int numSubseq(int[] nums, int target) {
        Arrays.sort(nums);
        int n = nums.length;
        int[] pow2 = new int[n + 1];
        pow2[0] = 1;
        for (int i = 1; i <= n; i++) {
            pow2[i] = (pow2[i - 1] * 2) % MOD;
        }
        long ans = 0;
        int j = n - 1;
        for (int i = 0; i < n; i++) {
            while (j >= i && nums[i] + nums[j] > target) {
                j--;
            }
            if (j < i) {
                break; // 后续 i 更大,更不可能满足
            }
            ans = (ans + pow2[j - i]) % MOD;
        }
        return (int) ans;
    }

    public int numSubseqBinary(int[] nums, int target) {
        Arrays.sort(nums);
        int n = nums.length;
        int[] pow2 = new int[n + 1];
        pow2[0] = 1;
        for (int i = 1; i <= n; i++) {
            pow2[i] = (pow2[i - 1] * 2) % MOD;
        }
        long ans = 0;
        for (int i = 0; i < n; i++) {
            if (nums[i] * 2 > target) {
                break; // 即使 nums[i] 自己配对也不满足
            }
            int lo = i, hi = n - 1, best = i;
            while (lo <= hi) {
                int mid = (lo + hi) >>> 1;
                if (nums[mid] <= target - nums[i]) {
                    best = mid;
                    lo = mid + 1;
                } else {
                    hi = mid - 1;
                }
            }
            ans = (ans + pow2[best - i]) % MOD;
        }
        return (int) ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.numSubseq(new int[]{3, 5, 6, 7}, 9)); // 4
        System.out.println(s.numSubseq(new int[]{3, 3, 6, 8}, 10)); // 6
        System.out.println(s.numSubseq(new int[]{2, 3, 3, 4, 6, 7}, 12)); // 61
        System.out.println(s.numSubseq(new int[]{1}, 1)); // 1
        System.out.println(s.numSubseqBinary(new int[]{3, 5, 6, 7}, 9)); // 4
        System.out.println(s.numSubseqBinary(new int[]{3, 3, 6, 8}, 10)); // 6
        System.out.println(s.numSubseqBinary(new int[]{2, 3, 3, 4, 6, 7}, 12)); // 61
        System.out.println(s.numSubseqBinary(new int[]{1}, 1)); // 1
    }
}

复杂度分析

复杂度:O(n log n + n) 时间(排序 + 扫描),O(n) 空间。


1499. Max Value of Equation(满足不等式的最大值)

题目描述

给你一个数组 points 和一个整数 k。数组中每个元素都表示二维平面上的点的坐标,

并按照横坐标 x 的值从小到大排序,即 pointsi = xi, yi,且 xi 严格递增。

请你找出 yi + yj + |xi - xj| 的最大值,其中 |xi - xj| <= k 且 i < j。

题目测试数据保证至少存在一对能够满足 |xi - xj| <= k 的点。

示例 1

复制代码
points = [[1,3],[2,0],[5,10],[6,-10]], k = 1 -> 4

示例 2

复制代码
points = [[0,0],[3,0],[9,2]], k = 3 -> 3

约束:

  • 2 <= points.length <= 10^5;xi, yi 属于 -10\^8, 10\^8;0 <= k <= 2 * 10^8。

解题思路

解法一:单调队列维护 (yi - xi)(主提交版本)

由于 xj > xi,目标值 = (xj + yj) + (yi - xi)。

固定当前点 j,需要从已扫描的 i 中找满足 xj - xi <= k 的 (yi - xi) 最大者。

从左到右扫描,用一个递减的双端队列维护候选 (yi - xi, xi):

  • 队头若 x < xj - k 则过期弹出;
  • 队头 (yi - xi) 最大,用它更新答案 (xj + yj) + (yi - xi);
  • 将当前点按 (yi - xi) 单调递减规则入队(队尾比它小的弹出)。
    复杂度:O(n) 时间,O(n) 空间。
    解法二:优先队列(大根堆)(对照)
    用堆维护 (yi - xi) 最大的点,惰性删除 x 距离超限的队顶。
    复杂度:O(n log n) 时间,O(n) 空间。

代码实现

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

public class Solution {

    public int findMaxValueOfEquation(int[][] points, int k) {
        ArrayDeque<int[]> dq = new ArrayDeque<>(); // 存 {xi, yi - xi},按 yi - xi 递减
        int n = points.length;
        int ans = Integer.MIN_VALUE;
        for (int j = 0; j < n; j++) {
            int xj = points[j][0], yj = points[j][1];
            // 移除距离超过 k 的过期候选
            while (!dq.isEmpty() && xj - dq.peekFirst()[0] > k) {
                dq.pollFirst();
            }
            if (!dq.isEmpty()) {
                // 队头是 yi - xi 最大且满足距离条件的
                ans = Math.max(ans, xj + yj + dq.peekFirst()[1]);
            }
            // 当前点入队(保持递减)
            int val = yj - xj;
            while (!dq.isEmpty() && dq.peekLast()[1] <= val) {
                dq.pollLast();
            }
            dq.offerLast(new int[]{xj, val});
        }
        return ans;
    }

    public int findMaxValueOfEquationHeap(int[][] points, int k) {
        // 大根堆:val = yi - xi 大的优先,其次 x 大的优先(更快淘汰也好)
        PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) ->
                a[1] != b[1] ? b[1] - a[1] : b[0] - a[0]);
        int n = points.length;
        int ans = Integer.MIN_VALUE;
        for (int j = 0; j < n; j++) {
            int xj = points[j][0], yj = points[j][1];
            // 惰性删除不满足距离约束的队顶
            while (!pq.isEmpty() && xj - pq.peek()[0] > k) {
                pq.poll();
            }
            if (!pq.isEmpty()) {
                ans = Math.max(ans, xj + yj + pq.peek()[1]);
            }
            pq.offer(new int[]{xj, yj - xj});
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.findMaxValueOfEquation(new int[][]{{1, 3}, {2, 0}, {5, 10}, {6, -10}}, 1)); // 4
        System.out.println(s.findMaxValueOfEquation(new int[][]{{0, 0}, {3, 0}, {9, 2}}, 3)); // 3
        System.out.println(s.findMaxValueOfEquationHeap(new int[][]{{1, 3}, {2, 0}, {5, 10}, {6, -10}}, 1)); // 4
        System.out.println(s.findMaxValueOfEquationHeap(new int[][]{{0, 0}, {3, 0}, {9, 2}}, 3)); // 3
    }
}

复杂度分析

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

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


1500. Design a File Sharing System(设计文件分享系统)

题目描述

我们需要使用一套文件分享系统来分享一个非常大的文件,该文件由 m 个从 1 到 m

编号的文件块组成。

当用户加入系统时,系统应为其注册一个独有的 ID。这个独有的 ID 应当被相应的用户

使用一次,但是当用户离开系统时,其 ID 应可以被(后续新注册的用户)再次使用。

用户可以请求文件中的某个指定的文件块,系统应当返回拥有这个文件块的所有用户的 ID。

如果用户收到 ID 的非空列表,就表示成功接收到请求的文件块。

实现 FileSharing 类:

  • FileSharing(int m) 初始化该对象,文件有 m 个文件块;
  • int join(int[] ownedChunks) 新用户加入并拥有一些文件块,为其注册未占用最小
    正整数 ID 并返回;
  • void leave(int userID) 用户离开,其拥有的文件块不再对其他用户可用,ID 可复用;
  • int[] request(int userID, int chunkID) 返回拥有该文件块的所有用户 ID(升序),
    该用户获得该块(把该块加入自己的拥有列表)。

示例

复制代码
join([1,2])->1, join([2,3])->2, join([4])->3,
request(1,3)->[2], request(2,2)->[1,2], leave(1),
request(2,1)->[], leave(2), join([])->1

约束:

  • 1 <= m <= 10^5;调用次数 <= 10^4;leave 对应 join;请求的用户在系统内。

解题思路

解法一:最小堆分配 ID + 所有者索引(主提交版本)

  • 用 PriorityQueue 维护空闲 ID(初始放入 1...?),再用 nextId 递增兜底;
    每次 join 优先从堆中弹出最小空闲 ID。
  • chunkOwnersc:维护拥有文件块 c 的用户集合(TreeSet 输出升序)。
  • userChunksuid:维护每个用户的拥有块集合(join/request 时加入,leave 时删除)。
    复杂度:均摊 O(log n + |owned| log m) 每次调用。
    解法二:布尔数组标记可用 ID(对照)
    用一个 used\[\] 布尔数组记住哪些 ID 已占用,join 时从 1 开始线性扫描找最小空闲 ID
    (总人数 <= 10^4,线性扫描整体可接受);块与用户的索引同上。
    复杂度:join 最坏 O(1e4)(总体 O(n * 1e4)),但实现最直观。

代码实现

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

public class Solution {

    public static class FileSharing {
        private int m;
        private PriorityQueue<Integer> freeIds; // 可复用的空闲 ID
        private int nextId;                     // 从未分配过的下一个 ID(单调递增)
        private List<TreeSet<Integer>> owners;  // owners[chunk] = 拥有该块的所有用户(升序)
        private List<TreeSet<Integer>> userChunks; // userChunks[uid] = 该用户拥有的块

        public FileSharing(int m) {
            this.m = m;
            freeIds = new PriorityQueue<>();
            nextId = 1;
            owners = new ArrayList<>();
            for (int i = 0; i <= m; i++) {
                owners.add(new TreeSet<>());
            }
            userChunks = new ArrayList<>();
            userChunks.add(null); // 占位下标 0
        }

        public int join(int[] ownedChunks) {
            int uid;
            if (!freeIds.isEmpty()) {
                uid = freeIds.poll(); // 复用最小空闲 ID
            } else {
                uid = nextId++;
                userChunks.add(new TreeSet<>());
            }
            for (int c : ownedChunks) {
                userChunks.get(uid).add(c);
                owners.get(c).add(uid);
            }
            return uid;
        }

        public void leave(int userID) {
            for (int c : userChunks.get(userID)) {
                owners.get(c).remove(userID);
            }
            userChunks.get(userID).clear();
            freeIds.offer(userID); // ID 归还
        }

        public int[] request(int userID, int chunkID) {
            TreeSet<Integer> set = owners.get(chunkID);
            int[] res = new int[set.size()];
            int i = 0;
            for (int uid : set) {
                res[i++] = uid;
            }
            // 该用户获得该块
            if (userID != -1 && !userChunks.get(userID).contains(chunkID)) {
                userChunks.get(userID).add(chunkID);
                owners.get(chunkID).add(userID);
            }
            return res;
        }
    }

    public static class FileSharingLinear {
        private int m;
        private boolean[] used;
        private List<TreeSet<Integer>> owners;
        private List<TreeSet<Integer>> userChunks;

        public FileSharingLinear(int m) {
            this.m = m;
            used = new boolean[10001]; // 总 join <= 10^4,ID 上限足够
            owners = new ArrayList<>();
            for (int i = 0; i <= m; i++) {
                owners.add(new TreeSet<>());
            }
            userChunks = new ArrayList<>();
            userChunks.add(null);
        }

        public int join(int[] ownedChunks) {
            int uid = 1;
            while (used[uid]) {
                uid++; // 找最小空闲 ID
            }
            used[uid] = true;
            userChunks.add(new TreeSet<>());
            for (int c : ownedChunks) {
                userChunks.get(uid).add(c);
                owners.get(c).add(uid);
            }
            return uid;
        }

        public void leave(int userID) {
            for (int c : userChunks.get(userID)) {
                owners.get(c).remove(userID);
            }
            userChunks.get(userID).clear();
            used[userID] = false;
        }

        public int[] request(int userID, int chunkID) {
            TreeSet<Integer> set = owners.get(chunkID);
            int[] res = new int[set.size()];
            int i = 0;
            for (int uid : set) {
                res[i++] = uid;
            }
            if (userID != -1 && !userChunks.get(userID).contains(chunkID)) {
                userChunks.get(userID).add(chunkID);
                owners.get(chunkID).add(userID);
            }
            return res;
        }
    }

    public static void main(String[] args) {
        // 官方示例
        FileSharing fs = new FileSharing(4);
        System.out.println(fs.join(new int[]{1, 2})); // 1
        System.out.println(fs.join(new int[]{2, 3})); // 2
        System.out.println(fs.join(new int[]{4})); // 3
        print(fs.request(1, 3)); // [2]
        print(fs.request(2, 2)); // [1, 2]
        fs.leave(1);
        print(fs.request(2, 1)); // []
        fs.leave(2);
        System.out.println(fs.join(new int[]{})); // 1(复用)

        FileSharingLinear fl = new FileSharingLinear(4);
        System.out.println(fl.join(new int[]{1, 2})); // 1
        System.out.println(fl.join(new int[]{2, 3})); // 2
        System.out.println(fl.join(new int[]{4})); // 3
        print(fl.request(1, 3)); // [2]
        print(fl.request(2, 2)); // [1, 2]
        fl.leave(1);
        print(fl.request(2, 1)); // []
        fl.leave(2);
        System.out.println(fl.join(new int[]{})); // 1
    }

    private static void print(int[] a) {
        StringBuilder sb = new StringBuilder("[");
        for (int i = 0; i < a.length; i++) {
            if (i > 0) {
                sb.append(",");
            }
            sb.append(a[i]);
        }
        sb.append("]");
        System.out.println(sb.toString());
    }
}

复杂度分析

复杂度:均摊 O(log n + |owned| log m) 每次调用。

复杂度:join 最坏 O(1e4)(总体 O(n * 1e4)),但实现最直观。

相关推荐
三无少女指南5 小时前
从这里开始:Java 后端与 AI 工程的三条阅读路线
java·学习路线·ai工程
香蕉大香蕉(06敬畏市场)6 小时前
MySQL 单点详解(07)
java·数据库·mysql·ai·博客·八股
nice_lcj5206 小时前
深入理解 Java 反射:从 Class 对象到方法调用(含性能实测 + 迷你 IOC 实战 + 面试考点)
java·开发语言·后端·面试
骑着蜗牛撵大象3277 小时前
2026 后端 AI 工程化:Spring AI 管调用、MCP 管工具、Agent 内嵌管自治
java·人工智能·后端·spring·ai·可观测性·mcp
tryxr7 小时前
JVM 垃圾回收机制
java·jvm·gc·垃圾回收
圣保罗的大教堂7 小时前
leetcode 453. 最小操作次数使数组元素相等 中等
leetcode
揣一尾游鱼yu8 小时前
Spring上(IOC)笔记
java·笔记·spring·ioc
荣码8 小时前
长上下文与大文档处理:200页PDF怎么喂给AI,我踩了4个坑
java·python
极限挑战哥8 小时前
共享电单车联营平台分账架构实践:二清合规与自动分账零人工方案
java·大数据·人工智能·微信小程序·架构·微信公众平台
皮皮林5518 小时前
比 VMware 更快,更轻的虚拟机,横空出世!
java