前缀和入门(c++语言)

在讲算法之前,我们先来思考一个问题:小明有n个编号为1~n的篮子,每个篮子里装有ai个苹果,求从 x至y 的篮子里的苹果数量之和。

如果没学过前缀和的同学,可能会打出这样的代码:

1.jpg

这种算法要得出一个区间之和,这题只需要取一次区间值,时间复杂度需要O(n),但如果2次,4次,1000次,数据再一大,暴力算法肯定会TLE(超时),这时前缀和的优势就体现出来了,因为它取区间之和,只需要O(1)。

那前缀和的思想是什么呢?又是如何实现用O(1)取区间之和的呢?其思想就是遍历1至n,算出1至当前数字的区间之和,有人或许会问了,那这样也无法算出特定区间之和啊,但我们观察后发现,在累加数组,前缀为右顶点的数字包含了前缀为左顶点的数字,通过用累加数组中的右顶点减去总顶点,再加上初始数组前缀为左顶点的值,可以得到:求x至y区间之和为:s[y]-s[x-1](s为累加数组)

3.jpg

前缀和的基本代码:

2.jpg

知识与要点:

前缀和不一定是和,也可以是前缀积··· ···

前缀和是一种预处理算法,能大大降低时间复杂度。 

前缀和的操作对象主要是数组。

前缀和主要是计算之前数组元素的值之和。在解决区域问题时,可以减少遍历操作,减少时间复杂度。

一维前缀和:

        记原数组为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])

4.jpg

原数组: 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,

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值