跳到主要内容

3 - 前缀和与差分

准备春招,算法题冲冲冲!

前缀和与差分是一对逆运算,它们与高效区间操作有关。

前缀和Sn

概念

可以简单理解为 数组的前n项和,是一种重要的预处理方式,能大大降低查询的时间复杂度。

C++标准库中实现了前缀和函数std::partial_sum,详见这里,位于头文件<numeric>中。

构造

一维

例如有一数组A = {1, 2, 3, 4, 5},要求一个数组S,满足S[n]为数组A的前n项和。

可通过递推实现:S[0] = A[0]S[i] = S[i - 1] + A[i]。代码如下:

int a[] = {0, 1, 2, 3, 4, 5};
int s[6];

// 求前缀和(1起点)
for (int i = 1; i <= 5; ++i)
{
s[i] = s[i - 1] + a[i];
}

使用std::partical_sum的代码如下:

vector<int> a = {1, 2, 3, 4, 5};
vector<int> s(a.size());

partial_sum(cbegin(a), cend(a), begin(s));

二维

    int n, m, q;
cin >> n >> m >> q;

// 构造二维前缀和(1起点)
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];
}
}

区间求和操作

一维

可以通过前缀和快速求出原数组A里面一段数的和,例如要求[L, R]区间的元素和,只需计算S[R] - S[L-1]

二维

如图,要想求二维前缀和S[i][j](黑色填充部分),就得求绿色填充 + 红色填充 - 重叠部分 + A[i][j]。也就是:S[i][j] = S[i - 1][j] + S[i][j - 1] - S[i - 1][j - 1] + A[i][j]

知道怎么求二维前缀和后,接下来看看如何运用它。例如,想求某矩阵的一块区间和(左上角[x1, y1],右下角[x2, y2]),可以这样求:S[x2][y2] - S[x2][y1 - 1] - S[x1 - 1][y2] + S[x1 - 1][y1 - 1]

例题

一维

795. 前缀和 - AcWing题库
#include <iostream>
using namespace std;

int a[100001], s[100001], n, m;

int main()
{
cin >> n >> m;

// 构造1起点的前缀和
for (int i = 1; i <= n; ++i)
{
cin >> a[i];
s[i] = s[i - 1] + a[i];
}

// 区间查询操作
int l, r;
for (int i = 0; i < m; ++i)
{
cin >> l >> r;
cout << s[r] - s[l - 1] << '\n';
}
}
209. 长度最小的子数组 - 力扣(LeetCode)

可以求出来数组的前缀和,然后去确定 前缀和 >= target的区间[i, j]。具体做法就是遍历前缀和中的每一个preSum[i - 1],找到preSum[j],使得preSum[j] - preSum[i - 1] >= target,找j的过程中可以用二分查找来加快速度。

int minSubArrayLen(int target, vector<int>& nums) {
// 初始化前缀和(1起点)
vector<int> preSum(nums.size() + 1, 0);
for (int i = 1; i < preSum.size(); ++i)
{
preSum.at(i) = preSum.at(i - 1) + nums.at(i - 1);
}

// 找子数组
int ans = 114514;
for (int i = 1; i <= nums.size(); ++i)
{
auto j = lower_bound(preSum.begin(), preSum.end(), target + preSum.at(i - 1));
if (j != preSum.end())
ans = min<int>(j - preSum.begin() - i + 1, ans);
}

return ans == 114514 ? 0 : ans;
}
560. 和为 K 的子数组 - 力扣(LeetCode)

这道题可以用前缀和来做,用两层循环,统计preSum[i] - preSum[j - 1] == k的次数就行:

int subarraySum(vector<int>& nums, int k) {
// 初始化前缀和
vector<int> preSum(nums.size() + 1, 0);
for (int i = 1; i <= nums.size(); ++i)
preSum[i] = preSum[i - 1] + nums[i - 1];

// 找答案
int ans = 0;
for (int i = 1; i <= nums.size(); ++i)
{
for (int j = 1; j <= i; ++j)
{
if (preSum[i] - preSum[j - 1] == k)
++ans;
}
}
return ans;
}

但提交一看,耗时太大了,是O(n2)O(n^2)的复杂度,有没有办法优化一下?

可以使用哈希表,和 两数之和 这道题一样,用哈希表存储前缀和的值,然后遍历一遍前缀和数组就行了:

int subarraySum(vector<int>& nums, int k) {
int ans = 0;
unordered_map<int, int> hash; // 哈希表记录对应值的出现次数
vector<int> preSum(nums.size() + 1, 0);

// 子数组只有它一个元素
hash[0]++;
for (int i = 1; i <= nums.size(); ++i)
{
preSum[i] = preSum[i - 1] + nums[i - 1];
// preSum[i] - preSum[j - 1] = k => preSum[j - 1] = preSum[i] - k
if (hash[preSum[i] - k])
ans += hash[preSum[i] - k];

hash[preSum[i]]++;
}
return ans;
}

二维

796. 子矩阵的和 - AcWing题库
#include <iostream>
using namespace std;

int a[1010][1010], s[1010][1010];

int main()
{
int n, m, q;
cin >> n >> m >> q;

// 构造二维前缀和(1起点)
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];
}
}

// 区间求和操作
int x1, x2, y1, y2;
for (int i = 0; i < q; ++i)
{
cin >> x1 >> y1 >> x2 >> y2;

cout << s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1] << '\n';
}
}

差分Bn

概念

