LeetCode2261~2280题目解析

LeetCode 2261-2280. 题解合集

文章目录


2261. K Divisible Elements Subarrays(含最多 K 个可整除元素的子数组)

题目描述

给你一个整数数组 nums 和两个整数 k 和 p,找出并返回满足要求的不同 的子数组数,要求子数组中最多 k 个可被 p 整除的元素。如果两数组长度不同,或存在至少一个下标 i 满足 nums1[i] != nums2[i],则认为两数组不同。子数组定义为数组中的连续元素组成的非空序列。

示例 1:

复制代码
输入:nums = [2,3,3,2,2], k = 2, p = 2
输出:11
解释:可被 2 整除的元素位于下标 0、3、4,共 11 个不同子数组最多含 2 个可整除元素,如 [2,3,3]、[3,3,2]、[3,2,2] 等;注意 [2] 和 [3] 虽然出现多次但只计数一次。

示例 2:

复制代码
输入:nums = [1,2,3,4], k = 4, p = 1
输出:10
解释:所有元素都可被 1 整除,每个子数组都满足条件,共 10 个子数组且互不相同。

约束:

  • 1 <= nums.length <= 200

  • 1 <= nums[i], p <= 10⁹

  • 1 <= k <= nums.length

解题思路

核心思想 :枚举子数组 + 去重。子数组长度有限、每个元素最多一个分隔符,用 String 表示子数组去重最直观。

步骤:

  1. 枚举左端点 i 与右端点 j,同时统计 [i, j] 中可被 p 整除的元素个数 cnt;一旦 cnt > k 直接 break(右端点再扩大必然不合法)。
  2. 把子数组元素以分隔符拼接成字符串(如 "2#3#3#"),放入 HashSet 去重------分隔符避免 [1,23] 与 [12,3] 撞串。
  3. 返回集合大小。

复杂度:O(n²) 个子数组、每个拼接 O(n),总体 O(n³);n ≤ 200 时约 8×10⁶ 字符,可接受。空间 O(n²)。

代码实现

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

/**
 * 2261. K Divisible Elements Subarrays 含最多 K 个可整除元素的子数组
 *
 * 给你一个整数数组 nums 和两个整数 k 和 p,找出并返回满足要求的不同的子数组数,要求子数组中
 * 最多 k 个可被 p 整除的元素。如果两数组长度不同,或存在至少一个下标 i 满足 nums1[i] != nums2[i],
 * 则认为两数组不同。子数组定义为数组中的连续元素组成的非空序列。
 *
 * 算法思路:枚举所有子数组的起点、终点,统计其中可被 p 整除的元素个数(<= k 才合法),把子数组
 * 转成字符串放入 HashSet 去重,返回集合大小。n <= 200,O(n² * n) 的拼接长度在可接受范围;
 * 也可把最长子数组裁剪(合法子数组总长有限)。为更快去重,把每个子数组的元素以分隔符间隔
 * 拼成字符串。
 *
 * 时间复杂度:O(n³)(n ≤ 200),空间复杂度:O(n²)
 */
public class Solution {

    public int countDistinct(int[] nums, int k, int p) {
        int n = nums.length;
        Set<String> set = new HashSet<>();
        for (int i = 0; i < n; i++) {
            int cnt = 0;
            StringBuilder sb = new StringBuilder();
            for (int j = i; j < n; j++) {
                if (nums[j] % p == 0) {
                    cnt++;
                }
                if (cnt > k) {
                    break;
                }
                // 用分隔符防止 [1,23] 与 [12,3] 撞串
                sb.append(nums[j]).append('#');
                set.add(sb.toString());
            }
        }
        return set.size();
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 11
        System.out.println(s.countDistinct(new int[]{2, 3, 3, 2, 2}, 2, 2));
        // 示例 2:预期 10
        System.out.println(s.countDistinct(new int[]{1, 2, 3, 4}, 4, 1));
        // 自测:只有 1 个元素,无重复,预期 1
        System.out.println(s.countDistinct(new int[]{5, 5}, 1, 5));
    }
}

复杂度分析

  • 解法:枚举 + 哈希去重(主提交版本)
  • 时间复杂度:O(n³)(n ≤ 200)
  • 空间复杂度:O(n²)

2262. Total Appeal of A String(字符串的总引力)

题目描述

字符串的引力 定义为字符串中不同 字符的数量,例如 "abbca" 的引力为 3。给你一个字符串 s,返回其所有子字符串的总引力。

示例 1:

复制代码
输入:s = "abbca"
输出:28
解释:各长度子串引力分别为 5、7、7、6、3,总和 28。

示例 2:

复制代码
输入:s = "code"
输出:20
解释:所有子串引力总和为 4 + 6 + 6 + 4 = 20。

约束:

  • 1 <= s.length <= 10⁵

  • s 由小写英文字母组成

解题思路

核心思想 :贡献法(每个位置对答案的独立贡献)。对位置 i 的字符 s[i],设它上一次出现的位置为 prev(未出现过记 -1)。以 i 结尾的所有子串中,只有左端点 l 满足 prev < l <= i 时,s[i] 在这个子串里是首次出现 ,因此为引力贡献 1、其他子串的引力不含它的新鲜度。

步骤:

  1. last[c] 记录字符 c 上一次出现的位置,初始 -1。
  2. 遍历 i:贡献数 = (i - prev) * (n - i)------左端点有 (i - prev) 种取法,右端点固定为 i,该子串末尾至串尾的后缀 (n - i) 种扩展不影响"首次出现"性质。
  3. 累加所有位置的贡献即答案。

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

代码实现

java 复制代码
/**
 * 2262. Total Appeal of A String 字符串的总引力
 *
 * 字符串的引力定义为字符串中不同字符的数量。例如 "abbca" 的引力为 3。给你一个字符串 s,
 * 返回其所有子字符串的总引力。
 *
 * 算法思路:按"每个字符对总和的贡献"计数。一个字符 c 出现在子串 [l, r] 中并对引力有贡献,
 * 当且仅当它是在这个子串里**首次出现的该字符**。对每个位置 i 的字符 s[i],设它上一次出现
 * 的位置为 prev,则以 i 结尾的所有子串中,能"新贡献" s[i] 的子串左端点 l 必须满足
 * prev < l <= i,右端点固定为 i,贡献数为 (i - prev) * (n - i)。累加所有位置即可。
 *
 * 举例:s = "abbca",下标 4 的 'a' 上一次出现 prev=0,贡献 (4-0)*(5-4)=4 个含新 'a' 的子串。
 *
 * 时间复杂度:O(n),空间复杂度:O(26)
 */
public class Solution {

    public long appealSum(String s) {
        int n = s.length();
        int[] last = new int[26];
        java.util.Arrays.fill(last, -1);
        long ans = 0;
        for (int i = 0; i < n; i++) {
            int c = s.charAt(i) - 'a';
            int prev = last[c];
            // 以 i 结尾且 s[i] 作为新字符出现贡献引力 1 的子串数
            ans += (long) (i - prev) * (n - i);
            last[c] = i;
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 28
        System.out.println(s.appealSum("abbca"));
        // 示例 2:预期 20
        System.out.println(s.appealSum("code"));
        // 自测:单字符,预期 1
        System.out.println(s.appealSum("z"));
    }
}

复杂度分析

  • 解法:贡献法(主提交版本)
  • 时间复杂度:O(n)
  • 空间复杂度:O(26)

2263. Make Array Non-decreasing or Non-increasing(数组变为有序的最小操作次数)

题目描述

给你一个下标从 0 开始的整数数组 nums。在一步操作中,你可以在 0 <= i < nums.length 内选出下标 i,将 nums[i] 的值变为 nums[i] + 1 或 nums[i] - 1。返回将数组 nums 变为非递减 或非递增 所需的最小操作次数。

示例 1:

复制代码
输入:nums = [3,2,4,5,0]
输出:4
解释:可变为 [3,3,3,3,0](非递增),共 4 次操作。

示例 2:

复制代码
输入:nums = [2,2,3,4]
输出:0
解释:数组已非递减,无需操作。

示例 3:

复制代码
输入:nums = [0]
输出:0

约束:

  • 1 <= nums.length <= 1000

  • 0 <= nums[i] <= 1000

解题思路

核心思想 :最小化 sum(|a[i] - b[i]|),其中 b 单调不减------经典"数组变为单调的最小修改"问题,贪心 + 大顶堆。

步骤:

  1. 对"非递减"情形:从左到右扫描,用大顶堆维护已扫描元素中的"最大值"。
  2. 若堆顶 > num(出现逆序),需要把堆顶"压到" num:代价 += 堆顶 - num,弹出堆顶并压入 num;随后把 num 再次入堆。
  3. 堆内元素最终构成非递减序列,累加代价即最小修改数。
  4. 对"非递增"情形,把数组取负后求一次同样的代价,二者取 min。

为什么贪心正确:每次遇到逆序只修改"较大的数"到当前值,不修改前面的序列,维护了"前缀已最优且堆内有序"的不变量。

复杂度:O(n log n) 时间、O(n) 空间。(更简单但慢一档的 DP:枚举终值 j ∈ 0,1000,fij = min(fi-1≤j) + |numsi-j|,O(n×1000)。)

代码实现

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

/**
 * 2263. Make Array Non-decreasing or Non-increasing 数组变为有序的最小操作次数
 *
 * 给你一个下标从 0 开始的整数数组 nums。在一步操作中,你可以选出下标 i 并将 nums[i] 的值变为
 * nums[i] + 1 或 nums[i] - 1。返回将数组 nums 变为非递减或非递增所需的最小操作次数。
 *
 * 算法思路:经典"使数组单调的最小修改代价"问题------最小化 sum(|a[i] - b[i]|) 且 b 单调,
 * 贪心 + 大顶堆:从左到右扫描,用大顶堆维护已扫过的"峰值"。当堆顶 > 当前值时,说明出现逆序,
 * 把堆顶降到当前值:代价 += 堆顶 - 当前值,弹出堆顶并把当前值入堆(这样堆里的序列被"压低"),
 * 同时无论如何把当前值再入堆一次。最终堆里元素就是非递减的最优序列,代价为最小总修改。
 * 对非递增,等价于把数组取负后再求一次非递减的代价,取两者最小值。
 *
 * 时间复杂度:O(n log n),空间复杂度:O(n)
 */
public class Solution {

