前言
看了很多视频之后,我觉得不论学习什么算法都应该明确一个对于算法学习的框架,避免很多时候自己都不知道自己在干嘛,很混乱,这里我自己总结了一下:
-
1.首先最重要的就是搞清楚为什么我们要学习这个算法
(比如举个例子,对于可以用二分查找来解决的问题,我们一般一开始不会就直接去用二分查找,而是先用暴力的解法先写出来看一看,分析一下时间和空间复杂度如何,再去考虑如何去优化它,比如从O(n)->O(1)就是非常大的提升) -
2.这个算法能帮我们做到哪些事情,例如可以帮助我们省去很多重复的操作,又例如可以快速的找到一个区间内的值等等
-
3.然后才对于一些细节的把控,例如这个会不会溢出啊,终止条件又是啥等等
前缀和
本文是基于我之前发过的同名文章所写,最近在复习相关知识,所以写了一个优化版本重新发出来供大家参考
引入(一维前缀和)
-
首先对于这个题目 如果你没有学过前缀和,对于这个问题可能就是会用暴力的方法,思路就非常的原始:
比如要求计算区间【3,5】的元素的和,我们就遍历这个数组,定义一个sum,起点是3,终点是5,sum【i】++就行了,但是对于数组长度为n,遍历完询问区间和的次数是m,那么时间复杂度就是O(m*n)! -
所以我们就要想办法去优化他,前缀和就是一个非常好的方法。
对于前缀和 我的理解是:对于长长的火车,一开始只有一节火车头,后面慢慢的一节一节往上接,每接一次然后求他的长度的过程 就是我们求前缀和数组的过程。 -
至于前缀和数组有什么作用,其实是非常好理解的,我们用O(n)的时间复杂度来遍历来创建所谓的前缀和数组,此时还是原来的问题如果我们要求某个区间的和,直接就是return右端点(R)减去左端点(L)就可以了,此时只需要只需要进行一次计算 也就是O(1)即可。
代码示例:视频
Q: 为什么L需要-1?
举个例子就能简单明白了:
比如要你求的是【3,4】的区间和,我们需要用sum【4】也就是从下标0一直加到4的和减去sum【2】,如果减去的是sum【3】,那么求出的就直接是数组元素a【4】了,希望大家能够理解
下面这种方法是labuladong的题解,对于上述的问题进行了优化,可以称之为数组的偏移,使得前缀和数组presum从0开始递增,从而与原数组sum一一对应,但是思路依旧是不变的
一般来说在初始化数组直接让他等于nums的大小即可
即presum.resize(nums.size()) -----这个其实很有用
#include <vector>
using namespace std;
class NumArray {
// 前缀和数组
vector<int> preSum;
// 输入一个数组,构造前缀和
public:
NumArray(vector<int>& nums) {
// preSum[0] = 0,便于计算累加和
preSum.resize(nums.size() + 1);
// 计算 nums 的累加和
for (int i = 1; i < preSum.size(); i++) {
preSum[i] = preSum[i - 1] + nums[i - 1];
}
}
// 查询闭区间 [left, right] 的累加和
int sumRange(int left, int right) {
return preSum[right + 1] - preSum[left];
}
};
二维前缀和
从名字就能看出来二维其实就是从一行或者一列的区间和问题
升维成了一个大矩阵的中 子矩阵和的问题 但是其实思想是一样的
对于这个一个矩阵中我们想要去算出它的子矩阵和,如果用暴力的方式每次询问都要去遍历它的每一行每一列,这时的算法时间复杂度是很高的;
随便举个例子,定义sum【i,j】表示从左上角【0,0】位置(即存在第0行\列)开始一直到右下角点【i,j】的区域和,那么sum【2,2】如果用暴力的方式每次询问就需要计算每一行或者每一列的前缀和再去给他加起来,十分繁琐。
这时我们就需要用到前缀和的思想
任意子矩阵的元素和可以转化成它周边几个大矩阵的元素和的运算:
如果要求计算一个子矩阵从左上角【x1,y1】到右下角【x2,y2】那么大概就是这个思路,具体公式如下:
为什么需要-1呢,图上的这些边界不都是重合的吗?
其实一开始我也不太能理解,后来才慢慢想通了,
对于图上的这些面积,表示的是区域和,对应的前缀和矩阵中只不过是一个数字,并不是真正的面积,其实这个边界看似是重合的,但是对应前缀和矩阵中却不是的 所以我们大的这个sum【x2,y2】才需要去减去不如sum【x2,y2-1】相当于把不包含子矩阵的列去掉了,这是一个非常关键的,需要去想通
可以看看下面的解析:
当行或者列为0的情况(一维前缀和):
代码示例:视频
例题
AC代码
#include <vector>
class NumMatrix {
// preSum[i][j] 记录矩阵 [0, 0, i-1, j-1] 的元素和
vector<vector<int>> preSum;
public:
NumMatrix(vector<vector<int>>& matrix) {
int m = matrix.size(), n = matrix[0].size();
if (m == 0 || n == 0) return;
// 构造前缀和矩阵
preSum.resize(m + 1, std::vector<int>(n + 1, 0));
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// 计算每个矩阵 [0, 0, i, j] 的元素和
preSum[i][j] = preSum[i-1][j] + preSum[i][j-1] + matrix[i - 1][j - 1] - preSum[i-1][j-1];
}
}
}
// 计算子矩阵 [x1, y1, x2, y2] 的元素和
int sumRegion(int x1, int y1, int x2, int y2) {
// 目标矩阵之和由四个相邻矩阵运算获得
return preSum[x2+1][y2+1] - preSum[x1][y2+1] - preSum[x2+1][y1] + preSum[x1][y1];
}
};
差分数组
差分数组其实是建立在对于前面前缀和数组的理解上的,对于区间的频繁修改尤为适用,有一点类似于以前找规律我们常常把每个数相减,看看其中有什么规律,差分数组其实就是由这些差值组成的一个diff数组。
一维差分
如果我们需要多次对一个数组的区间增减修改,就比如上面的三个操作,如果我们按照暴力的思想:就是每修改一次就遍历一次数组,在对应区间里的数进行更改;
但是我们询问的是最终的数组长什么样子,所以就很麻烦,拿11这个数举例,这三次的修改结果对于11这个数而言其实就是+4,所以其实我们是可以去优化的,这时就要借助我们的差分数组
我们不难发现,对于一个数组差分之后再求前缀和就可以回到原数组,我们就可以利用这一点性质来编写利用差分和前缀和来解决这个问题的代码
我觉得差分数组有点像一个多米诺骨牌,如果对差分数组中某一个元素加1,则会影响其对应原数组之后的所有元素都+1,也就是都变大了
形象样例:
核心原理:
原理很简单,回想 diff 数组反推 nums 数组的过程,diff[i] += 3 意味着给 nums[i…] 所有的元素都加了 3,
然后 diff[j+1] -= 3 又意味着对于 nums[j+1…] 所有元素再减 3,
那综合起来,是不是就是对 nums[i…j] 中的所有元素都加 3 了?
只要花费 O(1) 的时间修改 diff 数组,就相当于给 nums 的整个区间做了修改。
多次修改 diff,然后通过 diff 数组反推,即可得到 nums 修改后的结果。
例题1
AC代码
#include <iostream>
#include <vector>
using namespace std;
// 构造差分数组
void make(vector<int>& nums, vector<int>& diff) {
diff.resize(nums.size());
diff[0] = nums[0];
for (int i = 1; i < nums.size(); i++) {
diff[i] = nums[i] - nums[i - 1];
}
}
// 给闭区间 [i, j] 增加 val(可以是负数)
void increment(vector<int>& diff, int i, int j, int val) {
diff[i] += val;
if (j + 1 < diff.size()) {
diff[j + 1] -= val;
}
}
// 根据差分数组还原原数组
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() {
int n, m;
cin >> n >> m;
vector<int> nums(n);
vector<int> diff;
// 输入原数组
for (int i = 0; i < n; i++) {
cin >> nums[i];
}
// 构建差分数组
make(nums, diff);
// 处理每次操作
while (m--) {
int l, r, d;
cin >> l >> r >> d;
// 注意:题目中如果 l 和 r 是从 1 开始的索引,需要减 1
increment(diff, l - 1, r - 1, d);
}
// 根据差分数组还原原数组
vector<int> result = restore(diff);
// 输出结果
for (int i = 0; i < n; i++) {
cout << result[i];
if (i < n - 1) {
cout << " ";
}
}
cout << endl;
return 0;
}
例题2
AC代码
#include <iostream>
#include <vector>
using namespace std;
// 构造差分数组
void make(vector<int>& nums, vector<int>& diff) {
diff.resize(nums.size());
diff[0] = nums[0];
for (int i = 1; i < nums.size(); i++) {
diff[i] = nums[i] - nums[i - 1];
}
}
// 给闭区间 [i, j] 增加 val(可以是负数)
void increment(vector<int>& diff, int i, int j, int val) {
diff[i] += val;
if (j + 1 < diff.size()) {
diff[j + 1] -= val;
}
}
// 根据差分数组还原原数组
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() {
int n;
cin >> n;
vector<int> nums(n);
vector<int> diff;
for (int i = 0; i < n; i++) {
cin >> nums[i];
}
make(nums, diff);
int operations = 0;
// 计算最少操作次数
for (int i = 0; i < diff.size(); i++) {
if (i == 0) {
operations += abs(diff[i] - 1);
} else {
operations += max(0, diff[i]);
}
}
cout << operations << endl;
return 0;
}
例题3
AC代码
这里由于本题默认从1开始 而我在修改区间里面的值的时候默认是从0开始的 就导致我一直WA的原因,还好后来检查发现了
还有一个问题就是一开始我将min_grade 的值设置成了INT_MAX 会有一个问题就是当最小值就是s【0】的时候 我们的mingrade由于是在最后一个循环里更新的所以 就永远不会得到正确答案s[0](80分的原因)
#include <bits/stdc++.h>
using namespace std;
const int N=5e6+10;
int n,p;
int s[N],diff[N],presum[N];
//基本思路 :一维差分 (本题只有一次询问 p个操作)
int l,r,val;//代表修改区间的左右端点和值
int main()
{
cin>>n>>p;
for(int i=0;i<n;i++)
{
cin>>s[i];
}
//构造差分数组
diff[0]=s[0];
for(int i=1;i<n;i++)
{
diff[i]=s[i]-s[i-1];
}
while(p--)
{
cin>>l>>r>>val;
diff[l-1]+=val;
diff[r]-=val;
}
//对差分数组进行一次前缀和得到修改之后的数组
presum[0]=diff[0];
int min_grade = presum[0];
for(int i=1;i<n;i++)
{
presum[i]=presum[i-1]+diff[i];
min_grade=min(presum[i],min_grade);
}
cout<<min_grade;
return 0;
}