【算法与数据结构】—— 前缀和与差分

前缀和与差分



前缀和(一维)

若给出一组数列:   { a 1 , a 2 , … , a n } \ \left\{a_1,a_2,\ldots,a_n\right\}  {a1,a2,,an},再给出 m m m 组询问(每组询问含两个正整数 L , R L, R L,R),现要求你对每组询问都给出区间 [ L , R ] [L, R] [L,R] 里的数列之和。
该问题的常规做法是:定义一个对数组求和的函数,其参数为待求区间的两个边界值,返回值为这之间的和,于是可得到如下代码:

int SumOfSection(int ary[],int l,int r)		//ary[]数组用于存放原数列
{
	int sum=0;
	for(int i=l; i<=r; i++)
		sum += ary[i];
	return sum;
}

然后在一个循环次数为 m m m 的循环内,把输入的 L , R L,R L,R 都交给这个函数,最终由它产生结果。显然,这样的求解过程是非常低效的,因为每次询问都要执行 S u m O f S e c t i o n ( ) SumOfSection( ) SumOfSection() 函数。所以该方法实际上有两重循环,故其时间复杂度为 O ( m n ) O(mn) O(mn),这在面对较大数据范围时难以胜任。
高中时曾学过数列的前 n n n 项和 S n S_n Sn,即若存在一组数列 { a 1 , a 2 , … , a n } \left\{a_1,a_2,\ldots,a_n\right\} {a1,a2,,an},则 S n = a 1 + a 2 + … + a n S_n=a_1+a_2+\ldots+a_n Sn=a1+a2++an。在计算机领域,我们把 S n S_n Sn 称为“前缀和”。若设原数组为 a r y [   ] = { a 1 , a 2 , … , a n } ary[\ ]= \{a_1,a_2,…,a_n\} ary[ ]={a1,a2,,an},前缀和数组为 p r e f i x [   ] prefix[\ ] prefix[ ],则有:
p r e f i x [ i ] = p r e f i x [ i − 1 ] + a r y [ i ] prefix[i]=prefix[i-1]+ary[i] prefix[i]=prefix[i1]+ary[i]
不难看出,前缀和与前 n n n 项和的关系为:
p r e f i x [ i ] = S i prefix[i]= S_i prefix[i]=Si
同时,对于任意 m < n m<n m<n ,都有:
p r e f i x [ n ] − p r e f i x [ m ] = ( a 1 + a 2 + ⋯ + a m + a m + 1 + ⋯ + a n ) − ( a 1 + a 2 + ⋯ + a m ) = a m + 1 + ⋯ + a n \begin{aligned} prefix[n]-prefix[m] &= (a1+a2+⋯+am+am+1+⋯+an)-(a1+a2+⋯+am) \\ &= am+1+⋯+an \end{aligned} prefix[n]prefix[m]=(a1+a2++am+am+1++an)(a1+a2++am)=am+1++an

根据该公式,我们便找到了解决上面提出的问题的一种方法,即定义一个前缀和数组 p r e f i x [   ] prefix[\ ] prefix[ ] 保存数列中的前 n n n 项和,这样在求区间 [ L , R ] [L, R] [L,R] 里的数列之和时就能直接用 p r e f i x [ R ] − p r e f i x [ L − 1 ] prefix[R]-prefix[L-1] prefix[R]prefix[L1] 得到。构造前缀数组的方法如下(为了方便起见,通常要求前缀数组中的索引从1开始):

int prefix[N], num;
for(int i=1; i<=n; i++)
{
	cin>>num;
	prefix[i]=prefix[i-1]+num;
}


差分

若给出一组数列: { a 1 , a 2 , … , a n } \left\{a_1,a_2,\ldots,a_n\right\} {a1,a2,,an},并给出 m m m 组操作(每组操作给出三个正整数 L , R , V a l u e L,R,Value LRValue,其含义为对区间范围 [ L , R ] [L, R] [L,R] 中的每个数都加上 V a l u e Value Value),最后输出经过 m m m 组操作后的数列。
该问题的常规做法是,定义一个修改数组内容的函数,其参数为左右两个边界值以及一个 v a l u e value value,如下:

void ModifyOfSection(int ary[],int l,int r,int value)
{
	for(int i=l; i<=r; i++)
		ary[i]+=value;
}

