【算法技巧/第十三届蓝桥杯C++C组】4655.重新排序——使用一维差分法巧妙地对数组中不同区间的元素进行处理

我的个人主页
欢迎各位→点赞 + 收藏⭐️ + 留言​
总结:希望你看完之后,能对你有所帮助,不足请指正!共同学习交流
✉️今天你做别人不想做的事,明天你就能做别人做不到的事♐

题目:

给定一个数组 A 和一些查询 Li,Ri,求数组中第 Li 至第 Ri 个元素之和。

小蓝觉得这个问题很无聊,于是他想重新排列一下数组,使得最终每个查询结果的和尽可能地大。

小蓝想知道相比原数组,所有查询结果的总和最多可以增加多少?

输入格式

输入第一行包含一个整数 n。

第二行包含 n 个整数 A1,A2,⋅⋅⋅,An,相邻两个整数之间用一个空格分隔。

第三行包含一个整数 m 表示查询的数目。

接下来 m 行,每行包含两个整数 Li、Ri,相邻两个整数之间用一个空格分隔。

输出格式

输出一行包含一个整数表示答案。

数据范围

对于 30% 的评测用例,n,m≤50;
对于 50% 的评测用例,n,m≤500;
对于 70% 的评测用例,n,m≤5000;
对于所有评测用例,1≤n,m≤105,1≤Ai≤106,1≤Li≤Ri≤n。

输入样例:

5
1 2 3 4 5
2
1 3
2 5

输出样例:

4

样例解释
原来的和为 6+14=20,重新排列为 (1,4,5,2,3) 后和为 10+14=24,增加了 4。

代码

// 给出的样例看作可以先找出 1 ~ 3 和 2 ~ 5的交集,
//然后让 4 5 两个较大的数放在了 2 3 的位置上
//所以解题思路可以先猜想为:找出区间之间的交集,并记录重复次数,重复次数最多的位置放最大的值

// 那么我们该怎么统计这些交集?大家可以看下代码以及后面的总结

#include 
#include 
#include 

using namespace std; 
const int N = 1e5 + 10;

// long long 不用解释了吧,这题不很明显 可能会爆 int 嘛。
long long arr[N];
long long cnt[N]; // 一维差分,恢复后就是每个位置 出现的次数。
int main()
{
    int n; cin >> n;
    for(int i = 1; i <= n; ++i){
        scanf("%d", &arr[i]);
    }
    int m; cin >> m;
    while (m -- ){
        int l, r;
        scanf("%d%d", &l, &r);
        cnt[l]++;
        cnt[r + 1]--;
    }
    for(int i = 1; i <= n; ++i){
        cnt[i] += cnt[i - 1]; // 恢复成 统计次数的数组
    }
    // 后来看了其它人的题解,又发现 总和
    // 其实就是把所有询问的区间内出现过的 
    // 【数字 * 出现次数】 相加。。。(确实是等价的,你可以自己证明!还挺好证的。。)
    long long sumA = 0;
    for(int i = 1; i <= n; ++i){
        sumA += cnt[i] * arr[i]; // 这样确实也是总和。。我太笨了,居然没发现。
    }
    // 欸,可别小看了上述这个发现。它有大用。
    // 既然我们说 最后的和,可以用 所有提的范围内出现过的 数字 * 出现次数 相加!
    // 那么 你重新排序后,是不是 也仍然成立?当然成立!
    // 只不过,此时变成了 【较大的数 * 较大的次数】
    // 欸?这不就符合题意了嘛??题意就是让总和 尽可量的大呀。

    /* 重新认识下,开头说的 一一对应:看懂了吗?

       为什么说 不需要费劲 再去建立数组了?

       因为较大的值 4 5 已经与 较大的次数 2 2 正好对应了。
       所以两个 数组全部排序,再遍历一遍,进行求和就完事了啊~ 真就是太妙了,跪拜大佬 (●ˇ∀ˇ●)
        arr[]: 1 2 3 4 5
        cnt[]: 1 1 1 2 2
    */
    long long sumB = 0;
    sort(arr + 1, arr + n + 1); sort(cnt + 1, cnt + n + 1);
    for(int i = 1; i <= n; ++i){
        sumB += cnt[i] * arr[i];
    }

    cout << sumB - sumA << endl;

    return 0;
}

模板

b[l] += c;
b[r + 1] -= c;

l和r分别为要记录的数组的区间的左右界限

应用

1、记录元素的的出现次数

给定一个数组 A 和一些查询 Li,Ri,求数组中第 Li 至第 Ri 个元素之和。

输入格式

输入第一行包含一个整数 n。

第二行包含 n 个整数 A1,A2,⋅⋅⋅,An,相邻两个整数之间用一个空格分隔。

第三行包含一个整数 m 表示查询的数目。

接下来 m 行,每行包含两个整数 Li、Ri,相邻两个整数之间用一个空格分隔。

输入样例:

5
1 2 3 4 5
2
1 3
2 5

如何计算出每个位置在所有询问区间中出现的次数呢?其实可以每次询问的时候给相应的位置 pos + 1 即可。但是这样是暴力遍历,可能得 O(n^2),太慢了。这样我们可以采用一维差分法,

#include 
#include 
#include 
using namespace std; 
int main() {
    int m=2,n=5;
    int cnt[5]={0}; //初始化计数数组
    int arr[5] = {1, 2, 3, 4, 5};
    while (m -- ){
        int l, r;
        scanf("%d%d", &l, &r);
        cnt[l]++;
        cnt[r + 1]--;
    }
    for(int i = 1; i <= n; ++i){
        cnt[i] += cnt[i - 1]; // 恢复成 统计次数的数组
    }
    for(int i = 0; i < n; ++i){
        cout<<cnt[i]; // 恢复成 统计次数的数组
    }
    return 0;
}

在这里插入图片描述

总结

通过这种方法,不仅能计数,而且在很多对于数组的多个不同区间的处理的问题上都可以应用。

例子

你可能感兴趣的:(算法,蓝桥杯,算法,数据结构)