|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[327. 区间和的个数](https://leetcode.cn/problems/count-of-range-sum/solution/by-ac_oier-b36o/)** ,难度为 **困难**。 |
| 4 | + |
| 5 | +Tag : 「前缀和」、「离散化」、「树状数组」、「线段树」、「动态开点」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +给你一个整数数组 `nums` 以及两个整数 `lower` 和 `upper` 。求数组中,值位于范围 $[lower, upper]$ (包含 `lower` 和 `upper`)之内的 区间和的个数 。 |
| 10 | + |
| 11 | +区间和 $S(i, j)$ 表示在 `nums` 中,位置从 $i$ 到 $j$ 的元素之和,包含 $i$ 和 $j$ (`i ≤ j`)。 |
| 12 | + |
| 13 | +示例 1: |
| 14 | +``` |
| 15 | +输入:nums = [-2,5,-1], lower = -2, upper = 2 |
| 16 | +
|
| 17 | +输出:3 |
| 18 | +
|
| 19 | +解释:存在三个区间:[0,0]、[2,2] 和 [0,2] ,对应的区间和分别是:-2 、-1 、2 。 |
| 20 | +``` |
| 21 | +示例 2: |
| 22 | +``` |
| 23 | +输入:nums = [0], lower = 0, upper = 0 |
| 24 | +
|
| 25 | +输出:1 |
| 26 | +``` |
| 27 | + |
| 28 | +提示: |
| 29 | +* $1 <= nums.length <= 10^5$ |
| 30 | +* $-2^{31} <= nums[i] <= 2^{31} - 1$ |
| 31 | +* $-10^5 <= lower <= upper <= 10^5$ |
| 32 | +* 题目数据保证答案是一个 $32$ 位 的整数 |
| 33 | + |
| 34 | +--- |
| 35 | + |
| 36 | +### 树状数组(离散化) |
| 37 | + |
| 38 | +由于区间和的定义是子数组的元素和,容易想到「前缀和」来快速求解。 |
| 39 | + |
| 40 | +对于每个 $nums[i]$ 而言,我们需要统计以每个 $nums[i]$ 为右端点的合法子数组个数(合法子数组是指区间和值范围为 $[lower, upper]$ 的子数组)。 |
| 41 | + |
| 42 | +我们可以从前往后处理 $nums$,假设当前我们处理到位置 $k$,同时下标 $[0, k]$ 的前缀和为 $s$,那么以 $nums[k]$ 为右端点的合法子数组个数,等价于在下标 $[0, k - 1]$ 中前缀和范围在 $[s - upper, s - lower]$ 的数的个数。 |
| 43 | + |
| 44 | +我们需要使用一个数据结构来维护「遍历过程中的前缀和」,每遍历 $nums[i]$ 需要往数据结构加一个数,同时每次需要查询值在某个范围内的数的个数。涉及的操作包括「单点修改」和「区间查询」,容易想到使用树状数组进行求解。 |
| 45 | + |
| 46 | +但值域的范围是巨大的(同时还有负数域),我们可以利用 $nums$ 的长度为 $10^5$ 来做离散化。我们需要考虑用到的数组都有哪些: |
| 47 | + |
| 48 | +1. 首先前缀和数组中的每一位 $s$ 都需要被用到(添加到树状数组中); |
| 49 | +2. 同时对于每一位 $nums[i]$(假设对应的前缀和为 $s$),我们都需要查询以其为右端点的合法子数组个数,即查询前缀和范围在 $[s - upper, s - lower]$ 的数的个数。 |
| 50 | + |
| 51 | +因此对于前缀和数组中的每一位 $s$,我们用到的数有 $s$、$s - upper$ 和 $s - lower$ 三个数字,共有 $1e5$ 个 $s$,即最多共有 $3 \times 10^5$ 个不同数字被使用,我们可以对所有用到的数组进行排序编号(离散化),从而将值域大小控制在 $3 \times 10^5$ 范围内。 |
| 52 | + |
| 53 | + |
| 54 | + 代码: |
| 55 | +```Java |
| 56 | +class Solution { |
| 57 | + int m; |
| 58 | + int[] tr = new int[100010 * 3]; |
| 59 | + int lowbit(int x) { |
| 60 | + return x & -x; |
| 61 | + } |
| 62 | + void add(int x, int v) { |
| 63 | + for (int i = x; i <= m; i += lowbit(i)) tr[i] += v; |
| 64 | + } |
| 65 | + int query(int x) { |
| 66 | + int ans = 0; |
| 67 | + for (int i = x; i > 0; i -= lowbit(i)) ans += tr[i]; |
| 68 | + return ans; |
| 69 | + } |
| 70 | + public int countRangeSum(int[] nums, int lower, int upper) { |
| 71 | + Set<Long> set = new HashSet<>(); |
| 72 | + long s = 0; |
| 73 | + set.add(s); |
| 74 | + for (int i : nums) { |
| 75 | + s += i; |
| 76 | + set.add(s); |
| 77 | + set.add(s - lower); |
| 78 | + set.add(s - upper); |
| 79 | + } |
| 80 | + List<Long> list = new ArrayList<>(set); |
| 81 | + Collections.sort(list); |
| 82 | + Map<Long, Integer> map = new HashMap<>(); |
| 83 | + for (long x : list) map.put(x, ++m); |
| 84 | + s = 0; |
| 85 | + int ans = 0; |
| 86 | + add(map.get(s), 1); |
| 87 | + for (int i : nums) { |
| 88 | + s += i; |
| 89 | + int a = map.get(s - lower), b = map.get(s - upper) - 1; |
| 90 | + ans += query(a) - query(b); |
| 91 | + add(map.get(s), 1); |
| 92 | + } |
| 93 | + return ans; |
| 94 | + } |
| 95 | +} |
| 96 | +``` |
| 97 | +* 时间复杂度:去重离散化的复杂度为 $O(n\log{n})$;统计答案的复杂度为 $O(n\log{n})$ |
| 98 | +* 空间复杂度:$O(n)$ |
| 99 | + |
| 100 | +--- |
| 101 | + |
| 102 | +### 线段树(动态开点 + STL) |
| 103 | + |
| 104 | +值域范围爆炸,但是数组长度(查询次数)有限,容易想到「线段树(动态开点)」。 |
| 105 | + |
| 106 | +但如果按照我们 [729. 我的日程安排表 I](https://sharingsource.github.io/2022/05/04/729.%20%E6%88%91%E7%9A%84%E6%97%A5%E7%A8%8B%E5%AE%89%E6%8E%92%E8%A1%A8%20I%EF%BC%88%E4%B8%AD%E7%AD%89%EF%BC%89/)、[731. 我的日程安排表 II](https://sharingsource.github.io/2022/05/04/731.%20%E6%88%91%E7%9A%84%E6%97%A5%E7%A8%8B%E5%AE%89%E6%8E%92%E8%A1%A8%20II%EF%BC%88%E4%B8%AD%E7%AD%89%EF%BC%89/) 和 [732. 我的日程安排表 III](https://sharingsource.github.io/2022/05/04/732.%20%E6%88%91%E7%9A%84%E6%97%A5%E7%A8%8B%E5%AE%89%E6%8E%92%E8%A1%A8%20III%EF%BC%88%E5%9B%B0%E9%9A%BE%EF%BC%89/) 系列题解的求解方式「估点 + 静态数组 + 动态 `new`」,仍无法解决 `MLE` 问题。 |
| 107 | + |
| 108 | +究其原因,我们在 [日程安排表] 系列中使用到的方式存在「固定双边开点(一旦要对 $u$ 的子节点开点,会同时将左右子节点都开出来)」以及「查询时也会触发开点」的问题,导致我们最多处理值域范围在 $1e9$ 的情况。 |
| 109 | + |
| 110 | +对于本题的值域范围远超 $1e9$,我们需要考虑修「静态数组」和「开点方式」来解决 `MLE` 问题: |
| 111 | + |
| 112 | +1. 由于值域太大,采用估点方式来开精挑数组会直接导致 `MLE`,我们使用支持扩容的 `STL` 容器; |
| 113 | +2. 由于同时开点和查询也会触发开点,会导致我们不必要的空间浪费,我们直接将开点操作放在 `update` 实现中,并且只有当需要查询到左子节点才对左子节点进行开点,当查询到右子节点才对右子节点进行开点。 |
| 114 | + |
| 115 | + 代码: |
| 116 | +```Java |
| 117 | +class Solution { |
| 118 | + class Node { |
| 119 | + int ls = -1, rs = -1, val = 0; |
| 120 | + } |
| 121 | + List<Node> tr = new ArrayList<>(); |
| 122 | + void update(int u, long lc, long rc, long x, int v) { |
| 123 | + Node node = tr.get(u); |
| 124 | + node.val += v; |
| 125 | + if (lc == rc) return ; |
| 126 | + long mid = lc + rc >> 1; |
| 127 | + if (x <= mid) { |
| 128 | + if (node.ls == -1) { |
| 129 | + tr.add(new Node()); |
| 130 | + node.ls = tr.size() - 1; |
| 131 | + } |
| 132 | + update(node.ls, lc, mid, x, v); |
| 133 | + } else { |
| 134 | + if (node.rs == -1) { |
| 135 | + tr.add(new Node()); |
| 136 | + node.rs = tr.size() - 1; |
| 137 | + } |
| 138 | + update(node.rs, mid + 1, rc, x, v); |
| 139 | + } |
| 140 | + } |
| 141 | + int query(int u, long lc, long rc, long l, long r) { |
| 142 | + if (u == -1) return 0; |
| 143 | + if (r < lc || l > rc) return 0; |
| 144 | + Node node = tr.get(u); |
| 145 | + if (l <= lc && rc <= r) return node.val; |
| 146 | + long mid = lc + rc >> 1; |
| 147 | + return query(node.ls, lc, mid, l, r) + query(node.rs, mid + 1, rc, l, r); |
| 148 | + } |
| 149 | + public int countRangeSum(int[] nums, int lower, int upper) { |
| 150 | + long L = 0, R = 0, s = 0; |
| 151 | + for (int i : nums) { |
| 152 | + s += i; |
| 153 | + L = Math.min(Math.min(L, s), Math.min(s - lower, s - upper)); |
| 154 | + R = Math.max(Math.max(R, s), Math.max(s - lower, s - upper)); |
| 155 | + } |
| 156 | + s = 0; |
| 157 | + int ans = 0; |
| 158 | + tr.add(new Node()); |
| 159 | + update(0, L, R, 0, 1); |
| 160 | + for (int i : nums) { |
| 161 | + s += i; |
| 162 | + long a = s - upper, b = s - lower; |
| 163 | + ans += query(0, L, R, a, b); |
| 164 | + update(0, L, R, s, 1); |
| 165 | + } |
| 166 | + return ans; |
| 167 | + } |
| 168 | +} |
| 169 | +``` |
| 170 | +* 时间复杂度:令 $n$ 为数组长度,$m$ 为值域大小,复杂度为 $O(n\log{m})$ |
| 171 | +* 空间复杂度:$O(n\log{m})$ |
| 172 | + |
| 173 | +--- |
| 174 | + |
| 175 | +### 线段树(动态指针) |
| 176 | + |
| 177 | +更进一步,我们可以连 `STL` 都不使用,直接在 `Node` 的定义时,将左右子节点的引用存放起来。 |
| 178 | + |
| 179 | +虽然这样不会带来空间上的优化,但可以有效避免 `STL` 的创建、访问和扩容(实际运行相比解法二,用时少一半)。 |
| 180 | + |
| 181 | + |
| 182 | +代码: |
| 183 | +```Java |
| 184 | +class Solution { |
| 185 | + class Node { |
| 186 | + Node ls, rs; |
| 187 | + int val = 0; |
| 188 | + } |
| 189 | + void update(Node node, long lc, long rc, long x, int v) { |
| 190 | + node.val += v; |
| 191 | + if (lc == rc) return ; |
| 192 | + long mid = lc + rc >> 1; |
| 193 | + if (x <= mid) { |
| 194 | + if (node.ls == null) node.ls = new Node(); |
| 195 | + update(node.ls, lc, mid, x, v); |
| 196 | + } else { |
| 197 | + if (node.rs == null) node.rs = new Node(); |
| 198 | + update(node.rs, mid + 1, rc, x, v); |
| 199 | + } |
| 200 | + } |
| 201 | + int query(Node node, long lc, long rc, long l, long r) { |
| 202 | + if (node == null) return 0; |
| 203 | + if (r < lc || l > rc) return 0; |
| 204 | + if (l <= lc && rc <= r) return node.val; |
| 205 | + long mid = lc + rc >> 1; |
| 206 | + return query(node.ls, lc, mid, l, r) + query(node.rs, mid + 1, rc, l, r); |
| 207 | + } |
| 208 | + public int countRangeSum(int[] nums, int lower, int upper) { |
| 209 | + long L = 0, R = 0, s = 0; |
| 210 | + for (int i : nums) { |
| 211 | + s += i; |
| 212 | + L = Math.min(Math.min(L, s), Math.min(s - lower, s - upper)); |
| 213 | + R = Math.max(Math.max(R, s), Math.max(s - lower, s - upper)); |
| 214 | + } |
| 215 | + s = 0; |
| 216 | + int ans = 0; |
| 217 | + Node root = new Node(); |
| 218 | + update(root, L, R, 0, 1); |
| 219 | + for (int i : nums) { |
| 220 | + s += i; |
| 221 | + long a = s - upper, b = s - lower; |
| 222 | + ans += query(root, L, R, a, b); |
| 223 | + update(root, L, R, s, 1); |
| 224 | + } |
| 225 | + return ans; |
| 226 | + } |
| 227 | +} |
| 228 | +``` |
| 229 | +* 时间复杂度:令 $n$ 为数组长度,$m$ 为值域大小,复杂度为 $O(n\log{m})$ |
| 230 | +* 空间复杂度:$O(n\log{m})$ |
| 231 | + |
| 232 | +--- |
| 233 | + |
| 234 | +### 最后 |
| 235 | + |
| 236 | +这是我们「刷穿 LeetCode」系列文章的第 `No.327` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 237 | + |
| 238 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 239 | + |
| 240 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 241 | + |
| 242 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 243 | + |
0 commit comments