然后在一个循环次数为 m m m 的循环内,每次接受三个输入: L , R , V a l u e L,R,Value L,R,Value ,并将其交给 M o d i f y O f S e c t (   ) ModifyOfSect(\ ) ModifyOfSect( ) 函数,当此循环结束后即得到最后的数列。

显然,这样做的效率是极低的。因为对于每次输入的 L , R , V a l u e L,R,Value L,R,Value 都需要一重循环去修改值。所以该方法实际上也是一个二重循环,其时间复杂度也为 O ( m n ) O(mn) O(mn)。显然,在数据范围较大时将无法胜任。此时,差分便派上了用场。实际上,差分与前缀和是刚好相反的两个概念。前缀和的当前项等于原数组中的当前项再加上前缀和的前一项,而差分的当前项则等于原数组中的当前项减去原数组中的前一项,即:
s u b f i x [ i ] = a r y [ i ] − a r y [ i − 1 ] subfix[i]=ary[i]-ary[i-1] subfix[i]=ary[i]ary[i1]
其中, s u b f i x [   ] subfix[\ ] subfix[ ] 为差分数组, a r y [   ] ary[\ ] ary[ ] 为输入的原数组。

根据该式,可得到构造差分数组的方法如下(其索引也是从 1 开始):

int subfix[N],ary[N];
for(int i=1; i<=n; i++)
{
	cin>>ary[i];
	subfix[i]=ary[i]-ary[i-1];
}

从差分数组的定义不难看出,其记录了连续数组中的差值情况。如果我们对数组中的某段连续子段进行统一增减操作,那对这一系列的数据而言,他们的相对大小是不会发生改变的;放眼整个数组,其仅会影响该子段首尾两个数在其边界处的相对大小。例如,对以下数组(假设数组的索引从 1 开始计数):
a r y [   ] = { 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 } ary[\ ]=\{1,2,3,4,5,6,7,8\} ary[ ]={1,2,3,4,5,6,7,8}

可得到其对应的差分数组为:
s u b f i x [   ] = { 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 } subfix[\ ]=\{1,1,1,1,1,1,1,1\} subfix[ ]={1,1,1,1,1,1,1,1}

若我们对原数组在区间 [ 3 ,   6 ] [3,\ 6] [3, 6] 的数都增加 4,则得到:

a r y [   ] = { 1 , 2 , 7 , 8 , 9 , 10 , 7 , 8 } ary[\ ]=\{1,2,7,8,9,10,7,8\} ary[ ]={1,2,7,8,9,10,7,8}
其对应的差分数组为:

s u b f i x [   ] = { 1 , 1 , 5 , 1 , 1 , 1 , − 3 , 1 } subfix[\ ]=\{1,1,5,1,1,1,-3,1\} subfix[ ]={1,1,5,1,1,1,3,1}

观察数组 a r y [   ] ary[\ ] ary[ ] 在进行区间增值操作前后,尽管 a r y [   ] ary[\ ] ary[ ] 发生了相当多的变动(即指定区间中的所有数),但在其对应的差分数组中,仅有两处发生改变:一是指定增值操作区间的起始位置,二是指定增值操作区间结尾处的下一个位置。由此可知,利用差分数组来记录原始数组时,当对原始数组进行长度为 k k k 的统一增减操作时,其能将时间复杂度由 O ( k ) O\left(k\right) O(k) 降至常数级。

同时,基于差分数组公式,我们将其移项可得到:
a r y [ i ] = s u b f i x [ i ] + a r y [ i − 1 ] ary[i]=subfix[i]+ary[i-1] ary[i]=subfix[i]+ary[i1]

又因为差分数组在构建时,有:ary[1]=subfix[1],于是可得到从差分数组还原原始数组的公式如下:
a r y [ i ] = ∑ j = 1 i s u b f i x [ j ] ary[i]=\sum_{j=1}^isubfix[j] ary[i]=j=1isubfix[j]

