前缀和和差分,必须掌握的算法!

一、一维前缀和

假设我们有如下数组
image.png
如果有一题要求你求区间[0,3],[2,4],[1,3]的和,普通做法是不是必须要遍历区间次?第一次遍历[0,3]求和,然后[2,4]求和,最后[1,3]求和,得出结果为36
但是我们求个前缀和试试呢?

前缀和公式:sum[i]=arr[i]+sum[i-1]

image.png
这时我们求区间[2,4]的和是不是就相当于红色的前缀和减去蓝色的?image.png
用sum[right]-sum[left-1]

求区间和公式:sum[left,right]=sum[right]-sum[left-1]
在写前缀和题目的时候我们要有一个概念:用前缀和处理元素和,「元素和等于 0」等价于「两个前缀和之差等于 0」,进而等价于「两个前缀和相同」

这样可以快速节省时间的消耗,代码如下:

#include<iostream>
#include<vector>
using namespace std;
int get_sum(vector<int>sum,int left,int right)
{
    //left如果是0就不需要减了
    if(!left)
        return sum[right];
    else
        return sum[right]-sum[left-1];
}
int main()
{
    vector<int>arr={1,3,6,2,5};
    //求前缀和
    vector<int>sum(arr.size());
    //第一个要初始化
    sum[0]=arr[0];
    for(int i=1;i<arr.size();i++)
        {
            sum[i]=arr[i]+sum[i-1];
        }
    cout <<get_sum(sum,2,4) << endl;
    cout <<get_sum(sum,0,3) << endl;
    cout <<get_sum(sum,1,3) << endl;
    return 0;
}

二、一维差分

假设我们有如下数组
image.png
如果有一题要求你在区间[1,3]的值+2,区间[0,3]的值+3,区间[1,4]的值+1,常规做法就是在某个区间内在上对应的值,然后遍历区间次,得出结果如下:
image.png
但是我们求个差分试试呢?

差分公式:d[i]=arr[i]-arr[i-1]

image.png
这好像和我们答案没有什么关系对吧?在往下讲之前我还要告诉大家一个特殊的点

差分数组求前缀和就得出原数组

那么我们来试试
image.png
发现真的变回去了,是不是很神奇!
那么回归正题,如果我们要让区间[1,3]的值+2就需要在1的位置+2,4的位置减2,如果没错的话正确答案应该是1,5,8,4,5那么我们验证一下
image.png
发现果然是正确的,那么我们得出结论

arr[left,right]+val=求arr的差分 d[left]+val,d[right+1]-val

具体为什么会这样,原因在于如果我们在某一个位置统一加上某一个数以后,会改变后面所有值,但是我们需要在右区间的后面停下来,所以右区间+1的位置就需要减去val
代码如下:

#include<iostream>
#include<vector>
using namespace std;
//d[left]+val,d[right+1]-val
void add(vector<int>& d,int left,int right,int val)
{
    d[left]=d[left]+val;
    d[right+1]=d[right+1]-val;
}
//求前缀和
void get_sum(vector<int>&sum,vector<int>d)
{
    sum[0]=d[0];
    for(int i=1;i<d.size();i++)
    {
        sum[i]=d[i]+sum[i-1];
    }
}
int main()
{
    vector<int>arr={1,3,6,2,5};
    vector<int>d(arr.size());
    //第一个要初始化
    d[0]=arr[0];
    //求差分
    for(int i=1;i<arr.size();i++)
    {
        d[i]=arr[i]-arr[i-1];
    }
    //区间[1,3]+2
    add(d,1,3,2);
    //区间[0,3]+3
    add(d,0,3,3);
    //区间[1,4]+1
    add(d,1,4,1);
    vector<int>sum(d.size());
    get_sum(sum,d);
    //打印
    for(int i=0;i<sum.size();i++)
    {
        cout << sum[i] << endl;
    }
    return 0;
}

三、二维前缀和

二维和一维基本差不多,只不过更加复杂,一般题目会这样给:求区间[1,1]到[2,3]的和
image.png
我们如果用一维前缀和的思想就是把第二行和第三行的值求出来,然后减去2和3。
求二维前缀和相对比较复杂,在介绍之前,我先问读者一个问题,我们该如何用二维的思想去求红色区域的和呢呢?那么先看这张图的拆解:
image.png
假设我们已经求好二维前缀和了,已知sum[x2][y2]的位置存放的是[0][0]到[x2][y2]的前缀和,例如数字4的位置存放的就是区间[0][0]到[2][3]的前缀和,那么我们是不是只需要用sum[2][3]-sum[0][3]-sum[2][0]+sum[0][0]就可以啦?这里可能有读者不理解为什么是+sum[0][0],请读者注意!这里蓝色部分有重叠,所以我们多减去了一个,需要加回来!
得出结论(sum是二维前缀和数组):

