1. 前缀和与差分:算法竞赛中的高效工具
作为一名算法竞赛选手,我经常遇到需要快速处理区间查询和修改的问题。前缀和与差分这两个技巧,就像瑞士军刀一样,能帮我在O(1)时间内完成这些操作。今天我就来详细分享这两个技巧的原理、实现和应用场景。
2. 一维前缀和详解
2.1 基本概念与实现
前缀和的核心思想是预处理一个数组,使得我们能够快速查询任意区间的和。具体来说,给定一个数组a[],我们定义前缀和数组f[],其中f[i]表示从a[1]到a[i]的和。
cpp复制int a[N], f[N]; // 原始数组和前缀和数组
// 预处理前缀和数组
for(int i=1; i<=n; i++) {
f[i] = f[i-1] + a[i];
}
// 查询区间[l,r]的和
int sum = f[r] - f[l-1];
注意:数组下标通常从1开始,这样可以避免边界条件的特殊处理。如果从0开始,查询f[l-1]时可能会越界。
2.2 典型应用:区间求和
考虑这样一个问题:给定n个整数和m次查询,每次查询给出区间[l,r],要求输出该区间的和。
暴力解法是每次查询都遍历区间计算和,时间复杂度O(mn)。使用前缀和可以将每次查询优化到O(1),预处理时间O(n)。
cpp复制#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
typedef long long LL;
int n, q;
LL a[N], f[N];
int main() {
cin >> n >> q;
for(int i=1; i<=n; i++) {
cin >> a[i];
f[i] = f[i-1] + a[i];
}
while(q--) {
int l, r;
cin >> l >> r;
cout << f[r] - f[l-1] << endl;
}
return 0;
}
2.3 进阶应用:最大子段和
最大子段和问题要求找出数组中连续子数组的最大和。使用前缀和可以高效解决:
cpp复制#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
typedef long long LL;
int n;
LL f[N];
int main() {
ios::sync_with_stdio(false);
cin >> n;
for(int i=1; i<=n; i++) {
LL x; cin >> x;
f[i] = f[i-1] + x;
}
LL ret = -1e18, premin = 0;
for(int i=1; i<=n; i++) {
ret = max(ret, f[i] - premin);
premin = min(premin, f[i]);
}
cout << ret << endl;
return 0;
}
这个算法的关键在于维护一个premin变量,记录当前最小的前缀和,这样f[i]-premin就能得到以a[i]结尾的最大子段和。
3. 二维前缀和及其应用
3.1 二维前缀和的定义
对于二维数组,前缀和sum[i][j]表示从(1,1)到(i,j)矩形区域内所有元素的和。递推公式为:
code复制sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1] + a[i][j]
这个公式可以理解为:当前矩形和 = 上方矩形和 + 左侧矩形和 - 左上角重复计算的部分 + 当前格子的值。
3.2 子矩阵查询
给定左上角(x1,y1)和右下角(x2,y2),子矩阵和的计算公式为:
code复制sum = sum[x2][y2] - sum[x1-1][y2] - sum[x2][y1-1] + sum[x1-1][y1-1]
cpp复制#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
typedef long long LL;
LL a[N][N], sum[N][N];
int n, m, q;
int main() {
ios::sync_with_stdio(false);
cin >> n >> m >> q;
for(int i=1; i<=n; i++) {
for(int j=1; j<=m; j++) {
cin >> a[i][j];
sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1] + a[i][j];
}
}
while(q--) {
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
LL ans = sum[x2][y2] - sum[x1-1][y2] - sum[x2][y1-1] + sum[x1-1][y1-1];
cout << ans << endl;
}
return 0;
}
3.3 实际应用:黑白格问题
这个问题要求找到包含至少k个黑格的最小矩形。使用二维前缀和可以高效计算任意矩形内的黑格数量:
cpp复制#include <bits/stdc++.h>
using namespace std;
const int N = 200;
int mp[N][N];
int n, m, k, ans = N*N;
int main() {
ios::sync_with_stdio(false);
cin >> n >> m >> k;
for(int i=1; i<=n; i++) {
string s; cin >> s;
for(int j=1; j<=m; j++) {
int val = s[j-1] - '0';
mp[i][j] = val + mp[i-1][j] + mp[i][j-1] - mp[i-1][j-1];
}
}
for(int i=1; i<=n; i++) {
for(int j=1; j<=m; j++) {
for(int x=i; x<=n; x++) {
for(int y=j; y<=m; y++) {
int cnt = mp[x][y] - mp[i-1][y] - mp[x][j-1] + mp[i-1][j-1];
if(cnt >= k) {
int area = (x-i+1)*(y-j+1);
ans = min(ans, area);
}
}
}
}
}
cout << (ans == N*N ? 0 : ans) << endl;
return 0;
}
4. 差分技术详解
4.1 一维差分
差分是前缀和的逆操作,用于高效处理区间修改。给定数组a[],其差分数组d[]定义为:
code复制d[i] = a[i] - a[i-1] (i>1)
d[1] = a[1]
区间[l,r]加k的操作可以转化为:
code复制d[l] += k
d[r+1] -= k
然后通过前缀和还原修改后的数组:
cpp复制#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
typedef long long LL;
LL a[N], d[N];
int n, m;
int main() {
ios::sync_with_stdio(false);
cin >> n >> m;
for(int i=1; i<=n; i++) {
cin >> a[i];
d[i] = a[i] - a[i-1];
}
while(m--) {
int l, r; LL k;
cin >> l >> r >> k;
d[l] += k;
d[r+1] -= k;
}
for(int i=1; i<=n; i++) {
a[i] = a[i-1] + d[i];
cout << a[i] << " ";
}
cout << endl;
return 0;
}
4.2 实际应用:海底高铁
这个问题需要计算多条铁路的乘坐次数,然后选择最优购票方案。差分可以高效统计每条铁路的乘坐次数:
cpp复制#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
typedef long long LL;
LL f[N];
int n, m;
int main() {
ios::sync_with_stdio(false);
cin >> n >> m;
int x; cin >> x;
for(int i=2; i<=m; i++) {
int y; cin >> y;
int l = min(x, y), r = max(x, y);
f[l]++, f[r]--;
x = y;
}
for(int i=1; i<=n; i++) {
f[i] += f[i-1];
}
LL ans = 0;
for(int i=1; i<n; i++) {
LL a, b, c;
cin >> a >> b >> c;
ans += min(a * f[i], c + b * f[i]);
}
cout << ans << endl;
return 0;
}
5. 二维差分技术
5.1 基本概念
二维差分用于高效修改二维数组的子矩阵。给定矩阵a[][],其差分矩阵d[][]定义为:
code复制d[i][j] = a[i][j] - a[i-1][j] - a[i][j-1] + a[i-1][j-1]
对子矩阵(x1,y1)到(x2,y2)加k的操作:
code复制d[x1][y1] += k
d[x2+1][y1] -= k
d[x1][y2+1] -= k
d[x2+1][y2+1] += k
然后通过二维前缀和还原修改后的矩阵:
cpp复制#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int a[N][N], d[N][N];
int n, m, q;
int main() {
ios::sync_with_stdio(false);
cin >> n >> m >> q;
for(int i=1; i<=n; i++) {
for(int j=1; j<=m; j++) {
cin >> a[i][j];
d[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;
d[x1][y1] += k;
d[x2+1][y1] -= k;
d[x1][y2+1] -= k;
d[x2+1][y2+1] += k;
}
for(int i=1; i<=n; i++) {
for(int j=1; j<=m; j++) {
a[i][j] = d[i][j] + a[i-1][j] + a[i][j-1] - a[i-1][j-1];
cout << a[i][j] << " ";
}
cout << endl;
}
return 0;
}
5.2 实际应用:棋盘翻转
这个问题需要对棋盘进行多次子矩阵翻转操作,统计每个格子最终的状态:
cpp复制#include <bits/stdc++.h>
using namespace std;
const int N = 2010;
int d[N][N];
int n, m;
int main() {
ios::sync_with_stdio(false);
cin >> n >> m;
while(m--) {
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
d[x1][y1]++;
d[x2+1][y1]--;
d[x1][y2+1]--;
d[x2+1][y2+1]++;
}
for(int i=1; i<=n; i++) {
for(int j=1; j<=n; j++) {
d[i][j] += d[i-1][j] + d[i][j-1] - d[i-1][j-1];
cout << (d[i][j] % 2);
}
cout << endl;
}
return 0;
}
6. 总结与经验分享
在实际应用中,前缀和和差分经常能帮我们将O(n)的操作优化到O(1)。这里分享几个关键经验:
-
边界处理:数组下标从1开始可以简化边界条件的处理,避免复杂的越界检查。
-
数据类型选择:当处理大规模数据时,要注意使用足够大的数据类型(如long long)来防止溢出。
-
空间优化:在某些情况下,可以原地计算前缀和或差分,节省空间。
-
思维转换:很多问题看似复杂,但转化为前缀和或差分问题后,解法会变得非常简洁。
-
调试技巧:对于二维问题,可以画出矩阵和操作区域,帮助理解差分标记的影响范围。
前缀和与差分是算法竞赛中非常基础但强大的工具,掌握它们能显著提高解决区间问题的效率。希望这篇分享对你有帮助!