Map和离散化

1,236 阅读3分钟

离散

这节课我们将学习离散化和一些应用。那么在将离散化的方法之前,先给大家看两张图。

image.png 第一张图是一个函数图像,第二张图是一个点阵。那么它们呈现一种怎样的特点呢?

第一张图我们可以看到一条曲线,那么曲线上的某个点 (x,y)(x,y) 代表着y=arccot(x)y=arccot(x),在图像上所表示的区间里面是 连续的

然后再看看第二张图。图的点就不一样了,我们可以看到它们是独立地出现在了整个圆里面的某个位置,而且还有颜色,在图中是 不连续

离散化

离散的数据是混乱无序的,没有规律的,我们很难去直接对这些数据进行整体的操作。而且它们的范围可能很大,我们甚至连存储它们都会变得非常困难,因为我们需要一个很大的容器去分辨这些数据。

所以我们在处理这些规模和范围极其庞大的数据的时候,需要对数据进行 离散化 的操作。离散化的基本思想,其实就是在范围庞大的数据当中,去选择对题目的解决有意义的那么一部分,将它们重新编号,然后再进行处理。

第K个素数

看一下这个题目: 给定一个数字 nn ,有 qq个询问, 每次输出在 [0,n][0,n]范围内第kk小的素数

输入格式

第一行包括两个正整数 n,qn,q,分别表示查询的范围和查询的个数。

接下来 q 行,每行一个正整数 kk,表示查询第 k 小的素数。

n106,1q106n≤10^6,1≤q≤10^6,保证查询的素数不大于 nn

输出格式

输出q 行,每行一个正整数表示答案。

解答

最朴素的做法,就是遍历2n2-n,找到第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个元素. 那么我们可不可以构建一个素数数组, 通过数组,将素数们按照下标逐一排列.如下:

prime[]={2,3,5,7,11,13,17,19,....}prime[] = \{2,3,5,7,11,13,17,19,....\}

此时,当询问第k个素数时,只需要直接打印prime[k-1]即可.这一道题, 是在讲解预处理的思维的时候出现了. 我们预处理了is_prime数组,判断一个数是素数(埃式筛选法),再预处理了prime数组, 使得每次询问都可以直接O(1)O(1)的时间复杂度直接得出答案. 但实际这里构建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...}

}

房间

比如说有这样一个题目:

有 nn 个人,第 ii 个人会在第 aia_i 分钟开始的时候进入房间,在第bib_i 分钟结束的时候离开房间,房间里面人最多的时候会有多少个人?

1n1000,1aibi10181≤n≤1000,1≤a_i≤b_i≤10^{18}。

最简单粗暴的方式,我们开一个数组numnumnumjnum_j 表示第 j 分钟房间里面有多少人。然后对于第 ii 个人,将numnum 的 第 aia_i 到 bib_i个元素都加上 1,最后进行统计和计算。

for (int j = a[i]; j <= b[i]; j++) {
    num[j]++; 
}

1aibi10181≤a_i≤b_i≤10^{18},数据范围太大了,我们甚至存储不了每分钟房间里面有多少人这个关键信息。

但是我们再来思考一个问题,某一分钟开始的时候,只有在有人员进出的时候,房间的人数才会 产生变化。那么,对于每一个人而言,他只会进出房间各一次。所以,人员进出这个事件的出现次数,最多为 2×n2×n。换句话来讲,也就是所有的 aia_i 和 bi+1b_i+1(在 bi+1b_i+1 时出房间),不同的数最多有2×n2×n 个。 那么我们就可以这么做:将所有的 aia_i 和 bi+1b_i+1 组成一个按照元素从小到大排列成的集合 S={S1,S2,,St}S=\{S_1,S_2,⋯,S_t\}。后续的处理,我们用i 代替 SiSi,代表着在第 ii 个时间点发生了 人员增加 或者 人员减少 的事件。这里我们可以构造一组样例:

iiaia_ibib_ibi+1b_i+1
1134
2489
37910
4367
可以根据 aia_i 和 bi+1b_i+1 构造出集合 S=1,3,4,7,9,10S={1,3,4,7,9,10}
原数 aia_i1347910
新数 ii123456

我们将每个 SiS_i 替换成 ii

iiaia_ibi+1b_i+1
113
235
346
424

,可以得到下面这张表:

时间点(ii)/人员编号1234numinum_i
000000
110001
210012
301012
401102
500101
600000

我们可以在所有的 numnum 里面找到最大值为 2,代表房间内最多同时有 2 人。

小结

有些数据本身很大,自身无法作为数组的下标保存对应的属性。如果这时只是需要这些数据的相对属性,那么可以对其进行离散化处理。当数据只与它们之间的相对大小有关,而与具体是多少无关时,可以进行离散化。

离散化并不是一个单独的算法,一般而言,这个算法都是对数据的预处理,用于缩小数据的范围,然后使用其他的算法去处理问题。

map和离散化

回到刚才的问题。在离散化的后续的处理中,我们用 ii 代替 SiSi,代表着在第i 个时间点发生了 人员增加 或者 人员减少 的事件。

实际上我们可以将 i 代替SiSi 当作我们之前学习过的一种操作——映射。我们可以建立一个映射表,将 SiSi 映射为ii

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]];

至于逆映射,SS 本身就是映射表 TT 的逆映射。

但是需要说明的是,使用 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;

a[i]b[i]+1a[i] 和 b[i]+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 数组。

练习题

小X的质数 P1496 火烧赤壁