前缀和与差分算法

简介: 前缀和与差分算法

一维前缀和

先看一个例子,假如我们现在有一个数组

arr[]={
    3,2,5,6,7,8,9,4,2}

现在假如我们要想的得到区间 [ 3 , 6 ],上的数据和,那我们就需要遍历 [ 3 , 6 ] 这个区间进行求和。
代码如下:

#include 
int main()
{
    
    int arr[] = {
     3,2,5,6,7,8,9,4,2 };
    int sum = 0;
    int l, r;
    scanf("%d%d", &l, &r);//获取区间
    for (int i = l; i <= r; i++)
    {
    
        sum += arr[i];
    }
    printf("%d", sum);
    return 0;
}

假如我们需要多次询问区间和时

#include 
int main()
{
    
    int arr[] = {
     3,2,5,6,7,8,9,4,2 };
    int n = 0;
    scanf("%d", &n); //询问次数
    for (int j = 0; j < n; j++)
    {
    
        int l, r;
        int sum = 0;
        scanf("%d%d", &l, &r);//获取区间
        for (int i = l; i <= r; i++)
        {
    
            sum += arr[i];
        }
        printf("%d ", sum);
    }
    return 0;
}

它的时间复杂度将会是O(m*n),当数组很长,询问次数很多时,这样的求和方法效率就非常低下。
为了改进这种算法,就引入了前缀和

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

前缀和公式

sum[i]=arr[i]          (i==0)
sum[i]=sum[i-1]+arr[i]  (i>0)

这个和高中的数列前n项和是差不多的
对于数组

arr[]={
    3,2,5,6,7,8,9,4,2}

我们可以根据公式求出他的前缀和数组

sum[]={
    3,5,10,16,23,31,40,44,46}

当我们要询问区间 [ l , r ] 上的数字和时

sum1=sum[r]-sum[l-1];

其实原理很好理解,比如我们我要求 [3 , 6 ] 上的区间和
sum[6]=arr[0]+arr[1]+arr[2]+arr[3]+arr[4]+arr[5]+arr[6]
sum[2]=arr[0]+arr[1]+arr[2];
sum[6]-sum[2]=arr[3]+arr[4]+arr[5]+arr[6]
如下图所示·
在这里插入图片描述
代码如下:

#include 
#include
int main()
{
    
    int arr[] = {
     3,2,5,6,7,8,9,4,2 };
    int m = sizeof(arr) / sizeof(arr[0]); //求元素个数
    int* sum = (int*)malloc(sizeof(int) * m);//开辟前缀和数组
    if (sum == NULL)  //判断数组是否开辟成功
        return 0;
    for (int i = 0; i < m; i++)
    {
    
        sum[i] = (i == 0) ? arr[i] : arr[i] + sum[i - 1];
    }
    int n = 0;
    scanf("%d", &n); //询问次数
    for (int j = 0; j < n; j++)
    {
    
        int l, r;
        scanf("%d%d", &l, &r);//获取区间
        printf("%d ", sum[r]-sum[l-1]);
    }
    return 0;
}

通过求前缀和,我们可以把原本O(m*n)的时间复杂度化为O(n)

一维差分

还是刚刚的数组

arr[]={
    3,2,5,6,7,8,9,4,2}

假如我们要对区间 [ 3 , 6 ] 上的数据都加上 m ,我们就需要遍历这个区间。当我们需要多次进行如上操作时,
时间复杂度也将会成为O(m*n)
代码如下:

#include 
int main()
{
    
    int arr[] = {
     3,2,5,6,7,8,9,4,2 };
    int n = 0;
    scanf("%d", &n);//获取操作次数
    for (int i = 0; i < n; i++)
    {
    
        int l, r;
        scanf("%d%d", &l, &r);//获取操作区间
        int p = 0;
        scanf("%d", &p);//获取要加的数字大小
        for (int j = l; j <= r; j++)
        {
    
            arr[i] += p;
        }
    }
    return 0;
}

