前缀和心得

今天做牛客网西南民大的题遇到了二维前缀和的题目,是个比较经典出过很多次的题目了,

看来光掌握一维前缀和还是远远不够的,二维前缀和也必须很熟练。

前缀和

前缀和可以理解为数学上的数列的前n项和(对于一个一维数组的前缀和)。

我们定义一个数组a的前缀和数组sum[i]=a[1]+a[2]+...+a[i],一般用for循环从1遍历到n,sum[i]=sum[i-1]+a[i].有点DP的意味,从前一个推出后一个。将中间结果保存下来, 避免重复计算, 用以提高运算效率。

应用:求连续区间段和。

举例:已知n个数的数列a,有m次询问,每次询问给定l,r两个数,求 al到 ar 内所有数的和。注意l到r的区间包含 al和 ar 两个数。 

对于这个问题而言,我们一开始想到的思路是对于每一次查询操作都遍历一遍我们的数组进行求和,这样的操作每一次都会遍历数组,是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)的时间复杂度。

 

 

int init() {
    for(int i = 1; i <= n; i++) sum[i] = sum[i-1] + a[i];
}
int get(int l, int r) {
    return sum[r] - sum[l-1];
}

  

二维前缀和

与一维前缀和类似,设sum[i][j]表示所有a[i'][j'](1≤i'≤i,1≤j'≤j)的和。

有一点像“矩形的面积”那样,把一整块区域的值都加起来。

 

前缀和的用途

一般用来求区间和。

对于一维的情况,现在给你一个数列a,要求你回答m次询问,每次询问下标j到k的和。朴素的做法显然是对于每一次询问都执行一次相加操作,然后输出结果。这样做是对的,但是m过大的时候就会导致计算次数过多而超时。

超时的原因一目了然,重复计算。那么我们应该如何改进方案呢?想象一下,我们如果提前计算好了每一个位置的前缀和,再用s[k]-s[j],结果不就是我们这次询问的答案吗!这样使得计算量大大减小。

对于二维区间和也是类似的。

借助这个图片。假设在这个矩阵(二维数组)中,我们要求和的是上图中红色区域。现在我们已经预处理好了所有点的前缀和。现在给定两个点(x1,y1),(x2,y2)。我们要求以这两个点连线为对角线的一个子矩阵的数值之和。暴力法为直接挨个相加,早晚的TLE。考虑前缀和的快速做法。

首先可以把s[x2][y2]求出来,它代表整个大矩形的前缀和,然后我们分别减去左边多的一块前缀和和下边多出来的一块前缀和,但这还不是最终答案!显然,当我们减去两个多余的区间时,下边的一小块被减了两次,应该加回来。

所以对一次查询的答案ans为s[x2][y2]-s[x2][y1-1]-s[x1-1][y2]+s[x1-1][y1-1]

这个二维前缀和也称为差分序列。

 

int init() {
    for(int i = 1; i <= n; i++) {
        for(int j = 1; j <= m; j++) {
            sum[i][j] = sum[i][j-1] + sum[i-1][j] - sum[i-1][j-1] + a[i][j];
        }
    }
}
int get(int x1, int y1, int x2, int y2) {
    return sum[x1][y1] - sum[x1][y2 - 1] - sum[x2 - 1][y1] + sum[x2 - 1][y2 - 1];
}

 

int a[11][11],b[11][11],c[11][11];
int main()
{
    int x1,y1,x2,y2;//坐标的大小关系如图
    cin >> x1 >> y1 >> x2 >> y2;//输入

    for(int i = 1; i <= 10; ++i)//初始化矩阵a
        for(int j = 1; j <= 10; ++j)
            a[i][j] = 1;

    for(int i = 1; i <= 10; ++i)//b[i][j]记录矩阵a中第i行第1个数到第j个数的和
        for(int j = 1; j <= 10; ++j)
            b[i][j] = b[i][j - 1] + a[i][j];

    for(int j = 1; j <= 10; ++j)//c[i][j]记录矩阵b中第j列第1个数到第i个数的和
        for(int i = 1; i <= 10; ++i)
            c[i][j] = c[i - 1][j] + b[i][j];

    cout << c[x2][y2] - c[x1 - 1][y2] - c[x2][y1 - 1] + c[x1 - 1][y1 - 1];//参照下图
    return 0;
}

  补充:代码量很小的写法

void calc()
{
    for(int i = 1; i <= n; ++i)
        for(int j = 1; j <= m; ++j)
            sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + a[i][j];//很巧妙
}
int query(int x1,int y1,int x2,int y2)
{
    return sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1];
}

  

应用问题 

核心就两个字:降维

面对许多高维问题,往往前缀和是最先想到的降维方法。 
这样在降维的基础上,许多更进一步的优化才能实现。

 

 

/*
如图,a[2][2]的前缀和就是2+3+2+7,a[1][4]的前缀和就是2+3+4+8 
以此类推,每一个坐标的前缀和,都是该坐标到矩阵左上角的数字之和; 
这样,任意子矩阵的数字和,可以在O(1)时间内查询; 
*/ 
for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)
        b[i][j]+=b[i][j-1];

 for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)
        b[i][j]+=b[i-1][j];//计算前缀和

  

【推荐题目】:

1.http://acm.hdu.edu.cn/showproblem.php?pid=5084 hdu 5084 前缀和预处理 

2. bzoj1218 激光炸弹 二维前缀和

3.Codeforces 611C:New Year and Domino 二维前缀和

4.https://loj.ac/problem/6256   #6256. 「CodePlus 2017 12 月赛」可做题1

 

posted @ 2017-12-30 21:30  Roni_i  阅读(306)  评论(0编辑  收藏  举报