离散化
食用指南:
对该算法程序编写以及踩坑点很熟悉的同学可以直接跳转到代码模板查看完整代码
只有基础算法的题目会有关于该算法的原理,实现步骤,代码注意点,代码模板,代码误区的讲解
非基础算法的题目侧重题目分析,代码实现,以及必要的代码理解误区
本篇大改两次,三小时力求最简,这么良心的博主你不关注吗?👍
题目描述:
-
假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。
现在,我们首先进行 n 次操作,每次操作将某一位置 x 上的数加 c。
接下来,进行 m 次询问,每个询问包含两个整数 l 和 r,你需要求出在区间 [l,r] 之间的所有数的和。输入格式
第一行包含两个整数 n 和 m。
接下来 n 行,每行包含两个整数 x 和 c。
再接下来 m 行,每行包含两个整数 l 和 r。输出格式
共 m 行,每行输出一个询问中所求的区间内数字和。数据范围
−109≤x≤109,
1≤n,m≤105,
−109≤l≤r≤109,
−10000≤c≤10000
输入样例:
3 3
1 2
3 6
7 5
1 3
4 6
7 8
输出样例:
8
0
5 -
题目来源:https://www.acwing.com/problem/content/804/
题目分析:
-
数轴上一共n个点,同一点每出现一次,点对应的值+c
-
暴力:
arr[x] += c;
最坏空间复杂度:x最大十亿
int数组占据栈,最大长度五十万
存不下,有一个点存储不下,最终结果都出错 -
离散化:
给所有坐标排序,按照序号寻找坐标
最多十万个坐标,还是可以存储得下的将坐标x和c捆绑作一个点进行输入,pair<int,int>
上述先大数排序,后以序号索引的方式就是离散化
最坏空间复杂度:十万
可以存储得下求区间和,经典一维前缀和
算法原理:
模板算法:
离散化:
含义:
-
通过排序后赋予序号,以序号作为索引用一个或多个数组存储大数以及大数的pair<>
离散的本意就是非连续:数轴是连续的,但是数轴上的整数点是非连续的 大数范围是稀疏而巨大的,有序序号范围是密集而有限的
离散数轴:
- 现有四对键值对:(7,5),(1,2),(3,6),(2987654321,5)
将他们映射到离散数轴中:
- 离散数轴并不局限于一个向量/数组,
多个向量/数组可以共用x排序后的序号作为索引
如本题有两个向量:axis[]表示x,arr[]表示c
离散二分查找:
-
现要在上述离散数轴中查找是否存在(5,0)这个输入,可以采用二分查找
-
每次取数轴中点,
int mid = l+r >> 1;
若axis[mid] >= 5; 则r = mid;
若axis[mid] < 5; 则l = mid+1;
l == r时,arr[l] == 5 && arr[l]的键值对中值为0则存在,反之不存在 -
利用离散二分查找,我们可以容易得出坐标x的对应序号
所以也称离散二分查找为大数x到小数序号的映射
存储形式:
1.输入点:
-
每个点有两部分构成:x & c 以pair<int, int> 存储
输入时(x,c)作为一个点,进入离散数轴
询问时(l,0)和(r,0)作为一个点,进入离散数轴
所有的点都无序存放在vector<pair<int, int>>中,等待遍历
2. 离散数轴:
-
离散数轴不局限于一个向量/数组,可以采用多个向量/数组共用一套排序后序号索引
x 数组:x排序后的数组
c 数组:序号对应的x对应c加到c数组中
-
建轴:
输入点时x已经确定,直接写入axis[]
全部点输入完毕 & 排序后,将所有点遍历,以点的x找到索引,将点的c加到arr[索引]中
写作步骤:
1. 映射函数:
- 二分查找到x对应的有序序号
2. 输入点:
- 将(x,c)和(l,0),(r,0)输入到vector<pair<int,int>>中
3. x排序:
- 去重后排序axis[]
- 后来查找x的时候,用映射函数将x映射到axis[]的索引中
4. 前缀和:
- 遍历所有点,将x对应的c加到arr[映射(x)]中
- 为arr[]求前缀和,便于输出区间和
代码实现:
#include <iostream>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 3*100010;
int n, m;
//a[]是离散坐标的c数组侧
int a[N], s[N];
//axis是离散坐标的x数组侧
vector<int> axis;
//输入点 和 查询点 的链表
vector<PII> point, query;
//映射函数
int find(int x){
int l=0, r=alls.size()-1;
while(l < r){
int mid = (l + r)>>1;
if(axis[mid] >= x) r = mid;
else l = mid;
}
//注意点1:为了求前缀和,从1开始计数
return r+1;
}
int main(){
cin >>n >>m;
for(int i=0; i<n; i++){
int x, c;
cin >>x >>c;
point.push_back(x,c);
axis.push_back(x);
}
for(int i=0; i<m; i++){
int l=0, r=0;
cin >>l >>r;
query.push_back((l,r));
//询问点对应的值c是0
axis.push_back(l);
axis.push_back(r);
}
//注意点2:去重后排序
sort(axis.begin(), axis.end());
axis.erase(unique(axis.begin(), axis.end()), axis.end());
for(auto item : point){
//x经过find()变成了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){
//找到l r 在离散数轴上的位置
int l = find(item.first);
int r = find(item.second);
cout<<s[r] - s[l-1]<<endl;
}
return 0;
}
代码误区:
1. 为什么要去重后再排序:
- 重复的原因是同一个x,先后加了很多次c,这些c都加在了一起,作为x对应的一个值
- 不去重,各个x的c都是一部分,倒是不影响l - r的累加和,但是理念上错误
2. 去重函数:
- STL中unique(axis.begin(), axis.end()将axis数组不重复的部分放在前面,重复的部分放在后面,返回第一个重复点的索引
- erase(第一个重复点索引, 最后一个重复点索引);将重复点都擦除
- 手动档去重:
int[N] my_unique(int *arr, int n){
int *ans = new int[N];
ans[0] = arr[0];
int i=1, j=1;
for(i=1; i<n; i++){
if(arr[i] != arr[i-1])
ans[j++] = arr[i];
}
}
3. 索引问题:
- 已知一点(x,c),他的axis[]和arr[]和s[]索引是find(x)
- axis[] arr[] s[] 都从1开始取值,便于前缀和
4. 为什么点和x以vector存储而非向量?
- 虽然点最大到十万,int数组可以开到50万,但是有的OJ中数组连十万也没有
本篇感想:
-
本篇写了3个小时,删了两次,力求将算法描述到最简
如果你看懂了离散化算法,求给孩子一个赞👍吧QAQ
-
离散化本质就两句话:
-
离散化数轴不仅对应一个数组/向量,点有几个性质,则开几个数组/向量
-
所有数组/向量共用一套序号索引,即以点的一个性质进行排序后的序号
-
-
看完本篇博客,恭喜已登 《练气境-中期》=
距离登仙境不远了,加油