当数组元素多,操作次数多时,这样的代码也是十低效的。
这样我们就需要引出差分了
差分数组

sub[i]=arr[i]-arr[i-1]   (i!=0)
sub[i]=arr[i]            (i==0)

对上面的数组求差分可以得到

sub[]={
    3,-1,3,1,1,1,1,-5,-2}

差分数组的性质
1.对差分数组进行前缀和,就可以的到原数组
分析:

原数组:  a1       a2        a3       a4       a5
差分数组:a1      a2-a1     a3-a2    a4-a3    a5-a4
前缀和:  a1       a2        a3       a4       a5

2.要对数组区间 [ l, r ] 的数据都加上m时,我们只需要对差分数组的

sub[l]+=m;
sub[r+1]-=m;

即可,所有操作做完后,做前缀和求出原数组即可
注意事项:如果r+1可以超过原数组大小,所以我们可以在开辟sub数组时多开辟一块空间,也可以对超过原数组的情况不做任何处理(不求差分),也是不会影响结果的。
代码如下:

#include 
#include
int main()
{
    
    int arr[] = {
     3,2,5,6,7,8,9,4,2 };
    int m = sizeof(arr) / sizeof(arr[0]);//求数组大小
    int* sub=(int*)malloc(sizeof(int) * (m+1));
    if (sub == 0)
        return 0;//判断是否开辟成功
    for (int i = 0; i <= m; i++)
    {
    
        sub[i] = (i == 0) ? arr[i] : arr[i] - arr[i - 1];
    }
    int n = 0;
    scanf("%d", &n);//获取操作次数
    for (int i = 0; i < n; i++)
    {
    
        int l, r;
        scanf("%d%d", &l, &r);
        int p = 0;     //
        scanf("%d", &p);//获取要加的数
        sub[l] += p;
        sub[r + 1] -= p;
    }
    //恢复数组
    for (int i = 0; i < m; i++)
    {
    
        arr[i] = (i == 0) ? sub[i] : sub[i] + arr[i - 1];
    }

    return 0;
}

二维前缀和

上面我们讨论了一维数组,但二维数组我们需要怎样处理呢?
如下数组:

    int arr[][4] = { {1 ,2, 3, 4},
                     {5, 6, 7, 8},
                     {9, 10, 11,12},
                     {13,14,15, 16} };

如下图:
在这里插入图片描述
如过我们要求(1,1)到 (2,2)的数组和
也就是
在这里插入图片描述

也就是上图中红色框中和
当我们要多次询问的话,按普通算法来算的化时间复杂度是O(m*n)
代码如下

#include 
#include
int main()
{
    
    int arr[][4] = {
     {
    1 ,2, 3, 4},
                     {
    5, 6, 7, 8},
                     {
    9, 10, 11,12},
                     {
    13,14,15, 16} };
    int n = 0;
    scanf("%d", &n);//获取操作次数
    for (int i = 0; i < n; i++)
    {
    
        int x1, y1, x2, y2;
        scanf("%d%d",& x1, &y1);
        scanf("%d%d",& x2, &y2);
        int sum = 0;
        for (int p = x1; p <= x2; p++)
        {
    
            for (int q = y1; q <= y2; q++)
            {
    
                sum += arr[p][q];
            }
        }
        printf("%d ", sum);
    }


    return 0;

这种算法是很低效的,为了提高效率,我们可以先求出二位前缀和数组

sum[i][j]是从(0,0)到(i,j)的和

具体方法如下:
在这里插入图片描述
如有要求

sum[2][2]

就可以

sum[2][2]=sum[1][2]+sum[2][1]+arr[2][2]-sum[1][1];

结合图还是很好理解的
总结如下:
当 i!=0,j!=0;时

sum[i][j]=sum[i-1][j]+sum[i][j-1]+arr[i][j]-sum[i-1][j-1];

当i==0,j!=0;时

sum[0][j]=sum[0][j-1]+arr[0][j];

当j==0,i!=0时

sum[i][0]=sum[i-1][0]+arr[i][0];

当i== 0, j==0时

sum[0][0]=arr[0][0];

代码如下:

int arr[][4] = {
     {
    1 ,2, 3, 4},
                     {
    5, 6, 7, 8},
                     {
    9, 10, 11,12},
                     {
    13,14,15, 16} };
    int a = 4, b = 4;
    int** sum = (int**)malloc(sizeof(int*) * a);
    for (int i = 0; i < a; i++)
    {
    
        sum[i] = (int*)malloc(sizeof(int) * b);
        for (int j = 0; j < b; j++)
        {
    
            if (i == 0 && j == 0)
                sum[0][0] = arr[0][0];
            else if (i != 0 && j == 0)
                sum[i][0] = sum[i - 1][0] + arr[i][0];
            else if (i == 0 && j != 0)
                sum[0][j] = sum[0][j - 1] + arr[0][j];
            else
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] + arr[i][j] - sum[i - 1][j - 1];
        }
    }

