2145.统计隐藏数组数目

目标

给你一个下标从 0 开始且长度为 n 的整数数组 differences ,它表示一个长度为 n + 1 的 隐藏 数组 相邻 元素之间的 差值 。更正式的表述为:我们将隐藏数组记作 hidden ,那么 differences[i] = hidden[i + 1] - hidden[i] 。

同时给你两个整数 lower 和 upper ,它们表示隐藏数组中所有数字的值都在 闭 区间 [lower, upper] 之间。

比方说,differences = [1, -3, 4] ,lower = 1 ,upper = 6 ,那么隐藏数组是一个长度为 4 且所有值都在 1 和 6 (包含两者)之间的数组。
[3, 4, 1, 5] 和 [4, 5, 2, 6] 都是符合要求的隐藏数组。
[5, 6, 3, 7] 不符合要求,因为它包含大于 6 的元素。
[1, 2, 3, 4] 不符合要求,因为相邻元素的差值不符合给定数据。
请你返回 符合 要求的隐藏数组的数目。如果没有符合要求的隐藏数组,请返回 0 。

示例 1:

输入:differences = [1,-3,4], lower = 1, upper = 6
输出:2
解释:符合要求的隐藏数组为:
- [3, 4, 1, 5]
- [4, 5, 2, 6]
所以返回 2 。

示例 2:

输入:differences = [3,-4,5,1,-2], lower = -4, upper = 5
输出:4
解释:符合要求的隐藏数组为:
- [-3, 0, -4, 1, 2, 0]
- [-2, 1, -3, 2, 3, 1]
- [-1, 2, -2, 3, 4, 2]
- [0, 3, -1, 4, 5, 3]
所以返回 4 。

示例 3:

输入:differences = [4,-7,2], lower = 3, upper = 6
输出:0
解释:没有符合要求的隐藏数组,所以返回 0 。

说明:

  • n == differences.length
  • 1 <= n <= 10^5
  • -10^5 <= differences[i] <= 10^5
  • -10^5 <= lower <= upper <= 10^5

思路

已知某数组的差分数组 differences,以及数组元素值的上下界 lower upper,求有多少个满足条件的数组。

确定了第一个元素,后面整个数组就确定了。数组最大值与最小值的差是固定的,差值小于 upper - lower 就存在符合条件的数组,有 upper - lower - (max - min) + 1 个。注意求 maxmin 时数据可能溢出,使用 long 类型。

代码


/**
 * @date 2025-04-21 0:19
 */
public class NumberOfArrays2145 {

    public int numberOfArrays(int[] differences, int lower, int upper) {
        int n = differences.length;
        long max = 0;
        long min = 0;
        long val = 0;
        for (int i = 0; i < n; i++) {
            val += differences[i];
            max = Math.max(max, val);
            min = Math.min(min, val);
        }
        long res = upper - lower - (max - min) + 1;
        return (int) Math.max(res, 0);
    }

}

性能

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;
    }
}

性能

3192.使二进制数组全部等于1的最少操作次数II

目标

给你一个二进制数组 nums 。

你可以对数组执行以下操作 任意 次(也可以 0 次):

  • 选择数组中 任意 一个下标 i ,并将从下标 i 开始一直到数组末尾 所有 元素 反转 。

反转 一个元素指的是将它的值从 0 变 1 ,或者从 1 变 0 。

请你返回将 nums 中所有元素变为 1 的 最少 操作次数。

示例 1:

输入:nums = [0,1,1,0,1]
输出:4
解释:
我们可以执行以下操作:
选择下标 i = 1 执行操作,得到 nums = [0,0,0,1,0] 。
选择下标 i = 0 执行操作,得到 nums = [1,1,1,0,1] 。
选择下标 i = 4 执行操作,得到 nums = [1,1,1,0,0] 。
选择下标 i = 3 执行操作,得到 nums = [1,1,1,1,1] 。

示例 2:

输入:nums = [1,0,0,0]
输出:1
解释:
我们可以执行以下操作:
选择下标 i = 1 执行操作,得到 nums = [1,1,1,1] 。

说明:

  • 1 <= nums.length <= 10^5
  • 0 <= nums[i] <= 1

思路

有一个二进制数组 nums,每一次操作可以将当前元素以及之后的元素反转,问将所有元素变为 1 的最少操作次数。

