离散
这节课我们将学习离散化和一些应用。那么在将离散化的方法之前,先给大家看两张图。
第一张图是一个函数图像,第二张图是一个点阵。那么它们呈现一种怎样的特点呢?
第一张图我们可以看到一条曲线,那么曲线上的某个点 代表着,在图像上所表示的区间里面是 连续的。
然后再看看第二张图。图的点就不一样了,我们可以看到它们是独立地出现在了整个圆里面的某个位置,而且还有颜色,在图中是 不连续。
离散化
离散的数据是混乱无序的,没有规律的,我们很难去直接对这些数据进行整体的操作。而且它们的范围可能很大,我们甚至连存储它们都会变得非常困难,因为我们需要一个很大的容器去分辨这些数据。
所以我们在处理这些规模和范围极其庞大的数据的时候,需要对数据进行 离散化 的操作。离散化的基本思想,其实就是在范围庞大的数据当中,去选择对题目的解决有意义的那么一部分,将它们重新编号,然后再进行处理。
第K个素数
看一下这个题目: 给定一个数字 ,有 个询问, 每次输出在 范围内第小的素数
输入格式
第一行包括两个正整数 ,分别表示查询的范围和查询的个数。
接下来 行,每行一个正整数 ,表示查询第 小的素数。
,保证查询的素数不大于 。
输出格式
输出行,每行一个正整数表示答案。
解答
最朴素的做法,就是遍历,找到第k个素数,核心代码如下
while(q--){
int k;
scanf("%d",&k);
int cnt = 0;
for(int i = 2; i <= n;i ++){
if(prime(i)){//判定一个数是不是素数
cnt ++;
if(cnt == k){
printf("%d\n",i);
break;
}
}
}
}
优化
prime(i)代码可以通过埃氏筛选法预处理,所有的数
int is_prime[1000006];
void aishi(){
for(int i = 2; i* i <= N; i ++){
if(is_prime[i] == 0){
for(int j = i * i ; j <= N; j += i){
is_prime[j] = 1;
}
}
}
}
这样 只需要 is_prime[i] == 0即代表i是一个素数
但即便是这样, 我们也在做很多重复的事情, 例如当k=4的时候会先判断2 3 5三个素数,然后得到第4个素数是7,当第2次询问k = 5的时候,再次判断2,3,5,7 得到第5个素数 11;
两次询问中我们分别都求了2 3 5,在后续多次询问中更是无数次的重复这样的动作.
之所以我们要要这样做,是因为素数并不是均匀分布的.是一种离散的状态,我们无法通过一个等效的间距,直接得出素数,如同数组直接访问下标,便能得到数组的第k个元素. 那么我们可不可以构建一个素数数组, 通过数组,将素数们按照下标逐一排列.如下:
此时,当询问第k个素数时,只需要直接打印prime[k-1]即可.这一道题, 是在讲解预处理的思维的时候出现了.
我们预处理了is_prime数组,判断一个数是素数(埃式筛选法),再预处理了prime数组, 使得每次询问都可以直接的时间复杂度直接得出答案.
但实际这里构建prime数组,将所有的素数在自然数列从离散状态, 映射到数组中有序的状态.
代码如下:
代码
#include <iostream>
using namespace std;
int is_prime[1000006];
int prime[1000006];
int N = 1000000;
void aishi(){
for(int i = 2; i* i <= N; i ++){
if(is_prime[i] == 0){
for(int j = i * i ; j <= N; j += i){
is_prime[j] = 1;
}
}
}
}
int main(){
aishi();//预处理
int m = 0;//prime数组的元素个数
for(int i = 2; i <= N; i ++){
if(is_prime[i] == 0){
prime[m] = i;
m++;
}
}
int n , q;
cin >> n >> q;
while(q--){
int x;
cin >> x;
cout << prime[x- 1] << endl;
}
//构建一个素数数组 prime[] = {2,3,5,7,11,13,17,19...}
}
房间
比如说有这样一个题目:
有 个人,第 个人会在第 分钟开始的时候进入房间,在第 分钟结束的时候离开房间,房间里面人最多的时候会有多少个人?
最简单粗暴的方式,我们开一个数组, 表示第 分钟房间里面有多少人。然后对于第 个人,将 的 第 到 个元素都加上 1,最后进行统计和计算。
for (int j = a[i]; j <= b[i]; j++) {
num[j]++;
}
,数据范围太大了,我们甚至存储不了每分钟房间里面有多少人这个关键信息。
但是我们再来思考一个问题,某一分钟开始的时候,只有在有人员进出的时候,房间的人数才会 产生变化。那么,对于每一个人而言,他只会进出房间各一次。所以,人员进出这个事件的出现次数,最多为 。换句话来讲,也就是所有的 和 (在 时出房间),不同的数最多有 个。 那么我们就可以这么做:将所有的 和 组成一个按照元素从小到大排列成的集合 。后续的处理,我们用代替 ,代表着在第 个时间点发生了 人员增加 或者 人员减少 的事件。这里我们可以构造一组样例:
| 1 | 1 | 3 | 4 |
| 2 | 4 | 8 | 9 |
| 3 | 7 | 9 | 10 |
| 4 | 3 | 6 | 7 |
| 可以根据 和 构造出集合 。 |
| 原数 | 1 | 3 | 4 | 7 | 9 | 10 |
|---|---|---|---|---|---|---|
| 新数 | 1 | 2 | 3 | 4 | 5 | 6 |
我们将每个 替换成 :
| 1 | 1 | 3 |
| 2 | 3 | 5 |
| 3 | 4 | 6 |
| 4 | 2 | 4 |
,可以得到下面这张表:
| 时间点()/人员编号 | 1 | 2 | 3 | 4 | |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 1 | 0 | 0 | 0 | 1 |
| 2 | 1 | 0 | 0 | 1 | 2 |
| 3 | 0 | 1 | 0 | 1 | 2 |
| 4 | 0 | 1 | 1 | 0 | 2 |
| 5 | 0 | 0 | 1 | 0 | 1 |
| 6 | 0 | 0 | 0 | 0 | 0 |
我们可以在所有的 里面找到最大值为 2,代表房间内最多同时有 2 人。
小结
有些数据本身很大,自身无法作为数组的下标保存对应的属性。如果这时只是需要这些数据的相对属性,那么可以对其进行离散化处理。当数据只与它们之间的相对大小有关,而与具体是多少无关时,可以进行离散化。
离散化并不是一个单独的算法,一般而言,这个算法都是对数据的预处理,用于缩小数据的范围,然后使用其他的算法去处理问题。
map和离散化
回到刚才的问题。在离散化的后续的处理中,我们用 代替 ,代表着在第个时间点发生了 人员增加 或者 人员减少 的事件。
实际上我们可以将 代替 当作我们之前学习过的一种操作——映射。我们可以建立一个映射表,将 映射为。
map<int, int> T;
// ... 将 S[i] 存入 map 中
// 进行离散化映射
int i = 1;
for (auto it = T.begin(); it != T.end(); it++) {
it->second = i++;
}
// 离散化结果
int news = T[s[i]];
至于逆映射, 本身就是映射表 的逆映射。
但是需要说明的是,使用 map 进行离散化,更多的是创建 同类型,大范围 的数据的映射以 便于保存。
代码解析
#include <iostream>
#include <map>
using namespace std;
const int maxn = 1010;
int n, a[maxn], b[maxn], num[2 * maxn];
map < int, int > T;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i] >> b[i];
T[a[i]] = 1;
T[b[i] + 1] = 1;
}
int i = 1;
for(auto it = T.begin(); it != T.end(); it++){
it->second = i ++;
}
int maxx = 0;
for (i = 1; i <= n; i++) {
for (int j = T[a[i]]; j < T[b[i]+1]; j++) {
num[j]++;
if (num[j] > maxx) {
maxx = num[j];
}
}
}
cout << maxx << endl;
return 0;
}
解析代码
T[a[i]] = 1;
T[b[i]+1] = 1;
将放入容器map中.
int i = 1;
for(auto it = T.begin(); it != T.end(); it++){
it->second = i ++;
}
map 内部是有序的,可以遍历 map,修改映射值。
int maxx = 0;
for (i = 1; i <= n; i++) {
for (int j = T[a[i]]; j < T[b[i]+1]; j++) {
num[j]++;
if (num[j] > maxx) {
maxx = num[j];
}
}
}
cout << maxx << endl;
再次枚举每个人进入和离开房间的时间,并维护 num 数组。