学习目标
- 前缀和技巧
- 差分技巧
- 二维前缀和、二维差分
区间类问题
区间类问题在编程竞赛和算法设计中非常常见,它们通常涉及对数组或序列中的某个区间进行操作或查询。以下是一些常见的区间类问题类型:区间求和、区间更新、区间最值、区间统计、区间覆盖...选择合适的算法和数据结构是高效解决区间类问题的关键。比如用ST表求区间最值、用前缀和数组求区间和、用差分数组进行区间更新...
前缀和
什么是前缀和
● 前缀和的定义
数组a1~an,前缀和:si=a1+a2+ ...... +ai
s1= a1
s2= a1 + a2
s3= a1 + a2 + a3
● 递推关系
我们已知前缀和的公式:si=a1+a2+ ... +ai-1+ai
可以推出它的前一项:si - 1 = a1 + a2 + ... + ai - 1
所以递推公式是:si = si - 1 + ai
利用递推公式能在O(n)时间内求得所有前缀和
前缀和的作用
求区间和:给定n个整数,然后进行m次询问,每次询问求一个区间内值的和。
对于给定的查询区间i,j,区间和=ai+ai+1+ .... +aj-1+aj。

(1)暴力枚举法
区间和=ai+ai+1+ ...... +aj-1+aj
· 1次查询复杂度为O(n)
· m次查询复杂度为O(nm)
(2)前缀和法
区间和=sj-si-1
· 1次查询复杂度为O(1)
· m次查询复杂度为O(m)
注意,这里假设前缀和数组s\[\]已经存在。
求区间和分两步:
- 构建前缀和数组
利用递推公式:si=si-1+ai - 快速查询ij区间和
ijj\]区间和=s\[j\]-s\[i-1
时间复杂度为O(n)
单次时间复杂度为O(1)
· 大量查询更合算:总时间复杂度从暴力法的O(mn),变成O(n)+O(m)。
· 需要额外存储一个与原数组等长的前缀和数组,算法的空间复杂度通常为O(n)。
●前缀和的典型应用是加速区间和计算和其他相关操作
●前缀和的题目一般也能用暴力法求解
●当题目有完成时间要求,并且需要做大量区间计算时,可以用前缀和优化
●前缀和原理简单,方便在很多场景下应用,与其他考点结合,几乎必考
二维前缀和
二维数组---->二维前缀和数组--快速计算二维区间和
● 二维前缀和:sij表示所有ai'j'的和(1≤i'≤i,1≤j'≤j)
可以理解为"矩形的面积"那样,把一整块区域的值都加起来。例如,s33 =...

左边图所示面积可以由两个行数或列数少一的矩形面积相加后,删去重合部分
再加上右下角的值来构成(如右图所示)。

二维区间和

平衡序列
小杨有一个包含n(1 <= n <= 10000)个正整数的序列a,他认为一个序列是平衡的当且仅当
存在一个正整数i(1 <= i<n)使得序列第1到第i个数字的总和等于第i+1到第n个数字的总和;小杨想请你判断序列a是否是平衡。
【输入格式】
第一行是一个正整数t(1 <= t <= 100),表示测试用例组数。
接下来是t组测试用例。
对每组测试用例,一共两行。第一行包含一个正整数n,表示序列长度。第二行包含n个
正整数(1 <= ai <= 10000),代表序列a。
【输出格式】对每组测试用例输出一行一个字符串。如果a是平衡的,输出Yes,否则输
出No。
【输入样例】
3
3
1 2 3
4
2 3 1 4
5
1 2 3 4 5
【输出样例】
Yes
Yes
No
【样例说明】
对于第一组测试用例,令i=2,则有1+2=3,因此序列是平衡的;
对于第二组测试用例,令i=2,则有2+3=1+4,因此序列是平衡的;
对于第三组测试用例,不存在满足要求的i。
(1)模拟法
i的取值范围在1到n之间,所以最直观的想法就是对1到n进行枚举,测试所有的值,看看是否有i满足题目中的条件。
cpp
for(1~t)组
for(i = 1~n) {
① 循环得到a1到ai的和 ->s1(for 1~i)
②循环得到a(i+1)到an的和 ->s2(for i+1~n)
③ 比较 if(s1 == s2) ...
}
最多需要3层循环,时间复杂度O(tn2)。当t、n取最大值时,tn2=1001000010000。执行次数远远大于1亿次,执行效率极低,容易超时。
(2)前缀和法
使用前缀和的技巧,一次创建前缀和数组,方便多次查询。
①利用递推公式si=si-1+ai,完成前缀和数组的赋值:
cpp
for (int i=1;i <= n; i++) {//从下标1开始放。s[0]=0
cin >> a[i];
s[i] = s[i-1]+a[i];
}
② 第1到第个数字的总和(si)等于第i+1到第n个数字的总和?(sn - si)
if( si == sn - si)或者if( si*2 == sn)
相比模拟法,不用循环得到a1到ai的和,减少一层循环。
基于前缀和法的流程(伪代码):
cpp
for (1~t){//t组
① 输入这组n个数,并计算前缀和s[]
② 查找是否存在符合条件的i
for(i = 1~n) {
if (s[i] == s[n] - s[i])
找到了,设标记f=1
}
③ 根据f标记输出"Yes'或"No"
}
时间复杂度O(tn)
cpp
#include <iostream>
using namespace std;
int t,n,a[10010];//全局变量会自动初始化为0
int s[10010];//前缀和数组。
int main() {
cin >> t;
for (int i=l;i <= t; i++) {
cin >> n;
for (int j=1;j <= n;j++) {
cin >> a[j] ;
s[j] = s[j-1]+a[j] ;
}
bool f=0;//是否找到的符号,初始0,找到设为1
for (int j=1;j <= n; j++) {
if (s[j]*2 == s[n]) {
f =1;
break;//找到一个就停止继续寻找
}
}
if (f)
cout << "Yes" << endl;
else
cout << "No" << endl;
}
return 0;
}
· 如果a\[\]、s\[\]数组定义为局部变量,不要忘了初始化为0。
· 标记f每次查找前必须初始为0。
· 找到合适的i,可以加break优化。即马上停止继续查找。
· 原始数组a\[\]在后续并不会用到,也可以省略,节约内存使用。
两两相乘求和
给定n个整数a1,a2,...,an,求他们两两相乘再相加的和,即:
S = a1a2+ a1 a3+ ... + a1an+ a2 a3+ ... + an-2an-1+ an-2 an+ an-1*an
【输入】第一行包含一个整数n,第二行包含n个整数a1,a2,...,an。
【输出】输出一个整数S,表示所求的和。使用合适的数据类型进行运算。
【数据范围】
对于30%的数据,1≤n≤1000,1≤a;≤100。
对于所有评测用例,1≤n≤200000,1≤a;≤1000。
【输入样例】4
1369
【输出样例】117
(时间限制】1s
竞赛题中的时间限制
· 时间限制是编程竞赛中一个重要的约束条件,它不仅考验参赛者的算法设计和优化能
力,还要求参赛者在有限的时间内找到最佳的解决方案。
· 编程竞赛题的时间限制通常在题目中给出,C++编程竞赛常见的时间限制是1秒。
· 例如,如果一道题目的时间限制为1秒,而某个算法的时间复杂度为O(n^2),那么当输入规
模n较大时,该算法将无法在规定时间内完成,导致超时,测试不能通过。
· 因此,参赛者需要选择或优化算法,确保其时间复杂度尽可能低,以满足时间限制的要求。
(1)模拟法
直接按题目给的公式算,用两个for循环实现:
cpp
// 按题目的公式求和
for(int i=1;i <= n-1; i++)
for(int j=i+1;j <= n; j++)
s += a[i]*a[j];
有2层for循环,循环次数是:n-1+n-2+.+1~n2/2。时间复杂度O(n²)。
本题有最大运行时间1s的运行限制。
若n=200000,循环次数2000002/2=2×10¹º。很可能会因为超时,不能通过测试。
(2)前缀和法

基于前缀和法的流程(伪代码):

cpp
1. 输入这组n个数,并计算前缀和sum[]
2. 用上面的基于前缀和的公式计算s(初始为0)
for(i = 1~n) {
s += a[i]*(sum[n]-sum[i]);
}
3. 输出s
时间复杂度O(n)
完整代码案例
cpp
#include <iostream>
using namespace std;
int a[200010] ;
long long sum [200010] ; //前缀和数组
int main () {
int n;
scanf ("%d", &n) ;
for(int i=1; i <= n; i++){
scanf ("%d", &a[i]) ;
sum [i] = sum[i-1] +a[i] ;//预计算前缀和
}
long long s=0; //注意局部变量定义时要初始化为0
for(int i=1; i <= n; i++)
s += a[i] * (sum [n] -sum [i] ) ;
printf("%lld\n", s) ;
return 0;
}
· 如果a\[\]、s\[\]数组定义为局部变量,不要忘了整体初始化为0
· 变量s要开long long 防止溢出
差分
差分的概念
差分的定义:
即差分数组D\[\]是原数组a\[\]的相邻元素的差。
Dk= ak-ak-1

根据D\[\]的定义,可以反过来推出:
ak = D1 + D2 + ... + Dk = ak-1+Dk
即a\[\]是D\[\]的前缀和,所以"差分是前缀和的逆运算"。

差分的作用

区间修改:假设有m次操作,每次将a数组中下标为L,R之间的数都加上x。(数组长度n)
(1)暴力法
L~R的区间,逐个+X
· 1次修改O(n)
· m次修改O(mn)
(2)差分法
基于差分数组D\[\],只改动两个点的值:
- 把DL加上x:DL+=x
- 把DR+1减去x: DR+1 -= x
· 1次修改O(1)
· m次修改O(m)

差分数组的作用:
● 应用于区间的整体修改和询问问题,特别是多次修改后的询问
● 当所有的修改操作结束后,再利用差分数组,计算出新的a\[\]

二维差分


差分求二维前缀和

小A倒水
在一个桌子上摆放了n个杯子,每个杯子中有一定量的水。小A同学负责向杯子中倒
水,他总共倒了k次,每次会向从第L个杯子到第R个杯子中添加P毫升的水(注意:
水只可能增加,不可能减少)。请问小A同学倒了k次水之后,n个杯子每个杯子有
多少毫升的水。
【输入】第一行包含两个整数n和k。
第二行包含n个整数,表示一开始每个杯子中水的毫升数。
接下来k行,每行包含三个整数L,R,P,表示一次操作。
【输出】共一行,包含n个整数,表示最终n个杯子每个杯子有多少毫升的水。
【数据范围】
1≤n,k≤100000.
1≤L≤R≤n.
0≤P≤1000.
杯子中水的初始量在0,1000的范围内。
本题数据上保证所有的杯子在加水之后,水量值仍然在int范围内。
【输入样例】
8 3
1 2 10 8 15 1 1
7 8 12
1 8 4
2 3 12
【输出样例】
5 18 26 12 5 9 17 17
对数列进行k次任取区间的修改,问最终数列的值。差分法!
步骤:
①输入原数数据到a\[\],并计算差分D\[\]
Di = ai - ai-1
DL += x, DR+1 -= x
③求D\[\]的前缀和,就是a数组做了k次操作的后结果
ai = ai-1+Di
cpp
#include <iostream>
using namespace std;
int a[100010],D[100010];//a代表读入的原数组,D代表是差分数组
int n,k,L,R,p;
int main(){
cin>>n>>k;
// 输入原数数据,从下标1开始放。a[0]=0
for(int i = 1;i <= n; i++) {
cin>>a[i];
//求差分数组
D[i] = a[i] - a[i-1] ;
}
// k次倒水,更新差分数组
for(int i = 1;i <= k; i++) {
cin>>L>>R>>p;
D[L] += p;
D[R+1] -= p;
}
//求D数组的前缀和,就是a数组做了k次操作的结果
for(int i = 1;i <= n; i++) {
a[i] = a[i-1] + D[i];// 即累加 D[0]~D[i]
cout << a[i] << " ";
}
return 0;
}
时间复杂度O(n)
· 有效数据从a1、D1开始放
· D1=a1-a0,所以要确保a0=0
本次课程的知识点
-
二维前缀和数组、二维差分数组
-
前缀和的概念
-
用前缀数组和求区间和
-
差分的概念
-
用差分数组进行区间修改
1、已知字符'A'的ASCII编码的十六进制表示为0x41,则字符'L'的
ASCII编码的十六进制表示为?(C)
A、0x4A
B、0x4B
C、0x4C
D、0x52
【提示】'A'的十进制ASCII编码=4*16+1=65。可算出'L'的十进制ASCII编码=64+'[~'A'的
间隔=65+11=76,76转回十六进制=0x4C。
连续数的和
给出两个整数n和k,(2≤n≤70000,1≤k≤n),求出1、2、3、...、n中连续k个数
的和,并计算出和为平方数的个数。
【输入】n、k两个整数。
【输出】一个整数,即1、2、3、...、n中连续k个数的和为平方数的个数。
【输入样例1】
10 3
【输出样例1】
1
【输入样例2】
100 3
【输出样例2】
5
【样例1说明】
n=10, k=3.
在1,2,...,10中,连续3个数的和有:
1+2+3=6
2+3+4=9
3+4+5=12
4+5+6=15
5+6+7=18
6+7+8=21
7+8+9=24
8+9+10=27
其中和为平方数的仅有9,因为9=3×3。
(模拟法代码示例】
cpp
#include <iostream>
#include <cmath>
using namespace std;
// 一个整数n是否平方数
bool is square (long long n) {
long long m= sqrt(n);//求n的平方根,类型转换时向下取整
return m*m == n;
}
int main(){
int n,k;
scanf ("%d %d", &n, &k) ;
int cnt=0;//注意局部变量定义时要初始化为0
// 逐个查看区间和是否是平方数
// 连续k个数的区间[i,i+k-1]区间和= sum[i+k-1]-sum[i-
for(int i=1; i <= n-k+1; i++) {
// 累加 k个连续数:s=i+(i+1)+ ... +(i+k-1) =k*i
long long s = i*k + k*(k-1) /2;
if(is_square (s))
cnt++;
}
printf("%d\n", cnt) ;
return 0;
}
【前缀和法代码示例】
cpp
#include <iostream>
#include <cmath>
using namespace std;
long long sum [70010] ;//前缀和数组
// 一个整数n是否平方数
bool is_square (long long n) {
long long m=sqrt(n);//求n的平方根,类型转换时向下取整
return m*m == n;
}
int main(){
int n,k;
scanf ("%d %d", &n, &k) ;
for(int i=1; i <= n; i++)
sum[i] = sum[i-1] + i;//预计算前缀和
int cnt=0;//注意局部变量定义时要初始化为0
// 逐个查看区间和是否是平方数
// 连续k个数的区间[i,i+k-1]区间和=sum[i+k-1]-sum[i-1]
for(int i=1; i <= n-k+1; i++) {
if(is_square (sum[i+k-1] - sum[i-1] ))
cnt++;
}
printf("%d\n", cnt);
return 0;
}
可以用模拟法,也可以用前缀和法。模拟法可以借助等差数列和的计算公式进行优化:
1+2+ ... +k=k*(k+1)/2。优化后模拟法和前缀和法的时间复杂度相同O(n)。