AcWing 802. 区间和

题目传送门

一、理解题意

假定有一个无限长的数轴,数轴上每个坐标上的数都是 \(0\)

现在,我们首先进行 \(n\) 次操作,每次操作将某一位置 \(x\) 上的数加 \(c\)

接下来,进行 \(m\) 次询问,每个询问包含两个整数 \(l\)\(r\),你需要求出在区间 \([l,r]\) 之间的所有数的和

特殊说明一下,\(alls\)数组下标是从\(0\)开始的,而\(a\)数组下标是从\(1\)开始的,\(alls\)数组与\(a\)数组是一一对应的,但是错位对应的,即\(alls[0]->a[1],alls[1]->a[2],....\)

二、解题思路

  1. 为什么要离散化
    因为空间上不允许开那么大数据范围的数组!本题\(−{10}^9≤x≤10^9\),,两边都加上就是\(2*10^9\),\(c++\)开不到\(3e7\)以上,再大就空间超限了。

  2. 什么是离散化
    离散化:范围大,但比较稀疏,真正有数的不多,很多地方是空着的(空着的是0)。利用这一特点,把有用的位置“映射”到一个长度可控的范围上。

  3. 数组大小的确定
    既然要映射到一块小的、能装的下的区域内,那么这个区域最大是多大呢?
    因为原始数据坐标共\(n\)个,而查询时坐标是\([l,r]\),一次两个,一共\(m\)次,需要把所有的坐标全部放到\(a\)数组中,上限是:\(n+2*m\),依题意,就是\(3*1e5\)

  4. 离散化模板

//排序+去重
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());

5.关键代码解析

  • 变量的含义
    vector<int> alls 下标的集合,不管是原来录入的哪个下标增加多少值,还是后面的查询某个区间内(左右两个下标),都统统记录到这里。它是最终小映射区域的超集。通过排序+去重,就成为了最终的数据范围。
    vector<PII> add 哪个下标增加多少值。
    vector<PII> query 求从哪到哪中间的数字和

  • 映射关系的建立(二分思路)
    原来一个下标,现在在小的范围内下标是什么呢?怎么能找到现在新的下标呢?
    其实,缩小空间后,不变的是数据的顺序,我们只要根据原来下标中的数据值,在有序的新数组中通过二分法查找,就可以找出新的位置,这就是二分法搜索的原理。

  • 映射关系的建立(Hash表思路)
    既然\(alls\)数组是排序并去重的,那么它肯定是有序的。未来的\(a\)数组,也是按这个序来的,只不过值是“增加的数字”。我们只要记录好 值->绝对位置 的映射关系,就可以作到不用每次都通过二分去搜索知道新位置,直接就能以\(O(1)\)时间复杂度返回绝对位置下标。

三、离散化+二分法

#include <bits/stdc++.h>

using namespace std;
typedef pair<int, int> PII;

const int N = 300010;//上限,n+2*m+10
int a[N];   //映射后的数组
int s[N];   //a数组的前缀和数组

vector<int> alls; //坐标集合
vector<PII> add;  //1、哪个位置,2:加上多少
vector<PII> query;//1、开始点,2:结束点

int n, m;

/**
 * 功能:利用二分法查找原索引x的映射后索引值
 * 核心思路:原来是第几个是不变的,离散化后还是第几个,建立起了映射关系。
 * @param x 原来的索引号
 * @return 映射后小数组内的索引号
 */
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;
    }
    //一定会命中,返回r和l是一样的
    return r;
}

int main() {
    //加快输入
    ios::sync_with_stdio(false);
    //输入数据
    cin >> n >> m;

    //录入原始数据
    while (n--) {
        int x, c;
        cin >> x >> c;
        //记录位置与值
        add.push_back({x, c});
        //将位置记录到alls里
        alls.push_back(x);
    }

    //读入查询范围
    int l, r;
    while (m--) {
        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());

    //生成a数组,每次都在映射后的坐标位置将值变大c
    for (int i = 0; i < add.size(); i++) {
        PII item = add[i];
        //find返回的是下标从0开始的位置,这里为了使用前缀和,要求a数据下标从1开始,有1个偏移量
        int x = find(item.first) + 1;//映射为小范围的索引值
        a[x] += item.second;
    }

    //预处理前缀和
    for (int i = 1; i < N; i++) s[i] = s[i - 1] + a[i];

    //处理询问(前缀和应用)
    for (int i = 0; i < query.size(); i++) {
        PII item = query[i];
        //根据原来的位置值,计算出映射后的位置值
        l = find(item.first) + 1;
        r = find(item.second) + 1;
        //利用一维前缀和计算区间和
        cout << s[r] - s[l - 1] << endl;
    }
    return 0;
}
原文地址:https://www.cnblogs.com/littlehb/p/15241960.html