基础算法 - 前缀和与差分

前缀和

对于一个数组 A A A ,它的前缀和数组 S S S 是通过递推能求出的基本信息之一:
S [ i ] = ∑ j = 1 i A [ j ] S[i] = \sum_{j = 1}^{i}A[j] S[i]=j=1iA[j]
前缀和的作用往往用于快速的求数组的一部分和,即数组 A A A 某个下标区间内的数的和,可以表示为前缀和相减的形式:
s u m ( l , r ) = ∑ i = l r A [ i ] = S [ r ] − S [ l − 1 ] sum(l, r) = \sum_{i = l} ^ {r} A[i] = S[r] - S[l - 1] sum(l,r)=i=lrA[i]=S[r]S[l1]
在二维数组中,也可以推出类似的二维前缀和。

对于二维空间,我们可以表示成下面几张图:

S[i-1][j]
阴影部分为 S [ i − 1 ] [ j ] S[i - 1][j] S[i1][j]

S[i][j-1]阴影部分为 S [ i ] [ j − 1 ] S[i][j - 1] S[i][j1]

S[i][j-1] + S[i-1][j]阴影部分为 S [ i − 1 ] [ j ] + S [ i ] [ j − 1 ] S[i - 1][j] + S[i][j - 1] S[i1][j]+S[i][j1]

S[i][j-1] + S[i-1][j] - S[i-1][j-1]
阴影部分为 S [ i − 1 ] [ j ] + S [ i ] [ j − 1 ] − S [ i − 1 ] [ j − 1 ] S[i - 1][j] + S[i][j - 1]-S[i - 1][j - 1] S[i1][j]+S[i][j1]S[i1][j1]

容易的得出二维数组中,前缀和的递推式就是:
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]-S[i-1,j-1] +A[i,j] S[i,j]=S[i1,j]+S[i,j1]S[i1,j1]+A[i,j]
同理我要计算一个子矩阵的和,已知矩阵左上角坐标 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) ,右下角坐标为 ( x 2 , y 2 ) (x_2,y_2) (x2,y2) ,可得出:
∑ i = x 1 x 2 ∑ j = y 1 y 2 A [ i ] [ j ] = S [ x 2 , y 2 ] − S [ x 1 ] [ y 2 ] − S [ x 2 ] [ y 1 ] + S [ x 1 ] [ y 1 ] \sum_{i=x_1}^{x_2}\sum_{j=y_1}^{y_2}A[i][j] = S[x_2,y_2]-S[x_1][y_2]-S[x_2][y_1]+S[x_1][y_1] i=x1x2j=y1y2A[i][j]=S[x2,y2]S[x1][y2]S[x2][y1]+S[x1][y1]

推导式子其实使用了 “容斥原理” 的思想,利用它也可以推出三维数组的三维前缀和。

【例题】激光炸弹
一种新型炸弹,可以摧毁一个边长为 R R R 的正方形内所有目标。现在地图上有 N   ( N ≤ 1 0 4 ) N~(N\le 10^4) N (N104) 个目标,用整数 X i , Y i X_i,Y_i Xi,Yi (其值在区间 [ 0 , 5000 ] [0,5000] [0,5000] 之内)表示目标在地图上的位置,每个目标价值 W i W_i Wi

激光炸弹的投放是通过卫星定位的,但其有一个缺点,就是其爆破范围,即那个边长为 R R R 的正方形的边必须和 x x x y y y 轴平行。若目标位于爆破正方形的边上,该目标不会被摧毁。求一颗炸弹最多能炸掉地图上的总价值为多少的目标。

分析:
问题简述就是在一个二维平面找出一个长度为 R R R 的正方形,它的权值和最大。那么这个二维平面在 [ 0 , 5000 ] [0, 5000] [0,5000] 内,所有我们可以使用 O ( n 2 ) O(n^2) O(n2) 来预处理出一个二维前缀和数组,再用一个 O ( n 2 ) O(n^2) O(n2) 来遍历所有长度 R R R 的正方形的权值和,找出最大值就行了。

代码如下:

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

using namespace std;
const int N = 5010;
int n, r;
int sa[N][N];

int main()
{
    scanf("%d%d", &n, &r);

    // r最大5001有意义
    r = min(r, 5001);

    int x, y, w;
    for(int i = 0; i < n; ++i) {
        scanf("%d%d%d", &x, &y, &w);
        sa[x + 1][y + 1] += w;  // 前缀和通常以1开头,好处理。
    }

    for(int i = 1; i <= 5001; ++i) {
        for (int j = 1; j <= 5001; j ++ ) {
            sa[i][j] += sa[i - 1][j] + sa[i][j - 1] - sa[i - 1][j - 1];
        }
    }

    int res = 0;

    for(int i = r; i <= 5001; ++i) {
        for(int j = r; j <= 5001; ++j) {
            res = max(res, sa[i][j] - sa[i][j - r] - sa[i - r][j] + sa[i - r][j - r]);
        }
    }

    printf("%d", res);

    return 0;
}

差分

对于一个给定的数组 A A A ,他的差分数列 B B B 定义为:
B [ 1 ] = A [ 1 ] ,   B [ i ] = A [ i ] − A [ i − 1 ]   ( 2 ≤ i ≤ n ) B[1] = A[1],~B[i]=A[i]-A[i - 1] ~(2\le i \le n) B[1]=A[1], B[i]=A[i]A[i1] (2in)

“差分”与“前缀和”其实是一对互逆运算,差分数组 B B B 的前缀和数组就是数组 A A A ,前缀和数组 S S S 的差分数组也是数组 A A A

