当前位置: 首页 > news >正文

算法详细讲解 - 离散化/区间合并

离散化

讲解

这里的离散化特指整数有序离散化。整个值域跨度很大,但是值非常稀疏的情况。

问题背景

我们有一个无限长的数轴,初始时每个位置上的值都是0。我们需要进行两种操作:

  1. 修改操作:在某个位置 x 上增加一个值 c
  2. 查询操作:询问区间 [l, r] 内所有位置的值之和。

数据范围

  • 操作次数 n 和查询次数 m 最多为 10^5
  • 坐标 x 和区间端点 lr 的范围是 -10^9 到 10^9
  • 修改值 c 的范围是 -10000 到 10000

解题思路

由于坐标范围非常大(-10^910^9),直接使用数组存储每个位置的值会非常浪费空间。因此,我们需要使用离散化技术,将实际的坐标映射到一个小范围内。

离散化步骤

  1. 收集所有需要离散化的值:包括所有的修改位置 x 和查询区间的端点 l 和 r
  2. 排序并去重:对这些值进行排序,并去掉重复的值。
  3. 映射:将每个原始值映射到一个新的小范围内的索引。

模板题

802. 区间和 - AcWing题库

#include <bits/stdc++.h>
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; // 添加与查询// 二分查找找到 x 在 alls 中的位置
int find(int x) {int l = 0, r = alls.size() - 1;while (l < r) {int mid = l + r >> 1;if (alls[mid] >= x) r = mid;else l = mid + 1;}return r + 1; // 返回 x 映射后的位置
}int main() {cin >> n >> m;for (int i = 0; i < n; i++) {int x, c;cin >> x >> c;add.push_back({x, c});alls.push_back(x); // 收集需要离散化的值}for (int i = 0; i < m; i++) {int l, r;cin >> l >> r;query.push_back({l, r});alls.push_back(l);alls.push_back(r); // 收集需要离散化的值}// 对 alls 排序并去重sort(alls.begin(), alls.end());alls.erase(unique(alls.begin(), alls.end()), alls.end());// 处理插入操作for (auto item : add) {int x = find(item.first); // 找到 x 映射后的位置a[x] += item.second; // 在该位置增加 c}// 预处理前缀和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); // 找到 l 和 r 映射后的位置cout << s[r] - s[l - 1] << endl; // 输出区间 [l, r] 的和}return 0;
}

模板题讲解

pair 是为了 把“有关系的两个数据”绑在一起,方便一起存储和使用。

在本题中:

  • add 操作需要两个数:位置 x 和 要加的值 c
  • query 查询也需要两个数:左端点 l 和 右端点 r

我们用 pair<int, int> 把它们“配对”起来,这样就不会搞混。

想象你在记账:

时间事件
3点在超市花 50 元
5点在餐厅花 100 元

你想记录这些操作,你会怎么存?错误方式:

vector<int> times = {3, 5};
vector<int> money = {50, 100};

问题:如果排序或删除,很容易“时间”和“金额”对不上!

vector<pair<int, int>> records = {{3, 50}, {5, 100}};

这样,3点50元 永远是一对,不会错乱。

再回到题目:vector<PII> add; —— 存修改操作

for (int i = 0; i < n; i++) {int x, c;cin >> x >> c;add.push_back({x, c});  // 把“位置x”和“加的值c”绑在一起
}

比如输入:

1 2
3 6
7 5

add 变成:

add = { {1,2}, {3,6}, {7,5} }
  • {1,2} 表示:在位置 1 加 2
  • {3,6} 表示:在位置 3 加 6

好处:后面遍历的时候,item.first 就是 xitem.second 就是 c。

find函数的作用是什么?

int find(int x) {int l = 0, r = alls.size() - 1;  // l=左边界,r=右边界while (l < r) {                  // 二分查找开始int mid = l + r >> 1;        // 相当于 (l + r) / 2,找中间位置if (alls[mid] >= x)          // 如果中间的数 >= xr = mid;                 // 说明 x 在左边,把右边界缩小到 midelse                         // 如果中间的数 < xl = mid + 1;             // 说明 x 在右边,把左边界移到 mid+1}return r + 1;  // 返回位置(+1 是因为窗口号从 1 开始,且r是下标,从0开始)
}

这个 find 函数,就是在一个排好序的列表里,快速找到某个数 x 的“排名”(位置),然后返回它的“新编号”。find(x) 就像查字典:给你一个词(x),快速找到它在词典(alls)里的页码(新编号),方便后面查数据。

举个生活例子:排队领号

想象你去银行办事:

  • 银行有 10 个窗口,但今天只有 3 个人来办业务,他们的号码是:1, 3, 7
  • 银行不想浪费窗口资源,于是说:“我们重新分配窗口号!”
  • 把这三个号码 从小到大排序,然后按顺序分配新窗口号:
    • 1 → 窗口 1
    • 3 → 窗口 2
    • 7 → 窗口 3

这个过程就叫 离散化:把很大的原始号码(比如 1亿),映射到很小的窗口号(1,2,3)。

在你的代码里,alls 就是那个“排好序的号码列表”。比如:

alls = {1, 3, 7}  // 原始坐标,已经排序去重

我们想问:

“原始坐标 3 对应哪个窗口号?”

答案是:第 2 个位置 → 所以返回 2

原始坐标 x在 alls 中的下标返回值(r+1)新编号
100+1 = 11
311+1 = 22
722+1 = 33

为什么用二分?不用直接找?

因为 alls 可能有 30 万个数,如果一个一个找(线性查找),太慢了!

  • 线性查找:最多要查 30 万次
  • 二分查找:最多查 log₂(30万) ≈ 19 次

 快了上万倍!

处理修改与查询操作

第一部分:处理修改操作

for (int i = 0; i < n; i++) {int x, c;cin >> x >> c;           // 读入位置 x 和要加的值 cadd.push_back({x, c});   // 把 (x, c) 存起来,后面要用alls.push_back(x);       // 把 x 记录下来,准备离散化!
}
ixcadd 变成alls 变成
012{1,2}{1}
136{1,2},{3,6}{1,3}
275{1,2},{3,6},{7,5}{1,3,7}

所以现在:

  • add 存了所有“在哪加、加多少”
  • alls 存了所有被修改过的位置:1, 3, 7

第二部分:处理查询操作

for (int i = 0; i < m; i++) {int l, r;cin >> l >> r;           // 读入查询区间 [l, r]query.push_back({l, r}); // 把 (l, r) 存起来,后面要用alls.push_back(l);       // 把 l 记下来,也要离散化!alls.push_back(r);       // 把 r 记下来,也要离散化!
}
ilrquery 变成alls 新增
013{1,3}1, 3 → alls = {1,3,7,1,3}
146{1,3},{4,6}4,6 → alls = {1,3,7,1,3,4,6}
278{1,3},{4,6},{7,8}7,8 → alls = {1,3,7,1,3,4,6,7,8}

为什么查询的 l 和 r 也要放进 alls

因为我们要对所有出现过的坐标进行离散化!

你想啊,后面要查 [4,6] 的和,但 46 这两个位置根本没人改过,它们不在 add 里。

但我们必须知道:

  • 4 在离散化后对应哪个编号?
  • 6 在离散化后对应哪个编号?

否则没法查区间!

所以:只要是出现过的坐标(无论是修改还是查询),都要放进 alls

后面代码会做两件事:

sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());

