3097.或值至少为K的最短子数组II

目标

给你一个 非负 整数数组 nums 和一个整数 k 。

如果一个数组中所有元素的按位或运算 OR 的值 至少 为 k ,那么我们称这个数组是 特别的 。

请你返回 nums 中 最短特别非空 子数组 的长度,如果特别子数组不存在,那么返回 -1 。

示例 1:

输入:nums = [1,2,3], k = 2
输出:1
解释:
子数组 [3] 的按位 OR 值为 3 ,所以我们返回 1 。

示例 2:

输入:nums = [2,1,8], k = 10
输出:3
解释:
子数组 [2,1,8] 的按位 OR 值为 11 ,所以我们返回 3 。

示例 3:

输入:nums = [1,2], k = 0
输出:1
解释:
子数组 [1] 的按位 OR 值为 1 ,所以我们返回 1 。

说明:

  • 1 <= nums.length <= 2 * 10^5
  • 0 <= nums[i] <= 10^9
  • 0 <= k <= 10^9

思路

求数组 nums 的最短特别子数组长度,特别子数组的所有元素按位与的结果大于等于 k。

3095.或值至少K的最短子数组I 相比数据范围变大了,O(n^2) 的解法会超时。

记录每一位的出现次数,使用滑动窗口,枚举右收缩左。由于 按位或 没有逆运算,我们可以反向重新计算 按位与。

代码


/**
 * @date 2025-01-17 8:40
 */
public class MinimumSubarrayLength3097 {

    public int minimumSubarrayLength_v2(int[] nums, int k) {
        int res = Integer.MAX_VALUE;
        int right = 0;
        int n = nums.length;
        int or = 0;
        while (right < n) {
            do {
                or |= nums[right++];
            } while (right < n && or < k);
            if (or >= k) {
                int left = right - 1;
                int tmp = 0;
                while (left >= 0) {
                    tmp |= nums[left--];
                    if (tmp >= k) {
                        left++;
                        break;
                    } else {
                        or = tmp;
                    }
                }
                res = Math.min(res, right - left);
            } else {
                break;
            }
        }
        return res == Integer.MAX_VALUE ? -1 : res;
    }

}

性能

3066.超过阈值的最少操作数II

目标

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

一次操作中,你将执行:

  • 选择 nums 中最小的两个整数 x 和 y 。
  • 将 x 和 y 从 nums 中删除。
  • 将 min(x, y) * 2 + max(x, y) 添加到数组中的任意位置。

注意,只有当 nums 至少包含两个元素时,你才可以执行以上操作。

你需要使数组中的所有元素都大于或等于 k ,请你返回需要的 最少 操作次数。

示例 1:

输入:nums = [2,11,10,1,3], k = 10
输出:2
解释:第一次操作中,我们删除元素 1 和 2 ,然后添加 1 * 2 + 2 到 nums 中,nums 变为 [4, 11, 10, 3] 。
第二次操作中,我们删除元素 3 和 4 ,然后添加 3 * 2 + 4 到 nums 中,nums 变为 [10, 11, 10] 。
此时,数组中的所有元素都大于等于 10 ,所以我们停止操作。
使数组中所有元素都大于等于 10 需要的最少操作次数为 2 。

示例 2:

输入:nums = [1,1,2,4,9], k = 20
输出:4
解释:第一次操作后,nums 变为 [2, 4, 9, 3] 。
第二次操作后,nums 变为 [7, 4, 9] 。
第三次操作后,nums 变为 [15, 9] 。
第四次操作后,nums 变为 [33] 。
此时,数组中的所有元素都大于等于 20 ,所以我们停止操作。
使数组中所有元素都大于等于 20 需要的最少操作次数为 4 。

说明:

  • 2 <= nums.length <= 2 * 10^5
  • 1 <= nums[i] <= 10^9
  • 1 <= k <= 10^9
  • 输入保证答案一定存在,也就是说一定存在一个操作序列使数组中所有元素都大于等于 k 。