这与昨天的题目 3191.使二进制数组全部等于1的最少操作次数I 类似,那个是将当前元素及后面两个元素反转。

还沿用昨天的思路,遇到 0 就进行操作,每一次操作需要对大量元素进行取模运算,因此考虑使用差分数组。这里差分数组初始均为 0,每次操作是针对当前元素及以后的所有元素,无需考虑后续的减法操作,因此只需要一个变量计数即可。

网友最快的算法并非使用变量计数然后判断奇偶性,考虑到最终目标是将所有元素都变为 1,每一次操作会将自身与其后的元素反转,对于初始数组任意位置 i,如果它为 0,该位置一定需要执行奇数次操作,因为执行偶数次反转最后还是 0。同理,如果为 1,需要执行 偶数 次操作。实际上每个位置是否需要执行操作是 确定的

只要有一个初始条件,向后遍历的时候直接根据前一个元素与当前元素的关系判断是否需要累加操作即可,具体来说,如果它们值相同则不需要操作,如果不同则需要操作,直接累加这两个元素的异或值即可。

代码


/**
 * @date 2024-10-19 17:30
 */
public class MinOperations3192 {

    public int minOperations_v2(int[] nums) {
        int n = nums.length;
        int prev = nums[0];
        int res = prev ^ 1;
        for (int i = 1; i < n; i++) {
            res += prev ^ nums[i];
            prev = nums[i];
        }
        return res;
    }

    public int minOperations_v1(int[] nums) {
        int n = nums.length;
        int res = 0;
        for (int i = 0; i < n; i++) {
            if (((res % 2 ^ nums[i]) == 0)) {
                res++;
            }
        }
        return res;
    }

}

性能

2848.与车相交的点

目标

给你一个下标从 0 开始的二维整数数组 nums 表示汽车停放在数轴上的坐标。对于任意下标 i,nums[i] = [starti, endi] ,其中 starti 是第 i 辆车的起点,endi 是第 i 辆车的终点。

返回数轴上被车 任意部分 覆盖的整数点的数目。

示例 1:

输入:nums = [[3,6],[1,5],[4,7]]
输出:7
解释:从 1 到 7 的所有点都至少与一辆车相交,因此答案为 7 。

示例 2:

输入:nums = [[1,3],[5,8]]
输出:7
解释:1、2、3、5、6、7、8 共计 7 个点满足至少与一辆车相交,因此答案为 7 。

说明:

  • 1 <= nums.length <= 100
  • nums[i].length == 2
  • 1 <= starti <= endi <= 100

思路

用一个二维数组表示 n 个线段,每一行表示线段的两个端点,问这些线段覆盖的整数点个数。

简单的想法是将线段覆盖的每个整数加入 HashSet,最后返回集合大小即可。线段个数最多 100 个,端点的范围在 1 ~ 100 最多10000 次循环,可行。

更好的做法是将相交的线段合并,最终仅计算相交线段的长度之和即可。先将线段按照起点排序,如果后面线段的起点小于前面的终点,取当前线段的终点与前面合并线段的终点中的较大值,否则计算长度并累加。

还想到了之前有一道题使用了 差分思想,那个是给定一个整数,判断覆盖该整数的线段有多少个。它是将 cnt[start]++; cnt[end + 1]--,直接累加 [0, query] 内的 cnt[i] 即为答案。

差分数组 diff 的核心思想是记录相邻元素的差,当对连续区间 [i, j] 同时增加 k,可以简化为 diff[i] + k; diff[j + 1] - k。原数组的第 i 个元素可以通过累加差分数组得到。

刚开始理解差分数组可能会想不明白,为什么只修改差分数组两个端点的值就可以将区间内的值同时加 k。核心点是理解当前值是由差分数组累加得来的,从 i 开始,当前值加了 k,后面的差分值为0,因此值与 i 位置相同。当到达 j + 1 减去了 k,后面再累加就不会受到前面的影响。

针对这道题,将原数组视为每个整数点被覆盖的次数,开始时均为 0,差分数组也均为 0。最后只需统计覆盖次数大于 0 的整数个数即可。

代码


/**
 * @date 2024-09-15 21:53
 */
public class NumberOfPoints2848 {