[x1][y1]到[x2][y2]的和=sum[x2][y2]-sum[x2][y1-1]-sum[x1-1][y2]+sum[x1-1][y1-1]

既然我们得出这个结论了,那么二维前缀和不就出来了吗?反推即可

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

image.png
我们用33-12-6+1=就喝和原来求1+3+5+2+1+4的结果是一样的,这证明我们的结论正确
image.png
代码如下:

#include<iostream>
#include<vector>
using namespace std;
//求二维前缀和
void get_sum(vector<vector<int>> arr,vector<vector<int>>& sum)
{
    sum[0][0]=arr[0][0];
    //初始化第一行和第一列
    for(int j=1;j<arr[0].size();j++)
    {
        sum[0][j]=arr[0][j]+sum[0][j-1];
    }
    for(int i=1;i<arr.size();i++)
    {
        sum[i][0]=arr[i][0]+sum[i-1][0];
    }
    for(int i=1;i<arr.size();i++)
    {
        for(int j=1;j<arr[i].size();j++)
        {
            sum[i][j]=arr[i][j]+sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1];
        }
    }
}
int get_result(vector<vector<int>>sum,int x1,int y1,int x2,int y2)
{
    //如果第一个区间就是初始位置
    if(x1==0&&y1==0)
    {
        return sum[x2][y2];
    }
    //x1等于0的话只需要减去左边
    else if(x1==0)
    {
        return sum[x2][y2]-sum[x2][y1-1];
    }
    //y1等于0的话只需要减去上边
    else if(y1==0)
    {
        return sum[x2][y2]-sum[x1-1][y2];
    }
    else
    {
        return sum[x2][y2]-sum[x2][y1-1]-sum[x1-1][y2]+sum[x1-1][y1-1];
    }
    //没用
    return -1;
}
int main()
{
    vector<vector<int>>arr={{1,3,6,2},{2,1,3,5},{3,2,1,4}};
    vector<vector<int>>sum(arr.size(), vector<int>(arr[0].size()));
    get_sum(arr,sum);
    cout << get_result(sum,1,1,2,3) << endl;
    cout << get_result(sum,0,0,1,3) << endl;
    cout << get_result(sum,0,1,1,3) << endl;
    cout << get_result(sum,1,0,2,1) << endl;
    return 0;
}

四、二维差分

二维差分和二维前缀和的公式推导基本相似,这里直接给出求二维差分的公式
d[i][j]=arr[i][j]-arr[i-1][j]-arr[i][j-1]+arr[i-1][j-1]
这里我们重点介绍二维差分的应用,例如二维差分求红色区域+2,蓝色区域+3
image.png
如果想用二维差分,我们必须使用如下操作,具体为什么这里不过多介绍,和一维差分的思想是一样的,某个区域内+value后面就需要-value,因为二维会影响到x2+1,y2+1多减去一个value,所以我们需要加回来一个
image.png
得出结论:

d[x1][y1]+value
d[x2+1][y1]-value
d[x1][y2+1]-value
d[x2+1][y2+1]+value

然后使用二维前缀和公式:

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

代码如下:

#include<iostream>
#include<vector>
using namespace std;
//求二维差分
void get_d(vector<vector<int>>& d,vector<vector<int>> arr)
{
    //初始化
    d[0][0]=arr[0][0];
    //初始化第一行和第一列
    for(int j=1;j<arr[0].size();j++)
    {
        d[0][j]=arr[0][j]-arr[0][j-1];
    }
    for(int i=1;i<arr.size();i++)
    {
        d[i][0]=arr[i][0]-arr[i-1][0];
    }
    for(int i=1;i<arr.size();i++)
    {
        for(int j=1;j<arr[i].size();j++)
        {
            d[i][j]=arr[i][j]-arr[i-1][j]-arr[i][j-1]+arr[i-1][j-1];
        }
    }
}
//减去或加上特定值
void get_add(vector<vector<int>>&d,int x1,int y1,int x2,int y2,int val) {
    d[x1][y1] += val;
    //控制边界
    if (y2 + 1 < d[0].size()) {
        d[x1][y2 + 1] -= val;
    }
    if (x2 + 1 < d.size()) {
        d[x2 + 1][y1] -= val;
    }
    if (x2 + 1 < d.size() && y2 + 1 < d.size())
    {
        d[x2 + 1][y2 + 1] += val;
    }
}
void get_sum(vector<vector<int>>& arr,vector<vector<int>>& sum)
{
    sum[0][0]=arr[0][0];
    //初始化第一行和第一列
    for(int j=1;j<arr[0].size();j++)
    {
        sum[0][j]=arr[0][j]+sum[0][j-1];
    }
    for(int i=1;i<arr.size();i++)
    {
        sum[i][0]=arr[i][0]+sum[i-1][0];
    }
    for(int i=1;i<arr.size();i++)
    {
        for(int j=1;j<arr[i].size();j++)
        {
            sum[i][j]=arr[i][j]+sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1];
        }
    }
}
int main()
{
    vector<vector<int>>arr={{1,3,6,2},{2,1,3,5},{3,2,1,4}};
    vector<vector<int>>d(arr.size(),vector<int>(arr[0].size(),0));
    vector<vector<int>>sum(arr.size(),vector<int>(arr[0].size()));
    //常规写法
    get_d(d,arr);
    get_add(d,1,2,2,3,2);
    get_add(d,1,1,2,2,3);
    get_sum(d,sum);
    for(int i=0;i<sum.size();i++)
    {
        for(int j=0;j<sum[i].size();j++)
        {
            cout << sum[i][j] << " ";
        }
        cout << endl;
    }
    //另一种写法,在全是0的差分数组上先调用get_add,然后求前缀和,在加上原数组
    vector<vector<int>>d1(arr.size(),vector<int>(arr[0].size(),0));
    vector<vector<int>>sum1(arr.size(),vector<int>(arr[0].size()));
    vector<vector<int>>res(arr.size(),vector<int>(arr[0].size()));
    get_add(d1,1,2,2,3,2);
    get_sum(d1,sum1);
    for(int i=0;i<arr.size();i++)
    {
        for(int j=0;j<arr[i].size();j++)
        {
            res[i][j]=arr[i][j]+sum[i][j];
        }
    }
    return 0;
}

这里第二种方法也适用于一维差分,在一维差分里我们可以用全是0的数组在特定位置进行加减然后求前缀和,这样的好处在于我们可以**不用求一维差分或者二维差分,毕竟求差分的公式还是比较复杂的,**至于为什么请读者试一试就明白了

五、二维差分题目

截屏2023-01-18 21.43.10.png
这题是力扣第328竞赛的一题,我们可以利用二维差分的做法,我这里才用的做法是,现在特定位置进行加减val然后求前缀和即可

class Solution {
private:
//减去或加上特定值
void get_add(vector<vector<int>>&d,int x1,int y1,int x2,int y2,int val) {
    d[x1][y1] += val;
    //控制边界
    if (y2 + 1 < d[0].size()) {
        d[x1][y2 + 1] -= val;
    }
    if (x2 + 1 < d.size()) {
        d[x2 + 1][y1] -= val;
    }
    if (x2 + 1 < d.size() && y2 + 1 < d.size())
    {
        d[x2 + 1][y2 + 1] += val;
    }
}
void get_sum(vector<vector<int>>& arr,vector<vector<int>>& sum)
{
    sum[0][0]=arr[0][0];
    //初始化第一行和第一列
    for(int j=1;j<arr[0].size();j++)
    {
        sum[0][j]=arr[0][j]+sum[0][j-1];
    }
    for(int i=1;i<arr.size();i++)
    {
        sum[i][0]=arr[i][0]+sum[i-1][0];
    }
    for(int i=1;i<arr.size();i++)
    {
        for(int j=1;j<arr[i].size();j++)
        {
            sum[i][j]=arr[i][j]+sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1];
        }
    }
}
public:
    vector<vector<int>> rangeAddQueries(int n, vector<vector<int>>& queries) {
        vector<vector<int>>res(n,vector(n,0));
        for(int i=0;i<queries.size();i++)
        {
            get_add(res,queries[i][0],queries[i][1],queries[i][2],queries[i][3],1);
        }
        vector<vector<int>> sum(n,vector(n,0));
        get_sum(res,sum);
        return sum;
    }
};
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值