你是一名饭店的财务,月底了要和老板汇报这个月赚Q咋样。你手头记录了每天的营收额
- 老板要到第
l
天到第r
天这几天的营收额 - 老板要
每周
的营收额 - 老板发现你把某
连续几天
的营收额写错了,改正和求每周的营业额 - 老板发现你
不止一次
把连续几天的营收写错了,改正后求这个月的营收额
接下来,我们把上述情景依次对应到下面的几个场景中去,来理解前缀和差分
场景一 :有n
个数的序列a[n]
,求[l, r]
的区间和
for(int i = l; i <= r;i++)
sum = + a[i];
时间复杂度 : $O(n)$
(os :”不错嘛”)
场景二 :有n
个数的序列a[n]
,有m
次询问,每次询问[l, r]
的区间和
那还不简单把场景一重复m
次即可
时间复杂度 : $O(n*m)$
(os :”有点久哦”)
所有这时候前缀和数组
来帮忙
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;
}
时间复杂度 : $O(n + m)$ = 求前缀和 $O(n)$ + 区间求和 $O(m * 1)$
(os :”hin棒 时间换空间”)
场景三 :有n
个数的序列a[n]
,把某个区间[l1, r1]
的的所有数得加上c
,(假设修改区间的长度为p
)接下来有m
次询问,每次询问[l, r]
的区间和
那我把对应区间改了呗,还能咋地
for(int i = l1; i <= r1; i++)
a[i] += c;
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;
}
时间复杂度 : $O(p + n + m)$ = 修改区间$O(p)$ + 求前缀和 $O(n)$ + m次区间求和 $O(m * 1)$
(os :”TMD,不就改一改吗完事儿 “)
场景四 :有n
个数的序列a[n]
,接下来有k
个操作,每个操作包含三个整数l1, r1, c
,表示将序列中[l1, r1]
之间的每个数加上c
(假设修改区间的长度为p
); 再接下来有m
次询问,每次询问[l, r]
的区间和
那我修改对应区间k次呗,就是累点
// 假设每个修改区间的长度为p
while(k --)
{
int l1 ,r1 ,c;
cin >> l1 >> r1 >> c;
for(int i = l1; i <= r1; i++)
a[i] += c;
}
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;
}
时间复杂度: $O(k*p + n + m)$ = 修改区间$O(k * p)$ + 求前缀和 $O(n)$ + m次区间求和 $O(m * 1)$
(os :”TMD,终于改完事,不对啊,学过差分组数啊,我可以把这修改一次区间的时间复杂度将到常数级啊 “)
差分数组
-
定义 :
b[i] = a[i] - a[i-1]
- b是a的差分, a是b的前缀和
-
优势 :快速处理区间加减操作 如 区间
[L,R]
中的每个数加上c- 第一个受影响的差分数组中的元素为b[L],即
b[L]+=c
,那么由差分数组计算原数组的时候L后面的数都会加上c; - 最后一个受影响的差分数组中的元素为b[R],所以令
b[R+1] -= x
,即可保证不会影响到R以后原数组元素的计算
- 第一个受影响的差分数组中的元素为b[L],即
-
小结 :不必对区间内每一个数进行处理,只需处理两个差分后的数即可,再求差分数组的前缀计算出原数组
所以,利用差分数组,上述情景的时间复杂度变为 :
$O(k * 2 + n + m)$ = 修改区间 $O(k * 2)$ + 求前缀和 $O(n)$ + m次区间求和 $O(m * 1)$
总结
- 前缀和数组 :快速处理区间和操作
- 差分数组 : 快速处理区间加减操作
a[N] 原数组 |
关系 | 区间操作[HTML_REMOVED][l, r] |
$O(1)$的时间复杂度 |
---|---|---|---|
一维前缀 s[N] |
s[i] = s[i - 1] + a[i] |
求和 | s[i] = s[i - 1] + a[i] |
一维差分 b[N] |
b[i] = a[i] - a[i-1] |
整体修改 | b[l] += c, b[r+1] -= c |
小试牛刀
797. 差分 一次性练习到了区间修改和前缀和
题目要求
输入一个长度为n的整数序列。
接下来输入m个操作,每个操作包含三个整数l, r, c,表示将序列中[l, r]之间的每个数加上c。
请你输出进行完所有操作后的序列。
输入格式
第一行包含两个整数n和m。
第二行包含n个整数,表示整数序列。
接下来m行,每行包含三个整数l,r,c,表示一个操作。
输出格式
共一行,包含n个整数,表示最终序列。
数据范围
1≤n,m≤1000001≤n,m≤100000,
1≤l≤r≤n1≤l≤r≤n,
−1000≤c≤1000−1000≤c≤1000,
−1000≤整数序列中元素的值≤1000−1000≤整数序列中元素的值≤1000
输入样例:
6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1
输出样例:
3 4 5 3 4 2
c++代码
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int a[N],b[N];
// 区间修改
void insert(int l, int r, int c)
{
b[l] += c;
b[r + 1] -= c;
}
int main()
{
scanf("%d%d", &n, &m);
// 读入原数组
for(int i = 1; i <= n ; i ++)
scanf("%d",&a[i]);
// 构造差分
for(int i = 1; i <= n ; i ++)
insert(i, i, a[i]);
// 区间修改
while(m --)
{
int l, r, c;
scanf("%d%d%d",&l, &r, &c);
insert(l, r, c);
}
// 计算原数组 = 差分数组的前缀和
for(int i = 1; i <= n ; i++) a[i] = b[i] + a[i-1];
// 输入原数组
for(int i = 1; i <= n ; i++) printf("%d ", a[i]);
return 0;
}
思考与拓展
二维的前缀和差分 yxc常用模板1
- 二维前缀和
S[i, j] = 第i行j列格子左上部分所有元素的和
以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]
- 二维差分
给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c
如果数组频繁被修改,且频繁的查询区间和呢?那就得自己学学树状数组了
树状数组主要用于解决基于区间上的更新以及求和问题,一般的方法时间复杂度为O(N),树状数组可以把时间复杂度降低到O(logN)。
二维前缀和中:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]
为什么要 是 x1-1,y2 和 x2,y1-1 鸭?为什么要-1