思路

求使数组 nums 中所有元素均大于等于 k 的操作次数。每次操作可以将数组中最小的两个元素删除,并将 min(x, y) * 2 + max(x, y) 加入数组。

使用最小堆模拟即可

代码


/**
 * @date 2025-01-14 8:51
 */
public class MinOperations3066 {

    public int minOperations(int[] nums, int k) {
        PriorityQueue<Long> q = new PriorityQueue<>();
        for (int num : nums) {
            q.offer((long) num);
        }
        int res = 0;
        while (q.size() >= 2) {
            Long a = q.poll();
            Long b = q.poll();
            if (a >= k) {
                break;
            }
            q.offer(a * 2L + b);
            res++;
        }
        return res;
    }
}

性能

2270.分割数组的方案数

目标

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

如果以下描述为真,那么 nums 在下标 i 处有一个 合法的分割 :

  • 前 i + 1 个元素的和 大于等于 剩下的 n - i - 1 个元素的和。
  • 下标 i 的右边 至少有一个 元素,也就是说下标 i 满足 0 <= i < n - 1 。

请你返回 nums 中的 合法分割 方案数。

示例 1:

输入:nums = [10,4,-8,7]
输出:2
解释:
总共有 3 种不同的方案可以将 nums 分割成两个非空的部分:
- 在下标 0 处分割 nums 。那么第一部分为 [10] ,和为 10 。第二部分为 [4,-8,7] ,和为 3 。因为 10 >= 3 ,所以 i = 0 是一个合法的分割。
- 在下标 1 处分割 nums 。那么第一部分为 [10,4] ,和为 14 。第二部分为 [-8,7] ,和为 -1 。因为 14 >= -1 ,所以 i = 1 是一个合法的分割。
- 在下标 2 处分割 nums 。那么第一部分为 [10,4,-8] ,和为 6 。第二部分为 [7] ,和为 7 。因为 6 < 7 ,所以 i = 2 不是一个合法的分割。
所以 nums 中总共合法分割方案受为 2 。

示例 2:

输入:nums = [2,3,1,0]
输出:2
解释:
总共有 2 种 nums 的合法分割:
- 在下标 1 处分割 nums 。那么第一部分为 [2,3] ,和为 5 。第二部分为 [1,0] ,和为 1 。因为 5 >= 1 ,所以 i = 1 是一个合法的分割。
- 在下标 2 处分割 nums 。那么第一部分为 [2,3,1] ,和为 6 。第二部分为 [0] ,和为 0 。因为 6 >= 0 ,所以 i = 2 是一个合法的分割。

说明:

  • 2 <= nums.length <= 10^5
  • -10^5 <= nums[i] <= 10^5

思路

求数组的合法分割点个数,下标 i 是合法分割点的条件是 前 i + 1 个元素和大于剩余元素和,且至少要有一个元素。

直接的想法是计算前缀和,然后按照题意计算。

实际实现时发现只用到了 所有元素的和 sum 以及区间 [0, i] 的和 tmp,无需存储前缀,直接在遍历的时候计算就可以。

代码


/**
 * @date 2025-01-13 8:41
 */
public class WaysToSplitArray2270 {

    public int waysToSplitArray(int[] nums) {
        long sum = 0;
        for (int num : nums) {
            sum += num;
        }
        int res = 0;
        long tmp = 0;
        for (int i = 0; i < nums.length - 1; i++) {
            tmp += nums[i];
            if (2 * tmp >= sum) {
                res++;
            }
        }
        return res;
    }

}

性能

2275.按位与结果大于零的最长组合

目标

对数组 nums 执行 按位与 相当于对数组 nums 中的所有整数执行 按位与 。

  • 例如,对 nums = [1, 5, 3] 来说,按位与等于 1 & 5 & 3 = 1 。
  • 同样,对 nums = [7] 而言,按位与等于 7 。