    public int convertArray(int[] nums) {
        return Math.min(cost(nums), cost(negate(nums)));
    }

    /** 让数组单调不减的最小总修改次数 */
    private int cost(int[] nums) {
        int n = nums.length;
        Queue<Integer> maxHeap = new PriorityQueue<>((a, b) -> b - a);
        int ans = 0;
        for (int num : nums) {
            if (!maxHeap.isEmpty() && maxHeap.peek() > num) {
                // 出现逆序,把堆顶"压低"到 num,代价为差值
                ans += maxHeap.poll() - num;
                maxHeap.offer(num);
            }
            maxHeap.offer(num);
        }
        return ans;
    }

    private int[] negate(int[] nums) {
        int[] arr = nums.clone();
        for (int i = 0; i < arr.length; i++) {
            arr[i] = -arr[i];
        }
        return arr;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 4
        System.out.println(s.convertArray(new int[]{3, 2, 4, 5, 0}));
        // 示例 2:预期 0
        System.out.println(s.convertArray(new int[]{2, 2, 3, 4}));
        // 示例 3:预期 0
        System.out.println(s.convertArray(new int[]{0}));
        // 自测:降序数组,预期 0(本身非递增)
        System.out.println(s.convertArray(new int[]{4, 3, 2, 1}));
    }
}

复杂度分析

  • 解法:贪心 + 大顶堆(主提交版本)
  • 时间复杂度:O(n log n)
  • 空间复杂度:O(n)
  • 写法二:DP 枚举终值,O(n × 1000)/O(1000)

2264. Largest 3-Same-Digit Number in String(字符串中最大的 3 位相同数字)

题目描述

给你一个字符串 num,表示一个大整数。如果一个整数满足:是 num 的一个长度为 3 的子字符串 ,且由唯一一个数字重复 3 次组成,则称该整数是一个优质整数 。以字符串形式返回最大的优质整数 ;如果不存在,返回空字符串 ""。注意:num 或优质整数中可能存在前导零。

示例 1:

复制代码
输入:num = "6777133339"
输出:"777"
解释:num 中存在 "777" 和 "333",取较大的 "777"。

示例 2:

复制代码
输入:num = "2300019"
输出:"000"
解释:"000" 是唯一优质整数。

示例 3:

复制代码
输入:num = "42352338"
输出:""
解释:不存在优质整数。

约束:

  • 3 <= num.length <= 1000

  • num 仅含数字 0 - 9

解题思路

核心思想 :直接枚举候选。优质整数必为 "ddd" 形式,从大到小检查即得最大值。

步骤:

  1. 从 '9' 到 '0' 枚举数字 d,构造 t = "" + d + d + d。
  2. 若 num.contains(t),直接返回 t(从大到小保证第一个命中的就是最大值)。
  3. 结束返回 ""。

复杂度:O(10 × n) 时间、O(1) 空间。

代码实现

java 复制代码
/**
 * 2264. Largest 3-Same-Digit Number in String 字符串中最大的 3 位相同数字
 *
 * 给你一个字符串 num,表示一个大整数。如果一个整数满足:是 num 的一个长度为 3 的子字符串,
 * 且由唯一一个数字重复 3 次组成,则称其为优质整数。最大优质整数以字符串形式返回,
 * 不存在则返回空字符串 ""。num 中可能存在前导零。
 *
 * 算法思路:从左到右扫描,若 s[i] == s[i+1] == s[i+2],则 s.substring(i, i+3) 是一个候选。
 * 数字字符 '9' 到 '0',从大到小比即可:用字典序(数值)取最大。直接从 '9' 到 '0' 枚举
 * 更简单:检查字符串中是否包含 "ddd"。
 *
 * 时间复杂度:O(10 * n),空间复杂度:O(1)
 */
public class Solution {

    public String largestGoodInteger(String num) {
        // 从大到小枚举 999、888 ... 000,找到即可(保证最大)
        for (char d = '9'; d >= '0'; d--) {
            String t = "" + d + d + d;
            if (num.contains(t)) {
                return t;
            }
        }
        return "";
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 "777"
        System.out.println(s.largestGoodInteger("6777133339"));
        // 示例 2:预期 "000"
        System.out.println(s.largestGoodInteger("2300019"));
        // 示例 3:预期 ""
        System.out.println(s.largestGoodInteger("42352338"));
    }
}

复杂度分析

  • 解法:枚举优秀串(主提交版本)
  • 时间复杂度:O(10 × n)
  • 空间复杂度:O(1)

2265. Count Nodes Equal to Average of Subtree(统计值等于子树平均值的节点数)

题目描述

给你一棵二叉树的根节点 root,找出并返回满足要求的节点数,要求节点的值等于其子树 中值的平均值 。n 个元素的平均值由 n 个元素求和 再除以 n 并向下舍入 到最近的整数。root 的子树 由 root 和它的所有后代组成。

示例 1:

复制代码
输入:root = [4,8,5,0,1,null,6]
输出:5
解释:节点 4 的平均值为 24/6=4;节点 5 为 11/2=5;节点 0、1、6 平均值为自身,共 5 个满足。

示例 2:

复制代码
输入:root = [1]
输出:1

约束:

  • 树中节点数目在 [1, 1000] 内

  • 0 <= Node.val <= 1000

解题思路

核心思想 :后序遍历返回 (子树和, 子树节点数)。每个节点归并左右子结果后判断 node.val == sum / count(整除即向下取整)并计数。

步骤:

  1. dfs(node) 递归返回 long[]{子树和, 子树节点数},空节点返回 {0, 0}。
  2. 归并时若 node.val == sum / cnt,全局计数 +1。
  3. 注意每次调用主方法前重置计数,避免实例字段跨用例残留。

复杂度:O(n) 时间、O(h) 空间(h 为树高)。

代码实现

java 复制代码
/**
 * 2265. Count Nodes Equal to Average of Subtree 统计值等于子树平均值的节点数
 *
 * 给你一棵二叉树的根节点 root,找出并返回满足要求的节点数,要求节点的值等于其子树中值的平均值。
 * n 个元素的平均值 = 求和 / n 并向下舍入到最近的整数。root 的子树由 root 和它的所有后代组成。
 *
 * 算法思路:后序遍历,每个节点返回 (子树和, 子树节点数),主函数判断
 * 节点值 == sum / count(整除即向下取整),计数 +1。
 *
 * 时间复杂度:O(n),空间复杂度:O(h)(递归栈)
 */
public class Solution {

    private int ans = 0;

    public int averageOfSubtree(TreeNode root) {
        ans = 0; // 每次调用重置,避免实例字段残留
        dfs(root);
        return ans;
    }

    /** 返回 (子树和, 子树节点数) */
    private long[] dfs(TreeNode node) {
        if (node == null) {
            return new long[]{0, 0};
        }
        long[] l = dfs(node.left);
        long[] r = dfs(node.right);
        long sum = l[0] + r[0] + node.val;
        long cnt = l[1] + r[1] + 1;
        if (node.val == sum / cnt) {
            ans++;
        }
        return new long[]{sum, cnt};
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:[4,8,5,0,1,null,6],预期 5
        TreeNode root1 = new TreeNode(4,
                new TreeNode(8, new TreeNode(0), new TreeNode(1)),
                new TreeNode(5, null, new TreeNode(6)));
        System.out.println(s.averageOfSubtree(root1));
        // 示例 2:[1],预期 1
        System.out.println(s.averageOfSubtree(new TreeNode(1)));
    }
}

/**
 * 二叉树节点定义(力扣预置类,提交时不要粘贴本类)
 */
class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;

    TreeNode() {
    }

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

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

复杂度分析

  • 解法:后序遍历(主提交版本)
  • 时间复杂度:O(n)
  • 空间复杂度:O(h)

2266. Count Number of Texts(统计打字方案数)

题目描述

Alice 给 Bob 用手机打字,数字到字母的对应如九键键盘:按出按键上的第 i 个字母需要按键 i 次(数字 '0' 和 '1' 不映射任何字母)。Bob 收到的只是按键字符串 pressedKeys,返回 Alice 总共可能发出多少种文字信息 ,对 10⁹ + 7 取余。

示例 1:

复制代码
输入:pressedKeys = "22233"
输出:8
解释:可能的文字信息包括 "aaadd"、"abdd"、"badd"、"cdd"、"aaae"、"abe"、"bae"、"ce",共 8 种。

示例 2:

复制代码
输入:pressedKeys = "222222222222222222222222222222222222"
输出:82876089
解释:总共 2082876103 种,取余后为 82876089。

约束:

  • 1 <= pressedKeys.length <= 10⁵