可看做前缀和的逆运算,例如有数组A = {a1, ... ,an},那么B[n] = A[n] - A[n - 1], B[1] = A[1]

从上面可以发现,A[n]的值是B的前缀和/前n项和。

差分数组的主要使用场景是 对原始数组的某个区间的元素进行增减

区间插入操作(区间增减)

一维

A[L] ~ A[R]+ c,那么仅需:

void insert(int L, int R, int c)
{
b[L] += c; // 让A[L] ~ A[n]都被+c

// 这里注意数组越界问题
if (R + 1 < diff.size())
b[R + 1] -= c; // 让A[R+1] ~ A[n]恢复原状
}

二维

可由一维类推得到,如下图所示:

要想给A[x1][y1] ~ A[x2][y2]+ c,那么仅需:

void insert(int x1, int y1, int x2, int y2, int c)
{
b[x1][y1] += c; // a[x1][y1] ~ a[n][m] + c
b[x2 + 1][y1] -= c; // a[x2+1][y1] ~ a[n][m] - c
b[x1][y2 + 1] -= c; // a[x1][y2+1] ~ a[n][m] - c
b[x2 + 1][y2 + 1] += c; // a[x2+1][y2+1] ~ a[n][m] + c
}

构造

可以利用区间插入操作来构造差分,分一维和二维讨论。

一维

要想构造一维的差分,可在[1, 1]处插入a[0],... ,在[n, n]处插入a[n - 1]

接下来就能对差分进行区间操作了,如果想要获得操作后的a[n],仅需对b[n]求前缀和:

for (int i = 1; i <= n; i++)
b[i] += b[i-1];

二维

和一维同理,求前缀和代码如下:

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];

例题

一维

797. 差分 - AcWing题库
#include <iostream>
using namespace std;

int a[100010], b[100010];

void insert(int l, int r, int c)
{
b[l] += c;
b[r + 1] -= c;
}

int main()
{
int n, m;
cin >> n >> m;

// 构造差分(1起点)
for (int i = 1; i <= n; ++i)
{
cin >> a[i];
insert(i, i, a[i]);
}

// 区间操作
int l, r, c;
for (int i = 0; i < m; ++i)
{
cin >> l >> r >> c;
insert(l, r, c);
}

// 得到操作后的A[n]
for (int i = 1; i <= n; ++i)
{
b[i] += b[i - 1];
cout << b[i] << " ";
}
}
370. 区间加法 - 力扣(LeetCode)

差分模板题:

void insert(vector<int>& diff, int L, int R, int c)
{
diff[L] += c;
if (R + 1 < diff.size())
diff[R + 1] -= c;
}

vector<int> getModifiedArray(int length, vector<vector<int>>& updates) {
vector<int> diff(length, 0);

// 构建差分数组
for (auto& update : updates)
insert(diff, update[0], update[1], update[2]);

// 从差分数组构建修改后的区间
for (int i = 1; i < length; ++i)
diff[i] += diff[i - 1];

return diff;
}
1109. 航班预订统计 - 力扣(LeetCode)

差分模板题,套了个情景,参考代码如下:

void insert(vector<int>& diff, int L, int R, int c)
{
diff[L] += c;
if (R + 1 < diff.size())
diff[R + 1] -= c;
}

vector<int> corpFlightBookings(vector<vector<int>>& bookings, int n) {
vector<int> diff(n, 0);

// 构建差分数组(注意booking里的是1起点)
for (auto& booking : bookings)
insert(diff, booking[0] - 1, booking[1] - 1, booking[2]);

// 还原修改后的区间
for (int i = 1; i < n; ++i)
diff[i] += diff[i - 1];

return diff;
}
1094. 拼车 - 力扣(LeetCode)

我们可以将乘客接放的位置看成一个大区间(1~1000),然后开始模拟。例如[2,1,5]的意思就是把区间[1, 5)中所有元素+2。

代码如下:

void insert(vector<int>& diff, int L, int R, int c)
{
diff[L] += c;
if (R + 1 < diff.size())
diff[R + 1] -= c;
}

bool carPooling(vector<vector<int>>& trips, int capacity) {
// 构建差分数组
vector<int> diff(1001, 0);
for (auto& trip : trips)
// 注意在[L, R)中修改, 就是在[L, R - 1]中修改
insert(diff, trip[1], trip[2] - 1, trip[0]);

// 获取修改后区间
for (int i = 1; i < diff.size(); ++i)
{
diff[i] += diff[i - 1];
}

for (int i = 0; i < diff.size(); ++i)
{
if (diff[i] > capacity)
return false;
}

return true;
}

二维

798. 差分矩阵 - AcWing题库
#include <iostream>
using namespace std;

int b[1010][1010];

void insert(int x1, int y1, int x2, int y2, int c)
{
b[x1][y1] += c;
b[x1][y2 + 1] -= c;
b[x2 + 1][y1] -= c;
b[x2 + 1][y2 + 1] += c;
}

int main()
{
int n, m, q;
cin >> n >> m >> q;

// 构造二维差分 (1起点)
int c;
for (int i = 1; i <= n; ++i)
{
for (int j = 1; j <= m; ++j)
{
cin >> c;
insert(i, j, i, j, c);
}
}

// 区间插入操作
int x1, y1, x2, y2;
for (int i = 0; i < q; ++i)
{
cin >> x1 >> y1 >> x2 >> y2 >> c;
insert(x1, y1, x2, y2, c);
}

// 求操作后的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];
cout << b[i][j] << " ";
}
cout << '\n';
}
}

参考资料