给你一个正整数数组 candidates 。计算 candidates 中的数字每种组合下 按位与 的结果。

返回按位与结果大于 0 的 最长 组合的长度。

示例 1:

输入:candidates = [16,17,71,62,12,24,14]
输出:4
解释:组合 [16,17,62,24] 的按位与结果是 16 & 17 & 62 & 24 = 16 > 0 。
组合长度是 4 。
可以证明不存在按位与结果大于 0 且长度大于 4 的组合。
注意,符合长度最大的组合可能不止一种。
例如,组合 [62,12,24,14] 的按位与结果是 62 & 12 & 24 & 14 = 8 > 0 。

示例 2:

输入:candidates = [8,8]
输出:2
解释:最长组合是 [8,8] ,按位与结果 8 & 8 = 8 > 0 。
组合长度是 2 ,所以返回 2 。

说明:

  • 1 <= candidates.length <= 10^5
  • 1 <= candidates[i] <= 10^7

思路

求数组 nums 符合条件(子序列按位与的结果大于 0)的子序列的最大长度。

如果使用 dfs 考虑选或者不选,枚举所有子序列肯定超时。

看了题解,说是统计所有数字相同 bit 位上 1 的出现次数,取其最大值。

代码


/**
 * @date 2025-01-12 21:50
 */
public class LargestCombination2275 {

    public int largestCombination(int[] candidates) {
        int[] cnt = new int[24];
        for (int candidate : candidates) {
            for (int i = 0; i < 24; i++) {
                cnt[i] += (candidate >> i) & 1;
            }
        }
        int res = 0;
        for (int i : cnt) {
            res = Math.max(i, res);
        }
        return res;
    }

}

性能

3297.统计重新排列后包含另一个字符串的子字符串数目I

目标

给你两个字符串 word1 和 word2 。

如果一个字符串 x 重新排列后,word2 是重排字符串的 前缀,那么我们称字符串 x 是 合法的 。

请你返回 word1 中 合法 子字符串 的数目。

示例 1:

输入:word1 = "bcca", word2 = "abc"
输出:1
解释:
唯一合法的子字符串是 "bcca" ,可以重新排列得到 "abcc" ,"abc" 是它的前缀。

示例 2:

输入:word1 = "abcabc", word2 = "abc"
输出:10
解释:
除了长度为 1 和 2 的所有子字符串都是合法的。

示例 3:

输入:word1 = "abcabc", word2 = "aaabc"
输出:0

说明:

  • 1 <= word1.length <= 10^5
  • 1 <= word2.length <= 10^4
  • word1 和 word2 都只包含小写英文字母。

思路

有两个字符串 word1word2,求 word1 有多个子字符串 满足子串的每个字符的出现次数 均大于等于 word2 中对应字符的出现次数。

暴力解法是枚举 word1 的每个子字符串,比较子串中的每个字符个数。具体来说就是统计 word1word2 中各字符的个数,枚举 word1 起点,从后向前枚举终点,如果某字符个数小于 word2 相应字符的个数则停止计数,继续下一个起点。这种解法的时间复杂度是 O(n^2) 会超时。

考虑使用滑动窗口,当左边元素移出窗口时,向右扩展,直到移出的元素个数达到 word2 中对应字符的个数,累加右边界到结尾的字符个数。

代码


/**
 * @date 2025-01-09 14:28
 */
public class ValidSubstringCount3297 {