在数组 A A A 的区间 [ l , r ] [l,r] [l,r] 上加上 d d d (即把 A l , A l + 1 , ⋯   , A r A_l,A_{l+1},\cdots,A_r Al,Al+1,,Ar 都加上 d d d ),其差分数组 B B B 的变化就是在 B l B_l Bl d d d B r + 1 B_r + 1 Br+1 d d d ,其它位置不变。

通常这个操作可以使得原本在数组 A A A 上的区间操作,变为在数组 B B B 上的单点操作。

【例题】增减序列

给定一个长度为 n   ( n ≤ 1 0 5 ) n~(n\le 10^5) n (n105) 的数列 { a 1 , a 2 , ⋯   , a n } \{a_1,a_2,\cdots,a_n\} {a1,a2,,an} ,每次可以选择一个区间 [ l , r ] [l,r] [l,r] ,使下标在这个区间内的数都加 1 1 1 或者减 1 1 1

求至少需要多少次操作才能使数列中的所有数都一样,并求出在保证最少次数的前提下,最终得到的数列有多少种。

分析:

如果仅仅看区间操作,那么就很难处理这个情况,但是我们要是使用差分数组来看这个数列就不一样了。

假设数列 a a a 的全部元素已经相同了,即 { a , a , ⋯   , a } \{a,a,\cdots,a\} {a,a,,a} ,那么它的差分数就是 { a , 0 , ⋯   , 0 } \{a,0,\cdots,0\} {a,0,,0},可以看出除了第一项为 a a a ,其它数都为 0 0 0 ,也就是我们要通过区间操作使得差分数列的 b 2 , b 3 , ⋯   , b n b_2,b_3,\cdots,b_n b2,b3,,bn 变为 0 0 0 ,这时原来的区间操作就变为了差分序列的单点操作。

如果在 b 2 , b 3 , ⋯   , b n b_2,b_3,\cdots,b_n b2,b3,,bn 上使用单点操作可以使用使得两个正负相反的使用一次操作抵消,所有要优先去除他的,这里花费的次数就是 m i n ( p , q ) min(p,q) min(p,q) p p p 为在 b 2 , b 3 , ⋯   , b n b_2,b_3,\cdots,b_n b2,b3,,bn 上所有正数的和, q q q 为在 b 2 , b 3 , ⋯   , b n b_2,b_3,\cdots,b_n b2,b3,,bn 所有负数的绝对值和。

这样操作完了后,还剩下 ∣ p − q ∣ |p - q| pq 的值需要变为 0 0 0 ,这时可以与 b 1 b_1 b1 搭配消耗掉,也可以和 b n + 1 b_{n + 1} bn+1 一起搭配消耗掉,而差分数列的 b 1 b_1 b1 有多少种取值,就代表有多少原数组 A A A 全部相同的种类数。所以 b 1 b_1 b1 的变化区间就在 [ b 1 , b 1 + ∣ p − q ∣ ] [b_1,b_1+|p-q|] [b1,b1+pq] 这个区间内,总数为 ∣ p − q ∣ + 1 |p - q| + 1 pq+1 个。

代码如下:

#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1e5 + 5;
int n;
LL a[N], b[N];

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

    LL  ans = 0, q = 0, p = 0;
    for (int i = 2; i <= n; i ++ ) {
        if(b[i] > 0) q += b[i];
        if(b[i] < 0) p += -b[i];
    }

    LL res = a[1];

    printf("%lld\n%lld", min(q, p) + abs(p - q), abs(p - q) + 1);

    return 0;
}

【例题】最高的牛

N N N 头牛站成一行。两头牛能互相看见,当且仅当它们中间的牛的身高都比它们矮。现在,我们只知道其中最高的牛是第 P P P 头,它的身高是 H H H ,不知道剩余 N − 1 N - 1 N1 头牛的身高。但是,我们还知道 M M M 对关系,每对关系都指明了两头牛 A i A_i Ai B i B_i Bi 可以互相看见。求每头牛的身高最大可能是多少。

数据范围: 1 ≤ N , M ≤ 1 0 4 ,   1 ≤ H ≤ 1 0 6 1\le N,M\le 10^4, ~1\le H\le10^6 1N,M104, 1H106

分析:

确定了最高身高 H H H 后,我们也确定了每头牛最高也是 H H H 了。最初没有任何关系的约数,就代表所以牛都可以是 H H H ,但是有了关系的约束后,设这个关系就是 a i a_i ai b i b_i bi a i < b i a_i < b_i ai<bi),那么在 [ a i + 1 , b i − 1 ] [a_i +1,b_i-1] [ai+1,bi1] 这段区间内的牛至少身高得减去 1 1 1 ,这样才不会遮蔽视野。这时区间减一的操作我们就可以利用差分来变为单点操作了,而原本全为 H H H 的数组,它的差分数组也变为了 [ H , 0 , ⋯   , 0 ] [H,0,\cdots,0] [H,0,,0] 这样的。最后跑一个前缀和就可以求出满足条件的最大牛身高了。

因为关系可能重复,而我们做了一次后就不必再做一次,所以要去重操作。

代码如下:

#include <iostream>
#include <algorithm>
#include <map>
using namespace std;
typedef pair<int, int> PII;
const int N = 10010;
int b[N];
map<PII, bool> mp;

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

    while (m -- ) {
        int u, v;
        scanf("%d%d", &u, &v);
        if(u > v) swap(u, v);
        PII t = {u, v};

        if(mp.count(t)) continue;

        b[u + 1] -=1;
        b[v] += 1;

        mp[t] = true;
    }

    for (int i = 1; i <= n; i ++ )
        b[i] += b[i - 1];

    for (int i = 1; i <= n; i ++ ) {
        printf("%d\n", b[i]);
    }

    return 0;
}
  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值