排序 + 去重后变成:

{1, 3, 4, 6, 7, 8}

然后我们就可以离散化了:

原始坐标新编号(find 返回)
11
32
43
64
75
86

处理插入,预处理前缀和,处理询问(输出每个区间的和)

第一段:处理插入(把值加到离散化后的位置)

for (auto item : add) {int x = find(item.first);        // 找到原始位置对应的新编号a[x] += item.second;             // 把值加到新位置上
}

第一次:item = {1,2}

  • item.first = 1 → find(1) = 1 → x = 1
  • a[1] += 2 → a[1] = 2

第二次:item = {3,6}

  • find(3) = 2 → x = 2
  • a[2] += 6 → a[2] = 6

第三次:item = {7,5}

  • find(7) = 5 → x = 5
  • a[5] += 5 → a[5] = 5

现在 a 数组长这样(只看 1~6):

新编号123456
值 a[i]260050

注意:a[i] 存的是离散化后编号为 i 的位置上的值

第二段:预处理前缀和

for (int i = 1; i <= alls.size(); i++)s[i] = s[i - 1] + a[i];

alls.size() = 6,所以 i 从 1 到 6

我们来算 s[i],它表示:前 i 个离散化位置的值的总和

i0123456
s[i]028881313

第三段:处理询问(输出每个区间的和)

for (auto item : query) {int l = find(item.first), r = find(item.second);cout << s[r] - s[l - 1] << endl;
}

举个例子

查询 1:item = {1,3} → 查 [1,3] 的和

  • l = find(1) = 1
  • r = find(3) = 2
  • 输出:s[2] - s[1 - 1] = s[2] - s[0] = 8 - 0 = 8

区间合并

讲解

区间合并就是对区间求并集。

给定一些区间,比如 [1,3][2,6],如果这些区间有交集(包括端点相交),我们就需要把它们合并成一个更大的区间。最终我们要计算合并后剩下多少个不重叠的区间。

我们可以看到:

  • [1, 2] 和 [2, 4] 有交集,可以合并成 [1, 4]
  • [7, 8] 和 [7, 9] 有交集,可以合并成 [7, 9]
  • [5, 6] 没有和其他区间交集,保持不变

所以最终合并后的区间是:

  • [1, 4]
  • [5, 6]
  • [7, 9]

因此,合并后的区间个数是 3

模板题

AcWing 803. 区间合并 - AcWing

