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

离散化

讲解

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

问题背景

我们有一个无限长的数轴,初始时每个位置上的值都是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.pswp.cn/bicheng/93300.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/93300.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot 实现在线查看内存对象拓扑图 —— 给 JVM 装上“透视眼”

0. 你将获得什么 一个可嵌入任何 Spring Boot 应用的内存对象拓扑服务&#xff1a;访问 /memviz.html 就能在浏览器看见对象图。 支持按类/包名过滤、按对象大小高亮、点击节点看详情。 线上可用&#xff1a;默认只在你点击“生成快照”时才工作&#xff1b;日常零开销。 1.…

STM32 HAL驱动MPU6050传感器

STM32 HAL驱动MPU6050传感器 项目概述 本项目实现了基于STM32 HAL库的MPU6050传感器驱动&#xff0c;可以读取加速度计和陀螺仪数据。项目使用I2C接口与MPU6050通信&#xff0c;并通过UART接口输出数据。 项目仓库地址&#xff1a;STM32_Sensor_Drives 硬件连接 MPU6050 I2…

flex-wrap子元素是否换行

flex-wrap设置子元素是否换行&#xff0c;默认情况下&#xff0c;项目都排在一条线&#xff08;又称”轴线”&#xff09;上。flex-wrap属性定义&#xff0c;flex布局中默认是不换行的。1、div的宽度是600px&#xff0c;每个span的宽度是150px&#xff0c;总共有5个&#xff0c…

RabbitMQ面试精讲 Day 21:Spring AMQP核心组件详解

【RabbitMQ面试精讲 Day 21】Spring AMQP核心组件详解 开篇 欢迎来到"RabbitMQ面试精讲"系列第21天&#xff01;今天我们将深入探讨Spring AMQP的核心组件&#xff0c;这是Java开发者集成RabbitMQ最常用的框架。掌握Spring AMQP不仅能提升开发效率&#xff0c;更是…

Flink TableAPI 按分钟统计数据量

