题目描述
此题第一次看确实没看懂,所以此处略作分析,为什么要离散化呢,因为存储的下标实在太大了,如果直接开这么大的数组,根本不现实,第二个原因,本文是数轴,要是采用下标的话,可能存在负值,所以也不能,所以有人可能会提出用哈希表,哈希表可以吗?答案也是不可以的,因为哈希表不能像离散化那样缩小数组的空间,导致我们可能需要从-e9遍历到1e9(此处的含义就是假如我们需要计算1e-9和1e9区间内的值,那我们需要从前到后枚举,无论该值是否存在),因为哈希表不能排序,所以我们一般不能提前知道哪些数轴上的点存在哪些不存在,所以一般是从负的最小值到正的最大值都枚举一遍,时间负责度太高,于是就有了本题的离散化。
离散化的本质,是映射,将间隔很大的点,映射到相邻的数组元素中。减少对空间的需求,也减少计算量。
其实映射最大的难点是前后的映射关系,如何能够将不连续的点映射到连续的数组的下标。此处的解决办法就是开辟额外的数组存放原来的数组下标,或者说下标标志,本文是原来上的数轴上的非连续点的横坐标。
此处的做法是是对原来的数轴下标进行排序,再去重,为什么要去重呢,因为本题提前考虑了前缀和的思想,其实很简单,就是我们需要求出的区间内的和的两端断点不一定有元素,提前加如需要求前缀和的两个端点,有利于我们进行二分搜索,其实二分搜索里面我们一般假定有解的,如果没解的话需要特判,所以提前加入了这些元素,从而导致可能出现重复元素。
本文你用于存储这个关系的数组是alls[N];特地说明下,为什么要开300000+10呢,因为我前面说过了提前考虑了前缀和的因素,加上了2*m个点,又因为怕出现数组越界,多加了10。什么时候会用完300000个空间呢,那就是无重复元素,外加n和m都是1e5次方的打下。
下一步就是写提前数轴点对应的映射后的数组的下标的函数课,此题用的是二分,log(n + 2 * m)
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;
}
为什么返回r + 1,这是变相的让映射后的数组从1开始。此处描述映射后的数组下标对应的数值用的是a数组。
剩下的就是已经讲过的了,前缀后算法,本题的难点是理清楚这个映射关系。
算法1
(离散化) $O((n + 2 * m) log(n + 2 * m))$
C++ 代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int N = 300010;
int a[N], s[N];
int n, m;
vector<int> alls;
vector<PII> add, query;
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;
}
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])
a[j ++ ] = a[i];
return a.begin() + j;
}
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), 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;
}
https://blog.csdn.net/weixin_49486457/article/details/122770861?spm=1001.2014.3001.5502
赞啊
# Orz
大佬请教一下为什么l和r也要映射?
因为要求其前缀和;测试用例中的坐标轴4 、 6 ,其对应的坐标点 4 、 6值未加c,故为0,而其映射后在a[]中其值也为0;添加l和r才能利用a[]求其区间和, 不添加则无法获取映射后这段区间所对应在a[]中的坐标。
orz
简单的说就是多映射几个下标进去(反正本来就有10的9次方那么多)。为什么这么做?因为后面要求要有两边的坐标前缀和才能正确计算,反正加进去坐标但没给坐标对应的值加数,所以结果是不变的。
我对于作者的代码提出下面几点建议;
1、第一个是可以用map来进行映射,这样还不用去重
2、第二点就是可以直接调用stl数据库的算法来实现vector容器的去重(如果用了map就没必要去重了)
用 map 虽然可以去重,但是是无序的。map 的离散化用于无序映射。但是我们最后开始查询的时候,每个查询 $[l , r]$ 之间是有序的。所以用 map最后还是药回归到排序上面来,一样的。
map用的是红黑树,有序
我写了下大概明白他的意思了,map是的l r x有序,但是,你查找那个x的时候如果要找到映射的位置的话,那就是n的时间复杂度了,题解二分是log2n
可以用set去储存下标,既可以排序又可以去重,而后用map去映射,这样就不需要二分查找,例: set : 3 4 6 7,map[3] = 1,map[4] = 2…,然后对于一次插入{x,c},对应的数组为:a[map[x]] += c
想知道为什么要去重呢
因为原数组中可能会有重复元素
那有重复元素的影响是什么?
alls里面的元素相当于坐标轴上的坐标,每个坐标点都是唯一的,所以要去重
我觉得去重是为了离散化的更加彻底,就是说新开的代替数组更紧凑,如果有重复元素的话,那么二分的下标就会不紧凑,不去重应该也一样能过
不去重一样可以过不过麻烦一点要写两次二分,一次查找最先出现位置,一次查找最后出现的位置
唯一为什么还要去重呢
为了方便二分 方便了二分就方便了离散化
并不需要查找两次呀....第一次查找到最左边的那个位置,所有的操作都对这个位置的进行,后面的数值上全为0,在求前缀和上没有任何影响,直接删去去重也是可以ac的只是可能只是适用于这道题吧。
大佬们能不能解释下为什么求前缀和时是i<=alls.size(),为啥是alls.size()
个人理解alls数组在排序完后,其中的x有可能分布于alls数组的各个地方,比如极端情况下会存在在最后一个位置,所以构建前缀和数组时需要遍历整个alls数组。
真难
有大佬能解释下为什么要把l,r也加入alls数组吗,一直想不明白
因为要求前缀和啊
为了使二分一定能找到结果
因为之前加c的点离散化后可能不能包含需要查询的(l,r),比方说,a[1]=1,a[2]=2,当你要查(3,4)的时候就查不到数据
很棒
谢谢大佬
感谢大佬
为什么能用二分来找映射的下标为啊
因为数组是有序的啊,而且二分的时间复杂度比较低
不用二分O(nm)直接炸时间
return a.begin() + j;这一句是什么意思啊?
这个函数目的是去重,a.begin()+j返回的是去重后vector数组有实际意义的最后一个元素的地址,找到这个地址目的是方便后续删除。
其实在c++的stl数据库里面已经有vector容器的去重方法了,作者这是手写了
能不能用set去重排序呀
同问
set不能使用下标去遍历,需要使用迭代器,vector可以使用下标来遍历比较方便
set容器访问下标的复杂度是o(n)
可以 但是二分set需要用迭代器
用那个find函数是不是因为x不是按照升序输入的,所以要用find函数来找位置
是升序排列的,但是用二分查找可以降低时间复杂度,更快找到x对应的映射位置
我觉得可以把数轴上的点都加上1e9然后再用哈希就行了
为啥我用哈西过不了
为啥还要自己实现unique呀,可以直接用吧?
你说的有问题,离散化的本质其实还是哈希。
只不过由于存在负数、小数、分数这些难以映射到数组下标的时候,得想个办法构造哈希表,这题选取的办法是使用有序数组的次序来重构哈希表
问个问题哦,映射为什么要用二分呢?
时间复杂度更低
那我也问个问题,映射用hash存起来,为啥不采用这种方式?
这题映射的应该是是下标吧