离散化
1. 离散化原理
原理
-
一般来说,当数据范围很大,而我们只需要数据的相对顺序时,就可以使用保序离散化。
-
保序离散化:例如给定我们数据
-5, 7, 5211314, -1001, -5
,我们可以分为如下几步进行离散化:(1)排序,得到:
-1001, -5, -5, 7, 5211314
;(2)去重,得到:
-1001, -5, 7, 5211314
;(3)离散化,可以让这四个数据分别映射到
0~3
或者1~4
,这个需要根据题目选择(例如前缀和或者树状数组应该映射到1~4
)。 -
另外还有一个问题,就是如何得到每个数据映射到的值?这可以使用二分。可以单独使用一个函数实现(例如名为
find
或者get
)。
2. AcWing上的离散化题目
AcWing 802. 区间和
问题描述
-
问题链接:AcWing 802. 区间和
分析
-
根据数据范围可以看出,本题中的数据范围很大,在 [ − 1 0 9 , 1 0 9 ] [-10^9, 10^9] [−109,109]之间,但是我们用到的这个区间中的数据最多有 3 × 1 0 5 3 \times 10 ^ 5 3×105个,因此我们可以把使用到的数据映射到
1~n
(其中n
是不重复的数据的个数)。 -
因为要求某个区间和,上述离散化应该使用保序离散化。同时求区间和,可以使用前缀和技巧,前缀和要求下标从
1
开始,因此find
(返回数据对应离散化后的下标)函数返回的下标应该从1
开始。
代码
- C++
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 300010;
int n, m;
int a[N], s[N];
vector<int> alls; // 待离散的数据
vector<PII> add, query; // 操作
int find(int x) {
return lower_bound(alls.begin(), alls.end(), x) - alls.begin() + 1;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 0; i < n; i++) {
int x, c;
scanf("%d%d", &x, &c);
add.push_back({x, c});
alls.push_back(x);
}
for (int i = 0; i < m; i++) {
int l, r;
scanf("%d%d", &l, &r);
query.push_back({l, r});
alls.push_back(l);
alls.push_back(r);
}
// 排序、去重
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());
for (auto item : add) {
int x = find(item.first);
a[x] += item.second;
}
for (int i = 1; i <= alls.size(); i++) s[i] = s[i - 1] + a[i];
for (auto item : query) {
int l = find(item.first), r = find(item.second);
printf("%d\n", s[r] - s[l - 1]);
}
return 0;
}
- Java
import java.util.*;
public class Main {
private static class MyPair {
public int x;
public int y;
public MyPair(int x, int y) {
this.x = x;
this.y = y;
}
}
// 寻找 x 对应的离散化后的数据,这里从 1 开始,为了方便计算前缀和
private static int find(List<Integer> alls, int x) {
int l = 0, r = alls.size() - 1;
while (l < r) {
int mid = (r - l) / 2 + l;
if (alls.get(mid) >= x) r = mid;
else l = mid + 1;
}
return r + 1;
}
// Leetcode 0026
private static int unique(List<Integer> alls) {
int j = 0;
for (int i = 0; i < alls.size(); i++)
if (i == 0 || alls.get(i) != alls.get(i - 1))
alls.set(j++, alls.get(i));
// alls[0] ~ alls[j - 1] 所有a中不重复的数
return j;
}
public static void main(String[] args) {
// 读入数据
Scanner scan = new Scanner(System.in);
int n = scan.nextInt(); // 操作的个数
int m = scan.nextInt(); // 询问的个数
List<Integer> alls = new ArrayList<>(); // 需要离散化的数据
List<MyPair> add = new ArrayList<>(); // 操作
for (int i = 0; i < n; i++) {
int x = scan.nextInt(), c = scan.nextInt();
add.add(new MyPair(x, c));
alls.add(x);
}
List<MyPair> query = new ArrayList<>(); // 询问
for (int i = 0; i < m; i++) {
int l = scan.nextInt(), r = scan.nextInt();
query.add(new MyPair(l, r));
alls.add(l);
alls.add(r);
}
// 算法代码
int N = n + 2 * m + 10;
int[] a = new int[N]; // a[i] 代表离散到 i 的原始坐标点对应的数据
int[] s = new int[N]; // a数组前缀和
// 离散化:排序、去重
Collections.sort(alls);
int u = unique(alls);
alls = alls.subList(0, u);
// 处理插入
for (MyPair pair : add) {
int x = find(alls, pair.x); // 获取离散化后的值
a[x] += pair.y;
}
// 预处理前缀和
for (int i = 1; i <= alls.size(); i++) s[i] = s[i - 1] + a[i];
// 处理问询
for (MyPair pair : query) {
int l = find(alls, pair.x), r = find(alls, pair.y);
System.out.println(s[r] - s[l - 1]);
}
}
}
3. 力扣上的离散化题目
Leetcode 0327 区间和的个数
题目描述:Leetcode 0327 区间和的个数
分析
-
本题的考点:树状数组、前缀和、离散化。
-
我们首先求出数组
nums
的前缀和数组s
,当我们考虑s[i]
时,我们需要找到存在多少个j
( 0 ≤ j ≤ i − 1 0 \le j \le i-1 0≤j≤i−1),使得s[i] - s[j]
在lower
和upper
之间。即 l o w e r ≤ s [ i ] − s [ j ] ≤ u p p e r lower \le s[i] - s[j] \le upper lower≤s[i]−s[j]≤upper,变换一下得到: s [ i ] − u p p e r ≤ s [ j ] ≤ s [ i ] − l o w e r s[i] -upper \le s[j] \le s[i] - lower s[i]−upper≤s[j]≤s[i]−lower。 -
没有离散化之前,当前如果考虑的是
s[i]
,则树状数组中维护的是s[0]~s[i-1]
的个数,但是现在存在的问题是s
可能为负数,无法作为下标,因此需要进行离散化。 -
因为
j
可能取到0
,所以我们需要s[0]
,而s[0]
的值为0
,因此0
也需要被离散化。假设get(x)
返回x
离散化后对应的值,query(x)
返回1~x
中数据的个数,则对于当前考虑的s[i]
,存在的合法的连续子数组的个数为query(get(s[i]-lower)) - query(get(s[i]-upper-1))
。 -
因此,我们需要离散化的值有:
0
,s[i]
,s[i]-lower
,s[i]-upper-1
。 -
因为树状数组的下标是从1开始的,因此获取某个数据离散化后对应的下标需要加上一个偏移量
1
。
代码
- C++
typedef long long LL;
// 前缀和 + 离散化 + 树状数组(下标从1开始)
class Solution {
public:
int m;
vector<int> tr; // 树状数组
vector<LL> all; // 待离散化的数据
// 返回x在离散化后的数组中的位置(从1开始)
int get(LL x) {
return lower_bound(all.begin(), all.end(), x) - all.begin() + 1;
}
int lowbit(int x) {
return x & -x;
}
void add(int x, int v) {
for (int i = x; i <= m; i += lowbit(i)) tr[i] += v;
}
int query(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i))res += tr[i];
return res;
}
int countRangeSum(vector<int> &nums, int lower, int upper) {
int n = nums.size();
vector<LL> s(n + 1); // 前缀和
all.push_back(s[0]);
for (int i = 1; i <= n; i++) {
s[i] = s[i - 1] + nums[i - 1];
all.push_back(s[i]);
all.push_back(s[i] - lower);
all.push_back(s[i] - upper - 1);
}
sort(all.begin(), all.end());
all.erase(unique(all.begin(), all.end()), all.end());
m = all.size();
tr.resize(m + 1);
int res = 0;
// 为什么有下面这句话:https://www.acwing.com/activity/content/code/content/477194/
// 相当于考虑分析中的 sj = 0 --> lower <= si <= upper 这种情况
add(get(s[0]), 1);
for (int i = 1; i <= n; i++) {
res += query(get(s[i] - lower)) - query(get(s[i] - upper - 1));
add(get(s[i]), 1);
}
return res;
}
};
- Java
class Solution {
int m; // 离散化后数据的个数
int[] tr; // 树状数组(下标从1开始)
List<Long> alls = new ArrayList<>(); // 待离散化的数据,第一个数对应下标为1
public int countRangeSum(int[] nums, int lower, int upper) {
int n = nums.length;
long[] s = new long[n + 1];
alls.add(s[0]);
for (int i = 1; i <= n; i++) {
s[i] = s[i - 1] + nums[i - 1];
alls.add(s[i]);
alls.add(s[i] - lower);
alls.add(s[i] - upper - 1);
}
// 离散化:排序、去重
Collections.sort(alls);
alls = alls.subList(0, unique(alls));
m = alls.size();
tr = new int[m + 1];
int res = 0;
add(get(s[0]), 1);
for (int i = 1; i <= n; i++) {
res += query(get(s[i] - lower)) - query(get(s[i] - upper - 1));
add(get(s[i]), 1);
}
return res;
}
// 树状数组
private int lowbit(int x) {
return x & -x;
}
private void add(int x, int v) {
for (int i = x; i <= m; i += lowbit(i)) tr[i] += v;
}
private int query(int x) {
int res = 0;
for (int i = x; i > 0; i -= lowbit(i)) res += tr[i];
return res;
}
// 返回数据对应离散化后的值(从1开始)
private int get(long x) {
int l = 0, r = alls.size() - 1;
while (l < r) {
int mid = l + r >> 1;
if (alls.get(mid) >= x) r = mid;
else l = mid + 1;
}
return r + 1;
}
// 返回去重后应该保留的数据个数: Leetcode 0026 删除排序数组的重复项
private int unique(List<Long> alls) {
final int T = 1;
int k = 0;
for (int i = 0; i < alls.size(); i++)
if (k - T < 0 || alls.get(i) != alls.get(k - T))
alls.set(k++, alls.get(i));
return k;
}
}
时空复杂度分析
-
时间复杂度: O ( n × l o g ( n ) ) O(n \times log(n)) O(n×log(n)),
n
为数组长度。 -
空间复杂度: O ( n ) O(n) O(n)。