题目

给你一个整数数组 nums 以及两个整数 lower 和 upper 。求数组中,值位于范围 [lower, upper] (包含 lower 和 upper)之内的 区间和的个数 。

区间和 S(i, j) 表示在 nums 中,位置从 i 到 j 的元素之和,包含 i 和 j (i ≤ j)。

示例 1:
输入:nums = [-2,5,-1], lower = -2, upper = 2
输出:3
解释:存在三个区间:[0,0]、[2,2] 和 [0,2] ,对应的区间和分别是:-2 、-1 、2 。

示例 2:

输入:nums = [0], lower = 0, upper = 0
输出:1

提示:

1 <= nums.length <= 10^5
-2^31 <= nums[i] <= 2^31 - 1
-10^5 <= lower <= upper <= 10^5
题目数据保证答案是一个 32 位 的整数

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/count-of-range-sum
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

再来练习一下树状数组。

求区间和首先考虑前缀和,在求出前缀和数组的情况下,还需要枚举子数组的始末下标,会超时,因此需要想一个nlogn的算法。

对于一个子数组[i,j],其和等于preSum[j]-preSum[i]。对于下标j,题目需要求使得lower<=preSum[j]-preSum[i]<=upper成立的i的数目,将不等式变形一下,可以得到preSum[i]>=preSum[j]-upper和preSum[i]<=preSum[j]-lower,使这两个不等式同时成立的i的数目需要累计到答案中。

因此,问题的核心可以归结为:对于preSum[j],求使preSum[i]落在[preSum[j]-upper, preSum[j]-lower]范围内的下标i的个数。

那么,可以枚举下标j,计算满足条件的i的数目,为了满足nlogn的时间复杂度,可以使用平衡树,或者树状数组。

在枚举下标j之前,需要将前缀和数组维护为有序离散的,有序是必要的,因为要查询指定范围内的元素个数,离散是因为节省空间(nums[i]太大,不节省也放不下)。

代码

  1. class Solution {
  2. int len = 0;
  3. int[] tree;
  4. public int countRangeSum(int[] nums, int lower, int upper) {
  5. int n = nums.length;
  6. long[] pre = new long[n + 1];
  7. for (int i = 1; i <= n; i++) {
  8. pre[i] = pre[i - 1] + nums[i - 1];
  9. }
  10. // 将前缀和数组排序去重,然后放在Map中离散化
  11. long[] arr = Arrays.stream(pre).distinct().sorted().toArray();
  12. // 因为下面用到了floorKey和ceilingKey,所以这里用了TreeMap
  13. TreeMap<Long, Integer> map = new TreeMap<>();
  14. for (int i = 0; i < arr.length; i++) {
  15. map.put(arr[i], i + 1);
  16. }
  17. len = map.size();
  18. tree = new int[len + 1];
  19. int ans = 0;
  20. for (int i = 0; i <= n; ++i) {
  21. // sum - lower和sum - upper可能不存在于map中
  22. // 对于上界就找一个不大于自身的最大的数,下界找一个不小于自身的最小的数,得到新的上下界[g,f]
  23. Long f = map.floorKey(pre[i] - lower);
  24. Long g = map.ceilingKey(pre[i] - upper);
  25. if (f != null && g != null) {
  26. // 求前缀和落在[g,f]内的个数,注意左边要减1,才会包括g处的
  27. ans += query(map.get(f)) - query(map.get(g) - 1);
  28. }
  29. // 前缀和为sum的个数+1
  30. // 注意,必须先更新ans再update,因为对于下标0来说,当前还没有遍历到第一个元素,没有子数组满足条件,如果先update就会使ans变多
  31. update(map.get(pre[i]), 1);
  32. }
  33. return ans;
  34. }
  35. public void update(int i, int delta) {
  36. while (i <= len) {
  37. tree[i] += delta;
  38. i += lowbit(i);
  39. }
  40. }
  41. public int query(int i) {
  42. int sum = 0;
  43. while (i > 0) {
  44. sum += tree[i];
  45. i -= lowbit(i);
  46. }
  47. return sum;
  48. }
  49. public static int lowbit(int x) {
  50. return x & (-x);
  51. }
  52. }

也可以参考「官解」的做法,在map中加入pre[i] - upper和pre[i] - lower,这样就不用使用floorKey和ceilingKey了。

  1. class Solution {
  2. int len = 0;
  3. int[] tree;
  4. public int countRangeSum(int[] nums, int lower, int upper) {
  5. int n = nums.length;
  6. long[] pre = new long[n + 1];
  7. for (int i = 1; i <= n; i++) {
  8. pre[i] = pre[i - 1] + nums[i - 1];
  9. }
  10. Set<Long> set = new TreeSet<>();
  11. for (long num : pre) {
  12. set.add(num);
  13. set.add(num - upper);
  14. set.add(num - lower);
  15. }
  16. Map<Long, Integer> map = new HashMap<>();
  17. int cnt = 1;
  18. for (long num : set) {
  19. map.put(num, cnt++);
  20. }
  21. len = map.size();
  22. tree = new int[len + 1];
  23. int ans = 0;
  24. for (int i = 0; i <= n; ++i) {
  25. ans += query(map.get(pre[i] - lower)) - query(map.get(pre[i] - upper) - 1);
  26. // 前缀和为sum的个数+1
  27. // 注意,必须先更新ans再update,因为对于下标0来说,当前还没有遍历到第一个元素,没有子数组满足条件,如果先update就会使ans变多
  28. update(map.get(pre[i]), 1);
  29. }
  30. return ans;
  31. }
  32. public void update(int i, int delta) {
  33. while (i <= len) {
  34. tree[i] += delta;
  35. i += lowbit(i);
  36. }
  37. }
  38. public int query(int i) {
  39. int sum = 0;
  40. while (i > 0) {
  41. sum += tree[i];
  42. i -= lowbit(i);
  43. }
  44. return sum;
  45. }
  46. public static int lowbit(int x) {
  47. return x & (-x);
  48. }
  49. }