当我们有了前缀和数组后,解决最开始的问题就简单多了
如果要求(1,1)到(2,2)的和
在这里插入图片描述

sum2=sum[2][2]-sum[2][0]-sum[0][2]+sum[0][0]

总结如下:
(x1,y1),(x2,y2), 前提是 (x1<=x2,y1<=y2)
x1== 0&&y1==0

sum2=sum[x2][y2]

x1!=0&&y1==0

sum2=sum[x2][y2]-sum[x1-1][y2];

x1==0&y1!=0

sum2=sum[x2][y2]-sum[x2][y1-1]

x1!=0&&y1!=0

sum2=sum[x2][y2]-sum[x1-1][y2]-sum[x2][y1-1]+sum[x1-1][y2-1];

完整代码:

#include 
#include
int main()
{
    
    int arr[][4] = {
     {
    1 ,2, 3, 4},
                     {
    5, 6, 7, 8},
                     {
    9, 10, 11,12},
                     {
    13,14,15, 16} };
    int a = 4, b = 4;
    int** sum = (int**)malloc(sizeof(int*) * a);
    for (int i = 0; i < a; i++)
    {
    
        sum[i] = (int*)malloc(sizeof(int) * b);
        for (int j = 0; j < b; j++)
        {
    
            if (i == 0 && j == 0)
                sum[0][0] = arr[0][0];
            else if (i != 0 && j == 0)
                sum[i][0] = sum[i - 1][0] + arr[i][0];
            else if (i == 0 && j != 0)
                sum[0][j] = sum[0][j - 1] + arr[0][j];
            else
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] + arr[i][j] - sum[i - 1][j - 1];
        }
    }
    int n = 0;
    scanf("%d", &n);//获取操作次数
    for (int i = 0; i < n; i++)
    {
    
        int x1, y1, x2, y2;
        scanf("%d%d",& x1, &y1);
        scanf("%d%d",& x2, &y2);
        int sum2 = 0;
        if (!x1 && !x2)
            sum2 = sum[x2][y2];
        else if (!x1)
            sum2 = sum[x2][y2] - sum[x2][y1 - 1];
        else if (!y1)
            sum2 = sum[x2][y2] - sum[x1 - 1][y2];
        else
            sum2 = sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1];
        printf("%d ", sum2);
    }
    return 0;
}

二维差分

同理还是上面数组

int arr[][4] = {
     {
    1 ,2, 3, 4},
                     {
    5, 6, 7, 8},
                     {
    9, 10, 11,12},
                     {
    13,14,15, 16} };

假如我们想要把(1,1)到(2,2)这个区间都加上数字m,用原始的算法也是很慢的,为了加快效率,也是可以用的,也是可以用到差分的,也就是二维差分。
在这里插入图片描述
我们只需要对差分数组

sub[1][1]+=m
sub[3][1]-=m
sub[1][3]-=m
sub[3][3]+=m

总结:要在区间(x1,y1)到(x2,y2)都加上m

