算法基础:前缀和与差分

6 篇文章 0 订阅
6 篇文章 0 订阅

前缀和


S i = a 1 + a 2 + . . . + a i Si = a1 + a2 + ... + ai Si=a1+a2+...+ai ( 下标一定要从 1 1 1 开始 )

如何求 Si

f o r    i = 1 ; i < = n ; i + + : for ~~i = 1; i <= n; i ++: for  i=1;i<=n;i++:

​ $s[i] = s[ i - 1 ] + a_{i} $

s 0 = 0 s_{0} = 0 s0=0

作用是可以快速求出来原数组中一段数的和,如果没有前缀和数组,要计算 [ l , r ] [ l , r ] [l,r] 内的数之和,时间复杂度为 $O(n) $

但是如果有前缀和数组,则为 S r − S l − 1 S_{r} - S_{l-1} SrSl1

S r = a 1 + a 2 + . . . + a l − 1 + a l + . . . + a r S_{r} = a_{1}+a_{2}+...+a_{l-1}+a_{l}+...+a_{r} Sr=a1+a2+...+al1+al+...+ar

S l − 1 = a 1 + a 2 + . . . + a l − 1 S_{l-1}=a_{1}+a_{2}+...+a_{l-1} Sl1=a1+a2+...+al1

S r − S l − 1 = a l + . . . + a r Sr - S_{l-1} = a_{l}+...+a_{r} SrSl1=al+...+ar

时间复杂度为 O ( 1 ) O(1) O(1)

如果求 [ 1 , 10 ] [1,10] [1,10] 则是 S 10 − S 0 S_{10} - S_{0} S10S0 因为 $ S_{0}$ 已经为 0 0 0 ,所以求的就是 $a_{1} $ 加到 a 10 a_{10} a10

#include<iostream>

using namespace std;

const int N = 1e5 + 10;

int n,m;
int a[N], s[N];

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++) scanf("%d", &a[i]);

    for(int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i]; //前缀和的初始化

    while( m -- )
    {
        int l, r;
        scanf("%d%d", &l, &r); //用scanf比cin会快一倍
        printf("%d\n", s[r] - s[l - 1]); //区间和的计算
    }
    return 0;
}
ios::sync_with_stdio(false);
//提高cin的读取速度,但是副作用是不能再使用scanf,但是优化后还是没有scanf快,数据输入的规模如果大于等于一百万,建议用scanf,否则建议用cin
子矩阵的和

O ( n m ) − > O ( 1 ) O(nm) -> O(1) O(nm)>O(1)

原矩阵

1724
3628
2123

前缀和矩阵

181014
4172133
6202641
如何计算前缀和矩阵
image-20231008120423024

S x , y = S x − 1 , y + S x , y − 1 − S x − 1 , y − 1 + a x , y S_{x,y} = S_{x-1,y} + S_{x ,y-1} - S_{x-1,y-1} + a_{x,y} Sx,y=Sx1,y+Sx,y1Sx1,y1+ax,y

如何利用前缀和矩阵,计算某一个子矩阵的和

image-20231008115925219image-20231008120134299

S x 2 , y 2 − S x 2 , y 1 − 1 − S x 1 − 1 , y 2 + S x 1 − 1 , y 1 − 1 S_{x_{2},y_{2}} - S_{x_{2},y_{1}-1} - S_{x_{1}-1,y_{2}} + S_{x_{1}-1,y_{1}-1} Sx2,y2Sx2,y11Sx11,y2+Sx11,y11

#include<iostream>

using namespace std;

int n, m, q;
const int N = 1e3 + 10;
int s[N][N], a[N][N];

int main()
{
    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] - s[i - 1][j - 1] + a[i][j];
    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;
}

最大加权矩形

利用前缀和解决

s[x] [y] 表示 从 (1, 1) 到 (x, y) 的元素之和 (假设从 下标为 1 开始存储矩阵)

要计算 ( x 1 , y 1 ) (x1, y1) x1,y1) ( x 2 , y 2 ) (x2, y2) (x2,y2) 的元素之和

那么就是 紫色区域 - 橙色区域 - 绿色区域 + 灰色区域 (灰色区域被减了 2 2 2 遍)

s[x2] [y2] - s[x1] [y1 - 1] - s[x1 - 1] [y2] + s[x1 - 1] [y1 - 1]

