在讲算法之前,我们先来思考一个问题:小明有n个编号为1~n的篮子,每个篮子里装有ai个苹果,求从 x至y 的篮子里的苹果数量之和。
如果没学过前缀和的同学,可能会打出这样的代码:
这种算法要得出一个区间之和,这题只需要取一次区间值,时间复杂度需要O(n),但如果2次,4次,1000次,数据再一大,暴力算法肯定会TLE(超时),这时前缀和的优势就体现出来了,因为它取区间之和,只需要O(1)。
那前缀和的思想是什么呢?又是如何实现用O(1)取区间之和的呢?其思想就是遍历1至n,算出1至当前数字的区间之和,有人或许会问了,那这样也无法算出特定区间之和啊,但我们观察后发现,在累加数组,前缀为右顶点的数字包含了前缀为左顶点的数字,通过用累加数组中的右顶点减去总顶点,再加上初始数组前缀为左顶点的值,可以得到:求x至y区间之和为:s[y]-s[x-1](s为累加数组)
前缀和的基本代码:
知识与要点:
前缀和不一定是和,也可以是前缀积··· ···
前缀和是一种预处理算法,能大大降低时间复杂度。
前缀和的操作对象主要是数组。
前缀和主要是计算之前数组元素的值之和。在解决区域问题时,可以减少遍历操作,减少时间复杂度。
一维前缀和:
记原数组为a[n],前缀和数组为b[n]。那么b[i]存储的内容为a[1]~a[i]的和。
即b[1]=a[1],b[2]=a[1]+a[2],b[3]=a[1]+a[2]+a[3],… 或是b[1]=b[0]+a[1],b[2]=b[1]+a[2],…(数组下标从1开始)。
公式:b[i]=a[1]+a[2]+…+a[i] (或b[i]=b[i-1]+a[i])
原数组: a[1], a[2], a[3], a[4], a[5], …, a[n]
前缀和: S[i] = a[1] +a[2] + a[3] + … + a[i]
前缀和能够快速的求出某一区间的和。
一、一维前缀和
1.前缀和是什么?
用一个简单的列子去介绍
原数组: a[1], a[2], a[3], a[4], a[5], …, a[n]
前缀和: s[i] = a[1] + a[2] + a[3] + … + a[i]
前缀和就是用一个数组s去存数组a的前n项的和。
s[0] = 0
s[1] = a[1]
s[2] = a[1] + a[2]
s[n] = a[i] + a[2] + a[3] + …+a[n]
这样s[n]对应的就是a[1]—a[n]的和,s的每一项都这样对应,就构成了前缀和。
注:前缀和的下标一定要从1开始。
2.暴力做法
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,m;
cin>>n>>m;
int sum=0;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
while(m--)
{
int l,r;
cin>>l>>r;
sum=0;
for(int i=l;i<=r;i++)
{
sum+=a[i];
}
cout<<sum<<endl;
}
return 0;
}
这个就是用暴力的方法去做,也能求出区间[L,R]的和,但他的时间复杂度为O(n)那么当数据过于庞大的时候就会造成超时的情况。
暴力会超时。
3.前缀和求区间大小
如何利用前缀和去求区间大小呢?
有一个公式:s[r] - s[l - 1]。
就是这个公式,他的时间复杂度O(1),这就要比暴力的做法快上很多了。
3.1如何构成前缀和的形式?
for(int i=1;i<=n;i++)
s[i]=s[i-1]+a[i];
s[1] = s[0] + a[1];
s[2] = s[1] + a[2];
s[3] = s[2] + a[3];
s[n] = s[n - 1] + a[n]
去遍历a数组,把当前a[n]的数加上s[n-1]的数,就能得到s[n],这个s[n]就是a[1,n]的和。
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=1;i<=n;i++)
s[i]=s[i-1]+a[i];
while(m--){
int l,r;
cin>>l>>r;
cout<<s[r]-s[l-1]<<endl;
}
return 0;
}
二、二维前缀和
1.基本思路
二维前缀和是建立在一维前缀和的基础上实现的,唯一不同的就是,这个是二维的。
s[1][1] = s[0][1] + s[1][0] - s[0][0] + a[1][1];
s[2][2] = s[1][2] + s[2][1] - s[1][1] + a[2][2];
s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
注:下标从1开始
这样理解可能有点困难,画个图就知道了。
标红的区域代表的就是区间[i-1,j-1]的的和
标绿的区域就是区间[i-1,j]的和
标蓝的区域就是区间[i,j-1]的和
这个整体代表的就是区间[i,j]的和
由此可以看出,在计算s[i,j]的和的时候,是不是把区间s[i-1,j-1]多算了一次,所以应该把s[i-1,j-1]减去一次,就能得到区间s[i,j]正确的区间和了。
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+10;
int a[N][N],s[N][N];
int main(){
int n,m,q;
cin>>n>>m>>q;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[i][j];
for(int i=1;i<=n;i++)
for(int j=1;j<=m;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;
cin>>x1>>y1>>x2>>y2;
cout<<s[x2][y2]-s[x1-1][y2]-s[x2][y1-1]+s[x1-1][y1-1]<<endl;
}
return 0;
}
1. 前缀和的定义
对于一个给定的数列A,他的前缀和数中 S 中 S[ i ] 表示从第一个元素到第 i 个元素的总和。
如下图:绿色区域的和就是前缀和数组中的 S [ 6 ]。
这里你可能就会有一个疑问?为什么是 S[ 6 ] 的位置,而不是 S[ 5 ] 的位置呢??即前缀和组中 S[ 0 ] 并没有参与求和的运算。这里先卖个关子等会在做解释。
2. 一维前缀和
2.1 计算公式
前缀和数组的每一项是可以通过原序列以递推的方式推出来的,递推公式就是:S[ i ] = S[ i - 1 ] + A[ i ]。S[ i - 1 ] 表示前 i - 1 个元素的和,在这基础上加上 A[ i ],就得到了前 i 个元素的和 S [ i ]。
2.2 用途
一维前缀和的主要用途:求一个序列中某一段区间中所有元素的和。有如下例子:
有一个长度为 n 的整数序列。
接下来输入 m 个询问,每个询问输入一对 l,r。
对于每个询问,输出原序列中第 l 个数到第 r 个数的和。
这边是对前缀和的应用,如果用常规的方法:从 l 到 r 遍历一遍,则需要O(N)的时间复杂度。但是有前缀和数组的话,我们可以直接利用公式:sum = S[ r ] - S[ l - 1 ],其中sum是区间中元素的总和,l 和 r 就是区间的边界。下图可帮助理解这个公式。
当我们要求的是序列 A 的前 n 个数之和时,如果我们是从下标为 0 的位置开始存储前缀和数组,此公式:sum = S[ r ] - S[ l - 1 ] 显然就无法使用了,为了是这个公式适用于所有情况,我们将从下标为 1 的位置开始存储前缀和,并且将下标为 0 的位置初始化为 0。
这便是为什么 S[ 0 ] 并未参与求和的运算。
有了上面的分析我们就能轻松解决这道题啦!
有一个长度为 n 的整数序列。
接下来输入 m 个询问,每个询问输入一对 l,r。
对于每个询问,输出原序列中第 l 个数到第 r 个数的和。
输入格式
第一行包含两个整数n和m。
第二行包含n个整数,表示整数数列。
接下来m行,每行包含两个整数l和r,表示一个询问区间的范围。
void test01(){
const int N = 100;
int a[N] = { 0 };
int s[N] = { 0 };
int n, m;
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\n", s[r] - s[l - 1]);
}
}
int main()
{
test01();
system("pause");
return 0;
}
3. 二维前缀和
和一维前缀和的原理类似,只不过二维前缀和求的是一个矩阵中所有元素的和。
例如:对与 x = 4,y = 3 这么一组输入,就是将原矩阵序列中蓝色区域的元素相加,得到的结果便是前缀和矩阵S中 S[ 4 ][ 3 ] 的值。
3.1 用途
一维前缀和求的是某一个区间中所有元素的和,那么二维前缀和就是求一个大矩阵中某个小的矩阵中所有元素的和。
例如上图:我们要求蓝色矩阵中所有元素的和。
现在就差最后一步了,怎么求出前缀和矩阵中的每一个值嘞??同理利用递推关系求就阔以啦。
S[ i ][ j ] = S[ i - 1 ][ j ] + S[ i ][ j - 1 ] - S[ i - 1][ j - 1 ] + a[ i ][ j ]
其中a为原矩阵序列。可以尝试举一个具体的例子来理解。
有了以上知识,我们可以尝试写代码求一下。
输入一个n行m列的矩阵,在输入q个询问,每个询问包含四个整数x1,y1,x2,y2,表示一个子矩阵左上角的坐标和右下角的坐标。
对于每个询问输出子矩阵中所有数的和。
输入格式
第一行包含三个整数n,m,q
接下来n行,每行包含m个整数,表示整数矩阵。
接下来q行,每行包含四个整数x1,y1,x2,y2,表示一组询问。
void test02()
{
//定义数组的大小
const int N = 100;
//原矩阵序列a
int a[N][N] = { 0 };
//前缀和矩阵,同样需要初始化为0,原因同一维矩阵
int s[N][N] = { 0 };
//读入一个n * m 的矩阵
int n, m, q;
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];
}
}
//q个询问
while (q--)
{
int x1, y1, x2, y2;
scanf("%d %d %d %d", &x1, &y1, &x2, &y2);
//利用前面推导过的公式直接打印数据即可
printf("%d\n", s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
}
printf("\n");
}
int main()
{
//二维前缀和
test02();
system("pause");
return 0;
}
前缀和正如字面意思,用一个新数组把旧数组每个位置的前缀和存起来,希望下面的内容能加深大家对前缀和的理解。
二、前缀和
1、基本概念
数组 a[0]~a[n-1],前缀和 sum[i] 等于 a[0]~a[i] 的和:sum[0]=a[0]、sum[1]=a[0] +a[1]、sum[2] = a[0] + a[1] + a[2].......
能在 O(n) 时间内求得所有前缀和:sum[i] = sum[i-1] + a[i]
预计算出前缀和,能快速计算出区间和:a[i] + a[i+1] + ... + a[ j-1 ] + a[ j ] = sum[ j ] - sum[i-1]
复杂度为 O(n) 的区间和计算,优化到了 O(1) 的前缀和计算
2、前缀和与差分的关系
一维差分数组 D[k] = a[k] - a[k-1],即原数组 a[ ] 的相邻元素的差。
差分是前缀和的逆运算:把求 a[k] 转化为求 D 的前缀和
3、差分数组能提升修改的效率
把区间 [L,R] 内每个元素 a[ ] 加上 d,只需要把对应的 D[ ] 做以下操作:
(1)把 D[L] 加上 d:D[L] += d
(2)把 D[R+1] 减去 d:D[R+1] -= d
原来需要 O(n) 次计算,现在只需要 O(1)
前缀和 a[x] = D[1] + D[2] + ... + D[x],有:
(1)1≤x<L,前缀和 a[x] 不变;
(2) L≤x≤R,前缀和 a[x] 增加了 d;
(3) R<x≤N,前缀和 a[x] 不变,因为被 D[R+1] 中减去的 d 抵消了。
三、例题
1、统计子矩阵(lanqiao2109,2022年省赛)
【题目描述】
有 K 位小朋友到小明家做客。小明拿出了巧克力招待小朋友们。小明一共有 N 块巧克力,其中第 i 块是 Hi×Wi 的方格组成的长方形。为了公平起见,小明需要从这 N 块巧克力中切出 K 块巧克力分给小朋友们。切出的巧克力需要满足:(1)形状是正方形,边长是整数;(2)大小相同。
例如一块 6×5 的巧克力可以切出 6 块 2×2 的巧克力或者 2 块 3×3 的巧克力。小朋友们都希望得到的巧克力尽可能大,你能帮小明计算出最大的边长是多少?
【输入描述】
第一行包含两个整数 N,K (1<=N, K<=10^5)。以下 N 行每行包含两个整数 Hi,Wi (1<=Hi,Wi<=10^5)。输入保证每位小朋友至少能获得一块1×1 的巧克力。
【输出描述】
输出切出的正方形巧克力最大可能的边长。
【问题描述】
给定一个 N×M 的矩阵A,请你统计有多少个子矩阵 (最小 1×1,最大 N×M),满足子矩阵中所有数的和不超过给定的整数K ?
【输入格式】
第一行包含三个整数 N, M和K,