前缀和与差分
一.前缀和
1.概念
前缀和是应用于顺序表的算法,指的是某序列前n项的和,对于一些场景可以显著的提高运算效率。
2.朴素做法与前缀和算法
- 部分和:
给定一个数组,求出某一段连续子数组的和。
如果是朴素做法,就是对于求部分和的区间,枚举所有数进行相加:
int sum = 0;
for(int i = left; i <= right; i++){
sum += arr[i];
}
return sum;
我们可以发现
如果数组长度为n,这样做的时间复杂度就是O(n),如果执行m次,询问m次,时间复杂度就会变成O(n*m)。
这显然不是我们想要的,我们需要对其优化
-
前缀和
如果我们使用sum数组去存储对应坐标到起始位置的数据和呢?
也就是sum[i]代表的是前 i 项的和。
对应的公式为;
s u m [ i ] = a r r [ 0 ] + a r r [ 1 ] + . . . . a r r [ i ] sum[i] = arr[0] + arr[1] + .... arr[i] sum[i]=arr[0]+arr[1]+....arr[i]
s u m [ i ] = s u m [ i − 1 ] + a r r [ i ] sum[i] = sum[i - 1] + arr[i] sum[i]=sum[i−1]+arr[i]
-
边界问题
有了对应的公式,不难发现,当我们去求 i下标 的前缀和时,需要用到下标 i-1;
那么就会涉及到,如果i = 0怎么办,这样的边界问题
我们的边界值应该为 sum[-1] = 0;
可以理解为 sum[-1] 是一项都没有累加,自然值应该为0;
那么处理边界可以是单写一个函数,也可以是从 i = 1开始遍历
-
单写一个函数:
int preSum(int n){ if(n == -1){ return 0; } return sum[n]; }
-
从i = 1遍历
for(int i = 1;i <= n;i++){ sum[i] = sum[i - 1] + arr[i]; }
-
-
部分和
求的sum数组,再看之前说的
求下标left到right之间的部分和:
例如left = 2 , right = 5
r e s = a r r [ 2 ] + a r r [ 3 ] + a r r [ 4 ] + a r r [ 5 ] res = arr[2] + arr[3] + arr[4] + arr[5] res=arr[2]+arr[3]+arr[4]+arr[5]
r
e
s
=
s
u
m
[
5
]
−
s
u
m
[
1
]
=
a
r
r
[
1
]
+
a
r
r
[
2
]
+
.
.
.
+
a
r
r
[
5
]
−
a
r
r
[
1
]
res = sum[5] - sum[1] = arr[1] + arr[2] + ... + arr[5] - arr[1]
res=sum[5]−sum[1]=arr[1]+arr[2]+...+arr[5]−arr[1]
所以可以得到 部分和为sum[right] - sum[left - 1];
这样当我们把所有的前缀和求出来,后续查询就都是O(1)了
3.前缀积
与前缀和一样,我们可以数组 prod代表前缀积
prod[i] 代表前 i 项的积
操作与前缀和基本一致
但是这个时候的边界处理有所变化:
-
单写一个函数:
int preProd(int n){ if(n == -1){ return 1; } return prod[n]; }
-
从i = 1遍历
for(int i = 1;i <= n;i++){ prod[i] = prod[i - 1] * arr[i]; }
4.例题
通过一道例题加深理解;
给你一个数组 nums
。数组「动态和」的计算公式为:runningSum[i] = sum(nums[0]…nums[i])
。
请返回 nums
的动态和。
示例 1:
输入:nums = [1,2,3,4]
输出:[1,3,6,10]
解释:动态和计算过程为 [1, 1+2, 1+2+3, 1+2+3+4] 。
示例 2:
输入:nums = [1,1,1,1,1]
输出:[1,2,3,4,5]
解释:动态和计算过程为 [1, 1+1, 1+1+1, 1+1+1+1, 1+1+1+1+1] 。
示例 3:
输入:nums = [3,1,2,10,1]
输出:[3,4,6,16,17]
使用前缀和的写法:创建前缀和数组,为了避免边界问题,使用第一个方法进行处理;
class Solution {
public:
int prefixsum(int n,vector<int> &sum){
if(n == -1){
return 0;
}
return sum[n];
}
vector<int> runningSum(vector<int>& nums) {
int n = nums.size();
vector<int> sum(n,0);
for(int i = 0;i<n;i++){
sum[i] = prefixsum(i -1,sum) + nums[i];
}
return sum;
}
};
二.差分
如果给你一个数组,我们要在这个数组的基础上,在[l,r]区间每个数据都加x,那么我们要如何做呢?
我们依旧可以以此遍历,在这个区间内就加x,不在就跳过。
但是如果是多个操作呢,很显然这样就比较慢了,而且操作也比较多。
我们就可以使用差分的思想。
1.差分数组
首先给一个原数组a:
a[1],a[2],a[3],a[4]…a[n]
然后我们构造另外一个数组b:
b[1],b[2],b[3],b[4]…b[n]
要求是:a[n] = b[1] + b[2] + … + b[n]
也就是说,a数组是b数组的前缀和数组,反过来也可以说b数组是a数组的差分数组。
如何差分数组呢?
a[0] = 0; b [0] = 0;
b[1] = a[1] - a[0] = a[1] - 0;
b[2] = a[2] - a[1] = a[2] - b[1]; -> a[2] =b[0] + b[1] + b[2];
b[3] = a[3] - a[2] = a[3] - b[0] - b[1] - b[2] -> a[3] = b[0] + b[1] + b[2] + b[3]
所以可以得出
b[i] = a[i] - a[i-1];
我们只要有b数组,就可以在O(n)的时间内的出a数组,但是这个和一开始的问题有什么关联呢?
2.反推差分的操作
给定一个差分数组[0,0,0,0,0,0],在坐标2-4都加上2
那么进行反推得到进行操作后的差分数组:
可以发现得到的数组为[0,0,2,0,0,-2];
其实这样也很好理解,因为我们后面求的结果是前缀和数组,那么只需要在数据改动的开始位置 +改变的数据
但是这个是有范围的,肯定要在其他区间恢复数据的。所以在改动结束位置的后一个 -改变的数据,就可以恢复;
所以差分的操作的伪代码如下:
//cf为差分数组
cf[left] += x;
cf[right + 1] -= x;
再进行前缀和即可
例题:
牛客DP37 【模板】差分
描述
给你一个长度为n的正数数组a1,a2,…an.
接下来对这个数组进行m次操作,每个操作包含三个参数l,r,k,代表将数组中al*,…*ar部分都加上k。
请输出操作后的数组。
输入描述:
第一行包含两个整数n和m。
第二行包含n个整数表示a1,…an
接下来是m行,每行三个整数,分别代表每次操作的参数l,r,k.
输出描述:
输出1行,表示m次操作后的a1,…an
示例1
输入:
3 2
1 2 3
1 2 4
3 3 -2
输出:
5 6 1
代码为:
#include <iostream>
using namespace std;
const int N = 100010;
long long arr[N];
long long cf[N];
int main() {
int n , m;
while(cin>>n>>m){
if(n == 0 && m == 0){
break;
}
for(int i = 1;i<=n;i++){
cin>>arr[i];
cf[i] = arr[i] - arr[i-1];
}
int left,right,x;
while(m--){
cin>>left>>right>>x;
cf[left] += x;
cf[right + 1] -= x;
}
for(int i = 1;i<=n;i++){
cf[i] += cf[i - 1];
cout<<cf[i]<<" ";
}
}
return 0;
}
一维的前缀和与差分就是这样了