image-20230926073942222

对于样例来说,

76f816642e049525c7a4f56c6ebb226

计算红色区域 ( 2 , 2 ) (2, 2) (2,2) ( 4 , 4 ) (4, 4) (4,4) 中元素之和,

也就是:

( 1 , 1 ) (1, 1) (1,1) ( 4 , 4 ) (4, 4) 4,4) 元素之和 − ( ( 1 , 1 ) - ((1,1) ((1,1) ( 4 , 1 ) (4, 1) (4,1) 元素之和 $) - ((1, 1) $ 到 ( 1 , 4 ) (1, 4) (1,4) 元素之和 ) + ( 1 , 1 ) ) + (1, 1) )+1,1) 元素

计算s[x] [y]
image-20230926075346597
s[x][y] = s[x - 1][y] + 这一行元素到该点元素之和

这一行元素到该点元素之和 相当于是 这一行元素到 元素 a [ x ] [ y ] a [x] [ y ] a[x][y] 的前缀和

利用 l [ x ] [ y ] l [ x ] [ y ] l[x][y] 数组存储前缀和

递推关系式为

l[x][y] = l[x][y - 1] + a[i][j]

所以可以在边存储矩阵元素的过程中计算前缀和并且得到 s [ x ] [ y ] s [x] [y] s[x][y]

for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= n; j++)
        {
            cin >> a[i][j];
            l[i][j] = l[i][j - 1] + a[i][j];
            s[i][j] = s[i - 1][j] + l[i][j];
        }

因为要得到最大的矩阵元素之和

那就在边计算一个矩阵之和时边跟当前的最大值进行比较,如果比当前最大值还大,那就更新最大值

for(int x1 = 1; x1 <= n; x1++)
        for(int y1 = 1; y1 <= n; y1++)
            for(int x2 = 1; x2 <= n; x2++)
                for(int y2 = 1; y2 <= n; y2++)
                {
                    if(x2 < x1 || y2 < y1) continue;
                    ans = max(ans, s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
                }

注意边界情况

​ 当 x 2 < x 1 x2 < x1 x2<x1 或者 y 2 < y 1 y2 < y1 y2<y1 时就不用进行计算了

#include<bits/stdc++.h>

using namespace std;

const int  N = 150;

int a[N][N], l[N][N];
int n;
int s[N][N], b[N][N];
int ans, square;

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= n; j++)
        {
            cin >> a[i][j];
            l[i][j] = l[i][j - 1] + a[i][j];
            s[i][j] = s[i - 1][j] + l[i][j];
        }
    ans = -2147483647;
    for(int x1 = 1; x1 <= n; x1++)
        for(int y1 = 1; y1 <= n; y1++)
            for(int x2 = 1; x2 <= n; x2++)
                for(int y2 = 1; y2 <= n; y2++)
                {
                    if(x2 < x1 || y2 < y1) continue;
                    ans = max(ans, s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
                }
    cout << ans << endl;
    return 0;
}

2147483647 2147483647 2147483647 i n t int int 的最大值

[NOIP2011 提高] 聪明的质监员

[NOIP2011 提高组]聪明的质监员 image-20230926124832634

由题意可知,当 W W W 越大时,可以选择的矿石个数就越少, y y y 值也就越小。相反,当 W W W 越小时,可以选择的矿石个数就越多, y y y 值也就越大。

因此

可以把 y y y 看成关于 x x x 的函数,image-20230926125730766

该函数单调递减且在 s s s 附近不连续,所以还要再判断一下 s s s 附近两个点到 s s s 的距离哪个更近

ans = min(y1 - s, y2 - s)

又考虑到寻找 s s s 附近的两个点,就可以用二分查找的方法 (增加效率)

c h e c k ( ) check() check() 函数 记录 y y y

注意数据范围

​ 因为 0 < s < = 1 e 12 0 < s <= 1e12 0<s<=1e12,已经超过 i n t int int 范围,所以开 l o n g l o n g long long longlong

#define LL long long

判断边界:

​ 因为题目中 0 < w i , v i < = 1 e 6 0 < wi, vi <= 1e6 0<wi,vi<=1e6,所以左边界为 0 0 0,右边界为 1 e 6 1e6 1e6

