算法学习宝典_3_前缀和和差分


1、前缀和

前缀和是指某序列的前n项和,可以把它理解为数学上的数列的前n项和,而差分可以看成前缀和的逆运算。合理的使用前缀和与差分,可以将某些复杂的问题简单化。

加粗样式

2、前缀和算法有什么好处?

先来了解这样一个问题:

输入一个长度为n的整数序列。接下来再输入m个询问,每个询问输入一对l, r。对于每个询问,输出原序列中从第l个数到第r个数的和。

我们很容易想出暴力解法,遍历区间求和。

代码如下:

int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
while(m--)
{
    int l,r;
    int sum=0;
    scanf("%d%d",&l,&r);
    for(int i=l;i<=r;i++)
    { 
        sum+=a[i];
    }
    printf("%d\n",sum);
}

这样的时间复杂度为O(n*m),如果nm的数据量稍微大一点就有可能超时,而我们如果使用前缀和的方法来做的话就能够将时间复杂度降到O(n+m),大大提高了运算效率。

具体做法:

首先做一个预处理,定义一个sum[]数组,sum[i]代表a数组中前i个数的和。

求前缀和运算:

const int N=1e5+10;
int sum[N],a[N]; //sum[i]=a[1]+a[2]+a[3].....a[i];
for(int i=1;i<=n;i++)
{ 
    sum[i]=sum[i-1]+a[i];   
}

然后查询操作:

 scanf("%d%d",&l,&r);
 printf("%d\n", sum[r]-sum[l-1]);

对于每次查询,只需执行sum[r]-sum[l-1] ,时间复杂度为O(1)

原理

sum[r] =a[1]+a[2]+a[3]+a[l-1]+a[l]+a[l+1]......a[r];
sum[l-1]=a[1]+a[2]+a[3]+a[l-1];
sum[r]-sum[l-1]=a[l]+a[l+1]+......+a[r];

图解
在这里插入图片描述

这样,对于每个询问,只需要执行 sum[r]-sum[l-1]。输出原序列中从第l个数到第r个数的和的时间复杂度变成了O(1)

我们把它叫做一维前缀和。

总结:

练习题目:

560. 和为 K 的子数组 ---》 前缀和和UThash 配合
974. 和可被 K 整除的子数组 ---》 前缀和 和 UTahsh + 同余定理
523. 连续的子数组和 ---》 前缀和 和 UTahsh + 同余定理 + prefix

/// 此种解决方法会超出时间限制
int subarraySum1(int* nums, int numsSize, int k){
    int *presum = (int *)malloc(sizeof(int) * (numsSize + 1));
    memset(presum, 0, sizeof(int) * (numsSize + 1));
    presum[0] = 0;
    int res = 0;

    for(int i = 1; i <= numsSize; i++) {
        presum[i] = nums[i - 1] + presum[i - 1];
    }

    for(int i = 0; i < numsSize; i++) {
        for(int j = i + 1; j < numsSize + 1; j++) {
            if(presum[j] - presum[i] == k) {
                res++;
            }
        }
    }
    return res;

}


typedef struct{
    int key;  // key 为前缀和
    int cnt; // 记录 key 有多少个
    UT_hash_handle hh;
}HashTable;

//HashTable *users = NULL; 错误用法
HashTable *users;

int subarraySum(int* nums, int numsSize, int k){
    users = NULL;
    int res = 0;
    //前缀和
    int sum = 0;
    HashTable *tmp = (HashTable *)malloc(sizeof(HashTable));

    // 加 前缀和的第一个节点 
    tmp->key = sum;
    tmp->cnt = 1;
    HASH_ADD_INT(users, key, tmp); 

    for(int i = 0; i < numsSize; i++){
        sum+=nums[i];
        HashTable *tmp = NULL;
        int value  = sum - k;
        HASH_FIND_INT(users, &value, tmp);
        if(tmp != NULL) {
            res += tmp->cnt;
        }

        //增加新的节点
        HASH_FIND_INT(users, &sum, tmp);
        if(tmp != NULL){
            tmp->cnt++;
        } else {
            HashTable *tmp = (struct HashTable *)malloc(sizeof(HashTable));
            tmp->key = sum;
            tmp->cnt = 1;
            HASH_ADD_INT(users, key, tmp);
        }
    }
    return res;
}
//974
typedef struct{
    int key;
    int cnt;
    UT_hash_handle hh;
}HashTable;
// 同余定理
//若两个数a,b除以同一个数m得到的余数相同,则a,b的差一定能被m整除
HashTable *users;