进一步可计算出前缀和数组与差分数组的关系为:
p r e f i x [ i ] = ∑ j = 1 i a r y [ j ] = ∑ j = 1 i ∑ k = 1 j s u b [ k ] = ∑ j = 1 i ( i − j + 1 ) ⋅ s u b [ j ] prefix\left[i\right]=\sum_{j=1}^{i}{ary\left[j\right]=}\sum_{j=1}^{i}\sum_{k=1}^{j}sub\left[k\right]=\sum_{j=1}^i(i-j+1)·sub[j] prefix[i]=j=1iary[j]=j=1ik=1jsub[k]=j=1i(ij+1)sub[j]

根据上述分析不难发现,差分数组在“对连续数组进行统一增减操作”的情境下非常高效(能将修改操作从 O ( k ) O\left(k\right) O(k) 降至常数级)。基于差分数组的此特性,可给出求解上述问题的完整代码:

#include<iostream>
using namespace std;

const int N=100010;		// 数组的最大容量 
int ary[N], subfix[N];	// 原数组、差分数组

int main()
{
	// 数列长度、操作次数、m次操作的左右边界与Value
	int n,m,l,r,value;	cin>>n>>m;
	// 输入原数组 
	for(int i=1;i<=n;i++) cin>>ary[i];	
	// 构建subfix数组 
	for(int i=1;i<=n;i++) subfix[i] = ary[i] - ary[i-1];
	// 执行m次操作 
	for(int i=m;i>0;i--){
		cin>>l>>r>>value;
		subfix[l] += value;
		subfix[r+1] -= value;
	}
	// 还原原数组
	for(int i=1;i<=n;i++) ary[i] = ary[i-1] + subfix[i];
	// 输出最终的数组
	for(int i=l;i<=r;i++) cout<<ary[i]<<” ”;
	cout<<ans<<endl;
	return 0; 
}



前缀和数组(二维)

一维前缀和的主要用处是对一维数组的子区间进行快速求和,因此在面对矩阵时便失去了其作用。比如对于如下二维表:
二维表
若现在有 w w w 组询问,每组询问给出两个序数对 ( x 1 , y 1 ) , ( x 2 , y 2 ) (x_1, y_1),(x_2, y_2) (x1,y1)(x2,y2)(两个序数对满足 x 1 < x 2 , y 1 < y 2 x_1<x_2, y_1<y_2 x1<x2,y1<y2),对于每组询问,你需要输出以这两个序数对所确定的矩阵中的所有元素之和。

该问题的常规做法是先用一个二维数组将该二维表保存下来,然后再写一个对二维表子阵元素求和的函数,如下:

int SumOfMatrix(int matrix[][],int x1,int y1,int x2,int y2)	//matrix为已构建好的二维数组
{
	int sum=0;
	for(int i=x1;i<=x2;i++)
		for(int j=y1;j<=y2;j++)
			sum += matrix[i][j];
	return sum;
}

接下来对于每一组询问,都将输入的两个有序数对交给此函数,并由它返回最终结果。这样的做法很直接且易于理解,但是其效率却非常低。因为 S u m O f M a t r i x ( ) SumOfMatrix() SumOfMatrix() 函数内部是一个二重循环,在其外层又有w组询问,也就是说这样求解该问题的时间复杂度为 O ( w m n ) O(wmn) O(wmn)( m 和 n 是指题目中所给出的二维表的规格)。显然,这样的性能在遇到较大数据时必超时。

在这样的需求下,二维前缀和“千呼万唤始出来”。二维前缀和 p r e f i x [ i ] [ j ] prefix[i][j] prefix[i][j] 的数理定义是:

p r e f i x [ i ] [ j ] = ∑ x = 1 i ∑ y = 1 j m a t r i x [ x ] [ y ] prefix[i][j]=\sum_{x=1}^i\sum_{y=1}^jmatrix[x][y] prefix[i][j]=x=1iy=1jmatrix[x][y]

它表示从位置 m a t r i x [ 1 ] [ 1 ] matrix[1][1] matrix[1][1] m a t r i x [ i ] [ j ] matrix[i][j] matrix[i][j] 这之间所有元素的总和。下面我们来关注一下如何通过一个二维数组来构建二维前缀和(这就需要我们寻找二维前缀和的迭代公式)。