LL l = 0, r = 1e6;
LL mid;
while(l <= r)
{
    //先写下面,因为 l = mid + 1 所以这里不需要再加 1
    mid = (l + r) >> 1;
    //如果 y 值大于 s,将 W 调大,让 y 减小
    if(check(mid) > s)
        l = mid + 1;
    //如果 y 值小于等于 s,将 W 调小,让 y 变大
    else r = mid - 1;
    //这样找到 s 附近
}

判断 s s s 附近的哪个更小

LL ans = min(abs(check(l) - s), abs(check(r) - s));

c h e c k () check() check() 函数

利用 前缀和 可以降低时间复杂度
y i = ∑ j = l i r i [ w j ≥ W ] × ∑ j = l i r i [ w j ≥ W ] v j yi = \sum_{j = li}^{ri}{[wj \geq W]} \times \sum_{j = li}^{ri}{[wj \geq W]}vj yi=j=liri[wjW]×j=liri[wjW]vj
公式
y i = ∑ j = l i r i [ w j ≥ W ] yi = \sum_{j = li}^{ri}{[wj \geq W]} yi=j=liri[wjW]
表示 超过 W W W 项的个数

最终的 y y y
y = ∑ i = 1 m y i y = \sum_{i = 1}^{m}yi y=i=1myi

LL check(LL W)
{
	LL ans = 0;
    for(int i = 1; i <= n; i ++)
    {
		if(w[i] < W)
         //如果w[i] < W,就不用这个矿石
        {
            num[i] = num[i - 1]; //个数不变
            sum[i] = sum[i - 1]; //矿石的价值总和不变
		}
        else
        {
            num[i] = num[i - 1] + 1; //个数加 11
            sum[i] = sum[i - 1] + v[i]; //矿石的价值由上一次的加上该矿石的价值 
        }
    }
    //利用前缀和计算 y 的总和 
    for(int i = 1; i <= m; i ++)
        ans += (num[R[i]] - num[L[i] - 1]) * (sum[R[i]] - sum[L[i] - 1]);
   	return ans;
}

时间复杂度为 O ( m + n ) O ( m + n ) O(m+n)

完整代码

#include<bits/stdc++.h>
#define LL long long

using namespace std;

const int N = 1e6 + 10;

LL w[N], v[N];
int n, m;
LL s;
LL num[N], sum[N];
int L[N], R[N];

LL check(LL W)
{
	LL ans = 0;
    for(int i = 1; i <= n; i ++)
    {
		if(w[i] < W)
        {
            num[i] = num[i - 1]; 
            sum[i] = sum[i - 1]; 
		}
        else
        {
            num[i] = num[i - 1] + 1;
            sum[i] = sum[i - 1] + v[i]; 
        }
    }
    for(int i = 1; i <= m; i ++)
        ans += (num[R[i]] - num[L[i] - 1]) * (sum[R[i]] - sum[L[i] - 1]);
    return ans;
}

int main()
{
    scanf("%d%d%lld", &n, &m, &s);
    for(int i = 1; i <= n; i++)
    {
        scanf("%lld%lld", &w[i], &v[i]);
    }
    for(int i = 1; i <= m; i++)
    {
        scanf("%d%d", &L[i], &R[i]);
    }
    LL l = 0, r = 1e6;
    LL mid;
    while(l <= r)
    {
        mid = (l + r) >> 1;
        if(check(mid) > s)
            l = mid + 1;
        else r = mid - 1;
    }
    LL ans = min(abs(check(l) - s), abs(check(r) - s));
    printf("%lld", ans);
    return 0;   
}

整体时间复杂度 O ( ( n + m ) ∗ l o g ( m a x w ) ) O((n + m) * log(maxw)) O((n+m)log(maxw))

差分

用于快速修改数组中某一段区间的值
d i = a i − a i − 1 di = a_{i} - a_{i-1} di=aiai1
区间 [ l , r ] [ l , r ] [l,r] 加上 k k k

表示成
d l = d l + k , d r + 1 = d r + 1 − k d_{l} = d_{l} + k , d_{r+1} = d_{r+1}-k dl=dl+k,dr+1=dr+1k

a i = ∑ j = 1 i d j a_{i} = \sum_{j=1}^{i}d_{j} ai=j=1idj

