← 目录 / 算法文档 · 模块十六 树状数组与线段树 / 16.1 离散化

16.1 离散化

数值本身可能大到离谱,但真正出现过的数值没几个——只保留"排第几"这个相对顺序,把天文数字压缩成 1~n 的连续下标。

本页目录
① 为什么需要离散化

16.2、16.3 节要学的树状数组、线段树,通常要求维护的下标是 1~n 这样连续的整数——数组开多大、循环怎么写,都建立在"下标范围不大"这个前提上。但实际题目里,数据的取值范围可能非常夸张(比如坐标范围是 1~10⁹),而数据的个数却很少(比如只有 10⁵ 个数)。如果直接按数值大小开数组,要么开不下(内存不够),要么绝大部分空间都在浪费(真正用到的值稀稀拉拉分布在一个巨大的范围里)。

离散化解决的正是这个矛盾——把一堆"数值很大、但个数不多"的数,压缩成 1~n 的连续整数,压缩前后,数值之间原本的大小顺序保持不变

② 核心思想:只保留相对大小关系

很多题目真正关心的并不是某个数具体是多少,而是它在所有出现过的数里排第几(比如"求区间内比它小的数有多少个",只需要知道相对顺序,不需要知道具体数值)。离散化就是利用这一点:把原始数组排序、去重后,每个数在这个"去重排序后的数组"里的位置(下标),就是它离散化之后的新值——这个新值一定落在 1~nn 是去重后的个数)范围内。

步骤做什么
① 复制把原数组复制一份,准备排序(保留原数组不动,后面还要用)
② 排序对复制的数组从小到大排序
③ 去重相同的数值只保留一份(常用 unique
④ 查名次对原数组里的每个数,在"排序去重后的数组"里用二分查找(lower_bound)找到它的位置,就是离散化后的新值
③ 图解:一个具体数组的离散化过程

原数组 a = [100, 5000000, 3, 100, 50]——数值分布得很散,但只有 5 个数:

原数组
a[0]
100
a[1]
5000000
a[2]
3
a[3]
100
a[4]
50
↓ 复制一份,排序 + 去重(100 只保留一份)
排序去重后的数组(离散化"字典"),下标即排名
排名 1
3
排名 2
50
排名 3
100
排名 4
5000000
↓ 原数组每个数,去"字典"里查自己的排名
离散化之后的数组
a[0]=100
3
a[1]=5000000
4
a[2]=3
1
a[3]=100
3
a[4]=50
2
原本从 3 一路跨到 5000000 的数值范围,压缩成了 1~4 的连续整数。两个 100a[0]a[3])离散化后都是排名 3——大小关系完全保留:谁在原数组里更大,离散化后的排名也更大;相同的数,排名也相同。
④ 完整代码
C++ · 离散化模板
1int a[MAXN], n; // 原数组
2vector<int> b; // b:排序去重后的"离散化字典"
3
4void Discretize()
5{
6 b.assign(a + 1, a + 1 + n); // ① 复制一份原数组
7 sort(b.begin(), b.end()); // ② 排序
8 b.erase(unique(b.begin(), b.end()), b.end()); // ③ 去重(unique 把重复项挪到末尾,erase 删掉)
9
10 for (int i = 1; i <= n; i++)
11 a[i] = lower_bound(b.begin(), b.end(), a[i]) - b.begin() + 1; // ④ 二分查名次,+1 让排名从 1 开始
12}
💡
lower_bound 在这里做的事情,就是"查字典":lower_bound(b.begin(), b.end(), a[i]) 在有序数组 b 里,找到第一个"大于等于 a[i]"的位置——因为 a[i] 一定在 b 里出现过(b 就是从 a 复制来的),这个位置对应的正是 a[i] 本身。返回的是一个迭代器,减去 b.begin() 转换成从 0 开始的下标,再 +1 让排名从 1 开始(方便后续直接当树状数组、线段树的下标使用)。
⑤ 复杂度分析

排序 O(n log n);对每个数做一次二分查找 O(log n),一共 n 次,合计 O(n log n)。整体离散化的时间复杂度是 O(n log n),空间上多开一个大小为 nb 数组,O(n)

⑥ 常见陷阱
忘记去重,直接对排序后的数组做二分:如果跳过第 8 行的 unique,数组 b 里会保留重复的数值,lower_bound 依然能正确工作(原理不受影响),但 b 的长度不再等于"不同数值的个数",后续树状数组、线段树开的数组大小、下标范围都会跟着算错,很容易引发越界之类的问题。
离散化之后,丢失了原始数值,无法还原:离散化把 100 变成了排名 3,如果题目后续还需要"排名 3 对应的原始数值是多少",必须自己保留 b 这个数组(b[排名-1] 就是原始数值),不能只保留离散化后的结果就把原始信息丢掉。
多组测试数据之间,忘记清空 bvector 不会像普通数组那样"用完自动清零",如果每组数据前忘记 b.clear(),上一组遗留的数据会混进这一组的离散化结果里,导致排名算错。
🏆
接下来:离散化本身不是一个"目的",而是给 16.2 节的树状数组、16.3 节的线段树打前站——这两种数据结构都要求下标是连续的小范围整数,遇到值域巨大的题目,通常都要先离散化,再把离散化后的排名当下标使用。