算法基础1.7离散化

前言

离散化我感觉基本思路就是找有效信息然后映射,有那么一点点稀疏数组的感觉,实际上y总当时也说离散化的使用场景是针对于比较稀疏的数组使用

其实思路不是很难,但是实现起来还是有一定难度的,课后练习题的代码长度也是目前最长的吧好像

正文

大致介绍

离散化通常是针对于那些值域很大,比如1e9;但是有效数据不是很多,比如1e5的情况

我们可以把这有效的1e5个数映射到一个新的数组里面,依次对应从0开始的自然数,所以目前(我所学的)是针对于整数的离散化。

这次我们从题目分析开始,来了解离散化的意义。

image-20230312155841751

初见这道题,看到他是对数组中几个数进行加法操作,最后求一个区间内所有数的和,其实最容易想到的就是前缀和。实际上这道题确实要使用前缀和,但是由于数组过大(虽然给了一个很大的数据范围,但是题目还专门用“无限长”来再次强调这个数组有多么长),所以单纯使用数组绝对会爆的。

而这里面的思想就和我们的稀疏数组很像了,由于所有位置的初始值都是0,我们只对部分位置的值进行了修改,所以实际上绝大部分位置是没有记录的必要性的。我们只需要记录下那些修改的位置,在最后处理的时候对于那些没有变更的位置做统一处理即可。


数组处理思路:

  1. 我们把所有有效位置的位置值记录下来放到alls数组中
  2. 先把alls排序、去重,然后使用一个方法把alls数组中所有的数都映射为一个自然数,对应于a中的第x位,这个位置中保存的值为这个有效位置里面的数
  3. 我们对a数组使用前缀和算法,得出他的前缀和数组s
  4. 那么假如我们查询l...r区间数的和,映射到a数组就是L...R(用大写代表对应的映射值)区间的数的和,结果就是s数组的s[R] - s[L - 1]

信息保存思路:

那么我们需要记录两种操作产生的有效位置:

  • 加法操作:毫无疑问,加法操作让某一个位置变得与众不同,我们需要把他单独记录下来。同时,这个位置增加了多少也是一个关键的信息值得记录。
    • 记录:变动位置 + 增加的值
  • 查询操作:在“数组处理思路”的第四步里我们发现,最后其实是需要映射查询的两个端点位置的,所以这查询功能中输入的两个数我们也需要存到alls中,在后面对其进行映射
    • 记录:左端点的位置 + 右端点的位置

在实际保存时,我们使用pair这个数据结构,他的每一个单位里面可以存放两个数据,分别通过.first.second读取


由预备离散化数组转前缀和数组

我们的预备离散化数组为alls,里面保存了那些特殊的位置

我们的离散化映射数组为a,是对应于alls中所有值的存在

我们的前缀和数组为s,是对a进行求前缀和操作的结果

alls中存在两个问题我们需要处理

  • 无序:我们在映射的时候应该是一个单调映射,也就是最小的数映射值为0,然后依次映射1,2,3.....
    • 处理方法:直接使用sort函数
  • 有重复的可能:很有可能我们对同一个位置进行了多次加法操作,那么这个位置就会重复存放进alls中,或者查询端点重复等
    • 处理方法:alls.erase(unique(alls.begin(), alls.end()), alls.end())
      • unique(alls.begin(), alls.end())可以将alls中所有重复的数都移动到数组后面,将重复数的第一次出现的数保留下来。返回值为处理后多余的那一串数组的首位置m。比如对应一个长度为x的数组进行处理,返回了m,那么就说明现在这个数组0...m-1中是没有重复的数的,重复多余的那些都在m...x-1
      • erase(m , alls.end())我们书接上回unique返回了m,现在我们就通过erase把那多余的一段m....x-1删除。最后数组就只有不重复的那一串了

然后运用自定义函数find遍历alls,给所有位置找一个映射值

最后对a做前缀和处理得出数组s


离散化处理

前排提醒:由于后面要使用前缀和,而前缀和需要把第0位空出来,所以这里我们的映射是从1开始的

其实从alls映射到a很简单,已经排好序去完重了,那么allsx位上的位置值m就能直接对应a的第x位。也就是原来无限长数组的位置m映射为x

这里我们使用了二分算法函数find,其实为了能快速找到输入值malls数组中的第几位,因为查找的时候并不会告诉你。

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;
        else l = mid + 1;
    }
    return r + 1;
}

数组开多大

这个我们可以从题目中的得知

image-20230312171559173

nm的范围都是1e5

n是加法,一步加法的两个信息中有一个是位置

m是查询,一步查询的两个信息都是位置

所以最多(n,m拉满,而且他们产生的有效位置无一重复)为n + 2m也就是300000,我们为了防止数组越界再加个10,最后就是300010

代码

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

using namespace std;

// 给pair起别名
typedef pair<int, int> PII;

const int N = 300010;

int n, m;
int a[N], s[N];

vector<int> alls;
vector<PII> add, query;

// 找出alls中第一个大于等于x的数的位置
// 由于去重了,所以其实就是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;
        else l = mid + 1;
    }
    return r + 1;
}

int main()
{
    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);
    }

    // 获取查询操作的信息
    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;
}

结语

这个算法我多少有点模糊,我总感觉allsa属于并行,他们组合起来作为了原来无限长数组的映射,映射里面包括位置值和数值。

离散化里面其实只映射了位置值

我那个思想好像是往哈希表那边想了?但我没学过,等学过了再看看,我总感觉起码对于这道题,我认为我的那个思路也挺好,刚好可以用pair把并行的两个数组合并了,因为alls映射到a的方法太简单了

posted @ 2023-03-18 10:29  Zaughter  阅读(28)  评论(0编辑  收藏  举报