对一个 a a a 数组
1 5 3 7 2 8 (1) \begin{matrix} 1 & 5 & 3 & 7 & 2 & 8 \end{matrix} \tag{1} 153728(1)
存在 b b b 数组,使得
b i = a i − a i − 1 b_{i} = a_{i} - a_{i - 1} bi=aiai1
b b b 数组是 a a a 数组的差分数组
b i = a i − a i − 1 b i − 1 = a i − 1 − a i − 2 . . . b 1 = a 1 b_{i} = a_{i} - a_{i - 1}\\ b_{i - 1} = a_{i - 1} - a_{i - 2}\\ ...\\ b_{1} = a_{1} bi=aiai1bi1=ai1ai2...b1=a1
对于 ( 1 ) (1) (1) b b b 数组为
1 4 − 2 4 − 5 6 (2) \begin{matrix} 1 & 4 & -2 & 4 & -5 & 6 \end{matrix}{\tag 2} 142456(2)
那么 对他们进行累加,可以得到
b 1 + b 2 + . . . + b i = a i b_{1} + b_{2} + ... + b_{i} = a_{i} b1+b2+...+bi=ai
要对 a$ 数组在区间为 [ l , r ] [ l,r ] [lr] 中每个数加上 k k k


b l = b l + k b r + 1 = b r + 1 − k b_{l} = b_{l} + k \\ b_{r + 1} = b_{r + 1} - k bl=bl+kbr+1=br+1k
例如对 ( 1 ) (1) (1) 数组进行操作

对区间 [ 2 , 4 ] [ 2, 4 ] [2,4] 每个数都加 1 1 1
1 6 4 8 3 8 (3) \begin{matrix} 1 & 6 & 4 & 8 & 3 & 8 \end{matrix} \tag{3} 164838(3)
而对 b 2 + 1 b2 + 1 b2+1,对 b 5 − 1 b5 - 1 b51 之后, b b b 数组变成
1 5 − 2 4 − 5 5 (4) \begin{matrix} 1 & 5 & -2 & 4 & -5 & 5 \end{matrix}{\tag4} 152455(4)
该结果与再对 ( 3 ) (3) 3 进行求差分数组结果一致

即对 ( 4 ) (4) 4 求前缀和所得的结果就是 ( 3 ) (3) 3

也就是说,要在 a a a 数组一定区间内进行增减,只需要更改 差分数组中 左端点的值 和 右端点后一位 的值

再对更改后的差分数组进行求前缀和,即可得到更改后的 a a a 数组

地毯

image-20230926214617442 image-20230926214643069

暴力做法

#include<bits/stdc++.h>

using namespace std;

const int N = 1050;

int a[N][N], num[N][N];
int n, m;
int x1, y11, x2, y2;

int main()
{
    cin >> n >> m;
    for(int j = 1; j <= m; j++)
    {
        cin >> x1 >> y11 >> x2 >> y2;
        for(int i = x1; i <= x2; i ++)
            for(int k = y11; k <= y2; k++)
                num[i][k] ++;
    }
    for(int j = 1; j <= n; j++)
    {
        for(int i = 1; i <= n; i++)
        {
            cout << num[j][i] << " ";
        }
        cout << endl;
    }
    return 0;
}

利用差分

当一块区域上有地毯,则对差分数组中左上角的点 + 1 + 1 +1,右下角的点的右侧一位 − 1 - 1 1

再对差分数组进行求前缀和,则可以得到结果

合格数

image-20231001101013686

时空限制 1 s / 256 M B 1s / 256MB 1s/256MB

利用 差分 + 前缀和

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

const int N = 2e5 + 10;

int n, k, q;
int s[N], m[N];
//s 来表示每个数被覆盖了多少次,m 表示前面一共有多少个合格数(前缀和)

int main()
{
    cin >> n >> k >> q;
    for(int i = 1; i <= n; i ++)
    {
        int l, r;
        scanf("%d%d", &l, &r);
        s[l] += 1;
        s[r + 1] -= 1;
        //差分
    }

    for(int i = 1; i <= N; i ++)
        s[i] += s[i - 1];
        //差分的前缀和就是原数组

    for(int i = 1; i <= N; i ++)
    {
        m[i] = m[i - 1];
        if(s[i] >= k) m[i] ++;
        //如果当前端点满足,合格数数量加一
    }

    while(q --)
    {
        int l, r;
        scanf("%d%d", &l, &r);
        printf("%d\n", m[r] - m[l - 1]);
        //求出前缀和 O(1)
    }

    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值