前缀和与差分
一维前缀和
来看一道例题:
例题 前缀和
题目描述
输入一个长度为
对于每个询问,输出原序列中从第
输入格式
第一行包含两个整数
第二行包含
接下来
输出格式
共
样例输入
6 3
1 3 6 5 4 2
1 3
2 5
3 6
样例输出
10
18
17
题目分析
容易想到如下解法:
for (int i = 1; i <= n; i++) cin >> a[i];
while (q--)
{
int l, r, sum = 0;
cin >> l >> r;
for (int i = l; i <= r; i++) sum += a[i];
cout << sum << endl;
}
考虑上面算法的时间复杂度,外层循环
前缀和 是指一个数组中,从第一个元素开始,到当前元素为止的所有元素之和。
给定数组
我们定义
那么由
例如:假设有一个数组
这个前缀和数组的含义是,
可以发现,对于数组
...
所以前缀和的构造公式就是:
来看如下例子:
回到我们的问题,我们要求的是给定一段区间
到这里可以发现,要求
示例代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
int n, m, l, r;
int a[N], sum[N];
int main() {
cin >> n >> m;
for (int i=1; i<=n; i++) {
cin >> a[i];
sum[i] = sum[i-1]+a[i];
}
while (m--) {
cin >> l >> r;
cout << sum[r]-sum[l-1] << '\n';
}
return 0;
}
一维差分
差分为前缀和的逆运算,一般用来对数组某一段区间的值进行快速修改,我们也先来看一道题目:
例题 差分
题目描述
输入一个长度为
接下来输入
请你输出进行完所有操作后的序列。
输入格式
第一行包含两个整数
第二行包含
接下来
输出格式
共一行,包含
样例输入
6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1
样例输出
3 4 5 3 4 2
题目分析
这个问题很容易想到暴力解,每次都循环遍历
int a[100010];
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
while (m--)
{
int l, r, c;
cin >> l >> r >> c;
for (int i = l; i <= r; i++) a[i] += c;
}
for (int i = 1; i <= n; i++) cout << a[i] << " ";
不难看出这个代码的时间复杂度是
那么,有没有什么技巧可以 快速对序列中某一连续区间的值进行修改 呢?答案就是 —— 差分!
下面我们先来学习差分数组的概念:
设有
根据前面学习的前缀和数组和原数组的关系:
即差分数组的第
可以尝试一下,如果 差分数组
那么还原出来的
那如果此时再让
那么再往下推导,如果想要 原序列中区间
这样每次只需要改变两个元素的值,所以就达到了 快速对序列中某一连续区间的值进行修改 的目的。
总结,利用差分数组来改变原数组中的区间值,操作步骤如下:
- 根据原数组求出差分数组
。 - 要使
区间的值 增加 ,就让 。 - 求出差分数组
的前缀和,即可得到修改后的原数组。
下面来看看如何解决上面的问题,示例代码如下。
示例代码
#include<bits/stdc++.h>
using namespace std;
int n, q, a[100010], b[100010];
int main() {
cin >> n >> q;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) {
b[i] = a[i] - a[i - 1]; // 构造a的差分数组b
}
while (q--) {
int l, r, c;
cin >> l >> r >> c;
b[l] += c; // 操作差分数组b
b[r+1] -= c;
}
for (int i = 1; i <= n; i++) {
b[i] += b[i-1];
cout << b[i] << " "; // 对b求前缀和得到修改后的a
}
return 0;
}
二维前缀和
二维前缀和的原理和一维前缀和类似,不过求的是一个矩阵内的所有元素的和。
例如上图标蓝的部分,就是从
那么如何求出二维前缀和呢?对于上面展示的矩阵,如果要求
可以发现,根据容斥原理,其实就是
展开来说,
二维前缀和的作用:求矩阵中某一个小矩阵的元素总和。
思考一下,如果要求 左上角坐标为
即,求 左上角坐标为
例题:子矩阵的和
题目描述
输入一个
对于每个询问,输出子矩阵中所有数的和。
输入格式
第一行包含三个整数
接下来
接下来
输出格式
共
样例输入
3 5 4
1 1 6 7 4
6 10 4 9 9
2 6 7 3 7
1 2 2 4
2 4 3 5
2 2 3 5
1 3 2 4
样例输出
37
28
55
26
题目分析
二维前缀和模板题。
示例代码
#include<iostream>
using namespace std;
int a[1010][1010], s[1010][1010];
int main() {
int n, m, q;
cin >> n>> m >> q;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) // 读入数据并求前缀和
{
cin >> a[i][j];
s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] + a[i][j];
}
while (q--) { // 求子矩阵的和
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
int ans = s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1];
cout << ans << endl;
}
return 0;
}
二维差分
一维差分可以在一个序列中快速修改某个连续区间的值,那二维差分就可以在一个矩阵中 快速修改某一子矩阵的值。
根据一维差分的定义,我们已经得知,差分就是前缀和的逆运算,二维也是相同的概念,如果对于一个 矩阵
根据二维前缀和的递推公式:
可得,二维差分的递推公式:
那么,应该如何对差分矩阵
可以发现,如果要将 左上角坐标为
语句较多,建议定义成函数来完成这个操作。
来看例题。
例题:差分矩阵
题目描述
输入一个
其中
请你将进行完所有操作后的矩阵输出。
输入格式
第一行包含整数
接下来
接下来
数据范围:
输出格式
共
样例输入
3 4 3
1 2 2 1
3 2 2 1
1 1 1 1
1 1 2 2 1
1 3 2 3 2
3 1 3 4 1
样例输出
2 3 4 1
4 3 4 1
2 2 2 2
题目分析
二维差分模板题。
示例代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int a[N][N], b[N][N];
void insert(int x1, int y1, int x2, int y2, int c) {
// 使得差分矩阵的指定区域增加 c
b[x1][y1] += c;
b[x2+1][y1] -= c;
b[x1][y2+1] -= c;
b[x2+1][y2+1] += c;
}
int main() {
int n, m, q;
cin >> n >> m >> q;
// 数据读入a
for (int i=1; i<=n; i++)
for (int j=1; j<=m; j++)
cin >> a[i][j];
// 构造a的差分数组b
for (int i=1; i<=n; i++)
for (int j=1; j<=m; j++)
b[i][j] = a[i][j] + a[i-1][j-1] - a[i-1][j] - a[i][j-1];
// q次操作
while (q--) {
int x1, y1, x2, y2, c;
cin >> x1 >> y1 >> x2 >> y2 >> c;
insert(x1, y1, x2, y2, c);
}
// 对b求一次前缀和,得到修改后的a
for (int i=1; i<=n; i++)
for (int j=1; j<=m; j++)
b[i][j] = b[i-1][j] + b[i][j-1] - b[i-1][j-1] + b[i][j];
// 输出
for (int i=1; i<=n; i++) {
for (int j=1; j<=m; j++) {
cout << b[i][j] << " ";
}
cout << endl;
}
return 0;
}