sub[x1][y1]+=m;
sub[x1][y2+1]-=m;
sub[x2+1][y1]-=m;
sub[x2+1][y2+1]+=m;

代码:

#include 
#include
int main()
{
    
    int arr[][4] = {
     {
    1 ,2, 3, 4},
                     {
    5, 6, 7, 8},
                     {
    9, 10, 11,12},
                     {
    13,14,15, 16} };
    int a = 4, b = 4;
    int n = 0;
    scanf("%d", &n);//获取操作次数
    int** sub = (int**)calloc(a+1,sizeof(int*));
    for (int i = 0; i < a+1; i++)
    {
    
        sub[i] = (int*)calloc(b + 1, sizeof(int));   //先把数组开辟出来
    }
    for (int p = 0; p < n; p++)    //求差分数组
    {
    
        int x1, y1, x2, y2;
        scanf("%d%d", &x1, &y1);    
        scanf("%d%d", &x2, &y2);      //获取操作区间
        int m = 0;                  //要加的数字大小
        scanf("%d", &m);
        sub[x1][y1] += m;
        sub[x2 + 1][y2 + 1] += m;
        sub[x1][y2 + 1] -= m;
        sub[x2 + 1][y1] -= m;
    }
    //对差分数组求前缀和
    for (int i = 0; i < a; i++)
    {
    
        for (int j = 0; j < b; j++)
        {
    
            if (i == 0 && j == 0)
                sub[0][0] = sub[0][0];
            else if (i != 0 && j == 0)
                sub[i][0] = sub[i - 1][0] + sub[i][0];
            else if (i == 0 && j != 0)
                sub[0][j] = sub[0][j - 1] + sub[0][j];
            else
                sub[i][j] = sub[i - 1][j] + sub[i][j - 1] + sub[i][j] - sub[i - 1][j - 1];
        }
    }
    //映射回原数组
    for (int i = 0; i < a; i++)
    {
    
        for (int j = 0; j < b; j++)
        {
    
            arr[i][j] += sub[i][j];
        }
    }
    return 0;
}
相关文章
|
4月前
|
算法
【算法】前缀和——二维前缀和模板题
【算法】前缀和——二维前缀和模板题
|
4月前
|
算法
【算法】前缀和——前缀和
【算法】前缀和——前缀和
|
3月前
|
存储 算法 Java
前缀和算法
本文介绍了前缀和及其变种在解决区间求和问题中的应用。首先,一维前缀和可通过预处理数组快速求得任意区间的和。接着,二维前缀和扩展了这一思想,适用于矩阵操作。此外,文章探讨了如何利用前缀和解决诸如“寻找数组中心下标”、“除自身以外数组的乘积”等问题,并进一步讲解了涉及哈希表优化的“和为 K 的子数组”等相关题目。最后,通过实例展示了如何在矩阵中高效计算特定区域的元素之和。文中包含代码示例与图解说明,便于理解。
48 0
前缀和算法
|
2月前
|
人工智能 算法 BI
一篇带你速通差分算法(C/C++)
一篇带你速通差分算法(C/C++)
|
2月前
|
人工智能 算法 C++
一篇带你速通前缀和算法(C/C++)
一篇带你速通前缀和算法(C/C++)
|
5月前
|
人工智能 算法 JavaScript
【算法】前缀和与差分
算法学习——前缀和与差分(含一维和二维)
58 4
【算法】前缀和与差分
|
4月前
|
算法 C++
【算法】前缀和算法——和可被K整除的子数组
【算法】前缀和算法——和可被K整除的子数组
|
4月前
|
算法
【算法】前缀和算法——和为k的子数组之和
【算法】前缀和算法——和为k的子数组之和
|
4月前
|
算法
【算法】前缀和——除自身以外数组的乘积
【算法】前缀和——除自身以外数组的乘积
|
4月前
|
算法
【算法】前缀和——寻找数组的中心下标
【算法】前缀和——寻找数组的中心下标
下一篇
DataWorks