#include <bits/stdc++.h>
using namespace std;typedef pair<int, int> PII;// 合并区间的函数
void merge(vector<PII> &segs) {// 结果数组,存储合并后的区间vector<PII> res;// 对输入的区间按照起始位置进行排序sort(segs.begin(), segs.end());// 初始化当前区间的起始和结束位置int st = -2e9, ed = -2e9;// 遍历所有区间for (auto seg : segs) {// 如果当前区间的起始位置大于上一个区间的结束位置if (ed < seg.first) {// 如果st不是初始值,说明之前已经有一个区间了,将其加入结果数组if (st != -2e9) {res.push_back({st, ed});}// 更新当前区间的起始和结束位置st = seg.first, ed = seg.second;} else {// 如果当前区间的起始位置小于等于上一个区间的结束位置,说明有交集// 更新当前区间的结束位置为两者中的较大值ed = max(ed, seg.second);}}// 最后检查是否有剩余的区间未加入结果数组if (st != -2e9) {res.push_back({st, ed});}// 将结果数组赋值给原数组,完成合并操作segs = res;
}int main() {// 区间数量int n;cin >> n;// 创建一个vector来存储所有的区间vector<PII> segs;// 读取每个区间并存入vector中for (int i = 0; i < n; i++) {int l, r;cin >> l >> r;segs.push_back({l, r});}// 调用merge函数进行区间合并merge(segs);// 输出合并后的区间个数cout << segs.size() << endl;return 0;
}

模板题讲解

int st = -2e9, ed = -2e9; 这一步是怎么来的?

目的是初始化一个“虚拟”的起始区间,用来方便地处理第一个真实区间的合并逻辑。

注意:-2e9 就是 -2 * 10^9,题目说区间范围是 -10^9 ≤ li ≤ ri ≤ 10^9,所以 -2e9 比所有可能的左端点都小,可以当作“负无穷”。

int st = -2e9, ed = -2e9; // 初始状态:没有正在合并的区间
for (auto seg : segs) {if (ed < seg.first) {  // 当前区间无法和 seg 合并(断开了)if (st != -2e9) {  // 如果 st 不是初始值,说明我们之前已经在合并某个真实区间res.push_back({st, ed}); // 把之前合并好的区间存进去}// 开启一个新的合并区间:就是当前这个 segst = seg.first;ed = seg.second;} else {// 能合并!更新当前合并区间的右边界ed = max(ed, seg.second);}
}
// 循环结束后,最后一个正在合并的区间还没加入结果
if (st != -2e9) {res.push_back({st, ed});
}

为什么需要这个“虚拟起点”?

是为了统一处理逻辑,避免写一堆 if (第一个区间) 的判断。

比如如果没有这个技巧,你可能会写:

if (res.empty()) {st = seg.first;ed = seg.second;
} else if (ed < seg.first) {res.push_back({st, ed});st = seg.first;ed = seg.second;
} else {ed = max(ed, seg.second);
}

int st = -2e9, ed = -2e9; 是一种编程技巧,叫做“哨兵”或“虚拟初始值”,作用是:

  • 让你在处理第一个真实区间时,也能套用统一的逻辑;
  • 用 st != -2e9 来判断“是否已经开始合并一个真实区间”;
  • 避免特判第一个元素,让代码更简洁、不易出错。
http://www.lryc.cn/news/619771.html

相关文章:

  • 【慕伏白】Kali 系统下安装 docker
  • 弹性扩展新范式:分布式LLM计算的FastMCP解决方案
  • Python(二):MacBook安装 Python并运行第一个 Python 程序
  • 【QT】QT实现鼠标左右滑动切换图片
  • MySQL中的缓存机制
  • 如何在VS里使用MySQL提供的mysql Connector/C++的debug版本
  • 如何把ubuntu 22.04下安装的mysql 8 的 数据目录迁移到另一个磁盘目录
  • 设计模式笔记_行为型_策略模式
  • OpenJDK 17 源码 安全点轮询的信号处理流程
  • 资源查看-lspci命令
  • 如何准备一场技术演讲
  • 各种排序算法(二)
  • 磁悬浮轴承转子设计避坑指南:深度解析核心要点与高可靠性策略
  • 基于js和html的点名应用
  • 【电气】NPN与PNP
  • B系列树详细讲解
  • 16-docker的容器监控方案-prometheus实战篇
  • Python 类元编程(导入时和运行时比较)
  • Windows也能用!Claude Code硬核指南
  • [激光原理与应用-259]:理论 - 几何光学 - 平面镜的反射、平面透镜的折射、平面镜的反射成像、平面透镜的成像的规律
  • 网刻软件iVentoy软件使用方法
  • @进程管理工具 - Glances工具详细指南
  • Django REST Framework视图
  • Java 大视界 -- Java 大数据机器学习模型在金融资产配置优化与风险收益平衡中的应用(395)
  • 解惑rust中的 Send/Sync(译)
  • 基于Java的Markdown转Word工具(标题、段落、表格、Echarts图等)
  • 18.10 SQuAD数据集实战:5步高效获取与预处理,BERT微调避坑指南
  • 实战多屏Wallpaper壁纸显示及出现黑屏问题bug分析-学员作业
  • HTML <iframe> 标签 如何把html写入iframe标签
  • 版图设计学习2_掌握PDK中的层定义(工艺文档精读)