一、离散化
1. 离散化简介
离散化,把无限空间中有限的个体映射到有限的空间中去,以此提高算法的时空效率。通俗的说,离散化是在不改变数据相对大小的条件下,对数据进行相应的缩小。
离散化本质上可以看成是一种哈希,其保证数据在哈希以后仍然保持原来的全/偏序关系。
当有些数据因为本身很大或者类型不支持,自身无法作为数组的下标来方便地处理,而影响最终结果的只有元素之间的相对大小关系时,我们可以将原来的数据按照从大到小编号来处理问题,即离散化。
本文针对 整数、有序数组 进行离散化。
2.为什么要进行离散化处理?
- 离散化是程序设计中一个常用的技巧,它可以有效的降低时间复杂度。
- 其基本思想就是在众多可能的情况中,只考虑需要用的值。离散化可以改进一个低效的算法,甚至实现根本不可能实现的算法。
3. 举例说明
- 有一个数组 a[] ,包含 1、3、100、2000、50000,五个数,将其对应映射到 0、1、2、3、4,的过程就叫做离散化。
4. 离散化过程中的问题
- 数组 a[] 当中可能存在重复的元素,对此,我们需要 去重。
- 如何算出 a[n] 离散化后的值,对此,我们可以使用 二分法 。具体见前文 整数二分
二、离散化例题——区间和
题目描述
假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。
现在,我们首先进行 n 次操作,每次操作将某一位置 x 上的数加 c。
接下来,进行 m 次询问,每个询问包含两个整数 l 和 r,你需要求出在区间 [l,r] 之间的所有数的和。
输入格式
第一行包含两个整数 n 和 m。
接下来 n 行,每行包含两个整数 x 和 c。
再接下来 m 行,每行包含两个整数 l 和 r。
输出格式
共 m 行,每行输出一个询问中所求的区间内数字和。
数据范围
−1e9 ≤ x ≤ 1e9
1 ≤ n,m ≤ 1e5
−1e9 ≤ l ≤ r ≤ 1e9
−10000 ≤ c ≤ 10000
输入样例
3 3
1 2
3 6
7 5
1 3
4 6
7 8
输出样例
8
0
5
具体实现
1. 实现思路
- 首先进行样例模拟:
a[0] | a[1] | a[2] | a[3] | a[4] | a[5] | a[6] | a[7] | a[8] |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
- 在下标为 1 的位置加上 2 ,在下标为 3 的位置加上 6 ,在下标为 7 的位置加上5。即为:
a[0] | a[1] | a[2] | a[3] | a[4] | a[5] | a[6] | a[7] | a[8] |
0 | 2 | 0 | 6 | 0 | 0 | 0 | 5 | 0 |
- 3次询问,[1,3] 之间所有数字的和为 8,[4,6] 之间所有数字的和为 0,[7,8] 之间所有数字的和为 5。
- 通过以上步骤,我们可以发现,当我们的数据范围比较小时,便可以使用前面介绍过的 前缀和算法 进行解决。前缀和算法
2. 代码注解
大部分直接在实现代码当中直接进行批注。
unqiue() 函数 是 STL 当中比较常用的函数,功能是元素去重。”删除”序列中所有相邻的重复元素(只保留一个)。此处的删除,并不是真的删除,而是指重复元素的位置被不重复的元素给占领了(详细情况,下面会讲)。由于它”删除”的是相邻的重复元素,所以在使用unique函数之前,一般都会将目标序列进行排序。
- unqiue() 函数 函数指向的是去重后容器中不重复序列的最后一个元素的下一个元素。
- unqiue() 函数 的底层原理
vector<int>::iterator unique(vector<int> &a) { int j = 0; for (int i = 0; i < a.size(); ++i) { if (!i || a[i] != a[i - 1])//如果是第一个元素或者该元素不等于前一个元素,即不重复元素,我们就把它存到数组前j个元素中 { a[j] = a[i];//每存在一个不同元素,j++ j++; } } return a.begin() + j;//返回的是前j个不重复元素的下标 }
- for (auto item : add) 的循环条件就是add的元素个数大于0。其中 item 是C++11的新特性 range_for 可以理解为遍历容器add中的所有元素,等价于:
//支持随机访问的容器 如vector for (int i = 0; i < add.size(); ++i) { auto item = add[i]; int x = find(item.first); a[x] += item.second; } //不支持随机访问的容器 如list set for (auto it = add.begin(); it != add.end(); ++it) { auto item = *it; int x = find(item.first); a[x] += item.second; }
3. 实现代码
#include <bits/stdc++.h> using namespace std; typedef pair<int, int> PII; const int N = 300010; int n, m; int a[N], s[N]; vector<int> alls; //存入下标容器 vector<PII> add, query; //add增加容器,存入对应下标和增加的值的大小 //query存入需要计算下标区间和的容器 int find(int x) { int l = 0, r = alls.size() - 1; while (l < r) //查找大于等于x的最小的值的下标 { int mid = l + r >> 1; if (alls[mid] >= x) { r = mid; } else { l = mid + 1; } } return r + 1; //因为使用前缀和,其下标要+1可以不考虑边界问题 } int main() { cin >> n >> m; for (int i = 0; i < n; i ++ ) { int x, c; cin >> x >> c; add.push_back({x, c}); //存入下标即对应的数值c alls.push_back(x); //存入数组下标x=add.first } 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); //将add容器的add.secend值存入数组a[]当中, a[x] += item.second; //在去重之后的下标集合alls内寻找对应的下标并添加数值 } // 预处理前缀和 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); //在下标容器中查找对应的左右两端[l~r]下标, //然后通过下标得到前缀和相减再得到区间a[l~r]的和 cout << s[r] - s[l - 1] << endl; } system("pause"); return 0; }