  • pressedKeys 只含数字字符 '2' - '9'

解题思路

核心思想 :线性 DP。设 dp[i] 表示前 i 个字符(下标 [0,i))能构成的方案数。最后一次"按键"可以按 1 次、2 次、3 次,数字 7/9 还可以按 4 次(对应 4 个字母),但连续按多次要求这些字符都相同。

步骤:

  1. dp[0] = 1(空串一种)。
  2. 对 i:dp[i] = dp[i-1](最后一次按 1 次)。
  3. 若 s[i-1] == s[i-2],dp[i] += dp[i-2](按 2 次)。
  4. 若连续三个相同,dp[i] += dp[i-3](按 3 次)。
  5. 若 s[i-1] 为 '7' 或 '9' 且连续四个相同,dp[i] += dp[i-4](按 4 次)。
  6. 全程取模,返回 dp[n]。

复杂度:O(n) 时间、O(n) 空间(可滚动优化到 O(1))。

代码实现

java 复制代码
/**
 * 2266. Count Number of Texts 统计打字方案数
 *
 * Alice 给 Bob 用手机打字,数字到字母的对应如九键键盘:按出按键上的第 i 个字母需要按键 i 次
 * ('0' 和 '1' 不映射字母)。Bob 只收到按键字符串 pressedKeys,返回 Alice 可能发出的文字信息种数
 * 对 10⁹+7 取余。
 *
 * 算法思路:动态规划。设 dp[i] 为前 i 个字符(下标 [0,i))能构成的方案数。
 * 对第 i 个字符(1-index 即 s[i-1]):
 * - 它可以单独作为一次按键(任何数字都可按 1 次):dp[i] += dp[i-1];
 * - 若 s[i-1] == s[i-2],可以两个相同字符按 1 次组成一个字母:dp[i] += dp[i-2];
 * - 若 s[i-1] == s[i-2] == s[i-3] 且该数字对应 3 或 4 个字母(即数字 7/9 能连按 4 次,
 *   其余数字最多按 3 次):dp[i] += dp[i-3];
 * - 数字 7 和 9 对应 4 个字母,还能连按 4 次:若四个字符相同且数字是 7/9,
 *   dp[i] += dp[i-4]。
 * 最后返回 dp[n] 对 MOD 取余。
 *
 * 时间复杂度:O(n),空间复杂度:O(n)(可滚动优化为 O(1),但 n 为 1e5 用数组即可)
 */
public class Solution {

    private static final int MOD = 1_000_000_007;

    public int countTexts(String pressedKeys) {
        int n = pressedKeys.length();
        long[] dp = new long[n + 1];
        dp[0] = 1; // 空串一种方案
        for (int i = 1; i <= n; i++) {
            dp[i] = dp[i - 1]; // 最后一次按 1 次
            // 连按 2 次
            if (i >= 2 && pressedKeys.charAt(i - 1) == pressedKeys.charAt(i - 2)) {
                dp[i] = (dp[i] + dp[i - 2]) % MOD;
            }
            // 连按 3 次
            if (i >= 3 && pressedKeys.charAt(i - 1) == pressedKeys.charAt(i - 2)
                    && pressedKeys.charAt(i - 2) == pressedKeys.charAt(i - 3)) {
                dp[i] = (dp[i] + dp[i - 3]) % MOD;
            }
            // 连按 4 次(仅数字 7 或 9)
            if (i >= 4 && (pressedKeys.charAt(i - 1) == '7' || pressedKeys.charAt(i - 1) == '9')
                    && pressedKeys.charAt(i - 1) == pressedKeys.charAt(i - 2)
                    && pressedKeys.charAt(i - 2) == pressedKeys.charAt(i - 3)
                    && pressedKeys.charAt(i - 3) == pressedKeys.charAt(i - 4)) {
                dp[i] = (dp[i] + dp[i - 4]) % MOD;
            }
        }
        return (int) dp[n];
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 8
        System.out.println(s.countTexts("22233"));
        // 示例 2:预期 82876089
        System.out.println(s.countTexts("222222222222222222222222222222222222"));
        // 自测:"22" 可为 "b"(按 2 次)或 "aa"(两次单独按键),共 2 种,预期 2
        System.out.println(s.countTexts("22"));
    }
}

复杂度分析

  • 解法:线性 DP(主提交版本)
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)(可滚动 O(1))

2267. Check if There Is a Valid Parentheses String Path(检查是否有合法括号字符串路径)

题目描述

一个括号字符串是非空且只包含 '(' 和 ')' 的字符串,合法当且仅当:是 ()、可写为 AB(两段合法串连接)或 (A)。给你一个 m x n 的括号网格图矩阵 grid。合法括号路径 满足:开始于 (0,0)、结束于 (m-1,n-1)、每次只向下 或向右 移动,且路径经过的格子组成的括号字符串是合法 的。如果存在合法括号路径返回 true,否则 false。

示例 1:

复制代码
输入:grid = [["(","(","("],[")","(",")"],["(","(",")"],["(","(",")"]]
输出:true
解释:存在路径得到 "()(())" 或 "((()))"。

示例 2:

复制代码
输入:grid = [[")",")"],["(","("]]
输出:false
解释:两条可行路径分别得到 "))(" 和 ")((",均不合法。

约束:

  • m == grid.length

  • n == grid[i].length

  • 1 <= m, n <= 100

  • grid[i][j] 为 '(' 或 ')'

解题思路

核心思想 :动态规划 + 平衡度状态。合法括号串任意前缀的未匹配左括号数非负、整体为 0。用 dp[i][j][k] 表示能否到达 (i,j) 且当前未匹配 ( 数量为 k。

步骤:

  1. 若起终点字符非法(起点为 ) 或终点为 ()直接返回 false。
  2. dp[0][0][1] = true(起点 (,平衡度为 1)。
  3. 对每个格子 (i,j)、每种平衡度 k:可从上方/左方转移而来;若当前字符为 ( 则 k+1,为 ) 则要求 k >= 1 并转移到 k-1。
  4. 返回 dp[m-1][n-1][0]。

复杂度:O(m × n × (m+n)) 时间与空间。

代码实现

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

/**
 * 2267. Check if There Is a Valid Parentheses String Path 检查是否有合法括号字符串路径
 *
 * 给你一个 m x n 的括号网格图矩阵 grid。合法括号路径满足:从 (0,0) 开始、到 (m-1,n-1) 结束,
 * 每次只向下或向右移动,且路径经过的格子组成的括号字符串是合法的(即平衡括号序列)。
 * 如果存在一条合法括号路径返回 true,否则 false。
 *
 * 算法思路:动态规划 + 状态集合。用布尔三维数组 dp[i][j][k] 表示能否到达 (i,j) 且此时
 * "未匹配的左括号 '(' 个数"为 k(k 即当前前缀的括号平衡度)。转移:从左/上两个方向过来,
 * 若 grid[i][j] == '(' 则 k' = k + 1,若为 ')' 则需 k >= 1 且 k' = k - 1。
 * 任何时刻 k > m+n 即剪掉(路径长度有限)。最终若 dp[m-1][n-1][0] 为真说明存在合法路径。
 *
 * 时间复杂度:O(m * n * (m+n)),空间复杂度:O(m * n * (m+n))
 */
public class Solution {

    public boolean hasValidPath(char[][] grid) {
        int m = grid.length, n = grid[0].length;
        // 若首或末元素非法直接 false
        if (grid[0][0] == ')' || grid[m - 1][n - 1] == '(') {
            return false;
        }
        boolean[][][] dp = new boolean[m][n][m + n + 1];
        dp[0][0][1] = true; // 起点 '(',平衡度为 1
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (i == 0 && j == 0) {
                    continue;
                }
                char c = grid[i][j];
                for (int k = 0; k <= m + n; k++) {
                    boolean can = false;
                    if (i > 0) can |= dp[i - 1][j][k];
                    if (j > 0) can |= dp[i][j - 1][k];
                    if (!can) continue;
                    if (c == '(') {
                        // 平衡度 +1,不能超过路径总长
                        if (k + 1 <= m + n) dp[i][j][k + 1] = true;
                    } else {
                        // 需要 k >= 1
                        if (k >= 1) dp[i][j][k - 1] = true;
                    }
                }
            }
        }
        return dp[m - 1][n - 1][0];
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 true
        System.out.println(s.hasValidPath(new char[][]{
                {'(', '(', '('},
                {')', '(', ')'},
                {'(', '(', ')'},
                {'(', '(', ')'}}));
        // 示例 2:预期 false
        System.out.println(s.hasValidPath(new char[][]{
                {')', ')'},
                {'(', '('}}));
        // 自测:路径只能 "(" ")" "(" ")" 即 "(())" 或 "()()" 均合法?实际可选路径仅两条:
// (0,0)->(0,1)->(1,1) 得 "())" 非法;或 (0,0)->(1,0)->(1,1) 得 "((" ")" = "(()" 非法,
// 预期 false
        System.out.println(s.hasValidPath(new char[][]{
                {'(', ')'},
                {'(', ')'}}));
    }
}

复杂度分析

  • 解法:DP + 平衡度状态(主提交版本)
  • 时间复杂度:O(m × n × (m+n))
  • 空间复杂度:O(m × n × (m+n))

2268. Minimum Number of Keypresses(最少按键次数)

题目描述

你有一个 9 键键盘,按键按 1 到 9 编号,每个按键对应几个英文小写字母。你可以决定映射关系,但要满足:26 个字母全部映射、每个字母恰好 映射到一个按键、每个按键最多 对应 3 个字母。打出按键上的第 1 个字母按 1 次、第 2 个按 2 次、第 3 个按 3 次。给你字符串 s,返回基于你设计的键盘打出 s 需要的最少按键次数。

