一维数组和二维数组前缀和

来源:互联网 发布:windows date命令详解 编辑:程序博客网 时间:2024/05/21 06:14

原创地址如下

http://blog.csdn.net/XT_NOI/article/details/72666275      (一维数组)

http://m.blog.csdn.net/XT_NOI/article/details/72715904    (二维数组)

一维前缀和维护是一种基础的小算法,该算法用我们所熟知的数列求和方式优化我们的某些查询操作,是一种动态规划的思想。

这里为介绍一维前缀和维护问题,我们来思考这个最简单的问题:

问题描述:
已知n个数的数列a,有m次询问,每次询问给定l,r两个数,求 alar 内所有数的和。注意l到r的区间包含 alar 两个数。
输入数据:
第一行n和m,接下来一行有n个数,表示数列a,接下来有m行,每行有两个数l,r,详细解释参考问题描述。
输出数据:
m行,每行一个求和解。
输入样例:
5 2
1 2 3 4 5
2 4
3 5
输出样例:
9
12
数据范围:
0<N,M100000
ai 是int范围内的任意整数

对于这个问题而言,我们一开始想到的思路是对于每一次查询操作都遍历一遍我们的数组进行求和,这样的操作每一次都会遍历数组,是O(nm)的做法,而对于题目的数据量,这样一定会超时,于是我们要思考有没有更加优化的解。

先考虑暴力的情况,我们对于每一次查询操作,都遍历一遍数组,我们会发现,当我计算了某一区间值的时候,我计算其他区间,会重复计算某些值,我是否可以用一种方式,把这种重复利用起来。

于是我们想到了初中学过的前n项和问题。当我知道前r项和与前l-1项和,我是否就能求出从l到r的区间和?思考一下这个过程。前r项和里面包含了前l-1项和,所以做一个减法就能求出l到r的区间和。那么问题就到了我们如何求前n项和,这个问题更加简单,我们只需要遍历一次数组就可以求到这个前n项和的数组了,假设我们原数组是a,前n项和的数组是s,那么我们只需要s1=a1之后维护动规方程si=si1+ai(i>1)即可。

那么问题迎刃而解,我们分析一下新解法的复杂度,求出s数组需要遍历一遍原数组,所以是O(n)的复杂度,对于每一次查询操作我们只需要做一次减法,所以是O(1)的复杂度,一共有m次,所以是O(m)的查询,最后的结果就是O(n+m)的时间复杂度。

以上就是关于一维的前缀和维护问题和解法的介绍。

代码如下:

#include <iostream>#include <cstring>#include <cstdlib>#include <cstdio>#include <ctime>#include <cmath>#include <algorithm>using namespace std;const int MAXN=1000010;int n,m,a[MAXN];//读入优化 int read(){    char ch=getchar();    bool fl=0;    int r=0;    if(ch=='-'){        fl=1;        ch=getchar();    }     while(ch>='0'&&ch<='9'){        r*=10;        r+=ch-'0';        ch=getchar();    }    return fl?-r:r; }//输出优化 void write(int x){    if(x<0){        putchar('-');        x=-x;    }    if(x>9){        write(x/10);    }    putchar(x%10+'0');}int main(){    freopen("in.txt","r",stdin);    freopen("std.txt","w",stdout);    n=read();    m=read();    a[0]=read();    for(int i=1;i<n;++i){        a[i]=read();        a[i]+=a[i-1];//前n项和求法,O(n)复杂度     }    for(int i=0;i<m;++i){        int l,r;        l=read();        r=read();        if(l==1){            write(a[r-1]);//输出结果,单次O(1),总共O(m)         }else{            write(a[r-1]-a[l-2]);//隐含的数组下标越界问题,在我写二维前缀和的时候发现的         }        putchar('\n');    }    return 0;}

相信来看二维前缀和维护的各位一定是对一维前缀和维护问题有足够的了解了,那么二维的前缀和维护实际上是在一维前缀和维护的基础上的升级,把一个数列升级成了矩阵,但是思想是一样的,具体问题如下:

问题描述:
已知n*n的矩阵a,有m次询问,每次询问给定x1,y1,x2,y2四个数,求以(x1,y1)为左上角坐标和(x2,y2)为右下角坐标的子矩阵的所有元素和。注意仍然包含左上角和右下角的元素。
输入数据:
第一行n和m,接下来一行有n行,每行n个数,表示矩阵a,接下来有m行,每行有四个数x1,y1,x2,y2,详细解释参考问题描述。
输出数据:
m行,每行一个求和解。
输入样例:
5 2
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20
21 22 23 24 25
2 2 4 4
3 3 5 5
输出样例:
117
171
数据范围:
0<N10000<M100000
ai,j是int范围内的任意整数

