离散化及其写法
离散化及其写法
离散化是算法竞赛中常常要用到的一种技巧,经常会出现在一些数据结构的题目中,和一些数据结构结合起来。试想,假如你现在看到了一道线段树的裸题,在你极其兴奋的同时发现数据范围是1-1e10的,开4倍的线段树根本开不下。于是你极其懊恼地只拿了部分分,可能还会因为心情不好而出锅爆零(逃)...
所以这里来讲一下离散化的原理及实现方式(严肃)
PS:对于离散化零概念同学,建议从头翻到尾,如果只是来复习离散化的写法,请直接到文尾。
离散化的概念
首先,什么是离散化?
在鄙人的理解中,离散化就是比相对大小。
为了本篇题解的正规性和学术性,先来贴一波标准定义:(滑稽)
离散化,把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。
通俗的说,离散化是在不改变数据相对大小的条件下,对数据进行相应的缩小。
看不懂就看下面的例子:
原数:131021 546412 973324
离散化后数据:1 2 3
够形象了吧......
离散化的适用范围
试想,当你要对一个长度为\(10^{10}\)的序列进行处理,你是否会开一个如此之大的数组?
除非你想MLE,或者,你不会离散化。
在一些题目和算法中,我们会发现,我们实现我们的想法的时候,只跟原数据的相对大小有关,比如1000000 和 2000000,在实际实现的时候,和1 2的效果是完全一样的。那么,开2000000那么大的空间纯属浪费。那么我们就用离散化给它映射到一个较小的区间中。
通俗地来讲,当有些数据本身很大, 自身无法作为数组的下标保存对应的属性。如果这时只是需要这堆数据的相对属性, 那么可以对其进行离散化处理。当数据只与它们之间的相对大小有关,而与具体是多少无关时,可以进行离散化。
离散化的写法
离散化的实现比较简单。我们只需要维护两个事情不变:首先:保证离散化之后的数据尽可能地小而且非负。其次:离散后的数据要保持原本的大小关系,原本相等的也要保持相等,否则就是错误的离散。
因此,找出原数据在序列中的序位就是离散化的关键。
我们在正常实现离散化的时候,有两种方法:
结构体实现:
代码:
struct node
{
int x,id;
}a[maxn];
int rank[maxn],n;
-----------------------------
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i].x);
a[i]=id=i;
}
sort(a+1,a+n+1);//从小到大
for(int i=1;i<=n;i++)
rank[num[i].id]=i;//映射
解释:
这实际上就是一个结构体模拟映射的过程。
一开始输入了原数据,并且按次序保存了id,也就是原数列的位置。
然后进行排序,易知映射后的数据范围就是1-n。所以排序后的位置就放上我们当前的i即可。
但是,这个方式有一个弊端,就是不能处理数据相等的情况。如果碰到数据相等,那么离散化之后就变成了不等。
数组实现
代码:
int a[maxn],b[maxn];
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
b[i]=a[i];
}
sort(b+1,b+n+1,cmp);//cmp函数是自定义比较从小到大或从大到小的
int size=unique(b+1,b+n+1)-(b+1);
for(int i=1;i<=n;i++)
a[i]=lower_bound(b+1,b+size+1,a[i])-b;
解释:
数组实现离散化的码量很少,减少了编程的复杂度。但是稍稍难理解一些(可能对读者理解造成主要困难的就是不明白unique和lower-bound函数)。输入a[i]之后紧接着保存同样的b[i]作为副本。然后对副本b进行去重,并保存b数组去重后的长度(size)。
然后开始离散化,直接把对应元素转换成相应的数组下标即可。
对unique和lower_bound函数不明白的同学请看下面:
-
unique函数的功能是对一个数组进行去重,并返回去重之后的数组最后一个元素的下一个位置。这个细节很重要,因为C++的内置函数对区间所采用的所有操作都是左闭右开型的。所以这些函数会返回我们想象中“最后一个元素”的下一个位置,所以我们要对这个返回值进行加减。
-
根据指针的减法,unique函数已经返回了这个位置(我们可以把它理解成下标),那么,我们只需要把这个下标再减去首位置,就是这个序列的长度。
-
lower_bound函数会传入三个参数,返回第一、第二个参数所表示的区间中第一个大于等于第三个元素的位置。这个区间应该是事先排好序的。但是这个返回值并不是目标的下一个位置,所以在转换成数组下标的时候,我们只需要减去数组的第0个元素即可。
针对上面那段代码,已经被排好序的b数组的下标就是我们要映射到的东西,我们就得到了一个经过离散化之后的,去重的a数组。
补充(UPD:2020.3.16):也有一种写法,是每次需要用a[i]的离散后数值时才查找;个人不推荐这么写,因为当询问次数很多的时候会大量消耗时间,而像上文那样直接处理好的写法,每次只需要O(1)的时间就可以完成查询。
STL map实现(UPD:2020.3.16)
STL为我们提供了一个非常好的,可以处理映射问题的模板:map。
关于map容器,有不懂的小伙伴可以去翻:C++STL——map
模板:
#include<map>
map<int,int> mapp;
int a[maxn];
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
sort(a+1,a+n+1,cmp);
int cnt=0;
for(int i=1;i<=n;i++)
{
if(a[i]!=a[i-1])
map[a[i]]=++cnt;
else
map[a[i]]=cnt;
}
思想是一样的,应用了一个cnt变量来维护去重,调用的时候可以直接用键值,非常舒服方便。