【基础算法模板】离散化(整数保序)

作用:

对于一个有序序列 list ,序列个数比较小,元素数值比较大。大多数情况下不能去开一个长度为10^9 的数组 (取决于你的内存,一般开不了这么大),我们可以将这个序列 list 映射到从 0 开始的连续的自然数,也就是映射成该数在数组中的下标

  • a[ ] :a[0] = 1,a[1] = 2,a[2] = 100,a[3] = 2000,a[4] = 30000
  • 映射到下标:1->0,2->1,100->2,2000->3,30000->4

  • 为什么不直接映射到数组下标,要离散化?
  1. 存储的下标太大,普通的电脑开辟不了那么大的数组
  2. 即使可以开辟那么大的数组,但是序列元素本身不多,将导致转存的数组成为一个稀疏数组,导致空间浪费
  3. 假使存在元素为负数,数组的下标是没有负数的,则无法做映射

离散化的两个问题:

  • a[ ] 中可能有重复元素,需要去重
  • 如何算出 x 离散化后的值,因为 a[ ] 是有序的,用二分,把 x 的值映射到下标

模板:

vector<int> alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end());   // 将重复元素换到末尾,返回去重后有效数组的尾端点的后一位,再用erase把尾端点的后一位到结尾全删掉 

// 二分求出x对应的离散化的值
int find(int x) // 找到第一个大于等于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, 2, ...n,不加1的话从0开始映射 
}

测试样例:

该例程比较难懂,难点并不在离散化的过程中,离散化操作很好理解(二分找x的下标,找到后返回它的下标),难点在多个数组的作用的理解上,建议读者用提供好的样例在纸上模拟一下代码的运行过程。

/*
区间和 

题目描述:
假定有一个数轴,数轴上每个坐标上的数都是0
首先进行n次操作,每次操作将某一位置x上的数加c
进行m次询问,每个询问包含两个整数和r,你将要求出区间[l,r] 之间的所有数的和
 
输入格式:
第一行包含两个整数n和m
接下来n行,每行包含两个整数x和c
再接下来m行,每行包含两个整数l和r 

输出格式:
共m行,每行输出一个询问中所求的区间内数字和 

数据范围:
-10^9<=x<=10^9
1<=n,m<=10^5 
-10^9<=l<=r<=10^9
-10000<=c<=10000  

输入样例:
3 3
1 2
3 6
7 5
1 3
4 6
7 8

输出样例:
8
0
5
*/

//如果x的范围比较小的话就用差分,这里x的范围很大,n、m的范围很小,
//n次插入操作,每次插入操作会产生一个坐标。最大10^5 
//对于每个询问有两个坐标(l、r),2m个下标操作,最大2*10^5 
//总共最大(n+2m)个下标操作,所以只会用到3*10^5个坐标,
//整个值域的范围很大,但是序列稀疏,用保序离散化,映射出的值在 1 ~ 3*10^5

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

typedef pair<int, int> PII;	//存操作,每种操作两个数 

const int N = 3000010;

int n, m;
int a[N], s[N];	//a[]是存的数,s[]是前缀和 

vector<int> alls;	//alls是存的所有要离散化的值 
vector<PII> add, query;

//把 x 映射到下标 
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;	//找到alls[]中大于等于x的最小的数 
		else l = mid + 1; 
	} 
	return r + 1;	//从1开始映射 
}



int main(){
	//把所有插入操作读进add 
	cin >> n >> m;
	for(int i = 0; i < n; i ++){
		int x, c;
		cin >> x >> c;
		add.push_back({x, c});	
		 
		alls.push_back(x);		//插入操作会产生一个坐标 
	}
	
	//把所有查询操作读进query 
	for(int i = 0; i < m; i ++){
		int l, r;
		cin >> l >> r;
		query.push_back({l, r});
		
		alls.push_back(l); 		//查询操作会产生两个坐标 
		alls.push_back(r);
	}
	
	// 去重
	sort(alls.begin(),alls.end());
	alls.erase(unique(alls.begin(),alls.end()), alls.end());
	
	// 处理插入
	for(auto item : 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){
		int l = find(item.first), r = find(item.second);
		cout << s[r] - s[l - 1] << endl; 
	}	 
	
	
	return 0;
} 


/*
### 为什么l和r也要映射? ###

因为要求其前缀和;测试用例中的坐标轴4 、 6 ,
其对应的坐标点 4 、 6值未加c,故为0,
而其映射后在a[]中其值也为0;添加l和r才能利用a[]求其区间和, 
不添加则无法获取映射后这段区间所对应在a[]中的坐标


### 大佬的问题回答 ### 

此题第一次看确实没看懂,所以此处略作分析,为什么要离散化呢,
因为存储的下标实在太大了,如果直接开这么大的数组,根本不现实,
第二个原因,本文是数轴,要是采用下标的话,可能存在负值,所以也不能,
所以有人可能会提出用哈希表,哈希表可以吗?答案也是不可以的,
因为哈希表不能像离散化那样缩小数组的空间,导致我们可能需要从-e9遍历到1e9
(此处的含义就是假如我们需要计算1e-9和1e9区间内的值,那我们需要从前到后枚举,无论该值是否存在),
因为哈希表不能排序,所以我们一般不能提前知道哪些数轴上的点存在哪些不存在,
所以一般是从负的最小值到正的最大值都枚举一遍,时间负责度太高,于是就有了本题的离散化。

离散化的本质,是映射,将间隔很大的点,映射到相邻的数组元素中。减少对空间的需求,也减少计算量。
其实映射最大的难点是前后的映射关系,如何能够将不连续的点映射到连续的数组的下标。
此处的解决办法就是开辟额外的数组存放原来的数组下标,或者说下标标志,
本文是原来上的数轴上的非连续点的横坐标。此处的做法是是对原来的数轴下标进行排序,
再去重,为什么要去重呢,因为本题提前考虑了前缀和的思想,其实很简单,
就是我们需要求出的区间内的和的两端断点不一定有元素,提前加如需要求前缀和的两个端点,
有利于我们进行二分搜索,其实二分搜索里面我们一般假定有解的,如果没解的话需要特判,
所以提前加入了这些元素,从而导致可能出现重复元素。

本文你用于存储这个关系的数组是alls[N];特地说明下,为什么要开300000+10呢,
因为我前面说过了提前考虑了前缀和的因素,加上了2*m个点,又因为怕出现数组越界,
多加了10。什么时候会用完300000个空间呢,那就是无重复元素,外加n和m都是1e5次方的打下。

下一步就是写提前数轴点对应的映射后的数组的下标的函数课,此题用的是二分,log(n + 2 * m)

作者:此题有解否
链接:https://www.acwing.com/file_system/file/content/whole/index/content/1783/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

*/

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值