如下图所示, p r e f i x [ 2 ] [ 3 ] prefix[2][3] prefix[2][3] 表示图中黄色部分的所有元素之和。
二维表
由该图易知:
p r e f i x [ 2 ] [ 3 ] = m a t r i x [ 1 ] [ 1 ] + m a t r i x [ 1 ] [ 2 ] + m a t r i x [ 1 ] [ 3 ] + m a t r i x [ 2 ] [ 1 ] + m a t r i x [ 2 ] [ 2 ] + m a t r i x [ 2 ] [ 3 ] = p r e f i x [ 2 ] [ 2 ] + m a t r i x [ 1 ] [ 3 ] + m a t r i x [ 2 ] [ 3 ] \begin{aligned} prefix[2][3] &= matrix[1][1] + matrix[1][2] + matrix[1][3] + matrix[2][1] + matrix[2][2] + matrix[2][3] \\ &= prefix[2][2] + matrix[1][3] + matrix[2][3] \\ \end{aligned} prefix[2][3]=matrix[1][1]+matrix[1][2]+matrix[1][3]+matrix[2][1]+matrix[2][2]+matrix[2][3]=prefix[2][2]+matrix[1][3]+matrix[2][3]
如下图所示, p r e f i x [ 3 ] [ 2 ] prefix[3][2] prefix[3][2] 表示图中蓝色部分的所有元素之和。
二维表
由该图易知:
p r e f i x [ 3 ] [ 2 ] = m a t r i x [ 1 ] [ 1 ] + m a t r i x [ 1 ] [ 2 ] + m a t r i x [ 2 ] [ 1 ] + m a t r i x [ 2 ] [ 2 ] + m a t r i x [ 3 ] [ 1 ] + m a t r i x [ 3 ] [ 2 ] = p r e f i x [ 2 ] [ 2 ] + m a t r i x [ 3 ] [ 1 ] + m a t r i x [ 3 ] [ 2 ] \begin{aligned} prefix[3][2] &= matrix[1][1] + matrix[1][2] + matrix[2][1] + matrix[2][2] + matrix[3][1] + matrix[3][2] \\ &= prefix[2][2] + matrix[3][1] + matrix[3][2] \end{aligned} prefix[3][2]=matrix[1][1]+matrix[1][2]+matrix[2][1]+matrix[2][2]+matrix[3][1]+matrix[3][2]=prefix[2][2]+matrix[3][1]+matrix[3][2]
由于 p r e f i x [ 2 ] [ 3 ] + p r e f i x [ 3 ] [ 2 ] = 2 p r e f i x [ 2 ] [ 2 ] + m a t r i x [ 1 ] [ 3 ] + m a t r i x [ 2 ] [ 3 ] + m a t r i x [ 3 ] [ 1 ] + m a t r i x [ 3 ] [ 2 ] prefix [2][3] + prefix [3][2] = 2 prefix [2][2] + matrix[1][3] + matrix[2][3] + matrix[3][1] + matrix[3][2] prefix[2][3]+prefix[3][2]=2prefix[2][2]+matrix[1][3]+matrix[2][3]+matrix[3][1]+matrix[3][2]。也就是说将这两幅图进行重叠得到的效果如图3.1.4所示(其中绿色部分,即 p r e f i x [ 2 ] [ 2 ] prefix [2][2] prefix[2][2] 占了两份):
二维表
那么如果我们要用原数组和前缀和数组得到 p r e f i x [ 3 ] [ 3 ] prefix[3][3] prefix[3][3] 的话,则公式为:
p r e f i x [ 3 ] [ 3 ] = p r e f i x [ 2 ] [ 3 ] + p r e f i x [ 3 ] [ 2 ] − p f o f i x [ 2 ] [ 2 ] + m a t r i x [ 3 ] [ 3 ] prefix[3][3] = prefix[2][3] + prefix[3][2] - pfofix[2][2] + matrix[3][3] prefix[3][3]=prefix[2][3]+prefix[3][2]pfofix[2][2]+matrix[3][3]
实际上,上式正是二维前缀数组递推式的一个实例。根据斥容原理,我们也不难得出二维前缀数组的递推式为:
p r e f i x [ i ] [ j ] = p r e f i x [ i − 1 ] [ j ] + p r e f i x [ i ] [ j − 1 ] − p r e f i x [ i − 1 ] [ j − 1 ] + m a t r i x [ i ] [ j ] prefix[i][j] = prefix [i-1][j] + prefix [i][j-1] - prefix [i-1][j-1] + matrix[i][j] prefix[i][j]=prefix[i1][j]+prefix[i][j1]prefix[i1][j1]+matrix[i][j]
由该式,我们可以直接写出构建二维前缀数组的代码:

int matrix[M][N], prefix[M][N];					// M、N分别表示二维矩阵的高度和宽度
void CreatePrefix(int m, int n) {
	for(int i=1;i<=m;i++)						// 二维矩阵的录入以及二维前缀和数组的构建 
		for(int j=1;j<=n;j++){
			cin>>matrix[i][j];
			prefix[i][j] = prefix[i-1][j]+ prefix[i][j-1]- prefix[i-1][j-1]+matrix[i][j];
		}
}

接下来我们来讨论如何利用二位前缀数组来求解最初的问题。
假设现在给出两个序数对 ( 2 , 2 ) (2, 2) (2,2) ( 4 , 4 ) (4, 4) (4,4)(下图中红色部分),我们要怎么利用二维前缀数组来求出这两点所确定的子矩阵元素之和呢(图中红色与黄色的共同组成部分)?
二维表
如果仅仅是用 p r e f i x [ 4 ] [ 4 ] − p r e f i x [ 2 ] [ 2 ] prefix[4][4] - prefix [2][2] prefix[4][4]prefix[2][2],得到的结果如下图中黄色部分所示:
二维表
显然这个结果并不是我们所预想的。此时我们可以从二维前缀和的定义式中寻找突破。如果我们将所求子阵单独隔离出去,来观察剩余元素的位置特征,如下图所示(有色背景部分):
二维表
若设图中黄色部分的子阵为 S 1 S1 S1,蓝色部分的子阵为 S 2 S2 S2,绿色部分的子阵为 S 3 ( = p r e f i x [ 1 ] [ 1 ] ) S3(=prefix[1][1]) S3(=prefix[1][1]),待求子阵为 S S S。那么我们可以很容易地得到:
S = p r e f i x [ 4 ] [ 4 ] − ( S 1 + S 2 + S 3 ) = p r e f i x [ 4 ] [ 4 ] − ( ( S 1 + S 3 ) + ( S 2 + S 3 ) − S 3 ) = p r e f i x [ 4 ] [ 4 ] − ( p r e f i x [ 1 ] [ 4 ] + p r e f i x [ 4 ] [ 1 ] − p r e f i x [ 1 ] [ 1 ] ) = p r e f i x [ 4 ] [ 4 ] − p r e f i x [ 1 ] [ 4 ] − p r e f i x [ 4 ] [ 1 ] + p r e f i x [ 1 ] [ 1 ] \begin{aligned} S &= prefix[4][4] - ( S1 + S2 + S3 ) \\ &= prefix[4][4] - ( (S1+S3) + (S2+S3) - S3 ) \\ &= prefix[4][4] - ( prefix[1][4] + prefix[4][1] - prefix[1][1] ) \\ &= prefix[4][4] - prefix[1][4] - prefix[4][1] + prefix[1][1] \\ \end{aligned} S=prefix[4][4](S1+S2+S3)=prefix[4][4]((S1+S3)+(S2+S3)S3)=prefix[4][4](prefix[1][4]+prefix[4][1]prefix[1][1])=prefix[4][4]prefix[1][4]prefix[4][1]+prefix[1][1]
实际上,在给定了两个序数对 ( x 1 , y 1 ) 、 ( x 2 , y 2 ) (x_1, y_1)、(x_2, y_2) (x1,y1)(x2,y2)后,通过二维前缀和数组求解指定子阵元素和的公式为:
S = p r e f i x [ x 2 ] [ y 2 ] − p r e f i x [ x 1 − 1 ] [ y 2 ] p r e f i x [ x 2 ] [ y 1 − 1 ] + p r e f i x [ x 1 − 1 ] [ y 1 − 1 ] S=prefix[x_2][y_2]-prefix[x_1-1][y_2]prefix[x_2][y_1-1]+prefix[x_1-1][y_1-1] S=prefix[x2][y2]prefix[x11][y2]prefix[x2][y11]+prefix[x11][y11]



趁热打铁

【洛谷】 P1115 最大子段和
【蓝桥杯】 历届试题 最大子阵
【马蹄集】第十二周作业
【洛谷】P1404 平均数


END


评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

theSerein

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值