今天咱们要聊的是算法界的"孪生兄弟"——前缀和与差分。这俩货简直就是处理数组问题的"瑞士军刀",一个负责扮猪吃老虎,一个擅长暗度陈仓。准备好瓜子饮料,咱们这就开车!
一、前缀和:你的私人记账小秘书
1.1 前缀和思想
想象你是个每天剁手的败家子,现在老妈要查你过去30天的支付宝账单:"从双十一到黑五总共花了多少?"
这时候你有两种选择:
-
暴力解法:把11.11到11.25的账单一条条加起来(手抽筋警告⚠️)
-
聪明做法:提前做好"消费累计表",查账时直接大数减小数(深藏功与名✨)
这就是前缀和的核心思想用空间换时间,把O(n)的查询变成O(1),可以在暴力枚举的过程中, 快速查询出一段区间的结果。
1.2 一维前缀和模板
【解题】:
1.看到本题的第一反应就是暴力解法:每次区间询问的时候,从左端点到右端点依次累加,然后的出结果。很显然这种思想的时间复杂度时O(n^2)不能通过此题。
2.前缀和解法:多开一个数组f[i],表示从 1-i 位置的区间和,f[5] 表示从a[1] 加到a[5],所以它的递推公式很简单f[i] = f[i - 1] + a[i]; 然后查询的时候直接f[r] - f[l - 1];
🖥️code:
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
typedef long long LL;
LL s[N];
int a[N];
int main()
{
int n, q; cin >> n >> q;
for(int i = 1; i <= n; i++) cin >> a[i];
// 预处理
for(int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i];
while(q--)
{
int l, r; cin >> l >> r;
cout << s[r] - s[l - 1] << endl;
}
return 0;
}
1.3 一维前缀和例题
【解题】:这一题是非常经典的一道题,前缀和,贪心,分治,DP都可以解这道题。
预处理前缀和数组,但是枚举所有区间会超出内存限制,子段和最大左边界的前缀和一定最小,所以我们多维护一个prevmin表示 1-i 位置的最小的前缀和。
🖥️code:
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 2e5 + 10;
LL n, a[N];
int main()
{
cin >> n;
for(int i = 1; i <= n; i++)
{
cin >> a[i];
a[i] = a[i - 1] + a[i];
}
LL ret = -1e17;
LL prevmin = 0;
for(int i = 1; i <= n; i++)
{
ret = max(ret, a[i] - prevmin);
prevmin = min(prevmin, a[i]);
}
cout << ret << endl;
return 0;
}
1.4 二维前缀和模板
【解题】:同上述一维前缀和,暴力解法过不了, 我们还是利用前缀和的思想预处理数组。
此时前缀和数组 f[i][j] 表示从1,1 到 i,j位置矩形内的数组和。
下面是推导转移公式 f[i][j] = f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1] + a[i][j];
下面是推导区间公式x1,y1 到 x2,y2 的区域数组和,f[x2][y2] - f[x2][y1 - 1] - f[x1 - 1][y2] + f[x1 - 1][y1 - 1];
🖥️code:
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1000 + 10;
int a[N][N];
// 二位前缀和
LL s[N][N];
int q, n, m;
int main()
{
cin >> n >> m >> q;
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= m; j++)
{
cin >> a[i][j];
}
}
// 预处理二位前缀和
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= m; j++)
{
s[i][j] = a[i][j] + s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
}
}
while(q--)
{
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
cout << s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1] << endl;
}
return 0;
}
1.5 二维前缀和例题
【解题】:其实就是求m* m最大的区间和啦。
🖥️code:
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 5e3 + 10;
LL f[N + 1][N + 1], a[N + 1][N + 1];
int n, m;
int main()
{
cin >> n >> m;
while (n--)
{
int x, y, v; cin >> x >> y >> v;
x++; y++; // 从1开始计数
a[x][y] += v; // 可能不止一个物品
}
n = 5010;
// 前缀和
for (int i = 1; i <= N; i++)
{
for (int j = 1; j <= N; j++)
{
f[i][j] = f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1] + a[i][j];
}
}
LL ret = 0;
for (int x1 = 1; x1 + m - 1 <= N; x1++)
{
for (int y1 = 1; y1 + m - 1 <= N; y1++)
{
int x2 = x1 + m - 1, y2 = y1 + m - 1;
LL t = f[x2][y2] - f[x2][y1 - 1] - f[x1 - 1][y2] + f[x1 - 1][y1 - 1];
ret = max(ret, t);
}
}
cout << ret << endl;
return 0;
}
二、差分:时空穿越的魔法师
2.1 差分思想
假设你是植物大战僵尸里的园丁,要给第3到第5行的向日葵各浇10次水。这时候僵尸已经走到家门口了,你怎么办?
-
老实人做法:挨个给3、4、5行浇水(僵尸:真慢,开饭啦!)
-
聪明做法:在3行标记"+10",6行标记"-10",最后统一施法(僵尸:我眼花了?)
前缀和和查分运算是一对互逆运算。
2.2 一维差分模板
【解题】:每次操作都要在一段区间加上一个数。我们发现在这个区间加上数的时候,前后的差值不变,换句话说,差分数组就是差值的数组,l,r区间加b,就在差分数组 l位置+b,r+1位置-b。
1. 创建差分数组:
根据定义: f[i] = a[i] − a[i − 1]
也可以根据差分数组的性质: f[i] + = a[i], f[i + 1] − = a[i](这个可以理解成自己对自己差分,来了一个a[i]后,他与前面的差值f[i] + a[i],与后面的差值f[i + 1] - a[i])。
2. 对区间端点修改
3. 对差分数组求前缀和,还原数组。
🖥️code:
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int n, m;
LL a[N], f[N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++)
{
cin >> a[i];
f[i] += a[i];
f[i + 1] -= a[i];
}
while(m--)
{
int l, r, x; cin >> l >> r >> x;
f[l] += x;
f[r + 1] -= x;
}
for(int i = 1; i <= n; i++)
{
f[i] += f[i - 1];
cout << f[i] << " ";
}
return 0;
}
2.3 一维差分例题
【解题】:问题的关键在于,对于每个铁路段i,在所有的行程中被经过多少次。然后,对于每个段i来说,如果使用IC卡的总费用是否比每次买纸质票更便宜。这里可能需要比较两种情况的总费用:买卡的总成本(C_i + 次数*B_i)和不买卡的总成本(次数*A_i)。取其中较小的那个。
对于求每段铁路经过多少次就可以用差分啦。
🖥️code:
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int n, m;
int f[N];
int pf[N];
int main()
{
cin >> n >> m;
for(int i = 1; i <= m; i++)
{
cin >> pf[i];
}
for(int i = 1; i < m; i++)
{
int x = pf[i], y = pf[i + 1];
if(x > y) swap(x, y);
f[x]++; f[y]--;
}
for(int i = 1; i < n; i++) f[i] = f[i - 1] + f[i];
long long ans = 0;
for(int i = 1; i < n; i++)
{
long long a, b, c; cin >> a >> b >> c;
long long cost1 = a * f[i];
long long cost2 = c + b * f[i];
ans += min(cost1, cost2);
}
cout << ans << endl;
return 0;
}
2.4 二维差分模板
【解题】:
二维差分其实可以根据一维差分的意义推出来:
🖥️code:
#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1000 + 10;
// 原数组
LL a[N][N];
// 差分数组
LL f[N][N];
int q, n, m;
int main()
{
cin >> n >> m >> q;
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= m; j++)
{
cin >> a[i][j];
// 二维差分
f[i][j] = a[i][j] - a[i - 1][j] - a[i][j - 1] + a[i - 1][j - 1];
}
}
while(q--)
{
int x1, y1, x2, y2, k;
cin >> x1 >> y1 >> x2 >> y2 >> k;
f[x1][y1] += k;
f[x2 + 1][y1] -= k;
f[x1][y2 + 1] -= k;
f[x2 + 1][y2 + 1] += k;
}
// 前缀和
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= m; j++)
{
f[i][j] = f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1] + f[i][j];
cout << f[i][j] << " ";
}
cout << endl;
}
return 0;
}
2.5 二维差分例题
【解题】:直接二维差分即可。
🖥️code:
#include <iostream>
using namespace std;
const int N = 1010;
int f[N][N];
int n, m;
int main()
{
cin >> n >> m;
// 二维差分
while(m--)
{
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
f[x1][y1]++;
f[x2 + 1][y1]--;
f[x1][y2 + 1]--;
f[x2 + 1][y2 + 1]++;
}
// 前缀和
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= n; j++)
{
f[i][j] = f[i - 1][j] + f[i][j - 1] - f[i - 1][j - 1] + f[i][j];
cout << f[i][j] << " ";
}
cout << endl;
}
return 0;
}