• 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;
    }
    
  • 相关阅读:
    单向链表的创建、输出、插入、删除
    linux文件管理指令
    二叉树的创建与遍历(递归)
    小工具
    排序
    Project Euler Problem (1~10)
    福大软工 · 最终作业
    福大软工 · 第十二次作业
    Beta冲刺 7
    Beta冲刺 6
  • 原文地址:https://www.cnblogs.com/littlehb/p/15241960.html
Copyright © 2020-2023  润新知