离散化:既把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率
初步认识一下:
举个例子:现在又几个数{4,5,6,20,7},对其进行离散化得到的结果为{1,2,3,5,4},这既是离散化。
其适用这种场景,我们只需要知道两个数字的相对大小,而不需要知道两个数字的具体大小差值,就可以对其进行离散化
离散化的方法
方法1:直接上代码
public int[] dispersed(int[] nums) {
int[] cloneNums = nums.clone();
Arrays.sort(cloneNums);
// distinct:去重,返回去重后的数组的边界
int bound = distinct(cloneNums);
for (int i = 0; i < nums.length; i++) {
// lowerBound从cloneNums数组中,再0到bound-1这个区间,找到大于nums[i]的第一个数字的下标,没有大于的返回长度
nums[i] = lowerBound(cloneNums, 0, bound - 1, nums[i]);
}
return nums;
}
代码解释:
首先对数组进行排序,再进行去重,得到一个边界bound,这个bound就是离散之后的范围。
举例: { 6, 43, 9, 5, 6, 7, 4, 23, 43, 123 }
排序后:{4, 5, 6, 6, 7, 9, 23, 43, 43, 123}
去重后:{4, 5, 6, 7, 9, 23, 43, 123}
最后得到离散化的序列为:{3, 7, 5, 2, 3, 4, 1, 6, 7, 8}
其实很明显可以看出,离散化后的数据,只是包含原数组中不同数字之间的相对大小关系,而不能得到原来的差值关系
方法2:
方法2只是在效率上要稍微好一点,但是其不能处理重复元素,我们基本上都使用第一种方法,所以其实你也可以选择跳过这里!!
上代码
public int[] dispersed(int[] nums) {
Node[] nodes = new Node[nums.length];
for (int i = 0; i < nums.length; i++) {
nodes[i] = new Node(nums[i], i);
}
Arrays.sort(nodes);
for (int i = 0; i < nums.length; i++) {
nums[nodes[i].index] = i+1;
}
return nums;
}
class Node implements Comparable<Node> {
// 当前的一个数
int data;
// 记录当前数原本的下标位置
int index;
public Node(int data, int index) {
super();
this.data = data;
this.index = index;
}
@Override
public int compareTo(Node o) {
if (this.data > o.data) {
return 1;
} else if (this.data < o.data) {
return -1;
}
return 0;
}
}
利用一个类来保存数字在数组中原来的位置,然后根据数据大小进行排序,然后再根据其新位置给其赋离散后的值
注意:不能有重复元素
例题: POJ-2528:http://poj.org/problem?id=2528
题意:有一面墙,墙很宽很宽,有10000000这么长,现在打算往墙上贴上海报,给出海报所占位置的区间,问最终在这个墙上能看到多少种不同的海报,因为不同的海报之前可能会相互覆盖。
非常明显这就是一个线段树的题目,只不过需要注意的是,因为墙的宽度很宽,我们不可能直接开那么大一个数组,然后来进行区间的覆盖,这时候就需要利用到上面的离散化了,将给出的区间进行离散化。为什么可以进行离散化,因为我们不需要去关心两个海报之间的间隔有多大,或者一个海报到底有多宽,因为题目中最终要求的是只需要知道能看到多少海报,也就是说,我们只需要知道海报区间之间的覆盖问题就可以了,而离散化之后的区间就能保留下这些信息,而且极大的缩小的数组空间的要求。
将区间进行离散化与将点集进行离散化没有任何的区别,因为区间进行离散化的时候也会保留相对的大小关系,如果一个区间离散化之前是包含另外一个区间的,那么离散化之后他也是包含另外一个区间的,这就是离散化的魅力。
然后就是线段树,这个题在用线段树的时候,如果当前区间没有海报,将这个状态记录下来,且说明了其子区间也没有海报。然后当前区间是否是只有一种海报,如果是,就记录下当前海报的种类。如果当前区间不止有一种海报,那么当前区间的信息就失效了。然后需要知道当前区间有多少种海报就需要到当前区间的子区间去查找。
代码实现:代码中的离散化代码与上面介绍的不一样,但是思想是一样的,都是先记录数组中每一个数的原来的位置,然后将该数组进行排序,然后将数组中当前数原来的位置按照排序之后的大小顺序赋值就可以了。
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#define MAX 20100
typedef struct{
// 记录当前node的值,和当前node的原下标
int x,p;
}Node;
typedef struct{
// v的值,当v小于0的时候,说明当前段的颜色不止一个,那么当前段失效
// 要想获取当前段有多少种不同的海报,继续往下进行寻找
// v等于0,说明当前没有海报
// v大于0,即当前段能看到的海报的颜色
int l,r,v;
}Tree;
Tree tree[MAX<<2];
// 标识某一个海报是否能被看到
int v[MAX/2];
Node sge[MAX];
Node t[MAX];
// 标识某个点的离散化之后的值
int z[MAX];
int cmp(const void* a,const void* b){
return ((Node*)a)->x - ((Node*)b)->x;
}
void build(int l,int r,int index){
if(l == r){
tree[index].l = l;
tree[index].r = l;
tree[index].v = 0;
return;
}
int mid = (l + r)/2;
build(l,mid,index*2);
build(mid+1,r,index*2+1);
tree[index].v = 0;
tree[index].l = l;
tree[index].r = r;
}
void opera(int l,int r,int index,int k){
if(tree[index].l == l && tree[index].r == r){
tree[index].v = k;
return ;
}
int li = index*2;
int ri = index*2+1;
// 相当于延迟更新
if(tree[index].v>0){
tree[li].v = tree[ri].v = tree[index].v;
}
int mid = (tree[index].l + tree[index].r)/2;
if(r <= mid){
opera(l,r,li,k);
}else if(l > mid){
opera(l,r,ri,k);
}else{
opera(l,mid,li,k);
opera(mid+1,r,ri,k);
}
// 判断左右两个子段海报是否一样,不一样,当前段海报失效
if(tree[li].v == tree[ri].v && tree[li].v>0){
tree[index].v = tree[li].v;
}else{
tree[index].v = -1;
}
}
void view(int l,int r,int index){
if(tree[index].v >= 0 ){
v[tree[index].v] = 1;
return;
}
int li = index*2,ri = index*2+1;
int mid = (tree[index].l + tree[index].r)/2;
if(r <= mid){
view(l,r,li);
}else if(mid < l){
view(l,r,ri);
}else{
view(l,mid,li);
view(mid+1,r,ri);
}
}
int main(){
int count;
scanf("%d",&count);
for(;count>0;count--){
int n;
scanf("%d",&n);
for(int i=0;i<2*n;i++){
scanf("%d",&sge[i].x);
sge[i].p = i;
}
for(int i=0;i<n*2;i++){
t[i] = sge[i];
}
qsort(t,n*2,sizeof(Node),cmp);
// 开始位置的离散化的值为1
z[0] = 1;
// 求出每个位置离散化的值
for(int i=1;i<2*n;i++){
// 当前的值和前一个相同,离散化的值直接沿用
if(t[i].x == t[i-1].x){
z[i] = z[i-1];
}else{
// 区间之间的差距大于1的话,说明区间之间可能会包含其他区间边界
// 要预留出位置来
z[i] = z[i-1] + 1;
}
}
int max = z[2*n-1];
build(1,max,1);
for(int i=0;i<2*n;i++){
sge[t[i].p].x = z[i];
}
for(int i=0;i<n;i++){
opera(sge[i*2].x,sge[i*2+1].x,1,i+1);
}
int ans = 0;
memset(v,0,sizeof(v));
view(1,max,1);
for(int i=1;i<=n;i++){
if(v[i]){
ans++;
}
}
printf("%d\n",ans);
}
}
最后在补充一个简单的东西吧,lowerBound函数的实现,虽然很简单,但是感觉还是非常有意思的。
这个函数就存在几种情况
1.要找的数在给定的数组里面,这时候直接二分找到这个数的下标然后加1就可以了
2.要找的数不在给定的数组里面,但是比他大的数在数组中,这时候二分的时候,s的值会刚好留在比他大的数的位置上
3.要找的数不在给定的数组中,且比他大的数也不再数组中,那么s的值就会超出数组的范围
思想就是二分,尝试去查找这个数,如果找不到的话那么s的值会刚刚好留在比要找的这个数更大的一个位置
int lower_bound(int *f,int s,int e,int find){
for(;s <= e;){
int mid = s + (e - s)/2;
if(f[mid] < find){
s = mid + 1;
}else if(f[mid] > find){
e = mid - 1;
}else{
return mid + 1;
}
}
return s;
}