目录
前言
以前学算法挖的坑,终会在一场比赛里给你埋了。
还是要打好基础,学不会的算法,当时你可以先放放,因为可能有的前置算法你没学到,但是后面一定记得回头补,别到了赛场考到了才发现没学23333
含泪补基础。
一、离散化介绍
1.1 什么是离散化
当数据范围在10^6以内时,我们通常开一个数组就能直接映射,如:让值为i存到a[i]位置
当数据范围很大时,如1~10^9、1~10^18次方,但是数据量很少,如10^5个数,此时我们就不能直接映射。
以数据范围10^18,数据量10^5为例。
一是内存不够,不可能开一个a[10^18]的数组。
二是就算能开,遍历一遍时间复杂度也是10^18。
此时我们就需要一个辅助数组,把所有数字紧挨着放到一个数组中,就能压缩到10^5,就能支持后续操作。
通常不会单独考离散化,会和其他知识点一块考,比如离散化+区间合并、离散化+前缀和、离散化+差分 等等。
1.2 常见离散化处理
1.2.1 简单离散化
假如有不重复的数字1、1000、10000、10^9,我们可以直接挨着存到数组a中
数组下标 | 0 | 1 | 2 | 3 |
a[i] | 1 | 1000 | 10000 | 10^9 |
但是如果有重复数字呢?我们一般是不能存重复数字的。当我们想往数组存一个数时,如何判断这个数是否已经存在?或者说 如何快速去重得到一个不重复的序列?
1.2.2 去重离散化
对于判断当前想存的数字是否重复,
简单方法O(n^2):暴力枚举,遍历一遍数组看是否有重复的,一般数据量10^3才能用。
高效方法O(nlogn):这里用c++的函数实现,先把值全存进来,再去掉重复的。
先排序O(nlogn),再去重O(n)
sort(a.begin(),a.end()); //将所有的原位置排序
alls.erase(unique(a.begin(),a.end()),a.end()); //去掉a中重复的位置
具体来说就是先从小到大排序,然后unique(a.begin(),a.end())会把不重复的元素放到数组前面,然后返回不重复元素序列的最后一位,这个元素之后的都是重复数字,直接用erase()函数删去即可
我们就可以得到一个从小到大排列的 无重复的 离散化的数组
但是还有一个问题,我们如何快速找到原来的值离散化后的位置?
1.2.3 离散化后按值查找
现在已经有 有序的离散化的数组a[N],我们想找 值为x 离散化后的数组下标
简单方法O(n^2):暴力枚举,直接遍历数组a寻找值为x的下标,找一个值复杂度为O(n),找n个值的话,时间复杂度来到O(n^2)。数据范围10^3才能用
高效方法O(nlogn):既然数列a已经有序,我们当然可以二分查找,可以高效的找到值为x的数组下标,找n个值的话,时间复杂度就是O(nlogn)
int find(int x)//二分查找,用来寻找原位置对应的离散后的位置
{
int l = 0, r = a.size() - 1;
while(l < r)
{
int mid = l + r >> 1;
if(a[mid] >= x)
r = mid;
else
l = mid + 1;
}
return r + 1; //+1是为了从1开始,即映射到1,2,3到n
}
到这里我们已经学会了基本的离散化的操作。作者去做题了,如果遇到的题目还有什么常见的操作我会继续补充博客。下面是一些例题的练习。
二、例题练习
2.1 离散化 + 区间合并
题目描述
题目链接:P1496 火烧赤壁 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
思路分析
首先给定n个左闭右开的区间,而且是在一维上的,求n个区间的长度和,这里基本就是裸的区间合并,然后我们注意到范围是-10^9 ~ 10^9,我们需要稍微离散化下
这里只是简单用了一下离散化,不需要去重、查找什么的,直接开个pair<int, int>类型的数组存下每个区间即可。
代码模板
#include<iostream>
#include<cstring>
#include<algorithm>
#define x first //左边界
#define y second //右边界
using namespace std;
const int N = 2e4 + 10;
const int INF = 0x3f3f3f3f;
typedef long long ll;
typedef pair<int, int> PII;
int n;
PII a[N]; //定义一个pair类型数组,记录左右边界
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; i++)
scanf("%d %d", &a[i].x, &a[i].y);
sort(a + 1, a + n + 1); //区间合并前要先按左端点从小到大排序
int l = -INF, r = -INF; //初始左右边界
ll res = 0; //记录答案
for(int i = 1; i <= n; i++)
{
if(a[i].x > r) //不相交
{
res += r - l;
l = a[i].x;
r = a[i].y;
}
else
{
r = max(a[i].y, r);
if(i == n) //如果是最后一个了 直接结算
res += r - l;
}
}
printf("%lld", res);
return 0;
}
2.2 离散化 + 前缀和
题目描述
题目链接:802. 区间和 - AcWing题库
思路分析
数据范围是-10^9 ~ 10^9,数据量是10^5,我们可以先把要用到的点(包括操作和查询)存下来,把要加的位置和值一块存下来,然后把位置离散化去重,根据得到的离散化数组,再执行加值操作,然后求一个前缀和即可。
代码模板
#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector>
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
const int N = 3e5 + 10;
int n, m;
int a[N], s[N]; //a记录离散后的位置上的值,s前缀和
vector<int> alls; //记录所有要用到的位置
vector<PII> add, query; //add记录原位置和要加的数,query记录查询的位置区间
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; //+1是为了从1开始,即映射到1,2,3......n
}
int main()
{
scanf("%d %d", &n, &m);
for(int i = 1; i <= n; i++)// 加值操作
{
int x, c;
scanf("%d %d",&x, &c);
add.push_back({x, c}); //将原位置和要加的数加入add
alls.push_back(x); //记下要用到的位置
}
for(int i = 1; i <= m; i++) // 查询操作
{
int l, r;
scanf("%d %d", &l, &r);
alls.push_back(l); //记下要用到的位置
alls.push_back(r);
query.push_back({l, r}); // 记录查询的位置区间
}
sort(alls.begin(), alls.end()); //排序
alls.erase(unique(alls.begin(), alls.end()), alls.end()); //去重
for(auto it : add) // 执行加值操作, add 第一个值是位置,第二个值是加数
{
int x = find(it.x); //寻找原位置离散后的数组下标
a[x] += it.y; //然后加值
}
for(int i = 1; i <= alls.size(); i++) //预处理一下前缀和, alls是去重后的
s[i] = s[i - 1] + a[i];
for(auto it : query) // 处理查询操作
{
int l = find(it.x); //寻找原位置离散后的数组下标
int r = find(it.y);
printf("%d\n", s[r] - s[l - 1]);
}
return 0;
}