刷题笔记-前缀和&差分

前缀和

前缀和是快速求取静态数组内某一区间内所有数的和的一种方法。如果在求取的过程中数组发生变动,则可以采用树状数组、线段树等方法。

例题1 一维前缀和

输入一个长度为n的整数序列。
接下来再输入m个询问,每个询问输入一对l, r。
对于每个询问,输出原序列中从第l个数到第r个数的和。
输入格式
第一行包含两个整数n和m。
第二行包含n个整数,表示整数数列。
接下来m行,每行包含两个整数l和r,表示一个询问的区间范围。
输出格式
共m行,每行输出一个询问的结果。

数据范围

(1≤l≤r≤n)
(1≤n,m≤100000)
(−1000≤数列中元素的值≤1000)

#include <cstring>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n,m;
int s[N];
int a[N];
int main(void)
{
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++)
    {
        scanf("%d",&a[i]);
        s[i] = s[i-1] + a[i];
    } 
    while(m--)
    {
        int l,r;
        scanf("%d%d",&l,&r);
        printf("%d
",s[r] - s[l-1]);
    }
    return 0;
    
}

例题2 二维前缀和

输入一个n行m列的整数矩阵,再输入q个询问,每个询问包含四个整数x1, y1, x2, y2,表示一个子矩阵的左上角坐标和右下角坐标。
对于每个询问输出子矩阵中所有数的和。
输入格式
第一行包含三个整数n,m,q。
接下来n行,每行包含m个整数,表示整数矩阵。
接下来q行,每行包含四个整数x1, y1, x2, y2,表示一组询问。
输出格式
共q行,每行输出一个询问的结果。
数据范围
(1≤n,m≤1000)
(1≤q≤200000)
(1≤x1≤x2≤n)
(−1000≤矩阵内元素的值≤1000)

#include <cstring>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;
int a[N][N];
int s[N][N];
int n,m,q;
int main(void)
{
    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]);
            s[i][j] = s[i -1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j];
        }
    while(q--)
    {
        int x1,x2,y1,y2;
        scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
        printf("%d
",s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1]);
    }
    return 0;
    
            

}

练习1 K倍区间

给定一个长度为N的数列,如果其中一段连续的子序列之和是K的倍数,我们就称这个区间[i,j]是K倍区间。
你能求出数列中总共有多少个K倍区间吗?

数据范围

(1≤N,K≤100000)
(1≤Ai≤100000)

思路:

  1. 通过二重循环控制子序列的起始和终止位置,遍历求每个每个子序列的和(S[r] - S[l-1] % k)是否等于0,时间复杂度是(O(n^2)),大约(10^9)量级,所以需要考虑降低时间复杂度。
    2.降低时间复杂度的一般方式是用空间换时间.
  2. S[r] - S[l-1] % k == 0 等价于 S[r] % k == S[l-1] % k,所以可以声明一个数组用来保存余数为(s[i] % k)的个数,这样只需要一层循环即可.
    4.注意数据溢出的情况

代码:

#include <cstring>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 100010;
int n,k;
LL s[N],cnt[N];
int main(void)
{
    scanf("%d %d",&n,&k);
    //计算前缀和
    for(int i = 1; i <= n;i++){
        scanf("%lld",&s[i]);     //注意LL的读取
        s[i] += s[i-1];
    }
    LL ans = 0;                 //注意答案可能溢出
    for(int i = 0;i <= n;i++)   //考虑和为0也也是k倍区间,所以从0开始遍历
    {
        //假设已经有两个S[i]的余数为x,则又有一个S[i]%k == x时,会产生两个新的答案,所以有下面两句
        ans += cnt[s[i] % k];
        cnt[s[i] % k]++;
    }
    printf("%lld",ans);
    return 0;
}

练习2 -激光炸弹

思路:

这也是一个典型的二维前缀和的问题,大致方向很好想,但是一些处理细节还是很有技巧的。
需要注意一点:对于一个(R×R)的炸弹,最多只能炸到((R)×(R))个格点,因为边界是炸不到的:

代码1:

#include <cstring>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 5010;
int s[N][N];
int n,r;
int ans;
int main(void)
{
    scanf("%d%d",&n,&r);
    int x,y,w;
    int xr = 0,yr = 0,xl = 1e5,yl = 1e5;
    for(int i = 0;i < n;i++)
    {
        scanf("%d%d%d",&x,&y,&w);
        xr = max(xr,x+1);   yr = max(yr,y+1);
        xl = min(xl,x+1);   yl = min(yl,y+1);
        s[x+1][y+1] = w;
    }
    for(int i = 1; i <= xr;i++)
        for(int j = 1;j <= yr;j++)
            s[i][j] += s[i-1][j] + s[i][j-1] - s[i-1][j-1];
                
    int chang = xr - xl+ 1, kuan = yr - yl + 1;
    if(chang < r && kuan >= r)
    {
        int x1 =  xl,x2 = xr;
        for(int y1 = yl,y2 = y1+r-1;y2 <= yr; y1++,y2++ )
            ans = max(ans,   s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1] );
    }
    else if(chang >= r && kuan < r)
    {
        int y1 =  yl,y2 = yr;
        for(int x1 = xl,x2 = x1+r-1;x2 <= xr; x1++ ,x2++)
            ans = max(ans,   s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1] );
    }
    else if(chang < r && kuan < r)
            ans = s[xr][yr];
    else
    {
        for(int x1 = xl,x2 = x1+r-1; x2 <= xr;x1++, x2++)
            for(int y1 = yl,y2 = y1+r-1; y2 <= yr; y1++, y2++){
                int tmp = s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1];
                ans = max(ans,  tmp );
            }
                
    }
    printf("%d",ans);
    return 0;
}

这一份代码冗余在:1.对边长R和阵地长宽进行分类讨论(其实没有必要). 2.使用了两个坐标来计算,不如使用一个坐标 + 边长R的方式好。

代码2 -重构:

#include <cstring>
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 5010;
int s[N][N];
int n,r;
int ans;
int main(void)
{
    scanf("%d%d",&n,&r);
	//阵地面积最多5000 * 5000
    r = min(5001,r);     
    int xr = r,yr = r;
    for(int i = 0;i < n;i++)
    {
        int x,y,w;
        scanf("%d%d%d",&x,&y,&w);
        xr = max(xr,x+1);   yr = max(yr,y+1);
        s[x+1][y+1] = w;
    }
    //求前缀和
    for(int i = 1; i <= xr;i++)
        for(int j = 1;j <= yr;j++)
            s[i][j] += s[i-1][j] + s[i][j-1] - s[i-1][j-1];
    //枚举右下角坐标(i,j)则子矩阵(i-r+1,j-r+1) (i,j)的和计算如下:        
    for(int i = r;i <= xr;i++)     
        for(int j = r;j <= yr;j++)
            ans = max(ans ,  s[i][j] - s[i-r][j] - s[i][j-r] + s[i-r][j-r]);

    printf("%d",ans);
    return 0;
}

差分

差分用来简化对原序列的一部分加上同一个数a的操作,如果直接操作原数组,则时间复杂度O(n),但是使用差分则可以降低到O(1);
在需要多次进行上述操作的情况下,使用差分会带来时间上显而易见的好处。

一维差分 -模板题

思路:

  • 差分是前缀和的逆运算,也就是说将原数组看作S数组,则差分数组b[i] = s[i] - s[i-1]
  • 关键操作:将s[l,r]加上c ,等价于b[l] += c , b[r + 1] -= c
  • 原数组s和差分数组b都需要从1开始存储,且差分数组b还需要再后面多留出1位。

代码:

#include <cstdio>
#include <iostream>
using namespace std;

const int N = 100010;

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


int main(void)
{
    cin >> n >> m;
    
    for(int i = 1; i<= n; i++)
    {
        scanf("%d",&s[i]);
        //构造差分数组
        b[i] = s[i] - s[i - 1];
    }

    int l,r,v;
    while(m --)
    {
        scanf("%d%d%d" ,&l , &r ,&v);
        b[l] += v, b[r + 1] -= v;
    }
    //反推原数组
    for(int i = 1; i <= n; i++)
    {
        s[i] = s[i-1] + b[i];
        printf("%d ",s[i]);
    }
    return 0;
}

二维差分

思路:

  1. b(x,y) = s(x,y) - s(x-1,y) - s(x,y-1) + s(x-1,y-1)
    2.对原矩阵(x1,y1),(x2,y2)加上c:

代码:

#include <cstdio>
#include <iostream>
using namespace std;

const int N = 1010;
int s[N][N],b[N][N];

int n, m, q;