    public long validSubstringCount_v1(String word1, String word2) {
        int[] cnt1 = new int[26];
        int[] cnt2 = new int[26];
        char[] chars1 = word1.toCharArray();
        char[] chars2 = word2.toCharArray();
        for (char c : chars1) {
            cnt1[c - 'a']++;
        }
        for (char c : chars2) {
            cnt2[c - 'a']++;
        }
        for (int i = 0; i < 26; i++) {
            if (cnt1[i] < cnt2[i]) {
                return 0;
            }
        }
        int n = word1.length();
        int r = n - 1;
        while (--cnt1[chars1[r] - 'a'] >= cnt2[chars1[r] - 'a']) {
            r--;
        }
        long res = n - r;
        cnt1[chars1[r++] - 'a']++;
        for (int i = 0; i < n - word2.length(); i++) {
            int c = chars1[i] - 'a';
            cnt1[c]--;
            while (r < n && cnt1[c] < cnt2[c]) {
                cnt1[chars1[r++] - 'a']++;
            }
            if (cnt1[c] >= cnt2[c]) {
                res += n - r + 1;
            } else {
                break;
            }
        }
        return res;
    }

}

性能

2274.不含特殊楼层的最大连续楼层数

目标

Alice 管理着一家公司,并租用大楼的部分楼层作为办公空间。Alice 决定将一些楼层作为 特殊楼层 ,仅用于放松。

给你两个整数 bottom 和 top ,表示 Alice 租用了从 bottom 到 top(含 bottom 和 top 在内)的所有楼层。另给你一个整数数组 special ,其中 special[i] 表示 Alice 指定用于放松的特殊楼层。

返回不含特殊楼层的 最大 连续楼层数。

示例 1:

输入:bottom = 2, top = 9, special = [4,6]
输出:3
解释:下面列出的是不含特殊楼层的连续楼层范围:
- (2, 3) ,楼层数为 2 。
- (5, 5) ,楼层数为 1 。
- (7, 9) ,楼层数为 3 。
因此,返回最大连续楼层数 3 。

示例 2:

输入:bottom = 6, top = 8, special = [7,6,8]
输出:0
解释:每层楼都被规划为特殊楼层,所以返回 0 。

说明:

  • 1 <= special.length <= 10^5
  • 1 <= bottom <= special[i] <= top <= 10^9
  • special 中的所有值 互不相同

思路

给定一个区间 [bottom, top],从中剔除一些整数,求最大的连续整数个数。

排序 special 数组计算相邻区间的最大值。注意 special 数组的元素都在区间范围内,可以直接处理首尾区间 special[i] - bottomtop - special[i],然后再处理内部区间 special[i] - special[i - 1] - 1

也可以将 bottom--top++,然后统一处理。

代码


/**
 * @date 2025-01-06 8:42
 */
public class MaxConsecutive2274 {

    public int maxConsecutive(int bottom, int top, int[] special) {
        Arrays.sort(special);
        bottom--;
        top++;
        int res = 0;
        int n = special.length;
        int prev = bottom;
        for (int i = 0; i < n; i++) {
            res = Math.max(res, special[i] - prev - 1);
            prev = special[i];
        }
        return Math.max(res, top - special[n - 1] - 1);
    }

}

性能

2241.设计一个ATM机器

目标

一个 ATM 机器,存有 5 种面值的钞票:20 ,50 ,100 ,200 和 500 美元。初始时,ATM 机是空的。用户可以用它存或者取任意数目的钱。

取款时,机器会优先取 较大 数额的钱。

  • 比方说,你想取 $300 ,并且机器里有 2 张 $50 的钞票,1 张 $100 的钞票和1 张 $200 的钞票,那么机器会取出 $100 和 $200 的钞票。
  • 但是,如果你想取 $600 ,机器里有 3 张 $200 的钞票和1 张 $500 的钞票,那么取款请求会被拒绝,因为机器会先取出 $500 的钞票,然后无法取出剩余的 $100 。注意,因为有 $500 钞票的存在,机器 不能 取 $200 的钞票。

请你实现 ATM 类:

  • ATM() 初始化 ATM 对象。
  • void deposit(int[] banknotesCount) 分别存入 $20 ,$50,$100,$200 和 $500 钞票的数目。
  • int[] withdraw(int amount) 返回一个长度为 5 的数组,分别表示 $20 ,$50,$100 ,$200 和 $500 钞票的数目,并且更新 ATM 机里取款后钞票的剩余数量。如果无法取出指定数额的钱,请返回 [-1] (这种情况下 不 取出任何钞票)。

