zoukankan      html  css  js  c++  java
  • 刷题笔记-前缀和&差分

    前缀和

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

    例题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;
    }
    
    
    
  • 相关阅读:
    EF框架 处理decimal字段 Sum() 遇到NULL时的特殊处理
    RSA加密解密及RSA签名和验证
    SQL 类似switch的东东用法
    js抛物线动画
    MyBatis的结果映射(resultMap)
    mybatis中#和$符号的区别
    MyBatis 中Mapper.xml中注意事项
    MyBatis sql映射器 Mapper
    MyBatis的自定义别名和内置别名
    MyBatis简单认识(入门)
  • 原文地址:https://www.cnblogs.com/zy200128/p/12616856.html
Copyright © 2011-2022 走看看