int main(void)
{
    cin >> n >> m >> q;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
        {
            scanf("%d",&s[i][j]);
            //构造差分数组
            b[i][j] = s[i][j] - s[i-1][j] - s[i][j-1] + s[i-1][j-1];
        }
    
    while(q --)
    {
        int x1,x2,y1,y2,c;
        scanf("%d%d%d%d%d", &x1, &y1, &x2, &y2, &c);
        b[x1][y1] += c;
        b[x2 + 1][y2 + 1] += c;
        b[x1][y2 + 1] -= c;
        b[x2 + 1][y1] -= c;
    }
    //生成原数组
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            s[i][j] = b[i][j] + s[i-1][j] + s[i][j-1] - s[i-1][j-1];
            printf("%d ",s[i][j]);
        }
        puts("");
    }
    
    return 0;
}

三体攻击 (二分 + 三维差分)

思路:

1.第一艘飞船可能再任意一次攻击之后爆炸,所以可以使用二分,分界条件是刚好有飞船爆炸。
2.记忆三维前缀和:写出0~7的二进制,如果1的个数是奇数,则符号是+,反之符号是-;
3.关键操作:写出0~7的二进制,1对应的位置是右下角坐标+1,如果1的个数是奇数,则符号是+ ,反之符号是-;

代码:

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

typedef long long LL;

const int N = 2000010;

LL bp[N], s[N], b[N];
int op[N / 2][7];
int A , B , C ,m;

int d[8][4] = {
    {0 , 0 , 0 , 1},
    {0 , 0 , 1 , -1},
    {0 , 1 , 0 , -1},
    {0 , 1 , 1 , 1},
    
    {1 , 0 , 0 , -1},
    {1 , 0 , 1 , 1},
    {1 , 1 , 0 , 1},
    {1 , 1 , 1 , -1}
};

int getIndex(int i ,int j ,int k)
{
    return (i * B + j) * C + k;
}

bool check(int  mid)
{
    memset(s , 0 ,sizeof s);
    memcpy(b, bp , sizeof b);
    
    for(int i = 1; i <= mid; i++)
    {
        int x1 = op[i][0], x2 = op[i][1], y1 = op[i][2] , y2 = op[i][3] , z1 = op[i][4] , z2 = op[i][5], h = op[i][6];
        b[getIndex(x1 ,     y1 ,     z1)]        -= h;
        b[getIndex(x1 ,     y1 ,     z2 + 1)]    += h;
        b[getIndex(x1 ,     y2 + 1 , z1)]        += h;
        b[getIndex(x1 ,     y2 + 1 , z2 + 1)]    -= h;
        
        b[getIndex(x2 + 1 , y1 ,     z1)]        += h;
        b[getIndex(x2 + 1 , y1 ,     z2 + 1)]    -= h;
        b[getIndex(x2 + 1 , y2 + 1 , z1)]        -= h;
        b[getIndex(x2 + 1 , y2 + 1 , z2 + 1)]    += h;
    }
    
    for(int i = 1; i <= A ; i++)
        for(int j = 1; j <= B; j++)
            for(int k = 1; k <= C; k++)
            {
                s[getIndex(i , j , k)] += b[getIndex(i , j , k)];
                for(int u = 1; u < 8; u++)
                {
                    int x = i - d[u][0], y = j - d[u][1] , z = k - d[u][2] , t = d[u][3];
                    s[getIndex(i ,j ,k)] -= s[getIndex(x , y , z)] * t;       //不要写成b[]
                }
                if(s[getIndex(i , j, k)] < 0)  return true;
            }
    
    return false;
}
int main(void)
{
    cin >> A >> B >> C >> m;
    
    for(int i = 1; i <= A; i++)
        for(int j = 1; j <= B; j++)
            for(int k = 1; k <= C; k++)
                scanf("%lld", &s[getIndex(i , j , k)]);
    
    //构造差分数组
    for(int i = 1; i <= A; i++)
        for(int j = 1;j <= B; j++)
            for(int k = 1; k <= C ; k++)
                //使用偏移量枚举的方式
                for(int u = 0; u < 8; u++)
                {
                    int x = i - d[u][0], y = j - d[u][1] , z = k - d[u][2] , t = d[u][3];
                    bp[getIndex(i , j , k)] += s[getIndex(x , y , z)] * t; 
                }
    
    //读取进攻的坐标,从1读取可以和次数对应
    for(int i = 1; i <= m; i++)
        for(int j = 0; j < 7; j++)
            scanf("%d", &op[i][j]);
    
    //二分求刚好有飞船爆炸的分界点
    int l = 1 ,r = m;
    while(l < r)
    {
        int mid = l + r >> 1;
        if(check(mid))
            r = mid;
        else
            l = mid + 1;
    }
    
    cout << r << endl;
    
    return 0;
}


原文地址:https://www.cnblogs.com/zy200128/p/12616856.html