示例 1:

复制代码
输入:s = "apple"
输出:5
解释:把常用字母放在第 1 位只需各按 1 次,可做到 5 次。

示例 2:

复制代码
输入:s = "abcdefghijkl"
输出:15
解释:前 9 个字母各按 1 次,'j'、'k'、'l' 各按 2 次,共 9 + 6 = 15。

约束:

  • 1 <= s.length <= 10⁵

  • s 只含小写英文字母

解题思路

核心思想:贪心。出现越频繁的字母越应放在"按次数少"的位置:9 个键的第一格按 1 次,第二格按 2 次,第三格按 3 次。

步骤:

  1. 统计 26 个字母的出现次数。
  2. 按次数降序排序。
  3. 对排序后的前 9 个字母乘 1,接下来 9 个乘 2,剩余 8 个乘 3,累加即最小总按键数(用 round 变量,每满 9 个 round++,从 'a' 到 'z' 依次分配)。

复杂度:O(n + 26 log 26) 时间、O(26) 空间。

代码实现

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

/**
 * 2268. Minimum Number of Keypresses 最少按键次数
 *
 * 你有一个 9 键键盘,按键按 1 到 9 编号。每个按键最多对应 3 个英文小写字母,26 个字母必须全部
 * 映射且每个字母恰好映射到一个按键。打出按键上的第 1 个字母按 1 次,第 2 个按 2 次,第 3 个
 * 按 3 次。给你字符串 s,返回基于你设计的键盘打出 s 需要的最少按键次数。
 *
 * 算法思路:贪心。出现频率越高的字母越应该放在"按次数少"的位置上。统计 26 个字母出现次数,
 * 降序排序。前 9 个字母按 1 次(9 个键的第一个位置),接下来 9 个按 2 次,剩下 8 个按 3 次。
 * 总按键 = sum(次数 * 层级)。
 *
 * 时间复杂度:O(n + 26 log 26),空间复杂度:O(26)
 */
public class Solution {