    /**
     * 差分数组 1ms  O(n)
     */
    public int numberOfPoints_v2(List<List<Integer>> nums) {
        // 数据范围为 1~100,0~100 共101个数,但是由于需要访问 end+1,因此初始化为102个
        int[] diff = new int[102];
        for (List<Integer> segment : nums) {
            diff[segment.get(0)]++;
            diff[segment.get(1) + 1]--;
        }
        int res = 0;
        int cnt = 0;
        for (int i : diff) {
            cnt += i;
            if (cnt > 0){
                res++;
            }
        }
        return res;
    }

    /**
     * 合并区间 排序 O(nlogn) 3ms
     */
    public int numberOfPoints_v1(List<List<Integer>> nums) {
        PriorityQueue<int[]> q = new PriorityQueue<>((a, b) -> a[0] - b[0]);
        for (List<Integer> num : nums) {
            q.offer(new int[]{num.get(0), num.get(1)});
        }
        // 初始化为-1,是因为循环中第一次 res += maxEnd - start + 1; start maxEnd并不是实际的线段数据
        int res = -1;
        int start = 0;
        int maxEnd = 0;
        while (!q.isEmpty()) {
            int[] segment = q.poll();
            int s = segment[0];
            int e = segment[1];
            if (s > maxEnd) {
                res += maxEnd - start + 1;
                start = s;
                maxEnd = e;
            } else {
                maxEnd = Math.max(maxEnd, e);
            }
        }
        // 如果最后一个线段无需合并,那么需要单独将其长度加进来
        // 如果最后一个线段需要合并,由于队列中没有数据了,也需要将最后合并的线段长度加进来
        res += maxEnd - start + 1;
        return res;
    }

    /**
     * 5ms O(c*n) c为区间最大长度
     */
    public int numberOfPoints(List<List<Integer>> nums) {
        Set<Integer> s = new HashSet<>(nums.size());
        for (List<Integer> num : nums) {
            for (int i = num.get(0); i <= num.get(1); i++) {
                s.add(i);
            }
        }
        return s.size();
    }
}

性能

1450.在既定时间做作业的学生人数

目标

给你两个整数数组 startTime(开始时间)和 endTime(结束时间),并指定一个整数 queryTime 作为查询时间。

已知,第 i 名学生在 startTime[i] 时开始写作业并于 endTime[i] 时完成作业。

请返回在查询时间 queryTime 时正在做作业的学生人数。形式上,返回能够使 queryTime 处于区间 [startTime[i], endTime[i]](含)的学生人数。

示例 1:

输入:startTime = [1,2,3], endTime = [3,2,7], queryTime = 4
输出:1
解释:一共有 3 名学生。
第一名学生在时间 1 开始写作业,并于时间 3 完成作业,在时间 4 没有处于做作业的状态。
第二名学生在时间 2 开始写作业,并于时间 2 完成作业,在时间 4 没有处于做作业的状态。
第三名学生在时间 3 开始写作业,预计于时间 7 完成作业,这是是唯一一名在时间 4 时正在做作业的学生。

示例 2:

输入:startTime = [4], endTime = [4], queryTime = 4
输出:1
解释:在查询时间只有一名学生在做作业。

示例 3:

输入:startTime = [4], endTime = [4], queryTime = 5
输出:0

示例 4:

输入:startTime = [1,1,1,1], endTime = [1,3,2,4], queryTime = 7
输出:0

示例 5:

输入:startTime = [9,8,7,6,5,4,3,2,1], endTime = [10,10,10,10,10,10,10,10,10], queryTime = 5
输出:5

说明:

  • startTime.length == endTime.length
  • 1 <= startTime.length <= 100
  • 1 <= startTime[i] <= endTime[i] <= 1000
  • 1 <= queryTime <= 1000

思路

当满足 startTime[i] <= queryTime && endTime[i] >= queryTime 时计数即可。

当 queryTime 是一个数组时,可以使用差分数组或者二分查找来解,具体参考官网题解。

代码


/**
 * @date 2024-09-01 14:35
 */
public class BusyStudent1450 {

    public int busyStudent(int[] startTime, int[] endTime, int queryTime) {
        int n = startTime.length;
        int res = 0;
        for (int i = 0; i < n; i++) {
            if (startTime[i] <= queryTime && endTime[i] >= queryTime) {
                res++;
            }
        }
        return res;
    }
}

性能