一、环境版本环境版本Flink1.17.0Kafka2.12MySQL5.7.33二、MySQL建表脚本 create table user_log (id int auto_increment comment 主键primary key,uid int not null comment 用户id,event int not null comment 用户行为,logtime bigint null comment 日志时…

18.13 《3倍效率提升!Hugging Face datasets.map高级技巧实战指南》

3倍效率提升!Hugging Face datasets.map高级技巧实战指南 实战项目:使用 datasets.map 进行高级数据处理 在大模型训练过程中,数据预处理的质量直接决定了模型最终的表现。Hugging Face Datasets 库提供的 datasets.map 方法是处理复杂数据场景的瑞士军刀,本章将深入解析…

实体店获客新引擎:数据大集网如何破解传统门店引流难题

在商业竞争日益激烈的当下&#xff0c;实体店的生存与发展正面临前所未有的挑战。无论是街边的小型便利店&#xff0c;还是大型购物中心的连锁品牌&#xff0c;都在为"如何吸引顾客进店"而绞尽脑汁。传统广告投放效果不佳、线下流量持续萎缩、客户转化率难以提升………

LeetCode 分类刷题:2302. 统计得分小于 K 的子数组数目

题目一个数组的 分数 定义为数组之和 乘以 数组的长度。比方说&#xff0c;[1, 2, 3, 4, 5] 的分数为 (1 2 3 4 5) * 5 75 。给你一个正整数数组 nums 和一个整数 k &#xff0c;请你返回 nums 中分数 严格小于 k 的 非空整数子数组数目。子数组 是数组中的一个连续元素序…

TDengine IDMP 基本功能(1.界面布局和操作)

UI 布局和操作说明 TDengine IDMP 的用户界面&#xff08;UI&#xff09;设计旨在提供直观、易用的操作体验。下面介绍 UI 的主要区域和典型操作&#xff1a; 主要区域 IDMP 的用户界面是完全基于浏览器的。登录后的典型 UI 界面具有几个区域&#xff1a; 主菜单&#xff1a;AI…

QT(概述、基础函数、界面类、信号和槽)

一、概述1、QTQT是一个c的第三方库&#xff0c;是专门用来进行界面编程的一个库 1. QT本身实现了多种软件&#xff1a; 2. ubuntu系统中所有界面都是QT做的 3. 最新版本的QQ也是QT做的 4. 嵌入式编程中&#xff0c;几乎所有的上位机&#xff0c;都可以使用QT来做 QT本身除了实现…

【从零开始java学习|第六篇】运算符的使用与注意事项

目录 一、算术运算符 1. 基本算术运算符&#xff08;二元&#xff09; 2. 自增 / 自减运算符&#xff08;一元&#xff09; 二、类型转换&#xff08;隐式与强制&#xff09; 1. 隐式转换&#xff08;自动类型转换&#xff09; ​编辑 2. 强制转换&#xff08;显式类型转…

shellgpt

一、介绍 官网&#xff1a;https://github.com/TheR1D/shell_gpt ShellGPT&#xff08;shell_gpt&#xff09; 是一款把 GPT 系列大模型能力直接搬到终端 的开源命令行生产力工具。用日常英语或中文描述需求&#xff0c;就能帮你 生成、解释甚至自动执行 Shell 命令&#xff…

geoserver sql视图调用Postgis自定义函数问题记录

一、问题描述&#xff1a;geoserver sql视图调用Postgis自定义函数对点图层增加一条记录时&#xff0c;返回结果主键自增ID加了2&#xff0c;但表中数据只增加一条记录。 但在pgAdmin中直接写SQL调用Postgis自定义函数对点图层增加一条记录时&#xff0c;返回结果主键自增ID只加…

#T1224. 最大子矩阵

题目传送 题目描述 已知矩阵的大小定义为矩阵中所有元素的和。给定一个矩阵&#xff0c;你的任务是找到最大的非空(大小至少是11)子矩阵。 比如&#xff0c;如下44的矩阵 0 -2 -7 09 2 -6 2 -4 1 -4 1-1 8 0 -2的最大子矩阵是 9 2-4 1-1 8这…

2025年大模型安全岗的面试汇总(题目+回答)

安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 1. Transformer核心机制及其对LLM突破的基石作用 2. LLM能力边界评估框架设计 3. 模型层级安全风险分析 …

《关于省级政务云服务费支出预算标准的规定》豫财预〔2024〕106号解读

《关于省级政务云服务费支出预算标准的规定》豫财预〔2024〕106号文件由河南省财政厅编制经省政府同意后于2024年12月3日印发执行&#xff0c;规定作为省级政务云服务费支出预算编制和审核的依据&#xff0c;旨在加强省级部门预算管理&#xff0c;规范政务云服务费支出预算编制…

使用HalconDotNet实现异步多相机采集与实时处理

文章目录 一、核心功能与原理 功能目标: 工作原理: 关键机制: 二、完整C#实现代码 三、关键实现解析 1. 零拷贝图像传输 2. 动态帧率控制 3. HALCON并行优化 4. 异常隔离机制 四、高级优化策略 1. 硬件加速配置 2. 内存池管理 3. 实时性保障 一、核心功能与原理 功能目标:…

《疯狂Java讲义(第3版)》学习笔记ch4

ch4流程控制与数组1.switch语句后的expression表达式的数据类型只能是byte、short、char、int四种证书类型。2.建议不要在循环体内修改循环变量&#xff08;也叫循环计数器&#xff09;的值&#xff0c;否则会增加程序出错的可能性。3.定义数组推荐语法格式&#xff1a;type[] …

COLMAP进行密集重建,三维重建的步骤

密集重建是在稀疏重建的基础上进行的 稀疏重建见&#xff1a;用 COLMAP GUI 在 Windows 下一步步完成 相机位姿估计&#xff08;SfM&#xff09; 和 稀疏点云重建的详细步骤&#xff1a;_colmap database导入图片位姿-CSDN博客 完成稀疏重建后直接进入以下步骤进行密集重建&am…

基于飞算JavaAI实现Reactor模式服务器的深度实践

一、飞算JavaAI技术概述 1.1 飞算JavaAI平台简介飞算JavaAI是飞算科技推出的智能化Java开发平台&#xff0c;通过AI技术赋能传统软件开发流程&#xff0c;为开发者提供从需求分析到代码实现的全流程智能化解决方案。该平台深度融合了人工智能技术与软件开发实践&#xff0c;具备…