int subarraysDivByK(int* nums, int numsSize, int k){
    users = NULL;
    HashTable *tmp = (HashTable *)malloc(sizeof(HashTable));
    tmp->key = 0;
    tmp->cnt = 1;
    HASH_ADD_INT(users, key, tmp);

    int value = 0;
    int presum = 0;
    int res = 0;

    for(int i = 0; i < numsSize; i++) {
        presum+=nums[i];
        value = (presum % k + k) % k ; // 处理负数 ,同余定理
        HASH_FIND_INT(users, &value, tmp);
        if(tmp != NULL) {
            res += tmp->cnt;
            tmp->cnt++;
        } else {
            tmp = (HashTable *)malloc(sizeof(HashTable));
            tmp->key = value;
            tmp->cnt = 1;
            HASH_ADD_INT(users, key, tmp);
        }
    }
    return res;
}
typedef struct{
    long long key;
    long long preindex;
    UT_hash_handle hh;
}HashTable;

HashTable *g_users;

bool checkSubarraySum(int* nums, int numsSize, int k){
    g_users = NULL;
    
    HashTable *tmp = NULL;
    tmp = (HashTable *)malloc(sizeof(HashTable));

    tmp->key = 0;
    tmp->preindex = -1;

    HASH_ADD_INT(g_users, key, tmp);

    long long presum = 0;
    long long remainder = 0;

    for(int i = 0; i < numsSize; i++) {
        presum += nums[i];
        remainder = (presum %k + k)%k;
        HASH_FIND_INT(g_users, &remainder, tmp);
        if(tmp != NULL) {
            /// 不更新只判断。。。。
            if((i - (tmp->preindex)) >= 2) {
                return true;
            }
        } else {
            tmp = (HashTable *)malloc(sizeof(HashTable));
            tmp->preindex = i;
            tmp->key = remainder;
            HASH_ADD_INT(g_users, key, tmp);
        }
    }
    return false;
}

3、二维前缀和

如果数组变成了二维数组怎么办呢?

先给出问题:

输入一个n行m列的整数矩阵,再输入q个询问,每个询问包含四个整数x1, y1, x2, y2,表示一个子矩阵的左上角坐标和右下角坐标。对于每个询问输出子矩阵中所有数的和。

同一维前缀和一样,我们先来定义一个二维数组s[][], s[i][j]表示二维数组中,左上角(1,1)到右下角( i,j )所包围的矩阵元素的和。接下来推导二维前缀和的公式。

先看一张图:

紫色面积是指(1,1)左上角到(i,j-1)右下角的矩形面积, 绿色面积是指(1,1)左上角到(i-1, j )右下角的矩形面积。每一个颜色的矩形面积都代表了它所包围元素的和。


在这里插入图片描述

从图中我们很容易看出,整个外围蓝色矩形面积s[i][j] = 绿色面积s[i-1][j] + 紫色面积s[i][j-1] - 重复加的红色的面积s[i-1][j-1]+小方块的面积a[i][j];

因此得出二维前缀和预处理公式

s[i] [j] = s[i-1][j] + s[i][j-1 ] + a[i] [j] - s[i-1][ j-1]

接下来回归问题去求以(x1,y1)为左上角和以(x2,y2)为右下角的矩阵的元素的和。

如图:

紫色面积是指 ( 1,1 )左上角到(x1-1,y2)右下角的矩形面积 ,黄色面积是指(1,1)左上角到(x2,y1-1)右下角的矩形面积;

不难推出:
在这里插入图片描述

绿色矩形的面积 = 整个外围面积s[x2, y2] - 黄色面积s[x2, y1 - 1] - 紫色面积s[x1 - 1, y2] + 重复减去的红色面积 s[x1 - 1, y1 - 1]

因此二维前缀和的结论为:

(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
s[x2, y2] - s[x1 - 1, y2] - s[x2, y1 - 1] + s[x1 - 1, y1 - 1]

总结:

练习一道完整题目:
输入一个n行m列的整数矩阵,再输入q个询问,每个询问包含四个整数x1, y1, x2, y2,表示一个子矩阵的左上角坐标和右下角坐标。

对于每个询问输出子矩阵中所有数的和。

输入格式
第一行包含三个整数n,m,q。

接下来n行,每行包含m个整数,表示整数矩阵。

接下来q行,每行包含四个整数x1, y1, x2, y2,表示一组询问。

输出格式
共q行,每行输出一个询问的结果。

数据范围
1≤n,m≤1000,
1≤q≤200000,
1≤x1≤x2≤n,
1≤y1≤y2≤m,
?1000≤矩阵内元素的值≤1000
输入样例:
3 4 3
1 7 2 4
3 6 2 8
2 1 2 3
1 1 2 2
2 1 3 4
1 3 3 4
输出样例:
17
27
21
#include
#include
using namespace std;
const int N=1010;
int a[N][N],s[N][N];
int main()
{
    int n,m,q;
    scanf("%d%d%d",&n,&m,&q);
    for(int i=1;i<=n;i++)
      for(int j=1;j<=m;j++)
       scanf("%d",&a[i][j]);
    for(int i=1;i<=n;i++)
      for(int j=1;j<=m;j++)
        s[i][j]=s[i-1][j]+s[i][j-1]+a[i][j]-s[i-1][j-1];
    while(q--)
    {
        int x1,y1,x2,y2;
        scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
        printf("%d\n",s[x2][y2]-s[x2][y1-1]-s[x1-1][y2]+s[x1-1][y1-1]);
    }
    return 0;
}

4、差分


在这里插入图片描述

5、一维差分

类似于数学中的求导和积分,差分可以看成前缀和的逆运算。

差分数组:

首先给定一个原数组a:a[1], a[2], a[3],,,,,, a[n];

然后我们构造一个数组b : b[1] ,b[2] , b[3],,,,,, b[i];

使得 a[i] = b[1] + b[2 ]+ b[3] +,,,,,, + b[i]

也就是说,a数组是b数组的前缀和数组,反过来我们把b数组叫做a数组的差分数组。换句话说,每一个a[i]都是b数组中从头开始的一段区间和。

考虑如何构造差分b数组?

最为直接的方法

如下:

a[0 ]= 0;

b[1] = a[1] - a[0];

b[2] = a[2] - a[1];

b[3] =a [3] - a[2];

........

b[n] = a[n] - a[n-1];

图示:

我们只要有b数组,通过前缀和运算,就可以在O(n) 的时间内得到a数组 。

知道了差分数组有什么用呢? 别着急,慢慢往下看。

话说有这么一个问题:

给定区间[l ,r ],让我们把a数组中的[ l, r]区间中的每一个数都加上c,即 a[l] + c , a[l+1] + c , a[l+2] + c ,,,,,, a[r] + c;

暴力做法是for循环l到r区间,时间复杂度O(n),如果我们需要对原数组执行m次这样的操作,时间复杂度就会变成O(n*m)。有没有更高效的做法吗? 考虑差分做法,(差分数组派上用场了)。

始终要记得,a数组是b数组的前缀和数组,比如对b数组的b[i]的修改,会影响到a数组中从a[i]及往后的每一个数。

首先让差分b数组中的 b[l] + c ,通过前缀和运算,a数组变成 a[l] + c ,a[l+1] + c,,,,,, a[n] + c;

然后我们打个补丁,b[r+1] - c, 通过前缀和运算,a数组变成 a[r+1] - c,a[r+2] - c,,,,,,,a[n] - c;

为啥还要打个补丁?

我们画个图理解一下这个公式的由来:

在这里插入图片描述

b[l] + c,效果使得a数组中 a[l]及以后的数都加上了c(红色部分),但我们只要求l到r区间加上c, 因此还需要执行 b[r+1] - c,让a数组中a[r+1]及往后的区间再减去c(绿色部分),这样对于a[r] 以后区间的数相当于没有发生改变。

因此我们得出一维差分结论:给a数组中的[ l, r]区间中的每一个数都加上c,只需对差分数组b做 b[l] + = c, b[r+1] - = c。时间复杂度为O(1), 大大提高了效率。

总结:
在这里插入图片描述

题目一:

输入一个长度为n的整数序列。
接下来输入m个操作,每个操作包含三个整数l, r, c,表示将序列中[l, r]之间的每个数加上c。
请你输出进行完所有操作后的序列。
输入格式
第一行包含两个整数n和m。
第二行包含n个整数,表示整数序列。
接下来m行,每行包含三个整数l,r,c,表示一个操作。
输出格式
共一行,包含n个整数,表示最终序列。
数据范围
1≤n,m≤100000,
1≤l≤r≤n,
?1000≤c≤1000,
?1000≤整数序列中元素的值≤1000
输入样例:
6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1
输出样例:
3 4 5 3 4 2


//差分 时间复杂度 o(m)
#include
using namespace std;
const int N=1e5+10;
int a[N],b[N]; 
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) 
    {
        scanf("%d",&a[i]);
        b[i]=a[i]-a[i-1];      //构建差分数组
    }
    int l,r,c;
    while(m--)
    {
        scanf("%d%d%d",&l,&r,&c);
        b[l]+=c;     //表示将序列中[l, r]之间的每个数加上c
        b[r+1]-=c;
    }
    for(int i=1;i<=n;i++) 
    {
        b[i]+=b[i-1];  //求前缀和运算
        printf("%d ",b[i]);
    }
    return 0;
}