我们肯定能够想到,如果单纯的使用暴力解决这个问题是O(mn2)的做法,对于这个题的数据范围是绝对超时的。所以我们也能想到是使用了一个类似于一维前缀和维护的优化方法的优化方式。但那是什么?当我们转换成为矩阵的时候,前n项和的做法就不行了。

但我们还有一个东西叫做容斥定理。容斥定理是关于集合的一个定理。这个定理是小学奥数内容,而且我们在高中课程中学习集合的时候已经了解过了,但你或许不知道这个名字。

在小学奥数中,容斥定理被描述的极其简单,如果我有n个抽屉,有m个苹果,我往抽屉里面放苹果,保证每个抽屉都有苹果的情况下,如果kn<m,那么必定会有一个抽屉有至少k+1个苹果。

当然,在我们学习过集合之后,容斥定理就可以这样来描述:

设有A,B,C三个集合,这三个集合相互均有交集,并且三个集合之间也有交集。则这三个集合的并集可以表示为:

ABC=A+B+CABBCAC+ABC

我们使用文氏图可以得到一个更直观的解释:

文氏图

相信数学好的同学已经通过图看出来这个公式了,用更通俗易懂的道理来解释,三个集合,首先直接相加,发现ABBCAC的三个部分被重复添加了,之后减去这三个部分,又发现ABC的部分都被减去了三次,实际上对于这部分被加了三次(在A+B+C的时候这部分就被加了三次)之后又减了三次(在减去ABBCAC的时候分别被减去了一次),实际上就跟没加过没减过一样,所以我们要再加一次。所以公式就变成上面那个样子。

这里我们只推了3个集合的容斥定理,对于多个集合思路是一样的。我们可以把所有集合相加,同时减去所有偶数重复,加上所有奇数重复。简单来记忆就是减偶加奇。这个过程可以自己推导,所以这里不再赘述。

现在我们来把这个定理应用到我们的矩阵上。现在我们用S(x1,y1,x2,y2)表示以(x1,y1)为左上元素和以(x2,y2)为右下元素的矩阵中所有元素的和。对于任意S(x1,y1,x2,y2),它一定等于S(1,1,x2,y2)加上S(1,1,x11,y11)减去S(1,1,x11,y2)减去S(1,1,x2,y11)。也就是这个公式:

S(x1,y1,x2,y2)=S(1,1,x2,y2)+S(1,1,x11,y11)S(1,1,x11,y2)S(1,1,x2,y11)

下面我们举个例子来解释下这个公式:

原矩阵

以这个矩阵为例,i,j是这个矩阵的两个下标,我们计算S(2,2,4,4):

按照公式我们首先计算S(1,1,x2,y2),也就是S(1,1,4,4),这个矩阵是以(1,1)为左上角,以(4,4)为右下角,它的和是(1+2+3+4+6+7+8+9+11+12+13+14+16+17+18+19)。

矩阵(1,1)->(4,4)

之后我们调换一下顺序,计算一下S(1,1,x11,y2),也就是-S(1,1,1,4)这个矩阵,表示以(1,1)为左上角,以(1,4)为右下角的矩阵,减去这部分的和,即-(1+6+11+16)。

矩阵(1,1)->(1,4)

之后再计算S(1,1,x2,y11),也就是-S(1,1,4,1)这个矩阵,表示以(1,1)为左上角,(4,1)为右下角的矩阵,减去这部分的和,就是-(1+2+3+4)。

矩阵(1,1)->(4,1)

根据之前讲的容斥定理不难发现,以(1,1)为左上角且以(x11,y11)为右下角的矩阵(在这里就是(1,1)位置的元素)被加和了一次减去了两次,所以我们要再加一次,所以加1。

矩阵(1,1)->(1,1)

最终我们做了(1+2+3+4+6+7+8+9+11+12+13+14+16+17+18+19)-(1+6+11+16)-(1+2+3+4)+1,恰好是(2,2)为左上角(4,4)为右下角的矩阵和(即7+8+9+12+13+14+17+18+19)。下面我用一个图来解释刚才的四处矩阵。

结果

从图不难看出,我们首先加上蓝色矩阵,然后减去黄色矩阵,减去红色矩阵,发现棕色矩阵被减去了两次而加了一次,根据容斥定理,我们应该再加一次棕色矩阵,最后结果就是我们蓝色的9个数字的和,这就是我们要求得的子矩阵了。结合上面的解释,最后就是这个公式:

