前缀和与差分思想
前缀和
例题:P8218 [深进1.例1] 求区间和
给定
个正整数组成的数列 和 个区间 ,分别求这 个区间的区间和。
数据范围:。
分析:最直接的思路是对于每次询问,从
如果设
观察可知,
#include <cstdio> const int N = 1e5 + 5; int a[N], s[N]; int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); s[i] = s[i - 1] + a[i]; } int m; scanf("%d", &m); for (int i = 1; i <= m; i++) { int l, r; scanf("%d%d", &l, &r); printf("%d\n", s[r] - s[l - 1]); } return 0; }
例题:P1115 最大子段和
“最大子段和”问题是序列问题中的经典问题,其做法非常多。这里我们最终基于前缀和思想来解决它。
首先对于这个问题,我们可以想到直接模拟题意,因为要找区间和最大的子段,所以先枚举区间的两个端点,再利用一层循环去求和,找到每个区间和中的最大值。这么做合起来有三层循环,时间复杂度为
参考代码
#include <cstdio> #include <algorithm> using std::max; const int N = 2e5 + 5; const int INF = 1e5; int a[N]; int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); } int ans = -INF; // 理论上最大子段和的最小值是-1e4,因此初始化一个比-1e4小的极小值 for (int i = 1; i <= n; i++) { // 枚举左端点 for (int j = i; j <= n; j++) { // 枚举右端点 int sum = 0; // 准备计算[i,j]的区间和 for (int k = i; k <= j; k++) sum += a[k]; ans = max(ans, sum); // 更新最大子段和 } } printf("%d\n", ans); return 0; }
实际上可以发现,当区间左端点固定时,依次向右枚举右端点时,区间和相当于不断新增一个数,因此求区间和不用单独再开一个循环计算,而是可以和右端点的移动过程融合。这么做省去了一次循环,时间复杂度降到
参考代码
#include <cstdio> #include <algorithm> using std::max; const int N = 2e5 + 5; const int INF = 1e5; int a[N]; int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); } int ans = -INF; // 理论上最大子段和的最小值是-1e4,因此初始化一个比-1e4小的极小值 for (int i = 1; i <= n; i++) { // 枚举左端点 int sum = 0; for (int j = i; j <= n; j++) { // 枚举右端点 sum += a[j]; ans = max(ans, sum); } } printf("%d\n", ans); return 0; }
但是
这个形式和前缀和很像,实际上前缀和代表了一类预处理思想。前缀和的思想,除了可以用来求一个区间内的和以外,也可以用来预处理最大最小值。我们除了预处理前缀和以外,还去预处理每个前缀和的“前缀最小值”,则对于上面那个式子,只需要利用“前缀和的前缀最小值”就可以快速计算出右端点固定时的最大子段和。而不管是前缀和还是前缀和的前缀最小值都可以在输入原数组中顺便处理出来,这样我们后面的计算只需要枚举每种右端点的情况即可,总的时间复杂度为
#include <cstdio> #include <algorithm> using std::max; using std::min; const int N = 2e5 + 5; const int INF = 1e5; int a[N], s[N], pre[N]; int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); s[i] = s[i - 1] + a[i]; // 前缀和 pre[i] = min(pre[i - 1], s[i]); // 前缀和的前缀最小值,注意因为s[0]=0,所以pre[0]也是0 } int ans = -INF; // 理论上最大子段和的最小值是-1e4,因此初始化一个比-1e4小的极小值 for (int i = 1; i <= n; i++) { // 枚举的i固定了右端点 ans = max(ans, s[i] - pre[i - 1]); // 右端点固定的情况下希望减去s[0]~s[i-1]里的最小值 } printf("%d\n", ans); return 0; }
习题:P3131 [USACO16JAN] Subsequences Summing to Sevens S
解题思路
首先将区间和用前缀和之差的形式表示,则区间和能被
#include <cstdio> #include <algorithm> using std::max; const int N = 5e4 + 5; int first[7], last[7]; // first和last数组记录每种余数第一次和最后一次出现的位置 int main() { int n; scanf("%d", &n); int s = 0; first[0] = last[0] = 0; // 注意0位置也有个前缀和0 for (int i = 1; i < 7; i++) first[i] = last[i] = -1; // 其余几种余数暂时标记为没出现过 for (int i = 1; i <= n; i++) { int x; scanf("%d", &x); s = (s + x) % 7; // 不需要记前缀和的值,记它除以7的余数即可 if (first[s] == -1) first[s] = i; last[s] = i; } int ans = 0; for (int i = 0; i < 7; i++) ans = max(ans, last[i] - first[i]); printf("%d\n", ans); return 0; }
习题:P6067 [USACO05JAN] Moo Volume S
解题思路
原本要求的问题中包含像
#include <cstdio> #include <algorithm> using ll = long long; using std::sort; const int N = 1e5 + 5; int x[N]; ll s[N]; int main() { int n; scanf("%d", &n); ll sum = 0; for (int i = 1; i <= n; i++) { scanf("%d", &x[i]); sum += x[i]; } sort(x + 1, x + n + 1); ll ans = 0; for (int i = 1; i <= n; i++) { s[i] = s[i - 1] + x[i]; ans += 1ll * x[i] * (2 * i - 1 - n) + sum - s[i] - s[i - 1]; } printf("%lld\n", ans); return 0; }
例题:P1719 最大加权矩形
有一个
的矩阵,矩阵中每个元素都有一个权值,权值是 之间的整数。从中找一矩形,矩形大小没有限制,要求其中包含的所有元素的和最大。
分析:最直接的做法是枚举左上角端点和右下角端点坐标,再将矩形内的元素求和,时间复杂度为
与一维数组上的前缀和类似,设
首先需要能够快速计算出
完成
#include <cstdio> #include <algorithm> using std::max; const int N = 125; int a[N][N], s[N][N]; int query(int x1, int y1, int x2, int y2) { return s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]; } int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { scanf("%d", &a[i][j]); s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j]; } } int ans = -127; for (int x1 = 1; x1 <= n; x1++) { for (int y1 = 1; y1 <= n; y1++) { for (int x2 = x1; x2 <= n; x2++) { for (int y2 = y1; y2 <= n; y2++) { ans = max(ans, query(x1, y1, x2, y2)); } } } } printf("%d\n", ans); return 0; }
进一步优化,枚举上边界为第
#include <cstdio> #include <algorithm> using std::max; using std::min; const int N = 125; int a[N][N], s[N][N]; // s[i][j]表示a[1][j]+a[2][j]+...+a[i][j] int tmp[N], sum[N], pre[N]; int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { scanf("%d", &a[i][j]); s[i][j] = s[i - 1][j] + a[i][j]; } } int ans = -127; for (int i = 1; i <= n; i++) { // 枚举上边界 for (int j = i; j <= n; j++) { // 枚举下边界 // 将每一列压成一个数 for (int k = 1; k <= n; k++) { tmp[k] = s[j][k] - s[i - 1][k]; sum[k] = sum[k - 1] + tmp[k]; pre[k] = min(pre[k - 1], sum[k]); } // 对tmp数组求最大子段和 for (int k = 1; k <= n; k++) { ans = max(ans, sum[k] - pre[k - 1]); } } } printf("%d\n", ans); return 0; }
习题:P2004 领地选择
解题思路
预处理二维前缀和,枚举正方形的左上角坐标,根据给定的边长求出右下角坐标,利用二维前缀和快速求出整个正方形的矩形和。
#include <cstdio> #include <algorithm> using std::max; const int N = 1005; int a[N][N], s[N][N]; int query(int x1, int y1, int x2, int y2) { return s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]; } int main() { int n, m, c; scanf("%d%d%d", &n, &m, &c); for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { scanf("%d", &a[i][j]); s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j]; } } int maxs = query(1, 1, c, c), ansx = 1, ansy = 1; for (int i = 1; i <= n - c + 1; i++) { for (int j = 1; j <= m - c + 1; j++) { int x = i + c - 1, y = j + c - 1; // 计算正方形右下角坐标 int sum = query(i, j, x, y); if (sum > maxs) { maxs = sum; ansx = i; ansy = j; } } } printf("%d %d\n", ansx, ansy); return 0; }
习题:P2280 [HNOI2003] 激光炸弹
解题思路
先把输入数据里的目标标记到二维数组上,注意可能有多个目标在某个位置重合,因此价值要累加。然后求一遍二维前缀和,注意输入的
#include <cstdio> #include <algorithm> using std::max; const int N = 5005; int a[N][N], s[N][N]; int query(int x1, int y1, int x2, int y2) { return s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]; } int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { int x, y, v; scanf("%d%d%d", &x, &y, &v); // 由于原来的坐标范围是0~5000,不方便前缀和处理,所以统一加一 a[x + 1][y + 1] += v; // 注意一个位置上存在多个目标,需要叠加 } for (int i = 1; i < N; i++) { for (int j = 1; j < N; j++) { s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j]; } } int ans = 0; for (int i = 1; i <= N - m; i++) { for (int j = 1; j <= N - m; j++) { int x = i + m - 1, y = j + m - 1; ans = max(ans, query(i, j, x, y)); } } printf("%d\n", ans); return 0; }
差分
例题:P2367 语文成绩
班上共有
个学生,语文老师在统计成绩的时候总是出错。语文老师需要对学生成绩进行 次修改,每次修改需要给第 个学生到第 个学生每人增加 分。语文老师想知道成绩修改后全班的最低分。
数据范围:。
分析:如果直接模拟修改,时间复杂度为
对于数组
如果我们对
如果将
因此,将
#include <cstdio> #include <algorithm> using std::min; const int N = 5e6 + 5; const int INF = 1e9; int a[N], b[N]; int main() { int n, p; scanf("%d%d", &n, &p); for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); b[i] = a[i] - a[i - 1]; // 求差分数组b } for (int i = 1; i <= p; i++) { int x, y, z; scanf("%d%d%d", &x, &y, &z); // 修改操作,将a[x],a[x+1],...,a[y]加上z b[x] += z; b[y + 1] -= z; } int ans = INF; for (int i = 1; i <= n; i++) { a[i] = a[i - 1] + b[i]; // 对差分数组做一遍前缀和复原出原数组 ans = min(ans, a[i]); } printf("%d\n", ans); return 0; }
例题:P3397 地毯
在
的格子上有 个地毯,给出地毯的信息,每块地毯覆盖的左上角是 ,右下角是 ,问每个点被多少个地毯覆盖。
分析:设数组
考虑一次覆盖操作,地毯左上角为
#include <cstdio> const int N = 1005; int a[N][N]; int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= m; i++) { int x1, y1, x2, y2; scanf("%d%d%d%d", &x1, &y1, &x2, &y2); // 二维差分 a[x1][y1]++; a[x2 + 1][y2 + 1]++; a[x1][y2 + 1]--; a[x2 + 1][y1]--; } for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { // 在差分数组上重新求一遍前缀和 a[i][j] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1] + a[i][j]; printf("%d ", a[i][j]); } printf("\n"); } return 0; }
习题:P3406 海底高铁
解题思路
每一段地铁办卡还是不办卡取决于整个行程中坐到这段地铁的次数,而出差行程中的每一小段可以看作是对经过的一系列地铁乘坐次数区间加一,因此可以利用差分和前缀和技术求出每段地铁的乘坐次数。对于某段地铁来说,不办卡就是
#include <cstdio> #include <algorithm> using ll = long long; using std::min; using std::max; const int N = 1e5 + 5; int p[N], cnt[N]; // cnt[i]维护i~i+1这段铁路的经过次数 int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= m; i++) { scanf("%d", &p[i]); if (i > 1) { // p[i-1]---p[i] 注意p[i-1]和p[i]谁更大不一定 // 利用差分思想 int l = min(p[i - 1], p[i]), r = max(p[i - 1], p[i]); cnt[l]++; cnt[r]--; // 注意r-1~r才是最后一段铁路,所以差分减一的位置是r } } ll ans = 0; for (int i = 1; i < n; i++) { cnt[i] += cnt[i - 1]; // 对差分数组求前缀和得到实际次数 int a, b, c; scanf("%d%d%d", &a, &b, &c); ans += min(1ll * a * cnt[i], 1ll * b * cnt[i] + c); // 选择便宜的方案 } printf("%lld\n", ans); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?