2.差分代表题目
1094. 拼车
1109. 航班预订统计

1094

车上最初有 capacity 个空座位。车 只能 向一个方向行驶(也就是说,不允许掉头或改变方向)

给定整数 capacity 和一个数组 trips ,  trip[i] = [numPassengersi, fromi, toi] 表示第 i 次旅行有 numPassengersi 乘客,接他们和放他们的位置分别是 fromi 和 toi 。这些位置是从汽车的初始位置向东的公里数。

当且仅当你可以在所有给定的行程中接送所有乘客时,返回 true,否则请返回 false。


示例 1:

输入:trips = [[2,1,5],[3,3,7]], capacity = 4
输出:false

bool carPooling(int** trips, int tripsSize, int* tripsColSize, int capacity){
    int people[MAX] = { 0 };
    for(int i = 0; i < tripsSize; i++) {
        people[trips[i][1]] += trips[i][0];
        people[trips[i][2]] -= trips[i][0];
    }
    
    int num = 0;
    for(int i = 0; i < MAX; i++) {
        num+=people[i];
        if(num > capacity) {
            return false;
        }
    }
    return true;
}

1109

这里有 n 个航班,它们分别从 1 到 n 进行编号。

有一份航班预订表 bookings ,表中第 i 条预订记录 bookings[i] = [firsti, lasti, seatsi] 意味着在从 firsti 到 lasti (包含 firsti 和 lasti )的 每个航班 上预订了 seatsi 个座位。

请你返回一个长度为 n 的数组 answer,里面的元素是每个航班预定的座位总数。

 

示例 1:

输入:bookings = [[1,2,10],[2,3,20],[2,5,25]], n = 5
输出:[10,55,45,25,25]
解释:
航班编号        1   2   3   4   5
预订记录 110  10
预订记录 220  20
预订记录 325  25  25  25
总座位数:      10  55  45  25  25
因此,answer = [10,55,45,25,25]


int* corpFlightBookings(int** bookings, int bookingsSize, int* bookingsColSize, int n, int* returnSize){
    int *diff = malloc(sizeof(int) * n);
    memset(diff, 0, sizeof(int) * n);

    *returnSize = n;

    // 差分 
    for(int i = 0;i < bookingsSize; i++) {
             // 原始数组[start ... end]全部增加add ==> 差分数组diff[start]增加add,diff[end+1]减少add
        diff[bookings[i][0] - 1] += bookings[i][2];
        if(bookings[i][1] < n) {
            diff[bookings[i][1]] -= bookings[i][2];
        }
    }
    // 差分数组的前缀和数组 == 原始数组更新后的数组 
    for(int i = 1; i < n ;i++) {
        diff[i] += diff[i - 1];
    }
    return diff;
}

参考: 网站:https://blog.csdn.net/weixin_45629285/article/details/111146240

相关