前缀和、差分
听到这个名字,大家一定感觉不陌生吧,毕竟学过动态规划的人大部分 都知道,那我就简单介绍一下,并奉上习题供大家参考!
题目描述我就不放了,根据题号参见 东方博宜OJ
东方博宜OJ 网址:东方博宜OJ
------------------------------------------------------------我是分割线----------------------------------------------------------
第一部分:前缀和
1、前缀和简介
前缀和是一种在计算机科学中常用的算法技术,主要用于快速计算数组或序列的前缀和。前缀和可以用于解决一系列与数组或序列元素的相关问题,例如求解某个区间元素的和、求解某个区间元素的平均值等。
具体来说,前缀和是指通过依次累加数组元素,得到一个新的数组,新数组的第i个元素即为原数组中前i个元素的累加和。使用前缀和可以在O(1)的时间内计算出任意一个区间的元素和,而不需要遍历整个区间。具体的计算公式如下:
prefix_sum[i] = prefix_sum[i-1] + array[i]
在C++中,可以使用一个数组来存储前缀和,然后根据需要的区间范围快速计算出区间的和。
应用方面,前缀和可以解决很多实际问题,例如求解数组中某个区间的元素和、求解连续子数组的最大和、求解最长连续递增子序列等。通过利用前缀和,可以大大提高问题的计算效率,减少不必要的重复计算。
2、C++前缀和示例代码
#include <iostream>
using namespace std;
void prefixSum(int arr[], int n, int prefix[]) {
prefix[0] = arr[0];
for (int i = 1; i < n; i++) {
prefix[i] = prefix[i-1] + arr[i];
}
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
int n = sizeof(arr) / sizeof(arr[0]);
int prefix[n];
prefixSum(arr, n, prefix);
cout << "Prefix Sum: ";
for (int i = 0; i < n; i++) {
cout << prefix[i] << " ";
}
return 0;
}
这个代码中,prefixSum函数计算了给定数组的前缀和,并将结果存储在prefix数组中。主函数中,我们定义了一个整数数组arr,并调用prefixSum函数来计算前缀和。最后,我们输出了计算得到的前缀和。以上代码的输出结果为:Prefix Sum: 1 3 6 10 15。
3、前缀和的优缺点
前缀和是一种常用的算法技巧,它的优点有:
- 时间复杂度低:前缀和可以在O(n)的时间复杂度内计算出整个数组的和,其中n是数组的长度。这比每次都遍历整个数组来计算和的时间复杂度要低。
- 加速子数组求和:通过使用前缀和,可以在O(1)的时间复杂度内计算出任意子数组的和。这对于需要频繁查询子数组和的问题非常有用。
- 提供计算问题的便利:前缀和可以将一些复杂的问题转化为简单的计算问题。例如,可以使用前缀和来计算数组中是否存在一个子数组的和等于给定的目标值。
然而,前缀和也有一些缺点:
- 需要额外的空间:为了计算前缀和,需要创建一个额外的数组来存储中间结果。这会占用一定的空间。
- 需要预处理:为了使用前缀和,需要先对原始数组进行预处理,计算出前缀和数组。这会增加计算的时间和空间开销。
综上所述,前缀和是一种强大而常用的算法技巧,适用于需要频繁查询子数组和的问题。然而,对于不需要频繁查询子数组和的问题,使用前缀和可能会增加额外的计算和空间开销。
第二部分:差分
1、差分简介
差分是一种常用的算法技术,用来计算数组元素之间的差异或变化。在C++中,差分可以通过遍历数组,并将相邻元素之间的差保存在另一个数组中来实现。差分技术在很多实际问题中都有广泛应用。
差分的应用之一是计算数组的前缀和或后缀和。通过差分技术,可以在O(n)的时间复杂度内计算出原数组的前缀和或后缀和数组。前缀和指的是数组中每个元素之前所有元素的和,而后缀和指的是数组中每个元素之后所有元素的和。前缀和和后缀和的计算可以用来快速求解子数组的和或者计算区间内的元素之和。
另一个应用是求解区间的元素变化。通过差分技术,可以很方便地更新数组中某个区间的元素值,而不需要对整个区间进行遍历。这种技术在解决一些需要频繁更新数组元素的问题中很有用,如求解动态区间最大值、最小值、和等问题。
差分还可以用于一些离散化问题。离散化是将连续的数值映射为离散的数值,常用于解决某些需要将数据映射为有限个数的问题。通过差分技术,可以将原数组转化为差分数组,再进行离散化操作,可以简化问题的处理过程。
总的来说,差分是一种简单而实用的算法技术,可以应用于很多实际问题中,如计算数组的前缀和或后缀和、求解区间的元素变化、离散化等。在C++中,通过遍历数组并计算相邻元素之间的差,可以很方便地实现差分算法。差分是一种常用的算法技术,用来计算数组元素之间的差异或变化。在C++中,差分可以通过遍历数组,并将相邻元素之间的差保存在另一个数组中来实现。差分技术在很多实际问题中都有广泛应用。
看,差分是不是和前缀和有着密切关系呢
2、C++差分示例代码
#include <iostream>
#include <vector>
using namespace std;
// 差分操作:给区间[l, r]的所有元素加上c
void add(vector<int>& diff, int l, int r, int c) {
diff[l] += c;
if (r + 1 < diff.size()) {
diff[r + 1] -= c;
}
}
// 还原操作:将差分数组还原为原始数组
vector<int> restore(vector<int>& diff) {
vector<int> result(diff.size());
result[0] = diff[0];
for (int i = 1; i < diff.size(); i++) {
result[i] = result[i - 1] + diff[i];
}
return result;
}
int main() {
vector<int> original = {1, 2, 3, 4, 5};
vector<int> diff(original.size());
// 进行差分操作
add(diff, 1, 3, 2);
add(diff, 2, 4, 1);
// 还原差分数组
vector<int> restored = restore(diff);
// 输出还原后的数组
for (int i = 0; i < restored.size(); i++) {
cout << restored[i] << " ";
}
cout << endl;
return 0;
}
输出:
1 4 7 6 5
以上代码实现了差分操作,并将差分数组还原为原始数组。
3、差分的优缺点
差分的优点:
- 差分操作可以将原始数组的更新操作转化为差分数组的更新操作,从而减少了计算量和时间复杂度。
- 差分数组的更新操作只需要修改两个位置的元素,不需要对整个数组进行修改,从而节省了空间。
差分的缺点:
- 差分数组只能处理一维数组的更新操作,无法直接应用于多维数组。
- 差分数组的还原操作需要额外的计算,时间复杂度较高。
- 差分数组在某些特殊情况下可能会导致数据溢出,需要注意处理。
总结:
差分是一种通过差分数组记录原始数组的增量更新操作的方法,它在一些场景下可以减少计算量和时间复杂度,但同时也存在一些限制和注意事项。在实际应用中,需要根据具体问题和需求来选择是否使用差分。
第三部分:习题
正文开始
讲解我都注释在代码里面了,这里我就不再一一赘述了
1、前缀和(∑)
2060 - 计算能力
这道题,妥妥经典的求区间和的题目(模板题),记住公式 sum[l, r] = b[r] - b[l - 1] 即可!如果记不住,模拟也可得到!
#include <iostream>
#include <vector>
#include <cmath>
#include <algorithm>
using namespace std;
typedef long long ll;
const ll N = 100010;
//a数组表示读入的值,b数组表示前缀和
vector<ll> a(N), b(N);
ll n, m;
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
//如果不放心,可用scanf(), printf()
//cin >> n >> m;
scanf("%lld%lld", &n, &m);
//读入n个数(下标从1开始更方便!从0开始会越界!)
for (ll i = 1; i <= n; i++) {
//cin >> a[i]
scanf("%lld", &a[i]);
//求前缀和
b[i] = b[i - 1] + a[i];
}
//x代表l,y代表r
ll x, y;
//读入m个问题
for (ll i = 1; i <= m; i++) {
//cin >> x >> y;
scanf("%lld%lld", &x, &y);
//cout << b[y] - b[x - 1] << endl;
printf("%lld\n", b[y] - b[x - 1]);
}
return 0;
}
2061 - 子矩阵求和
二维数组前缀和应用
#include <iostream>
#include <vector>
#include <cmath>
#include <algorithm>
using namespace std;
typedef long long ll;
//a数组表示读入的值,b数组表示前缀和
const ll N = 1010;
vector<vector<ll>> a(N, vector<ll>(N)), b(N, vector<ll>(N));
ll n, m, k;
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m >> k;
//读入二维数组
for (ll i = 1; i <= n; i++) {
for (ll j = 1; j <= m; j++) {
cin >> a[i][j];
//求前缀和
b[i][j] = a[i][j] + b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1];
}
}
//读入k次询问
ll x1, y1, x2, y2;
for (ll i = 1; i <= k; i++) {
cin >> x1 >> y1 >> x2 >> y2;
//输出区间和
cout << b[x2][y2] - b[x1 - 1][y2] - b[x2][y1 - 1] + b[x1 - 1][y1 - 1] << endl;
}
return 0;
}
2119 - 任务的最少完成时间
/*
有n个数,可以删除连续的k个数
求剩余的和最小是多少?
相当于求:连续k个数的和最大是多少?
*/
#include <iostream>
#include <vector>
#include <cmath>
#include <algorithm>
using namespace std;
typedef long long ll;
const ll N = 1000010;
ll n, k, maxn;
//b代表前缀和
vector<ll> a(N), b(N);
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> k;
//读入n个整数
for (ll i = 1; i <= n; i++) {
cin >> a[i];
//求前缀和
b[i] = b[i - 1] + a[i];
}
//求从每个数开始连续k个数的和,在所有的和中求最大
for (ll i = 1; i <= n - k + 1; i++) {
//区间范围:i ~ i + k - 1
maxn = max(maxn, b[i + k - 1] - b[i - 1]);
}
//求剩余任务时间和的最小值
cout << b[n] - maxn << endl;
return 0;
}
2105 - 不太甜的糖果
这道题考察的比较综合(考察内容如下):
题目的基本思想如下(题目长,但大部分都是FH,一定记得提取关键信息!):
代码显然比上面的要长,不信请看:
/*
关键信息:
给定一排长度为n的糖果串,每个糖果有一个甜度;
在不改变糖果顺序的前提下,求出一个最短的糖果串使得它的甜度之和大于等于m
题目原意:
给定一个数组,求最短的字符串,使得 元素之和 >= m
归纳法!
要考虑特殊情况!
由此,可构建出伪代码:
//穷举所有可能出现的长度
for (len = 1 ~ n){
//从每个数开始求连续len个数的和
//穷举每个数的开头
for (i = 1 ~ n - len + 1){
//用前缀和优化
}
}
解题关键:
1、二分可能的长度(知识点:二分)
2、用前缀和求区间和 (知识点:前缀和)
*/
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;
const ll N = 230010;
vector<ll> a(N), f(N);
ll n, m;
// 检验字串长度为mid的情况下,子串和是否可能 >= m
bool check(ll mid) {
// 从每个可能的开头开始求连续mid个数的和
for (ll i = 1; i <= n - mid + 1; i++) {
// 区间范围:i ~ i + mid - 1
if (f[i + mid - 1] - f[i - 1] >= m)
return true; // 找到了满足条件的子串
}
return false; // 没有找到满足条件的子串
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
for (ll i = 1; i <= n; i++) {
cin >> a[i];
// 求前缀和
f[i] = f[i - 1] + a[i];
}
// 二分可能的长度
ll l = 1, r = n, ans = n + 1;
while (l <= r) {
ll mid = (l + r) / 2;
// 如果长度为mid的字串和满足 >= m
if (check(mid)) {
ans = mid; // 更新答案
r = mid - 1; // 缩小右边界
}
else {
l = mid + 1; // 扩大左边界
}
}
// 如果找不到这样的串,输出0
if (ans == n + 1) cout << 0 << endl;
else cout << ans << endl;
return 0;
}
本蒟蒻看前缀和的题目似乎有点少了,于是乎自己又加了一道(也是基础,供新手食用):
题目:最大利润区间
背景:
小明是一个股票交易员。他有一份记录了某只股票在连续N天中的每日利润(可能为正,也可能为负)。小明想要知道在这N天中,他如果选择一个连续的交易区间,最多能获得多少利润。
问题:
给定一个长度为N的整数数组,表示股票在N天中的每日利润。请计算可以获得的最大连续利润。如果不可能获得正利润,则输出0。
输入格式:
第一行包含一个整数N(1 ≤ N ≤ 100000),表示天数。
第二行包含N个整数,每个整数的绝对值不超过10000,表示每天的利润。
输出格式:
输出一个整数,表示最大连续利润。如果不可能获得正利润,输出0。
样例输入:
8
-2 -3 4 -1 -2 1 5 -3
样例输出:
7
解释:
最大利润区间为第3天到第7天,总利润为4 + (-1) + (-2) + 1 + 5 = 7。
提示:
考虑使用=Kadane算法来解决这个问题。
注意处理全是负数的情况。
这里,我先介绍一下卡丹(Kadane)算法:
卡丹算法(Kadane’s algorithm)是一种用于解决最大子数组和问题的动态规划算法。最大子数组和问题是指在一个整数数组中,找到一个子数组,使得子数组元素之和最大。
卡丹算法的基本思想是通过迭代的方式,计算以每个元素为结尾的子数组的最大和,并在迭代过程中维护一个全局的最大和。具体步骤如下:
- 初始化全局最大和和当前最大和为数组的第一个元素。
- 从数组的第二个元素开始迭代:
- 计算当前元素和当前最大和加上当前元素的和,并将其与当前元素的值比较,取较大者作为当前 最大和。
- 将当前最大和与全局最大和比较,取较大者作为全局最大和。
- 返回全局最大和作为结果。
通过这种方式,卡丹算法能够在 O(n) 的时间复杂度内解决最大子数组和问题,其中 n 是数组的长度。这是因为在每次迭代中,只需进行常数次的比较和计算即可。
卡丹算法在解决最大子数组和问题的同时,也能够给出最大子数组的起始和终止位置。在每次更新全局最大和时,记录当前最大和对应的子数组的起始和终止位置,即可得到最大子数组的起始和终止位置。
总结:卡丹算法是一种高效解决最大子数组和问题的算法,通过动态规划的思想,在迭代过程中不断更新当前和全局的最大和,从而得到最大子数组和及其位置。
本题模拟(亲自手推!!!):
代码:
#include <iostream>
#include <vector>
#include <algorithm>
typedef long long ll;
using namespace std;
ll maxProfit(const vector<ll>& profits) {
ll max_so_far = 0; // 初始化为0,因为如果全是负数,我们选择不交易
ll max_ending_here = 0;
bool all_negative = true;
for (ll profit : profits) {
if (profit > 0) all_negative = false;
max_ending_here = max(profit, max_ending_here + profit);
max_so_far = max(max_so_far, max_ending_here);
}
// 如果全是负数,返回0
return all_negative ? 0 : max_so_far;
}
int main() {
ll N;
cin >> N;
vector<ll> profits(N);
for (ll i = 0; i < N; ++i) {
cin >> profits[i];
}
cout << maxProfit(profits) << endl;
return 0;
}
2、差分
2062 - 倒水 解法一:利用原数组求差分数组
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
const int N = 100010;
vector<int> a(N), b(N); // a代表原数组,b代表差分数组
int n, k, l, r, p;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
// 求a数组对应的差分数组
b[i] = a[i] - a[i - 1];
}
// k次操作
for (int i = 1; i <= k; i++) {
cin >> l >> r >> p;
b[l] += p;
if (r + 1 < N) { // 避免数组越界错误
b[r + 1] -= p;
}
}
// 求b数组的前缀和,就是a数组做了k次操作的结果
for (int i = 1; i <= n; i++) {
a[i] = b[i] + a[i - 1];
cout << a[i];
if (i < n) {
cout << " ";
}
}
cout << endl; // 确保输出结束时有一个换行符
return 0;
}
2062 - 倒水 解法二:读入时直接求差分数组
#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
const int N = 100010;
vector<int> a(N), b(N); // a代表原数组,b代表差分数组
int n, k, l, r, p;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> n >> k;
for (int i = 1; i <= n; i++) {
//cin >> a[i];
//每读入一个数,直接求对应的差分数组
cin >> p;
b[i] = b[i] + p;
b[i + 1] -= p;
}
// k次操作
for (int i = 1; i <= k; i++) {
cin >> l >> r >> p;
b[l] += p;
if (r + 1 < N) { // 避免数组越界错误
b[r + 1] -= p;
}
}
// 求b数组的前缀和,就是a数组做了k次操作的结果
for (int i = 1; i <= n; i++) {
a[i] = b[i] + a[i - 1];
cout << a[i];
if (i < n) {
cout << " ";
}
}
cout << endl; // 确保输出结束时有一个换行符
return 0;
}
1538 - 小 X 与煎饼达人(flip)
此题为 提高 题目,需要足够的思考!
/*
开始时这些煎饼都是反面朝上(用0表示)
m次操作后,问:有多少正面是朝上的?
求:每个位置翻的次数,如果翻了奇数次,就是正面朝上!
*/
#include <iostream>
#include <cmath>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long ll;
const ll N = 1000010;
//a表示差分数组
vector<ll> a(N);
ll n, m, cnt;
int main() {
ios::sync_with_stdio(0);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> n >> m;
//读入操作
ll x, y;
for (ll i = 1; i <= m; i++) {
cin >> x >> y;
a[x]++;
a[y + 1]--;
}
//求a数组的前缀和,统计奇数出现的次数
for (ll i = 1; i <= n; i++) {
a[i] = a[i - 1] + a[i];
//不能写 a[i] % 2 == 1 !
if (a[i] % 2 != 0) cnt++;
}
cout << cnt << endl;
return 0;
}
PS:参考资料
1、前缀和