示例 1:

输入:
["ATM", "deposit", "withdraw", "deposit", "withdraw", "withdraw"]
[[], [[0,0,1,2,1]], [600], [[0,1,0,1,1]], [600], [550]]
输出:
[null, null, [0,0,1,0,1], null, [-1], [0,1,0,0,1]]
解释:
ATM atm = new ATM();
atm.deposit([0,0,1,2,1]); // 存入 1 张 $100 ,2 张 $200 和 1 张 $500 的钞票。
atm.withdraw(600);        // 返回 [0,0,1,0,1] 。机器返回 1 张 $100 和 1 张 $500 的钞票。机器里剩余钞票的数量为 [0,0,0,2,0] 。
atm.deposit([0,1,0,1,1]); // 存入 1 张 $50 ,1 张 $200 和 1 张 $500 的钞票。
                          // 机器中剩余钞票数量为 [0,1,0,3,1] 。
atm.withdraw(600);        // 返回 [-1] 。机器会尝试取出 $500 的钞票,然后无法得到剩余的 $100 ,所以取款请求会被拒绝。
                          // 由于请求被拒绝,机器中钞票的数量不会发生改变。
atm.withdraw(550);        // 返回 [0,1,0,0,1] ,机器会返回 1 张 $50 的钞票和 1 张 $500 的钞票。

说明:

  • banknotesCount.length == 5
  • 0 <= banknotesCount[i] <= 10^9
  • 1 <= amount <= 10^9
  • 总共 最多有 5000 次 withdraw 和 deposit 的调用。
  • 函数 withdraw 和 deposit 至少各有 一次 调用。

思路

设计一个ATM机,支持存入面额为 20,50,100,200,500 的钞票,取款时优先使用大额的钞票,即只要存在大额的钞票,不论最终能否凑成给定的数额,都要尽量多的取。如果无法取出指定数额的钱,返回 [-1],否则返回组合方案。

直接根据题意模拟即可,可以定义一个面额数组来避免硬编码。

代码


/**
 * @date 2025-01-05 15:10
 */
public class ATM {

    public int[] cnt = new int[5];
    public int[] value = new int[]{20, 50, 100, 200, 500};

    public ATM() {

    }

    public void deposit(int[] banknotesCount) {
        for (int i = 0; i < 5; i++) {
            cnt[i] += banknotesCount[i];
        }
    }

    public int[] withdraw(int amount) {
        int[] res = new int[5];
        for (int i = 4; i >= 0; i--) {
            res[i] = Math.min(amount / value[i], cnt[i]);
            amount -= res[i] * value[i];
        }
        if (amount == 0) {
            for (int i = 0; i < 5; i++) {
                cnt[i] -= res[i];
            }
            return res;
        } else {
            return new int[]{-1};
        }
    }
}

性能

732.我的日程安排表III

目标

当 k 个日程存在一些非空交集时(即, k 个日程包含了一些相同时间),就会产生 k 次预订。

给你一些日程安排 [startTime, endTime) ,请你在每个日程安排添加后,返回一个整数 k ,表示所有先前日程安排会产生的最大 k 次预订。

实现一个 MyCalendarThree 类来存放你的日程安排,你可以一直添加新的日程安排。

  • MyCalendarThree() 初始化对象。
  • int book(int startTime, int endTime) 返回一个整数 k ,表示日历中存在的 k 次预订的最大值。

示例:

输入:
["MyCalendarThree", "book", "book", "book", "book", "book", "book"]
[[], [10, 20], [50, 60], [10, 40], [5, 15], [5, 10], [25, 55]]
输出:
[null, 1, 1, 2, 3, 3, 3]
解释:
MyCalendarThree myCalendarThree = new MyCalendarThree();
myCalendarThree.book(10, 20); // 返回 1 ,第一个日程安排可以预订并且不存在相交,所以最大 k 次预订是 1 次预订。
myCalendarThree.book(50, 60); // 返回 1 ,第二个日程安排可以预订并且不存在相交,所以最大 k 次预订是 1 次预订。
myCalendarThree.book(10, 40); // 返回 2 ,第三个日程安排 [10, 40) 与第一个日程安排相交,所以最大 k 次预订是 2 次预订。
myCalendarThree.book(5, 15); // 返回 3 ,剩下的日程安排的最大 k 次预订是 3 次预订。
myCalendarThree.book(5, 10); // 返回 3
myCalendarThree.book(25, 55); // 返回 3

说明:

  • 0 <= startTime < endTime <= 10^9
  • 每个测试用例,调用 book 函数最多不超过 400次

思路

参考 731.我的日程安排表II,同样的思路,只不过求得是相交区间数量的最大值。

代码

/**
 * @date 2025-01-04 15:40
 */
public class MyCalendarThree {

    TreeMap<Integer, Integer> cnt = new TreeMap<>();

    public MyCalendarThree() {

    }

    public int book(int startTime, int endTime) {
        cnt.put(startTime, cnt.getOrDefault(startTime, 0) + 1);
        cnt.put(endTime, cnt.getOrDefault(endTime, 0) - 1);
        int res = 0;
        int appearanceCnt = 0;
        for (Map.Entry<Integer, Integer> entry : cnt.entrySet()) {
            int value = entry.getValue();
            appearanceCnt += value;
            res = Math.max(appearanceCnt, res);
        }
        return res;
    }
}

性能

731.我的日程安排表II

目标

实现一个程序来存放你的日程安排。如果要添加的时间内不会导致三重预订时,则可以存储这个新的日程安排。

当三个日程安排有一些时间上的交叉时(例如三个日程安排都在同一时间内),就会产生 三重预订。

事件能够用一对整数 startTime 和 endTime 表示,在一个半开区间的时间 [startTime, endTime) 上预定。实数 x 的范围为 startTime <= x < endTime。

实现 MyCalendarTwo 类:

  • MyCalendarTwo() 初始化日历对象。
  • boolean book(int startTime, int endTime) 如果可以将日程安排成功添加到日历中而不会导致三重预订,返回 true。否则,返回 false 并且不要将该日程安排添加到日历中。

示例 1:

输入:
["MyCalendarTwo", "book", "book", "book", "book", "book", "book"]
[[], [10, 20], [50, 60], [10, 40], [5, 15], [5, 10], [25, 55]]
输出:
[null, true, true, true, false, true, true]
解释:
MyCalendarTwo myCalendarTwo = new MyCalendarTwo();
myCalendarTwo.book(10, 20); // 返回 True,能够预定该日程。
myCalendarTwo.book(50, 60); // 返回 True,能够预定该日程。
myCalendarTwo.book(10, 40); // 返回 True,该日程能够被重复预定。
myCalendarTwo.book(5, 15);  // 返回 False,该日程导致了三重预定,所以不能预定。
myCalendarTwo.book(5, 10); // 返回 True,能够预定该日程,因为它不使用已经双重预订的时间 10。
myCalendarTwo.book(25, 55); // 返回 True,能够预定该日程,因为时间段 [25, 40) 将被第三个日程重复预定,时间段 [40, 50) 将被单独预定,而时间段 [50, 55) 将被第二个日程重复预定。

说明:

  • 0 <= start < end <= 10^9
  • 最多调用 book 1000 次。

思路

本题与 729.我的日程安排表I 的区别是允许相交一次。

使用差分数组记录区间元素被覆盖的次数,由于数据范围太大,这里使用 TreeMap 计数。

// todo 线段树

代码


/**
 * @date 2025-01-03 10:32
 */
public class MyCalendarTwo {

    TreeMap<Integer, Integer> cnt = new TreeMap<>();

