什么是离散化?
离散化是一种常用的程序设计技巧,它可以有效地降低算法的时间复杂度。离散化的基本思想是在众多可能的情况中,只考虑需要用的值。具体来说,离散化就是把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。在不改变数据相对大小的条件下,对数据进行相应的缩小。
例如,当我们只关心数据的大小关系时,可以用排名代替原数据进行处理。这在原数据很大或含有负数、小数时,难以表示为数组下标,一些算法和数据结构(如BIT)无法运作,这时我们就可以考虑将其离散化。
举个例子:
我们有这样的一些数字:
1 3 15 19 25 44 3000000 99999999
虽然数字很少,但是如果我们想要去做一些标记处理的话,通常就需要开辟一个至少有99999999大小的数组了,但是在竞赛中,这样的算法通常过不了题目要求(不是超时就是超出内存),因此我们需要将他们离散化处理成为这样的数:
1 2 3 4 5 6 7 8
我们只去关心他们之间的相对大小而不是绝对大小,这样处理之后,对于空间的利用率大大提升。
离散化可以改进一个低效的算法,甚至实现根本不可能实现的算法。它在保持原序列大小关系的前提下把其映射成正整数。这样就可以极大地缩小空间复杂度和时间复杂度,且不改变原来的属性。即原来比你大,离散化后仍然比你大,只不过差距变小了而已。这就是离散化的基本概念和优势。
离散化怎么实现?
离散化的实现总体来说需要三个步骤:
1.排序
2.去重
3.偏移量转换
其中,最核心的步骤是第三步,这一步的作用就是将数值转换为离散化之后的值,而前两步都是为第三步做准备的。
接下来结合代码注释进行讲解:
#include <bits/stdc++.h>
using namespace std;
int a[200000+10];
int main() {
int n ;
cin >> n;
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]); //将要离散化的数据读入一个数组
}
sort(a + 1, a + n + 1); //进行排序
int len = unique(a + 1, a + n + 1) - (a + 1);//去重
for (int i = 1; i <= len; i++) { //输出离散化之前和离散化之后的数据
cout << a[i] << ' ';
a[i] = lower_bound(a + 1, a + len + 1, a[i]) - a;
cout << a[i] << '\n';
}
return 0;
}
其中,lower_bound 函数返回的是一个迭代器,指向第一个大于等于 n 的元素的位置。
unique函数并非是将数组中的函数全部去重了,而是将其重复的函数放在了最后面,若我们不去访问他们,就实现了伪去重。
因此,若要实现真正的去重可以这这样做:
a.erase(unique(a.begin(), a.end()), a.end());
当然,lower_bound函数也可以自己去实现:
int find(int x)
{
int l = 0, r = a.size() - 1;
while (l < r)
{
int mid = l + r >> 1; //运用了位处理技巧,本质上就是除以2
if(alls[mid] >= x) r = mid; //二分查找
else l = mid + 1;
}
return r + 1;
}
补充
在竞赛中,离散化往往是优化而不是解决问题的一种方式,例如,如何实现离散化之前和离散化之后的值的对应?这通常需要开辟多个数组来实现,我们以一道竞赛题来讲解:
样例输入:
10 5 4
2 4
3 5
4 6
5 7
1 10
4 6 3 5
样例输出:
4
3
3
4
代码实现:
#include <bits/stdc++.h>
#define PII pair<int, int>
#define MAX_N 300005
using namespace std;
int a[MAX_N] = {0};
int s[MAX_N] = {0};
vector<PII> add;
vector<int> alls, query;
void inputadd(int m) {
for (int i = 1, x, y; i <= m; i++) {
scanf("%d %d", &x, &y);
alls.push_back(x);
alls.push_back(y);
add.push_back({x, y});
}
return ;
}
void inputque(int q) {
for (int i = 1, t; i <= q; i++) {
scanf("%d", &t);
query.push_back(t);
alls.push_back(t);
}
return ;
}
void soruqe() {
sort(alls.begin(), alls.end());
unique(alls.begin(), alls.end());
return ;
}
int nfind(int n) {
return lower_bound(alls.begin(), alls.end(), n) - alls.begin();
}
void a_set() {
for (auto it : add) {
a[nfind(it.first)] += 1;
a[nfind(it.second) + 1] -= 1;
}
return ;
}
void s_set() {
s[0] = a[0];
for (int i = 1; i <= alls.size(); i++) {
s[i] = s[i - 1] + a[i];
}
}
int main() {
int n, m ,q;
cin >> n >> m >> q;
inputadd(m);
inputque(q);
soruqe();
a_set();
s_set();
for (auto it : query) {
printf("%d\n", s[nfind(it)]);
}
return 0;
}
本题利用前缀和和离散化的知识,没有学过前缀和处理方式的需要先去补课喽。
解释一下其中这些数组和容器的作用:
alls容器:记录所有值,包括要查询的草地值和释放E技能的值,排序和去重之后用作离散化求对应关系
query容器:记录要查询的值
add容器:记录E技能释放范围的值
a数组:存放的是E技能释放之后,离散值对应的范围(举个例子,例如25这个值,通过nfind(25)之后得到25离散化之后,在a中的下标,假设是2,那么a[2] += 1,表示从这开始往后是E技能释放的范围,直到遇到一个减一之后停下来)
s数组:表示a数组的前缀和。