1. 前缀和
1.1 一维前缀和
- 核心思想
- 预处理:提前计算并储存数组的累加和,空间换时间;
- 查询优化:
O(n)->O(1);
- 构建前缀和数组
- 原数组
a[i],前缀和数组f[i];
cpp
f[0] = 0;
f[i] = f[i-1] + a[i]; (1 ≤ i ≤n)
- 区间和查询
- 查询闭区间
[l,r]的和
cpp
sum(l, r) = f[r] - f[l-1];
1.2 二维前缀和
- 核心思想
- 扩展一维思想到矩阵,快速求子矩阵的和
- 构建前缀和矩阵
- 原矩阵
a[m][n],前缀和矩阵f[i][j]((1,1)到(i,j)矩阵中所有元素和);
cpp
f[0][j] = f[i][0] = 0; (边界置0)
f[i][j] = f[i-1][j] + f[i][j-1] - f[i-1][j-1] + a[i][j];
- 子矩阵查询
- 查询左上角(x1,y1)右下角(x2,y2)的子矩阵和;
cpp
sum = f[x2][y2] - f[x1-1][y2] - f[x2][y1-1] + f[x1-1][y1-1];
几何意义:
全区域-上侧区域-左侧区域+重叠减去的部分
- 时间复杂度
- 预处理O(nm);
- 单次查询O(1);
2. 差分
2.1 一维差分
- 核心思想:把「区间
[L,R]全部元素 +k」 - 时间优化:O(R−L+1) 降到 O(1)
- 差分数组 f 是原数组 a 的相邻元素之差序列
- 差分数组两种构造方式
cpp
1.
f[i]=a[i]-a[i-1];//a[0]=0;
cpp
2.
f[i]+=a[i];
f[i+1]-=a[i];
- 区间修改(m次)
cpp
f[l]+=k;
f[r+1]-=k;
- 还原数组(对f进行[[前缀和]])
cpp
a[i]=f[1]+f[2]+...+f[i];
2.2 二维差分
- 核心思想:把「子矩阵 (x1,y1)~(x2,y2) 全部元素 +k」
- 时间优化 O(nm) 降到 O(1)
- 性质
在差分矩阵 f 中,给单点 (x,y) 加 k,会影响原矩阵中以 (x,y) 为左上角、(n,m) 为右下角的整个子矩阵全部 +k。 - 子矩阵+k的四角操作
cpp
f[x1][y1] += k;
f[x1][y2+1] -= k;
f[x2+1][y1] -= k;
f[x2+1][y2+1] += k;
- 还原原矩阵
对 f 做二维前缀和;[[前缀和#二维前缀和]]
直接上例题
3. 差分+前缀例题
P10903 [蓝桥杯 2024 省 C] 商品库存管理
题目描述
在库存管理系统中,跟踪和调节商品库存量是关键任务之一。小蓝经营的仓库中存有多种商品,这些商品根据类别和规格被有序地分类并编号,编号范围从 111 至 nnn。初始时,每种商品的库存量均为 000。
为了高效地监控和调整库存量,小蓝的管理团队设计了 mmm 个操作,每个操作涉及到一个特定的商品区间,即一段连续的商品编号范围(例如区间 [L,R][L, R][L,R])。执行这些操作时,区间内每种商品的库存量都将增加 111。然而,在某些情况下,管理团队可能会决定不执行某些操作,使得这些操作涉及的商品区间内的库存量不会发生改变,维持原有的状态。
现在,管理团队需要一个评估机制,来确定如果某个操作未被执行,那么最终会有多少种商品的库存量为 000。对此,请你为管理团队计算出,对于每个操作,如果不执行该操作而执行其它操作,库存量为 000 的商品的种类数。
输入格式
输入的第一行包含两个整数 nnn 和 mmm,分别表示商品的种类数和操作的个数。
接下来的 mmm 行,每行包含两个整数 LLL 和 RRR,表示一个操作涉及的商品区间。
输出格式
输出 mmm 行,每行一个整数,第 iii 行的整数表示如果不执行第 iii 个操作,则最终库存量为 000 的商品种类数。
输入 #1
5 3
1 2
2 4
3 5
输出 #1
1
0
1
说明/提示
【样例说明】
考虑不执行每个操作时,其余操作对商品库存的综合影响:
-
不执行操作 111 :剩余的操作是操作 222(影响区间 [2,4][2, 4][2,4])和操作 333(影响区间 [3,5][3, 5][3,5])。执行这两个操作后,商品库存序列变为 [0,1,2,2,1][0, 1, 2, 2, 1][0,1,2,2,1]。在这种情况下,只有编号为 111 的商品的库存量为 000。因此,库存量为 000 的商品种类数为 111。
-
不执行操作 222 :剩余的操作是操作 111(影响区间 [1,2][1, 2][1,2])和操作 333(影响区间 [3,5][3, 5][3,5])。执行这两个操作后,商品库存序列变为 [1,1,1,1,1][1, 1, 1, 1, 1][1,1,1,1,1]。在这种情况下,所有商品的库存量都不为 000。因此,库存量为 000 的商品种类数为 000。
-
不执行操作 333 :剩余的操作是操作 111(影响区间 [1,2][1, 2][1,2])和操作 222(影响区间 [2,4][2, 4][2,4])。执行这两个操作后,商品库存序列变为 [1,2,1,1,0][1, 2, 1, 1, 0][1,2,1,1,0]。在这种情况下,只有编号为 555 的商品的库存量为 000。因此,库存量为 000 的商品种类数为 111。
【评测用例规模与约定】
对于 20%20\%20% 的评测用例,1≤n,m≤5×1031 \le n,m \le 5 \times 10^31≤n,m≤5×103,1≤L≤R≤n1\le L \le R \le n1≤L≤R≤n。
对于所有评测用例,1≤n,m≤3×1051 \le n,m \le 3 \times 10^51≤n,m≤3×105,1≤L≤R≤n1 \le L \le R \le n1≤L≤R≤n。
题解
差分+前缀和:
- 所有的操作都是让区间内每一个元素增加1,因此可以用差分数组快速还原出所有操作之后原数组;
- 当某一个区间操作
[l,r]撤销时,只有这个区间内的1会在操作撤销后变成0,因此可以用前缀和统计出每次操作后[1,i]区间内1的个数,每次撤销之后,查询撤销区间内1的个数即可; - 原本为0的也是答案因此答案为区间1的个数+本来就为0的个数
反思: 正确的前缀和维护的是原库存有多少个1(1-1为0),而超时的前缀和用来每次还原区间数组思考与使用方式不一样导致算法错误
cpp
#include<iostream>
using namespace std;
const int N = 3e5 + 10;
int f[N];
int n, m;
struct node {
int l, r;
} a[N];
int sum[N];
int main()
{
cin >> n >> m;
for (int i = 1; i <= m; i++) {
cin >> a[i].l >> a[i].r;
f[a[i].l] += 1;
f[a[i].r + 1] -= 1;
}
int zero = 0; // 存储即使不执行也为0的个数
// 还原出原数组,用前缀和维护(1,i)中库存为1的数量
for (int i = 1; i <= n; i++) {
f[i] = f[i - 1] + f[i];
if (f[i] == 0) zero++;
if (f[i] == 1) sum[i] = sum[i - 1] + 1;
else sum[i] = sum[i - 1];
}
for (int i = 1; i <= m; i++) {
cout << sum[a[i].r] - sum[a[i].l - 1] + zero << endl;
}
return 0;
}
4. 正负抵消+前缀和+哈希表(平衡计数问题)
P1114 "非常男女"计划
题目描述
近来,初一年的 XXX 小朋友致力于研究班上同学的配对问题(别想太多,仅是舞伴),通过各种推理和实验,他掌握了大量的实战经验。例如,据他观察,身高相近的人似乎比较合得来。
万圣节来临之际,XXX 准备在学校策划一次大型的 "非常男女" 配对活动。对于这次活动的参与者,XXX 有自己独特的选择方式。他希望能选择男女人数相等且身高都很接近的一些人。这种选择方式实现起来很简单。他让学校的所有人按照身高排成一排,然后从中选出连续的若干个人,使得这些人中男女人数相等。为了使活动更热闹,XXX 当然希望他能选出的人越多越好。请编写程序告诉他,他最多可以选出多少人来。
输入格式
第一行有一个正整数 n (1≤n≤105)n\ (1\le n \le 10^5)n (1≤n≤105),代表学校的人数。
第二行有 nnn 个用空格隔开的数,这些数只能是 000 或 111,其中,000 代表是一个女生,111 代表是一个男生。
输出格式
输出一个非负整数。这个数表示在输入数据中最长的一段男女人数相等的子区间的长度。
如果不存在男女人数相等的子区间,请输出 000。
输入 #1
9
0 1 0 0 0 1 1 0 0
输出 #1
6
解法
- 看到的第一眼想到了,子区间之和为子区间长度的一半时,该区间是和为0的区间,两层for循环枚举区间长度和左端点,更新最长子区间;但如此会发现时间复杂度会很高,因此应该转换成下面一解法来优化
-
数值转换:将0转为-1,1保持为1
- 这样当0和1数量相等时,区间和就为0
-
[[前缀和]]原理:
- 如果
sum[i] == sum[j]((1,i)区间和 ==(1,j)区间和),说明区间[i+1, j]的和为0 - 因为
sum[j] - sum[i] = 0
- 如果
-
哈希表作用:
mp[sum]记录前缀和sum第一次出现的位置- 当同样的前缀和再次出现时,说明找到了一个0和1数量相等的区间
cpp
// 数值反转+前缀和+哈希
// 思路:将0视为-1,1视为1,计算前缀和
// 如果sum[i] == sum[j],说明区间[i+1, j]的和为0(0和1数量相等)
// 使用哈希表记录每个前缀和第一次出现的位置
#include<iostream>
#include<unordered_map>
using namespace std;
int main()
{
int n; cin >> n;
int ret = 0; // 记录最长子数组长度
int sum = 0; // 当前前缀和
unordered_map<int, int> mp; // 存储<前缀和, 第一次出现的位置>
// 关键:初始化前缀和为0的位置为0
// 表示在数组开始前,前缀和就是0(相当于空数组)
mp[0] = 0;
for (int i = 1; i <= n; i++)
{
int x; cin >> x;
// 数值反转:0变成-1,1保持1
// 这样当0和1数量相等时,区间和就为0
x = (x == 0 ? -1 : 1);
sum += x; // 更新前缀和
// 如果当前前缀和之前出现过
if (mp.count(sum)) {
// 计算当前区间长度:i - mp[sum]
// mp[sum]是前缀和sum第一次出现的位置
// 当前区间 [mp[sum]+1, i] 的和为0
ret = max(ret, i - mp[sum]);
}
else {
// 如果这个前缀和是第一次出现,记录它的位置
mp[sum] = i;
}
}
cout << ret << endl;
return 0;
}
P1147 连续自然数和
题目描述
对一个给定的正整数 MMM,求出所有的连续的正整数段(每一段至少有两个数),这些连续的自然数段中的全部数之和为 MMM。
例子:1998+1999+2000+2001+2002=100001998+1999+2000+2001+2002 = 100001998+1999+2000+2001+2002=10000,所以从 199819981998 到 200220022002 的一个自然数段为 M=10000M=10000M=10000 的一个解。
输入格式
包含一个整数的单独一行给出 MMM 的值(10≤M≤2,000,00010 \le M \le 2,000,00010≤M≤2,000,000)。
输出格式
每行两个正整数,给出一个满足条件的连续正整数段中的第一个数和最后一个数,两数之间用一个空格隔开,所有输出行的第一个按从小到大的升序排列,对于给定的输入数据,保证至少有一个解。
输入 #1
10000
输出 #1
18 142
297 328
388 412
1998 2002
题解
- 发现可以暴力n* n直接遍历,但是会超时
- 利用[[前缀和]]+哈希表;这两的组合拳啊;
- 从前往后遍历每⼀个数,对于当前的前缀和 sum[i] ,仅需在前⾯找到前缀和为 sum[i] - m 位置 p 。如果存在, 则 [p + 1, i] 就是要找的⼀个区间。
- 这也是维护平衡,平衡的值为n;
- 大区间-右区间=维护的左区间
- 因此复杂度转化为n
cpp
#include <iostream>
#include <unordered_map>
using namespace std;
typedef long long LL;
int m;
unordered_map<LL, int> mp; // 存储前缀和及其对应的位置
int main()
{
cin >> m; // 输入目标值m
int n = (m + 1) / 2; // 最大可能的区间右端点
// 因为1+2+...+n = n(n+1)/2 >= m 时,n的最小值约为√(2m)
// 这里简化处理,取(m+1)/2作为上限
mp[0] = 0; // 初始化:前缀和为0时对应的位置是0
LL sum = 0; // 当前前缀和
for(int i = 1; i <= n; i++)
{
sum += i; // 计算从1到i的累加和
// 关键思路:如果存在 sum - (sum - m) = m
// 即当前前缀和sum减去之前某个前缀和等于m
// 那么这两个位置之间的连续整数和就是m
if(mp.count(sum - m))
{
// 输出区间:从 mp[sum-m]+1 到 i
// 因为mp[sum-m]记录的是前缀和为sum-m时的右端点位置
cout << mp[sum - m] + 1 << " " << i << endl;
}
mp[sum] = i; // 记录当前前缀和对应的位置
}
return 0;
}
P3131 Subsequences Summing to Sevens S
题目描述
Farmer John 的 NNN 头奶牛站成一排,这是它们时不时会做的事情。每头奶牛都有一个独特的整数 ID 编号,以便 Farmer John 能够区分它们。Farmer John 希望为一组连续的奶牛拍照,但由于童年时与数字 1...61 \ldots 61...6 相关的创伤事件,他只希望拍摄一组奶牛,如果它们的 ID 加起来是 7 的倍数。
请帮助 Farmer John 确定他可以拍摄的最大奶牛组的大小。
输入格式
输入的第一行包含 NNN(1≤N≤50,0001 \leq N \leq 50,0001≤N≤50,000)。接下来的 NNN 行每行包含一头奶牛的整数 ID(所有 ID 都在 0...1,000,0000 \ldots 1,000,0000...1,000,000 范围内)。
输出格式
请输出 ID 之和为 7 的倍数的最大连续奶牛组中的奶牛数量。如果不存在这样的组,则输出 0。
输入 #1
7
3
5
1
6
2
14
10
输出 #1
5
说明/提示
在这个例子中,5+1+6+2+14=285+1+6+2+14 = 285+1+6+2+14=28。
题解
**前缀和+同余+哈希(这里用的是思想,但也可map)
-
如果
( f[right] - f[left-1] )%7 ==0;那么代表[ left+1,right ]区间符合,我们可以拆开表达式有f[ right ]%7==f[left-1]%7;如果满足这个表达式就是正解,因为区间要长故left要更靠左 -
对于前缀和 f[i] ,仅需找到 [1, i - 1] 区间内,前缀和模 7 等于 f[i] % 7 的最左位置j
**思路
- 初始化 :
id[0] = 0:表示前缀和为0(模7)在第0天就出现了- 这样如果前k天的和是7的倍数,区间长度就是
k - 0 = k
- 遍历每一天 :
- 计算当前前缀和模7的结果
- 如果这个余数之前出现过:
- 从上次出现的位置到现在的位置,这段区间的和模7为0
- 更新最大区间长度
- 如果这个余数第一次出现:
- 记录当前位置
cpp
#include <iostream>
#include <cstring>
using namespace std;
const int N = 10; // 因为要对7取模,所以只需要大小为7的数组,N取10足够
int n; // 输入的天数
int id[N]; // 记录每个余数第一次出现的位置(天数)
int main()
{
cin >> n; // 读入天数
// 初始化id数组为-1,表示该余数还没有出现过
memset(id, -1, sizeof id);
// 关键初始化:余数为0时,应该在第0天就出现了
// 这样可以处理从第一天开始就满足条件的情况
id[0] = 0; // 前缀和为7的倍数的情况从第0天开始计算
int sum = 0, ret = 0; // sum: 当前前缀和对7取模的结果
// ret: 保存最长连续天数的结果
for(int i = 1; i <= n; i++) // 从第1天开始遍历
{
int x;
cin >> x; // 读入第i天的降雨量
// 计算前缀和模7的结果
sum = (sum + x) % 7;
// 如果这个余数之前出现过,说明从上次出现到现在的区间和是7的倍数
if(id[sum] != -1)
// 计算这段区间的长度,并更新最大值
ret = max(ret, i - id[sum]);
else
// 如果这个余数第一次出现,记录它的位置
id[sum] = i;
}
cout << ret << endl; // 输出最长连续天数
return 0;
}