前缀和 &差分

总结 :前缀和多用于求区间和    原数组--->前缀和数组--->修改前缀和数组---->(修改过后)原数组
差分多用于区间修改和单点查询问题     原数组---->差分数组---->修改差分数组---->(修改过后)原数组

前提:数组均从下标1位置开始存数

a[1]~a[n] a[1][1]~a[n][m]

-------

有一个含有n个数据的数组a[1]~a[n]

给出m个区间[l,r] 对于每个区间,输出该区间中数据的和

eg. n=4, m=2 4 3 2 1

1,4--->10 2,3--->5

暴力破解

l[i] r[i] :储存第i个区间的范围 lrsum[i]:第i个区间的和

for(int i=1;i<=m;i++)

{

//for(int j=l[i] ; j<=r[i] ; j++) lrsum[i]+=j;

优化: lrsum[i]=sum[r[i]]-sum[l[i]-1] }

O(m*n) 可能超时

可以优化内层循环 优化方案:前缀和:

a数组的前缀和数组sum:

那么求出前缀和数组有什么用呢?

eg.求a[3]~a[9]的和: sum[3]=a1+a2+a3 sum[9]=a1+a2+…+a9 sum[2]=a1+a2

所以 a[3]~a[9]的和为 sum[9]-sum[2] //因为a[3]~a[9]是闭区间,a[3]是要包含进去的,所以只能减到a[2]

那么求a[l]~a[r]的和: sum[r]-sum[l-1]

总结:前缀和一般是用来进行数组的预处理,提高后续的操作效率,多用于求区间和

而且前缀和也只是一种对数组进行预处理的方式

二维数组的前缀和

eg.求a[1][1]~a[i][j]的前缀和:不是把每一行累加,而是算这两个点构成的矩形之和

这里引入一个概念

基于这种思想,我们想到a[i][j]之前已经计算过的 sum[i-1][j],sum[i][j-1]

把它们两个加起来发现有重叠部分(红色) 那就要减去它(sum[i-1][j-1]) 最后再加上自己

得公式 sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+a[i][j]

这样就规避了四层循环(i,j递增两次,1到i和1到j两次) 只有两层循环

要点1:公式不用记,记住推理过程

2.sum[][]必须全部初始化为0(全局变量自动赋值为0) 并且下标也必须从1开始,从0开始会越界

求a[x][y]~a[i][j]区间的和 同理:减去黄色部分就行了

ans=sum[i][j]-sum[i][y-1]-sum[x-1][j]+sum[x-1][y-1]

例题:P1387 最大正方形 - 洛谷


//这道题还有一个很坑人的就是它是用点表示边长,比如a[1][1]~a[1][3],边长为3: 3-1+1  从案例中得到

#include<iostream>
using namespace std;
int arr[105][105];
int sum[105][105];
int main()
{
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            cin >> arr[i][j];
        }
    }
    //求每个点的前缀和
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + arr[i][j];
        }
    }

    int l; //记录正方形当前边长,每次循环找到一个正方形就行了
    for (l = 1; l <= min(m, n); l++)
    {
        int flag = 0;
        for (int i = 1; i <= n - l+1; i++) {
            for (int j = 1; j <= m - l+1; j++) {  //(i,j)表示左上顶点
                //右下顶点
                int rx = i + l-1, ry = j + l-1;


                //找只包含 0 和 1 的矩阵里找出一个不包含 0 的正方形,结合所学知识,
                //是不是只要前缀和为l*l就行了
         
                if (sum[rx][ry] - sum[rx][j - 1] - sum[i - 1][ry] + sum[i - 1][j - 1] == l*l) {   
                    flag = 1;
                    break;
                }
            }
            if (flag) break;

        }
        if (flag == 0) {
            cout << l - 1;
            return 0;
        }

    }

}

-------

差分: 应用:对数组进行区间修改时避免用循环

有一个含有n个数据的数组a[1]~a[n]

给出m个区间[l,r] 对于每个区间,对于每个区间中的所有数据都+1

输出最终的a数组

n=6,m=3

1 3 2 7 6 8