    public int minimumKeypresses(String s) {
        int[] cnt = new int[26];
        for (char c : s.toCharArray()) {
            cnt[c - 'a']++;
        }
        Arrays.sort(cnt); // 升序,方便从尾取高频
        int ans = 0;
        int round = 1; // 1、2、3 次
        int used = 0;
        for (int i = 25; i >= 0; i--) {
            if (cnt[i] == 0) {
                break;
            }
            ans += cnt[i] * round;
            used++;
            if (used % 9 == 0) {
                round++; // 每 9 个字母层级 +1
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 5
        System.out.println(s.minimumKeypresses("apple"));
        // 示例 2:预期 15
        System.out.println(s.minimumKeypresses("abcdefghijkl"));
        // 自测:26 个字母各一次,预期 9*1 + 9*2 + 8*3 = 51
        System.out.println(s.minimumKeypresses("abcdefghijklmnopqrstuvwxyz"));
    }
}

复杂度分析

  • 解法:频率贪心(主提交版本)
  • 时间复杂度:O(n + 26 log 26)
  • 空间复杂度:O(26)

2269. Find the K-Beauty of a Number(找到一个数字的 K 美丽值)

题目描述

一个整数 num 的 k 美丽值 定义为 num 中符合以下条件的子字符串 数目:子字符串长度为 k,且子字符串能整除 num。注意允许前缀 0 ,且 0 不能整除任何值。返回 num 的 k 美丽值。

示例 1:

复制代码
输入:num = 240, k = 2
输出:2
解释:"24" 能整除 240,"40" 也能整除 240,美丽值为 2。

示例 2:

复制代码
输入:num = 430043, k = 2
输出:2
解释:"43"(开头和结尾) 各能整除 430043,"30"、"00"、"04" 都不能,美丽值为 2。

约束:

  • 1 <= num <= 10⁹

  • 1 <= k <= num.length(num 的十进制位数)

解题思路

核心思想 :滑动窗口取子串转整数判定。把 num 转成字符串,每次取长度为 k 的子串转成整数 sub(前导零会被自动忽略,如 "04" → 4);若 sub != 0 且 num % sub == 0 则计数。

步骤:

  1. s = Integer.toString(num),遍历所有起点。
  2. sub = Integer.parseInt(s.substring(i, i+k))。
  3. sub != 0 && num % sub == 0 时 ans++。

复杂度:O(n × k)(k ≤ 10 可视为 O(n))、空间 O(1)。

代码实现

java 复制代码
/**
 * 2269. Find the K-Beauty of a Number 找到一个数字的 K 美丽值
 *
 * 一个整数 num 的 k 美丽值定义为 num 中符合以下条件的子字符串数目:子字符串长度为 k,且子字符串
 * 能整除 num。num 拼接成字符串后,允许子字符串有前缀 0;0 不能整除任何值。
 * 返回 num 的 k 美丽值。
 *
 * 算法思路:把 num 转成字符串,滑动窗口取每个长 k 的子串,转成整数 sub(含前导零,
 * 如 "04" -> 4);若 sub != 0 且 num % sub == 0 则计数 +1。
 * 注意:不能用 Long 溢出,num <= 10⁹ 且 sub 是 k 位(k <= 10),用 long 保险。
 *
 * 时间复杂度:O(n * k)(n 为位数,k <= 10 可视为 O(n)),空间复杂度:O(1)
 */
public class Solution {

    public int divisorSubstrings(int num, int k) {
        String s = Integer.toString(num);
        int n = s.length();
        int ans = 0;
        for (int i = 0; i + k <= n; i++) {
            int sub = Integer.parseInt(s.substring(i, i + k));
            if (sub != 0 && num % sub == 0) {
                ans++;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 2
        System.out.println(s.divisorSubstrings(240, 2));
        // 示例 2:预期 2
        System.out.println(s.divisorSubstrings(430043, 2));
        // 自测:num=10, k=1,子串 '1' 整除 10、'0' 不能,预期 1
        System.out.println(s.divisorSubstrings(10, 1));
    }
}

复杂度分析

  • 解法:滑动窗口取子串(主提交版本)
  • 时间复杂度:O(n × k)
  • 空间复杂度:O(1)

2270. Number of Ways to Split Array(分割数组的方案数)

题目描述

给你一个下标从 0 开始长度为 n 的整数数组 nums。如果前 i + 1 个元素的和大于等于 剩下的 n - i - 1 个元素的和,且下标 i 右边至少有一个 元素(i <= n - 2),则称下标 i 处有一个合法的分割。返回合法分割的数目。

示例 1:

复制代码
输入:nums = [10,4,-8,7]
输出:2
解释:i=0:10 >= 3;i=1:14 >= -1,均合法;i=2 右边没有元素不计。

示例 2:

复制代码
输入:nums = [2,3,1,0]
输出:2
解释:i=1:5 >= 1;i=2:6 >= 0 均合法。

约束:

  • 2 <= nums.length <= 10⁵

  • -10⁵ <= nums[i] <= 10⁵

解题思路

核心思想 :前缀和一趟扫描。先求总和 total,再从左到右累加前缀和 pre;对 i ∈ [0, n-2] 判断 pre >= total - pre,满足则计数。用 long 防止和溢出。

步骤:

  1. 求 total(long)。
  2. pre 从左累加,比较 pre 与 total - pre。
  3. 只遍历到 n - 2(右侧需至少剩一个元素)。

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

代码实现

java 复制代码
/**
 * 2270. Number of Ways to Split Array 分割数组的方案数
 *
 * 给你一个下标从 0 开始长度为 n 的整数数组 nums。如果下标 i 满足:前 i+1 个元素的和大于等于
 * 剩下的 n-i-1 个元素的和,且下标 i 右边至少有一个元素(即 i <= n-2),则称其为合法分割。
 * 返回合法分割的数量。
 *
 * 算法思路:先求总和 total,再从左到右累加前缀和 pre;对每个 i 从 0 到 n-2,
 * 若 pre >= total - pre(剩下的和)则计数 +1。用 long 防止溢出(元素可为负)。
 *
 * 时间复杂度:O(n),空间复杂度:O(1)
 */
public class Solution {

    public int waysToSplitArray(int[] nums) {
        long total = 0;
        for (int v : nums) {
            total += v;
        }
        long pre = 0;
        int ans = 0;
        for (int i = 0; i < nums.length - 1; i++) {
            pre += nums[i];
            if (pre >= total - pre) {
                ans++;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:nums = [10,4,-8,7],合法分割 i=0、1,预期 2
        System.out.println(s.waysToSplitArray(new int[]{10, 4, -8, 7}));
        // 示例 2:nums = [2,3,1,0],合法分割 i=1、2,预期 2
        System.out.println(s.waysToSplitArray(new int[]{2, 3, 1, 0}));
        // 自测:[-1,-2],i=0 时 -1 >= -2 合法,预期 1
        System.out.println(s.waysToSplitArray(new int[]{-1, -2}));
    }
}

复杂度分析

  • 解法:前缀和扫描(主提交版本)
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

2271. Maximum White Tiles Covered by a Carpet(毯子覆盖的最多白色砖块数)

题目描述

给你一个二维整数数组 tiles,其中 tiles[i] = [li, ri] 表示位置 li 到 ri 之间(含端点)的每个瓷砖都被涂成白色。同时给你整数 carpetLen 表示一块毯子的长度,可以放在任何位置 。返回使用这块毯子最多可以盖住多少块白色瓷砖。

示例 1:

复制代码
输入:tiles = [[1,5],[10,11],[12,18],[20,25],[30,32]], carpetLen = 10
输出:9
解释:把毯子从 10 放到 19,可覆盖 [10,11] 与 [12,18] 共 9 块。

示例 2:

复制代码
输入:tiles = [[10,11],[1,1]], carpetLen = 2
输出:2
解释:把毯子从 10 放到 11 覆盖 2 块。

约束:

  • 1 <= tiles.length <= 5 * 10⁴

  • tiles[i].length == 2

  • 1 <= li <= ri <= 10⁹

  • 1 <= carpetLen <= 10⁹

  • tiles 互不重叠

解题思路

核心思想 :排序 + 滑动窗口。最优方案中毯子左端必然顶着某个区间的左端点(否则右移不劣)。按左端点排序后,枚举左端起点 tiles[i][0],计算毯子右端 right = tiles[i][0] + carpetLen - 1,用指针 j 扩展所有起点 <= right 的区间并累加完整覆盖长度;最后一个区间可能部分超出 right,要减去超出部分。

步骤:

  1. 按 l 升序排序。
  2. 双指针:j 只增不减地加入新区间,covered 累计 [i, j) 完整长度。
  3. 若 tiles[j-1] 的右端点 > right,cur = covered - (右端点 - right),否则 cur = covered。
  4. 更新答案,然后从 covered 减去区间 i 的长度。

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

代码实现

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

/**
 * 2271. Maximum White Tiles Covered by a Carpet 毯子覆盖的最多白色砖块数
 *
 * 给你一个二维整数数组 tiles,其中 tiles[i] = [li, ri] 表示位置 li 到 ri 之间的每个瓷砖
 * 都被涂成白色。同时给你整数 carpetLen 表示毯子长度。返回使用这块毯子最多可以盖住多少块
 * 白色瓷砖。
 *
 * 算法思路:排序 + 滑动窗口。按左端点排序后,枚举毯子左端放在 tiles[i][0] 处(最优方案
 * 一定可以从某个区间的左端开始,否则把毡子右移不会减少覆盖)。右端 right = tiles[i][0] + carpetLen - 1,
 * 用指针 j 扩展所有起点 <= right 的区间并累加完整长度 covered;若最后一个区间的右端点超出
 * right,则减去超出的部分(该区间只被部分覆盖)。维护覆盖数最大值,然后移除区间 i 的贡献。
 *
 * 时间复杂度:O(n log n),空间复杂度:O(1)
 */
public class Solution {

    public int maximumWhiteTiles(int[][] tiles, int carpetLen) {
        Arrays.sort(tiles, (a, b) -> a[0] - b[0]);
        int n = tiles.length;
        int ans = 0;
        int j = 0;
        int covered = 0; // 完整覆盖的瓷砖总数(窗口 [i, j-1] 区间长度和)
        for (int i = 0; i < n; i++) {
            int right = tiles[i][0] + carpetLen - 1; // 毯子右端
            // 扩展窗口右端:所有左端点 <= right 的区间加入
            while (j < n && tiles[j][0] <= right) {
                covered += tiles[j][1] - tiles[j][0] + 1;
                j++;
            }
            int cur = covered;
            // 最后一个区间(下标 j-1)可能右端点超出 right,只算落入的部分
            if (j > 0 && tiles[j - 1][1] > right) {
                cur -= tiles[j - 1][1] - right;
            }
            ans = Math.max(ans, cur);
            // 窗口左移:移除区间 i 的贡献
            covered -= tiles[i][1] - tiles[i][0] + 1;
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 9
        System.out.println(s.maximumWhiteTiles(new int[][]{{1, 5}, {10, 11}, {12, 18}, {20, 25}, {30, 32}}, 10));
        // 示例 2:预期 2(排序后 [1,1],[10,11],毯长 2)
        System.out.println(s.maximumWhiteTiles(new int[][]{{10, 11}, {1, 1}}, 2));
        // 自测:预期 3
        System.out.println(s.maximumWhiteTiles(new int[][]{{1, 2}, {3, 4}, {5, 6}}, 3));
    }
}

复杂度分析

  • 解法:排序 + 滑动窗口(主提交版本)
  • 时间复杂度:O(n log n)
  • 空间复杂度:O(1)

2272. Substring With Largest Variance(最大波动的子字符串)

题目描述

字符串的波动 定义为子字符串中出现次数最多 的字符次数与出现次数最少 的字符次数之差,即波动值 = freq(max) - freq(min),其中 freq(c) 表示字符 c 在子串中的出现次数。给你字符串 s(只含小写英文字母),返回 s 里所有子字符串 的最大波动 值。子字符串 是字符串中的一个连续字符序列;若 s 中每个字符出现次数均不超过 1(如 "abcde"),或出现最多的字符与最少的字符相同,则波动为 0。

示例 1:

复制代码
输入:s = "aababbb"
输出:3
解释:子串 "babbb" 中 'b' 出现 4 次、'a' 出现 1 次,波动 3。

示例 2:

复制代码
输入:s = "abcde"
输出:0
解释:每个字符至多出现 1 次,任何子串波动都是 0。

约束:

  • 1 <= s.length <= 10⁴

  • s 只含小写英文字母

解题思路

核心思想:枚举"最多字符 major"与"最少字符 minor",线性扫描求差的最大值。任一子串的波动只与串中最多的和最少的那两个字符相关,因此枚举 26×26 对字符即可覆盖所有情况。

步骤:

  1. 先统计每个字符总数 cnt。
  2. 对每对 (major, minor)(major != minor 且两者在原串中都出现过):
    • 遍历 s:遇到 major 时 majorCnt++,遇到 minor 时 minorCnt++ 且 restMinor--。
    • 若两者计数都 > 0,用 majorCnt - minorCnt 更新答案。
    • 若 minorCnt > majorCnt 且后面还有 minor(restMinor > 0),重置两计数------当前前缀已为负,丢弃该段让后续有机会更大。
  3. 取所有对的答案最大值。

复杂度:O(26² × n) = O(n)(常数 676 倍),空间 O(26)。

代码实现

java 复制代码
/**
 * 2272. Substring With Largest Variance 最大波动的子字符串
 *
 * 字符串的波动定义为子字符串中出现次数最多的字符次数与出现次数最少的字符次数之差。
 * 给你字符串 s(只含小写字母),返回 s 里所有子字符串的最大波动值。
 *
 * 算法思路:枚举"最多字符 major"与"最少字符 minor"(major != minor),对每个子串统计二者
 * 次数差。线性扫描:遇到 major 时 majorCnt++;遇到 minor 时 minorCnt++(并令 restMinor 减少)。
 * 当 minorCnt > 0 且 majorCnt > 0 时更新答案 majorCnt - minorCnt;
 * 当 minorCnt > majorCnt 且后面还有可用的 minor(restMinor > 0)时,重置计数------
 * 因为当前前缀 continue 下去必然为负,丢弃可让后续段有更大机会为正。
 * 遍历所有 26*26 对字符组合取最大。
 *
 * 时间复杂度:O(26² * n),空间复杂度:O(1)
 */
public class Solution {

    public int largestVariance(String s) {
        int[] cnt = new int[26];
        for (char c : s.toCharArray()) {
            cnt[c - 'a']++;
        }
        int ans = 0;
        for (int i = 0; i < 26; i++) {
            for (int j = 0; j < 26; j++) {
                if (i == j || cnt[i] == 0 || cnt[j] == 0) {
                    continue; // 子串必须同时含两种字符才可能贡献正波动
                }
                char major = (char) ('a' + i); // 出现次数最多
                char minor = (char) ('a' + j); // 出现次数最少
                int majorCnt = 0, minorCnt = 0;
                int restMinor = cnt[j]; // 剩余尚未扫描的 minor 个数
                for (char c : s.toCharArray()) {
                    if (c == major) {
                        majorCnt++;
                    } else if (c == minor) {
                        minorCnt++;
                        restMinor--;
                    }
                    if (minorCnt > 0 && majorCnt > 0) {
                        ans = Math.max(ans, majorCnt - minorCnt);
                    }
                    if (minorCnt > majorCnt && restMinor > 0) {
                        // 前缀已为负且还能从后面的 minor 重新开始,丢弃当前段
                        majorCnt = 0;
                        minorCnt = 0;
                    }
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 3
        System.out.println(s.largestVariance("aababbb"));
        // 示例 2:预期 0
        System.out.println(s.largestVariance("abcde"));
        // 自测:"bbbaaa" 最多 a(3 次)、最少 b(1 次),子串 "baaa" 波动 2,预期 2
        System.out.println(s.largestVariance("bbbaaa"));
        // 自测:全同字符,波动只能为 0,预期 0
        System.out.println(s.largestVariance("aaaa"));
    }
}

复杂度分析

  • 解法:枚举最值字符对 + 线性扫描(主提交版本)
  • 时间复杂度:O(26² × n)
  • 空间复杂度:O(26)

2273. Find Resultant Array After Removing Anagrams(移除字母异位词后的结果数组)

题目描述

给你一个下标从 0 开始的字符串数组 words,其中 words[i] 由小写英文字符组成。在一步操作中,选出下标 i(满足 words[i-1] 和 words[i] 是字母异位词 )并删除 words[i]。只要可以就持续操作。字母异位词是由重新排列源单词字母得到的新单词(如 "dacb" 是 "abdc" 的异位词)。返回执行所有操作后的 words,按任意顺序选择下标都会得到相同结果。

示例 1:

复制代码
输入:words = ["abba","baba","bbaa","cd","cd"]
输出:["abba","cd"]
解释:依次删除与前一词构成异位词的后词,直到不能删除。

示例 2:

复制代码
输入:words = ["a","b","c","d","e"]
输出:["a","b","c","d","e"]
解释:没有相邻异位词。

约束:

  • 1 <= words.length <= 100

  • 1 <= words[i].length <= 10

  • words[i] 只含小写英文字母

解题思路

核心思想 :只比较相邻保留项。每个词排序后的字符串作为"签名",异位词签名相同。若当前词的签名与上一个保留词的签名相同则删除,否则保留并更新签名。

步骤:

  1. 遍历 words,对每个词排序得到签名。
  2. 若签名等于 prevSign 则跳过(删除)。
  3. 否则保留原词,更新 prevSign。

复杂度:O(n × L log L)(L ≤ 10 可视为 O(n))、空间 O(n)。

代码实现

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

/**
 * 2273. Find Resultant Array After Removing Anagrams 移除字母异位词后的结果数组
 *
 * 给你一个下标从 0 开始的字符串数组 words,其中 words[i] 由小写英文字符组成。一步操作可选出
 * 下标 i 并删除 words[i],其中 i 需要满足 words[i-1] 和 words[i] 是字母异位词。只要可以就
 * 一直执行该操作。返回执行所有操作后的 words。字母异位词是重新排列源单词字母得到的新单词。
 *
 * 算法思路:只需比较相邻项。把每个词排序后的形态作为"签名",若当前词与前一个保留的词签名相同,
 * 则当前词是异位词,跳过;否则保留。用 List 收集结果即可(要求返回原字符串顺序)。
 *
 * 时间复杂度:O(n * L log L)(L 为词长),空间复杂度:O(n)
 */
public class Solution {

    public List<String> removeAnagrams(String[] words) {
        List<String> ans = new ArrayList<>();
        String prevSign = null;
        for (String w : words) {
            char[] arr = w.toCharArray();
            Arrays.sort(arr);
            String sign = new String(arr);
            if (sign.equals(prevSign)) {
                continue; // 与上一个保留词构成异位词,删除
            }
            ans.add(w);
            prevSign = sign;
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 [abba, cd]
        System.out.println(s.removeAnagrams(new String[]{"abba", "baba", "bbaa", "cd", "cd"}));
        // 示例 2:预期 [a,b,c,d,e]
        System.out.println(s.removeAnagrams(new String[]{"a", "b", "c", "d", "e"}));
        // 自测:与更前面的词构成异位词但不与相邻的构成,不能删,预期 [ab, ba, cd] 中 "ba" 删除 → [ab, cd]
        System.out.println(s.removeAnagrams(new String[]{"ab", "ba", "cd"}));
    }
}

复杂度分析

  • 解法:相邻签名比较(主提交版本)
  • 时间复杂度:O(n × L log L)
  • 空间复杂度:O(n)

2274. Maximum Consecutive Floors Without Special Floors(不含特殊楼层的最大连续楼层数)

题目描述

Alice 租用大楼从 bottom 到 top(含端点)的所有楼层,并将其中一些楼层作为特殊楼层 。给你 special 数组表示特殊楼层。返回不含特殊楼层 的最大连续楼层数。

示例 1:

复制代码
输入:bottom = 2, top = 9, special = [4,6]
输出:3
解释:不含特殊楼层的连续段为 (2,3)、5、(7,9),最大为 3。

示例 2:

复制代码
输入:bottom = 6, top = 8, special = [7,6,8]
输出:0
解释:所有楼层都是特殊楼层。

约束:

  • 1 <= special.length <= 10⁵

  • 1 <= bottom <= special[i] <= top <= 10⁹

  • special 中元素互不相同

解题思路

核心思想:排序后分段。把特殊楼层排序,普通楼层被切成若干连续段,最大段长度即答案。

步骤:

  1. 排序 special。
  2. 首段长度 = special[0] - bottom;中间段 = special[i+1] - special[i] - 1;末段 = top - special[m-1]。
  3. 取最大值。

复杂度:O(m log m) 时间、O(1) 空间。

代码实现

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

/**
 * 2274. Maximum Consecutive Floors Without Special Floors 不含特殊楼层的最大连续楼层数
 *
 * Alice 租用了从 bottom 到 top(含端点)的所有楼层,其中 special 数组是特殊楼层。
 * 返回不含特殊楼层的最大连续楼层数。
 *
 * 算法思路:把所有特殊楼层排序,则普通楼层被切成若干段:
 * [bottom, s0-1]、[s0+1, s1-1]、...、[sLast+1, top]。
 * 计算每段长度(区间为空则 0)取最大值。也可在首尾补上下界哨兵简化。
 *
 * 时间复杂度:O(m log m)(m 为 special 长度),空间复杂度:O(1)
 */
public class Solution {

    public int maxConsecutive(int bottom, int top, int[] special) {
        Arrays.sort(special);
        int m = special.length;
        int ans = 0;
        // 段1:bottom ~ special[0]-1
        ans = Math.max(ans, special[0] - bottom);
        // 中间段:special[i]+1 ~ special[i+1]-1
        for (int i = 0; i + 1 < m; i++) {
            ans = Math.max(ans, special[i + 1] - special[i] - 1);
        }
        // 末段:special[m-1]+1 ~ top
        ans = Math.max(ans, top - special[m - 1]);
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 3
        System.out.println(s.maxConsecutive(2, 9, new int[]{4, 6}));
        // 示例 2:预期 0
        System.out.println(s.maxConsecutive(6, 8, new int[]{7, 6, 8}));
        // 自测:预期 2
        System.out.println(s.maxConsecutive(1, 10, new int[]{3, 7, 10}));
    }
}

复杂度分析

  • 解法:排序分段(主提交版本)
  • 时间复杂度:O(m log m)
  • 空间复杂度:O(1)

2275. Largest Combination With Bitwise AND Greater Than Zero(按位与结果大于零的最长组合)

题目描述

对数组执行按位与 相当于对所有整数执行按位与(如 [1,5,3] 的按位与为 1 & 5 & 3 = 1)。给你一个正整数数组 candidates,计算其数字每种组合下按位与的结果,返回按位与结果大于 0 的最长组合的长度。

示例 1:

复制代码
输入:candidates = [16,17,71,62,12,24,14]
输出:4
解释:[16,17,62,24] 的按位与 = 16 > 0,长度 4;同理 [62,12,24,14] 结果 8。

示例 2:

复制代码
输入:candidates = [8,8]
输出:2
解释:8 & 8 = 8 > 0。

约束:

  • 1 <= candidates.length <= 10⁵

  • 1 <= candidates[i] <= 10⁷

解题思路

核心思想 :逐位统计。一组数的按位与非零 ⇔ 存在某一位在所有数中都是 1。因此对每一位统计 candidates 中该位为 1 的元素个数,最大值即为答案------这些元素构成一组合法组合,且任何合法组合的长度都不超过该位为 1 的元素总数。

步骤:

  1. 对每一位 bit ∈ [0, 31] 统计 (v >> bit) & 1 == 1 的个数。
  2. 取最大计数返回。

复杂度:O(32 × n) 时间、O(1) 空间。

代码实现

java 复制代码
/**
 * 2275. Largest Combination With Bitwise AND Greater Than Zero 按位与结果大于零的最长组合
 *
 * 对数组 nums 执行按位与即对数组所有整数执行按位与。给你正整数数组 candidates,
 * 计算其所有组合的按位与结果,返回按位与结果大于 0 的最长组合的长度。
 *
 * 算法思路:组合的按位与非零,当且仅当存在某一位,该位在这组合的所有元素中都是 1。
 * 因此对每一位(0..31)统计 candidates 中该位为 1 的元素个数,最大值即为答案------
 * 该位的所有元素就是一组合法组合,且任意合法组合的长度不超过某一位的置 1 个数。
 *
 * 时间复杂度:O(32 * n),空间复杂度:O(1)
 */
public class Solution {

    public int largestCombination(int[] candidates) {
        int ans = 0;
        for (int bit = 0; bit < 32; bit++) {
            int cnt = 0;
            for (int v : candidates) {
                if ((v >> bit & 1) == 1) {
                    cnt++;
                }
            }
            ans = Math.max(ans, cnt);
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 4
        System.out.println(s.largestCombination(new int[]{16, 17, 71, 62, 12, 24, 14}));
        // 示例 2:预期 2
        System.out.println(s.largestCombination(new int[]{8, 8}));
        // 自测:1、2、4 任何组合为 0,预期 1
        System.out.println(s.largestCombination(new int[]{1, 2, 4}));
    }
}

复杂度分析

  • 解法:逐位统计(主提交版本)
  • 时间复杂度:O(32 × n)
  • 空间复杂度:O(1)

2276. Count Integers in Intervals(统计区间中的整数数目)

题目描述

给你区间的空 集,设计并实现支持以下操作的数据结构:void add(int left, int right) 添加区间 [left, right] 到区间集合中;int count() 返回出现在至少一个 区间中的整数个数。实现 CountIntervals 类。

示例 1:

复制代码
输入:["CountIntervals", "add", "add", "count", "add", "count"]
参数:[[], [2,3], [7,10], [], [5,8], []]
输出:[null, null, null, 6, null, 8]
解释:先有 [2,3] 与 [7,10] 合计 6 个整数;再加 [5,8] 与 [7,10] 合并为 [5,10](6 个),总数为 2 + 6 = 8。

约束:

  • 1 <= left <= right <= 10⁹

  • 最多调用 add 和 count 方法总计 10⁵ 次

  • 调用 count 方法至少一次

解题思路

核心思想 :有序集合维护合并后的不相交区间 + 动态维护总数。用 TreeMap(左端点 → 右端点)存储区间,cnt 记录总覆盖整数数。add 时反复取出与新区间相交或相邻 (r >= left-1 且 l <= right+1)的旧区间进行合并。

步骤:

  1. add(left, right):floorKey(right + 1) 找到左端点 ≤ right+1 的最大键,若其右端点 ≥ left - 1 则与新区间合并:更新 left/right 的并集、cnt 减去旧长度、删除旧键;重复直到不再相交。
  2. 插入合并后的区间,cnt += right - left + 1。
  3. count() 直接返回 cnt。

复杂度 :add 摊还 O(log n)(每个区间至多被删一次),count O(1)。

代码实现

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

/**
 * 2276. Count Integers in Intervals 统计区间中的整数数目
 *
 * 实现 CountIntervals 类:CountIntervals() 初始化空区间集;void add(int left, int right)
 * 添加区间 [left, right] 到集合;int count() 返回出现在至少一个区间中的整数个数。
 *
 * 算法思路:有序集合维护互不重叠的(合并后)区间 + 动态维护总数。用 TreeMap
 * (左端点 → 右端点)存合并后的区间,cnt 记录当前总覆盖整数数。add 时反复取出
 * 与 [left, right] 相交或相邻的区间进行合并,减少 cnt 并删除旧区间,最后把合并后区间
 * 插入并增加 cnt。
 *
 * 时间复杂度:add 摊还 O(log n)(合并删除的区间总数为 O(区间数)),count O(1)
 */
public class Solution {

    public static void main(String[] args) {
        CountIntervals ci = new CountIntervals();
        ci.add(2, 3);
        System.out.println(ci.count()); // 2:区间 [2,3]
        ci.add(7, 10);
        System.out.println(ci.count()); // 6:2 + 4 = 6
        ci.add(5, 8);
        System.out.println(ci.count()); // 8:合并为 [2,3]、[5,10]
    }
}

class CountIntervals {
    private TreeMap<Integer, Integer> intervals; // 左端点 -> 右端点
    private int cnt;

    public CountIntervals() {
        intervals = new TreeMap<>();
        cnt = 0;
    }

    public void add(int left, int right) {
        // 反复与相交/相邻的区间合并:floorKey(right+1) 是左端点 <= right+1 的最大区间
        Integer key = intervals.floorKey(right + 1);
        while (key != null && intervals.get(key) >= left - 1) {
            // 当前区间 [key, r] 与 [left, right] 相交或相邻,合并
            int r = intervals.get(key);
            left = Math.min(left, key);
            right = Math.max(right, r);
            cnt -= r - key + 1;
            intervals.remove(key);
            key = intervals.floorKey(right + 1); // 继续找下一个可能相交的
        }
        intervals.put(left, right);
        cnt += right - left + 1;
    }

    public int count() {
        return cnt;
    }
}

复杂度分析

  • 解法:有序集合合并区间(主提交版本)
  • 时间复杂度:add 摊还 O(log n),count O(1)
  • 空间复杂度:O(区间数)

2277. Closest Node to Path in Tree(树中最接近路径的节点)

题目描述

给定一棵 n 个节点(编号 0 到 n - 1)的树 edges。query[i] = [starti, endi, nodei] 表示第 i 个查询:从 starti 到 endi 的路径上找到最接近 nodei 的节点。返回长度为 m 的答案数组,answer[i] 是第 i 个查询的答案。

示例 1:

复制代码
输入:n = 7, edges = [[0,1],[0,2],[0,3],[1,4],[2,5],[2,6]], query = [[5,3,4],[5,3,6]]
输出:[0,2]
解释:路径 5→3 为 5,2,0,3;节点 4 距路径上节点 0 的距离 2 最小;节点 6 距路径上节点 2 的距离 1 最小。

示例 2:

复制代码
输入:n = 3, edges = [[0,1],[1,2]], query = [[0,1,2]]
输出:[1]
解释:路径 0→1 为 0,1;节点 2 距离节点 1 最近。

示例 3:

复制代码
输入:n = 3, edges = [[0,1],[1,2]], query = [[0,0,0]]
输出:[0]

约束:

  • 1 <= n <= 1000

  • edges.length == n - 1

  • 1 <= query.length <= 1000

  • 0 <= starti, endi, nodei <= n - 1

  • 图是一棵树

解题思路

核心思想 :预处理全点对距离 + 路径判定。树中 u 在 start → end 路径上 ⇔ dist[start][u] + dist[u][end] == dist[start][end]。对每个查询遍历所有节点,找到满足该条件且距离 nodei 最近的节点。

步骤:

  1. 邻接表建树。
  2. 以每个节点为源点 DFS 一次,得到 dist[s][u] 全距矩阵(O(n²))。
  3. 每个查询:遍历 u ∈ [0, n),若在路径上则比较 dist[u][node],取最小者。

复杂度:预处理 O(n²)、每查询 O(n),总 O(n² + m·n);空间 O(n²)。

代码实现

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

/**
 * 2277. Closest Node to Path in Tree 树中最接近路径的节点
 *
 * 给定一棵 n 个节点(0..n-1)的树 edges。query[i] = [starti, endi, nodei] 表示第 i 个查询:
 * 在从 starti 到 endi 的路径上找到最接近 nodei 的节点。返回长度为 m 的答案数组。
 *
 * 算法思路:由于 n <= 1000,直接预处理任意两点间距离矩阵 dist(以每个节点为根 DFS 一次),
 * 每个查询遍历 start→end 路径上的所有节点,取 dist[pathNode][node] 最小者。
 * 找路径上的节点:从 start 沿父链上到 LCA、再从 LCA 下到 end(用 dist 判断方向)------
 * 更简单:对每个查询,暴力遍历所有节点 u,若 u 在 start→end 的路径上(
 * dist[start][u] + dist[u][end] == dist[start][end]),则候选,取 dist[u][node] 最小者。
 *
 * 时间复杂度:O(n² + m * n),空间复杂度:O(n²)
 */
public class Solution {

    private List<Integer>[] g;
    private int[][] dist;

    public int[] closestNode(int n, int[][] edges, int[][] query) {
        g = new List[n];
        for (int i = 0; i < n; i++) {
            g[i] = new ArrayList<>();
        }
        for (int[] e : edges) {
            g[e[0]].add(e[1]);
            g[e[1]].add(e[0]);
        }
        // 预处理所有点对距离
        dist = new int[n][n];
        for (int s = 0; s < n; s++) {
            Arrays.fill(dist[s], -1);
            dfsDist(s, s, 0);
        }
        int m = query.length;
        int[] ans = new int[m];
        for (int qi = 0; qi < m; qi++) {
            int s = query[qi][0], e = query[qi][1], node = query[qi][2];
            int best = -1, bestD = Integer.MAX_VALUE;
            for (int u = 0; u < n; u++) {
                // u 在 s→e 路径上当且仅当 dist[s][u] + dist[u][e] == dist[s][e]
                if (dist[s][u] + dist[u][e] == dist[s][e]) {
                    int d = dist[u][node];
                    if (d < bestD) {
                        bestD = d;
                        best = u;
                    }
                }
            }
            ans[qi] = best;
        }
        return ans;
    }

    private void dfsDist(int start, int u, int d) {
        dist[start][u] = d;
        for (int v : g[u]) {
            if (dist[start][v] == -1) {
                dfsDist(start, v, d + 1);
            }
        }
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 [0,2]
        System.out.println(Arrays.toString(s.closestNode(7,
                new int[][]{{0, 1}, {0, 2}, {0, 3}, {1, 4}, {2, 5}, {2, 6}},
                new int[][]{{5, 3, 4}, {5, 3, 6}})));
        // 示例 2:预期 [1]
        System.out.println(Arrays.toString(s.closestNode(3,
                new int[][]{{0, 1}, {1, 2}},
                new int[][]{{0, 1, 2}})));
        // 示例 3:预期 [0]
        System.out.println(Arrays.toString(s.closestNode(3,
                new int[][]{{0, 1}, {1, 2}},
                new int[][]{{0, 0, 0}})));
    }
}

复杂度分析

  • 解法:全距矩阵 + 路径判定(主提交版本)
  • 时间复杂度:O(n² + m × n)
  • 空间复杂度:O(n²)

2278. Percentage of Letter in String(字母在字符串中的百分比)

题目描述

给你一个字符串 s 和一个字符 letter,返回在 s 中等于 letter 字符所占的百分比,向下取整到最接近的百分比。

示例 1:

复制代码
输入:s = "foobar", letter = "o"
输出:33
解释:'o' 出现 2 次,2/6×100% = 33.3%,向下取整为 33。

示例 2:

复制代码
输入:s = "jjjj", letter = "k"
输出:0

约束:

  • 1 <= s.length <= 100

  • s 只含小写英文字母

  • letter 是小写英文字母

解题思路

核心思想 :统计计数后整除。cnt * 100 / s.length() 用整数除法即向下取整。

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

代码实现

java 复制代码
/**
 * 2278. Percentage of Letter in String 字母在字符串中的百分比
 *
 * 给你一个字符串 s 和一个字符 letter,返回在 s 中等于 letter 字符所占的百分比,
 * 向下取整到最接近的百分比。
 *
 * 算法思路:统计 letter 出现次数 cnt,百分比 = cnt * 100 / s.length()(整数除法即向下取整)。
 *
 * 时间复杂度:O(n),空间复杂度:O(1)
 */
public class Solution {

    public int percentageLetter(String s, char letter) {
        int cnt = 0;
        for (char c : s.toCharArray()) {
            if (c == letter) {
                cnt++;
            }
        }
        return cnt * 100 / s.length();
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 33
        System.out.println(s.percentageLetter("foobar", 'o'));
        // 示例 2:预期 0
        System.out.println(s.percentageLetter("jjjj", 'k'));
        // 自测:全部相同,预期 100
        System.out.println(s.percentageLetter("aaaa", 'a'));
    }
}

复杂度分析

  • 解法:计数整除(主提交版本)
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

2279. Maximum Bags With Full Capacity of Rocks(装满石头的背包的最大数量)

题目描述

现有编号从 0 到 n - 1 的 n 个背包。给你 capacity 和 rocks 数组,第 i 个背包最大能装 capacity[i] 块石头、当前已装 rocks[i] 块。另给整数 additionalRocks 表示可额外放置的石头数。返回放置后装满石头的背包的最大数量(不必用完所有额外石头)。

示例 1:

复制代码
输入:capacity = [2,3,4,5], rocks = [1,2,4,4], additionalRocks = 2
输出:3
解释:各给背包 0、1 放 1 块石头,3 个背包装满。

示例 2:

复制代码
输入:capacity = [10,2,2], rocks = [2,2,0], additionalRocks = 100
输出:3
解释:8 块补背包 0、2 块补背包 2,全部装满。

约束:

  • n == capacity.length == rocks.length

  • 1 <= n <= 5 * 10⁴

  • 1 <= capacity[i] <= 10⁹

  • 0 <= rocks[i] <= capacity[i]

  • 1 <= additionalRocks <= 10⁹

解题思路

核心思想 :贪心补缺口。每个背包的缺口 need[i] = capacity[i] - rocks[i],额外石头优先补缺口最小的背包能获得最多的满背包数。

步骤:

  1. 计算每个背包缺口。
  2. 升序排序。
  3. 从小到大累减 additionalRocks,能减到 0 的背包即装满,计数。
  4. 累减不动(剩余石头不足)即停止------后续缺口更大更补不满。

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

代码实现

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

/**
 * 2279. Maximum Bags With Full Capacity of Rocks 装满石头的背包的最大数量
 *
 * 现有编号从 0 到 n - 1 的 n 个背包。给你 capacity 和 rocks 数组,第 i 个背包最大能装
 * capacity[i] 块石头,当前装了 rocks[i] 块。另给整数 additionalRocks 表示可额外放置的石头数。
 * 返回放置后装满石头的背包的最大数量。
 *
 * 算法思路:贪心。计算每个背包还差多少块石头装满:need[i] = capacity[i] - rocks[i](负数视为 0)。
 * 用额外石头优先补"缺口最小"的背包(每补满一个就多一个满背包)。排序缺口数组,从小到大
 * 累加直到 additionalRocks 不足,统计补满数量。
 *
 * 时间复杂度:O(n log n),空间复杂度:O(n)
 */
public class Solution {

    public int maximumBags(int[] capacity, int[] rocks, int additionalRocks) {
        int n = capacity.length;
        int[] need = new int[n];
        for (int i = 0; i < n; i++) {
            need[i] = capacity[i] - rocks[i];
        }
        Arrays.sort(need);
        int ans = 0;
        long left = additionalRocks;
        for (int i = 0; i < n; i++) {
            if (need[i] <= left) {
                left -= need[i];
                ans++;
            } else {
                break; // 缺口从小到大,剩下的都补不满
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 3
        System.out.println(s.maximumBags(new int[]{2, 3, 4, 5}, new int[]{1, 2, 4, 4}, 2));
        // 示例 2:预期 3
        System.out.println(s.maximumBags(new int[]{10, 2, 2}, new int[]{2, 2, 0}, 100));
        // 自测:都已满,预期 2
        System.out.println(s.maximumBags(new int[]{2, 2}, new int[]{2, 2}, 5));
    }
}

复杂度分析

  • 解法:贪心补小缺口(主提交版本)
  • 时间复杂度:O(n log n)
  • 空间复杂度:O(n)

2280. Minimum Lines to Represent a Line Chart(表示一个折线图的最少线段数)

题目描述

给你一个二维整数数组 stockPrices,其中 stockPrices[i] = [dayi, pricei] 表示股票在 dayi 的价格为 pricei。折线图是二维平面上若干点组成的图(横坐标为日期、纵坐标为价格),由相邻的点连接而成,日期互不相同。返回表示这个折线图所需要的最少线段数。

示例 1:

复制代码
输入:stockPrices = [[1,7],[2,6],[3,5],[4,4],[5,4],[6,3],[7,2],[8,1]]
输出:3
解释:线段 (1,7)-(4,4)、(4,4)-(5,4)、(5,4)-(8,1),共 3 条。

示例 2:

复制代码
输入:stockPrices = [[3,4],[1,2],[7,8],[2,3]]
输出:1
解释:四个点共线,一条线段即可。

约束:

  • 1 <= stockPrices.length <= 10⁵

  • stockPrices[i].length == 2

  • 1 <= dayi, pricei <= 10⁹

  • 所有 dayi 互不相同

解题思路

核心思想 :排序 + 叉积判共线。按日期排序后,连续三点共线 ⇔ 斜率相等;用叉积避免浮点:(y2-y1)*(x3-x2) == (y3-y2)*(x2-x1)。每出现一次斜率变化线段数 +1。

步骤:

  1. 按 day 升序排序。
  2. 至少两个点时 lines = 1。
  3. 从第三个点起判断与前两点是否共线,不共线则 lines++。

复杂度 :O(n log n) 时间、O(1) 空间(用 long 防乘法溢出)。

代码实现

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

/**
 * 2280. Minimum Lines to Represent a Line Chart 表示一个折线图的最少线段数
 *
 * 给你一个二维整数数组 stockPrices,其中 stockPrices[i] = [dayi, pricei] 表示股票在 dayi 的
 * 价格为 pricei。折线图由相邻的点连接而成,横坐标日期是不同的。返回要表示折线图所需的最少
 * 线段数。
 *
 * 算法思路:按日期排序后,统计"斜率变化"的次数。相邻两点共线当且仅当斜率相等。为避免浮点
 * 精度问题,用叉积比较:三点 (a,b,c) 共线 iff
 * (b.price - a.price) * (c.day - b.day) == (c.price - b.price) * (b.day - a.day)。
 * 若当前点与前一线段的斜率不同,线段数 +1。至少 1 条线段(单点时为 0,但 n >= 1 时
 * 折线图需要 0 条或 1 条)。
 *
 * 时间复杂度:O(n log n),空间复杂度:O(1)
 */
public class Solution {

    public int minimumLines(int[][] stockPrices) {
        int n = stockPrices.length;
        if (n <= 1) {
            return 0;
        }
        Arrays.sort(stockPrices, (a, b) -> a[0] - b[0]);
        int lines = 1; // 至少两个点,初始一条线段
        for (int i = 2; i < n; i++) {
            int x1 = stockPrices[i - 2][0], y1 = stockPrices[i - 2][1];
            int x2 = stockPrices[i - 1][0], y2 = stockPrices[i - 1][1];
            int x3 = stockPrices[i][0], y3 = stockPrices[i][1];
            // 叉积判断三点是否共线:(y2-y1)*(x3-x2) == (y3-y2)*(x2-x1)
            if ((long) (y2 - y1) * (x3 - x2) != (long) (y3 - y2) * (x2 - x1)) {
                lines++;
            }
        }
        return lines;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        // 示例 1:预期 3
        System.out.println(s.minimumLines(new int[][]{{1, 7}, {2, 6}, {3, 5}, {4, 4}, {5, 4}, {6, 3}, {7, 2}, {8, 1}}));
        // 示例 2:预期 1
        System.out.println(s.minimumLines(new int[][]{{3, 4}, {1, 2}, {7, 8}, {2, 3}}));
        // 自测:单点,预期 0
        System.out.println(s.minimumLines(new int[][]{{1, 1}}));
        // 自测:共线多点,预期 1
        System.out.println(s.minimumLines(new int[][]{{1, 1}, {2, 2}, {3, 3}}));
    }
}

复杂度分析

  • 解法:排序 + 叉积判共线(主提交版本)
  • 时间复杂度:O(n log n)
  • 空间复杂度:O(1)

相关推荐
SL_staff24 分钟前
告别规则黑盒:JVS-Rules 在金融风控中的可视化决策编排实践
java·产品·全栈
逃逸线LOF1 小时前
服务网关Gateway
java·运维·gateway
灿飞屋1 小时前
Kotlin与Java虚拟机四:函数
java·python·kotlin·安卓·android jetpack
阡陌数智2 小时前
Claude API新版本重大变更与安全升级指南
java·linux·安全
学长毕设4 小时前
基于SpringBoot的野生动物公益保护平台设计与实现-附源码
java·spring boot·后端
SL_staff5 小时前
离散制造排程破局:JVS-APS 四步闭环落地实践(开发者视角)
java·产品·全栈
这个昵称不容易5 小时前
SpringBoot 美容院管理系统:232个Java文件315个接口,5套角色从预约到发货一整条链路
java·springboot·毕设
装不满的克莱因瓶6 小时前
【SpringAI Alibaba】ReactAgent记忆管理MessageTrimmingHook与MessageDeletionHook
java·人工智能·ai·ai编程·springai·springaialibaba
文丑颜不良啊6 小时前
LeetCode2241~2260题目解析
java·leetcode