    public MyCalendarTwo() {

    }

    public boolean book(int startTime, int endTime) {
        cnt.put(startTime, cnt.getOrDefault(startTime, 0) + 1);
        cnt.put(endTime, cnt.getOrDefault(endTime, 0) - 1);
        int appearanceCnt = 0;
        for (Map.Entry<Integer, Integer> entry : cnt.entrySet()) {
            int key = entry.getKey();
            int value = entry.getValue();
            if (key >= endTime) {
                break;
            }
            appearanceCnt += value;
            if (appearanceCnt > 2) {
                cnt.put(startTime, cnt.getOrDefault(startTime, 0) - 1);
                cnt.put(endTime, cnt.getOrDefault(endTime, 0) + 1);
                return false;
            }
        }
        return true;
    }
}

性能

729.我的日程安排表I

目标

实现一个 MyCalendar 类来存放你的日程安排。如果要添加的日程安排不会造成 重复预订 ,则可以存储这个新的日程安排。

当两个日程安排有一些时间上的交叉时(例如两个日程安排都在同一时间内),就会产生 重复预订 。

日程可以用一对整数 startTime 和 endTime 表示,这里的时间是半开区间,即 [startTime, endTime), 实数 x 的范围为, startTime <= x < endTime 。

实现 MyCalendar 类:

  • MyCalendar() 初始化日历对象。
  • boolean book(int startTime, int endTime) 如果可以将日程安排成功添加到日历中而不会导致重复预订,返回 true 。否则,返回 false 并且不要将该日程安排添加到日历中。

示例:

输入:
["MyCalendar", "book", "book", "book"]
[[], [10, 20], [15, 25], [20, 30]]
输出:
[null, true, false, true]
解释:
MyCalendar myCalendar = new MyCalendar();
myCalendar.book(10, 20); // return True
myCalendar.book(15, 25); // return False ,这个日程安排不能添加到日历中,因为时间 15 已经被另一个日程安排预订了。
myCalendar.book(20, 30); // return True ,这个日程安排可以添加到日历中,因为第一个日程安排预订的每个时间都小于 20 ,且不包含时间 20 。

说明:

  • 0 <= start < end <= 10^9
  • 每个测试用例,调用 book 方法的次数最多不超过 1000 次。

提示:

  • Store the events as a sorted list of intervals. If none of the events conflict, then the new event can be added.

思路

判断给定区间是否与已有区间相交,如果不相交将其加入已有区间。

最直接的想法是枚举每一个区间,判断是否相交,如果不相交则加入集合。判断区间 [a, b)[c, d) 是否相交,可以固定一个区间,然后让另一个区间滑动,可以发现相交需要满足 d > a && c < b,注意取等号是不相交的。

当然也可以使用二叉搜索树。

TreeSet 中查找特定元素的 API:

  • ceiling 返回的是 大于等于 target 的最小元素/ null
  • floor 返回的是 小于等于 target 的最大元素/ null
  • higher 返回的是 大于 target 的最小元素 / null
  • lower 返回的是 小于 target 的最大元素 / null

代码


/**
 * @date 2025-01-02 9:58
 */
class MyCalendar {

    TreeSet<int[]> ts = new TreeSet<>((a, b) -> a[0] - b[0]);

    public MyCalendar() {

    }

    public boolean book(int startTime, int endTime) {
        int[] interval = {startTime, endTime};
        if (ts.isEmpty()) {
            ts.add(interval);
            return true;
        }
        int[] param = new int[]{endTime, 0};
        // 查找 起点 大于等于 endTime 的 起点最小的区间,即要插入间隙的右边区间 right
        int[] right = ts.ceiling(param);
        // 如果 right 是第一个元素 或者 前面一个区间的右边界 end 小于等于 startTime,说明不相交
        if (right == ts.first() || ts.lower(param)[1] <= startTime) {
            ts.add(interval);
            return true;
        }
        return false;
    }

}

性能