S(x1,y1,x2,y2)=S(1,1,x2,y2)+S(1,1,x11,y11)S(1,1,x11,y2)S(1,1,x2,y11)

所以接下来我们发现,对于任何一个矩阵的求和,我都只需要知道以(1,1)为左上角的四个矩阵的和就行了,这个问题只需要一个O(n2)的算法就能解决。这个算法类似于刚刚求子矩阵的和的算法,我们要新算出一个数组Si,j,对于这个数组中的某一元素Si,j表示以(1,1)为左上角,(i,j)为右下角的矩阵的元素和。

以刚刚的数组为例:

S数组

观察这里的S数组,我们不难发现,对于第一行来说,都可以按照一维的处理方法处理,对于第一列来说也是如此的处理,那么对于其他位置的数呢?我们假设原来的数组是ai,j则满足Si,j=ai,j+Si1,j+Si,j1Si1,j1,这个可以用刚刚的容斥定理来解释,对于4格的矩阵而言,这里以(2,2)为左上角(3,3)为右下角的矩阵举例,计算S3,3的公式就是S3,3=a3,3+S2,3+S3,2S2,2,可以看着上面的两个数组表来对应一下,a3,3就是13,S2,3是39,S3,2是27,S2,2是16,最后结果就是63。并且我们不难发现,我们计算Si,j的值只需要Si1,jSi,j1Si1,j1这三个数,而这三个数按照自上而下自左而右的遍历顺序已经被计算出来了,而最上一行和最左一列都可以单独处理,于是我们的S数组就求出来了,接下来根据之前的公式就可以算出任意的子矩阵的求和了。这样的算法时间复杂度就是O(m+n2)

以上就是对二维前缀和问题的介绍。

我的代码如下:

#include <iostream>#include <cstring>#include <cstdlib>#include <cstdio>#include <ctime>#include <cmath>#include <algorithm>using namespace std;const int MAXN=1010;int n,m,a[MAXN][MAXN];//读入优化 int read(){    char ch=getchar();    bool fl=0;    int r=0;    if(ch=='-'){        fl=1;        ch=getchar();    }     while(ch>='0'&&ch<='9'){        r*=10;        r+=ch-'0';        ch=getchar();    }    return fl?-r:r; }//输出优化 void write(int x){    if(x<0){        putchar('-');        x=-x;    }    if(x>9){        write(x/10);    }    putchar(x%10+'0');}int main(){    freopen("in.txt","r",stdin);    freopen("std.txt","w",stdout);    n=read();    m=read();    //前n项子矩阵和求法,O(n*n)复杂度     a[0][0]=read();    for(int i=1;i<n;++i){        a[0][i]=read();        a[0][i]+=a[0][i-1];    }    for(int i=1;i<n;++i){        a[i][0]=read();        a[i][0]+=a[i-1][0];        for(int j=1;j<n;++j){            a[i][j]=read();            a[i][j]=a[i][j]+a[i-1][j]+a[i][j-1]-a[i-1][j-1];        }    }    for(int i=0;i<m;++i){        int x,y,x1,y1;        x=read();        y=read();        x1=read();        y1=read();        if(x==1 && y==1){            write(a[x1-1][y1-1]);         }else if(x==1){            write(a[x1-1][y1-1]-a[x1-1][y-2]);        }else if(y==1){            write(a[x1-1][y1-1]-a[x-2][y1-1]);        }else{            write(a[x1-1][y1-1]-a[x-2][y1-1]-a[x1-1][y-2]+a[x-2][y-2]);//输出结果,单次O(1),总共O(m)         }        //这里我一开始犯了个致命的错误,当我和我的暴力程序打对拍的时候才发现        //当x或者y为1的时候,我们可能不需要减去某些不存在的矩阵        //如果你是按照1为数组第一个数的下标的时候是完全不需要考虑这个问题的        //因为0下标的一定是0        //但对于我的程序,我作为一名专业的程序员 不是很喜欢1开始,所以这里我发现了致命的越界问题        //这也是导致我在对拍的时候爆0的原因        //所以我才会用连环的if-else-if来解决这个问题         putchar('\n');    }    return 0;} 
原博主数组下标从0开始,因此每次要减一,为了好操作也可以从一开始让下表为零的数组为零,代码中减二是因为单纯的下标相减后得到的结果不包含前坐标那个位置包含的数,所以要向前移动一格。
原创粉丝点击