[3,5]--->1 3 3 8 7 8 [1,4]--->2 4 4 9 7 8

l[i] r[i] :储存第i个区间的范围 数组a

for(int i=1;i<=m;i++)

{

//for(int j=l[i] ; j<=r[i] ; j++) a[j]++; }

O(n*m)

优化方法: 差分数组

差分:前缀和的逆运算 已知sum[n],求原数组(也叫做差分数组)

a[1]=sum[1] a[2]=sum[2]-sum[1] a[i]=sum[i]-sum[i-1]

这跟题目有什么关系呢? 举个例子

a[1] a[2] a[3] a[4] a[5] a[6] -----差分数组(相对于前缀和数组来说,前缀数组就是差分数组求前缀和求出来的)

sum[1]~~sum[6]

a[1] a[2] a[3]+1 a[4] a[5] a[6]

sum[1]=a[1] sum[2]=a[1]+a[2]

sum[3]=sum[2]+a3+1 sum[4]=a[1]+ a[2] + a[3]+1 +a[4]

sum[5]=a[1] +a[2]+ a[3] + 1+a[4] + a[5]

sum[6]= a[1]+ a[2] + a[3]+1 + a[4] + a[5] + a[6]

我只是在a[3]一个数字上+1,导致sum[3]后面的数字都加了1

并且不会影响递推性,a[3]=sum[3]-sum[2] a[4]=sum[4]-sum[3] //1给约掉了

结论:我在差分数组进行单点修改,其前缀和数组会进行相应的区间修改

a[j]+k sum[j]+x~~sum[n]+x

若要求:sum[l]+x~~sum[r]+x----->a[l]+x &&a[r+1]-x //sum[r]上也加了x,这是闭区间,所以往后挪

回到最初的问题

1 3 2 7 6 8

[3,5]--->1 3 3 8 7 8

[1,4]--->2 4 4 9 7 8 怎么优化?

把1 3 2 7 6 8当作前缀和数组,我去求它的差分数组b[],然后再差分数组上进行操作

最后转化为前缀和数组,就是修改后的原数组

[3,5]--->1 3 3 8 7 8 ---->b[3]+1 b[6]-1

[1,4]--->2 4 4 9 7 8----> b[1]+1 b[5]-1

这样等价后我相当于只进行了两个单点修改,就不需要遍历来修改原数组

所以我感觉核心就是对数组的某个区间内进行加减某个数,可以转化为对差分数组的两个单点加减

二维差分数组

二维数组的某个面进行加减某个数(把它当成前缀和数组),可以转化为对差分数组的四个单点加减

已知sum[][] 求a[][]

a[i][j]=sum[i][j]-sum[i][j-1]-sum[i-1][j]+sum[i-1][j-1] sum<--a<--b

那么我差分数组已知,我想让红色部分全部+1,怎么做?

如果差分数组a[x][y]++,那么受到影响的前缀和有哪些呢(前缀和包含了a[x][y]的

即从a[x][y]到a[m][n]差分点的前缀和)

那么我只想要红色矩形区域的点的前缀和受影响, b[x][y]++,b[x+1][y]-- ,b[x][y+1]--,b[x+1][y+1]++

例题:

//在某个范围内对数据进行相加减,可以转为对它的差分数组的几个点进行相加减
#include<iostream>
using namespace std;
int arr[1005][1005];
int a[1005][1005]; //arr的差分数组,都是0
int main()
{
    int n, m;
    cin >> n >> m;
    int x1, y1, x2, y2;
    for (int i = 1; i <= m; i++) {
        cin >> x1 >> y1 >> x2 >> y2;
        //一般思路是两层循环遍历地毯加上去,但是这样会超出时间
        a[x1][y1]++;
        a[x2 + 1][y1]--;
        a[x1][y2 + 1]--;
        a[x2 + 1][y2 + 1]++; //n=1000也没关系
    }
    //差分数组求前缀和得到原数组

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            arr[i][j] = arr[i - 1][j] + arr[i][j - 1] - arr[i - 1][j - 1] + a[i][j];
            cout << arr[i][j] << " ";
        }
        cout << "\n";
    }

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值