当前位置: 首页 > news >正文

算法【前缀和与差分】

差分的应用场景主要是那些对于数组进行一系列操作后,才查询数组中的值,不是边操作边查询。在这种情况下,如果我们每一次操作都是通过遍历数组实现的,速度就会大打折扣。而使用差分我们只需遍历一次数组就可以完成。

一、一维差分

一维差分就是只需对一维数组(初始值为0,否则需要一个初值为0的新数组)进行若干次(一般是1次)求前缀和,就可以得到最终的结果数组。比如,我想在结果数组上的[left, right]区域整体加上3,如果通过遍历数组一个一个的加3,就太慢了。我们可以在数组的left位置加上3,right+1位置减去3,这样只需对每个位置求一次前缀和就可以得到每个位置的值。下面通过一个题目理解一下。

题目一

测试链接:https://leetcode.cn/problems/corporate-flight-bookings/

分析:这个就是一个一维差分,注意题目中是从1开始的。代码如下。

class Solution {
public:vector<int> corpFlightBookings(vector<vector<int>>& bookings, int n) {vector <int> ans;ans.assign(n+1, 0);for(int i = 0;i < bookings.size();++i){ans[bookings[i][0]-1] += bookings[i][2];ans[bookings[i][1]] -= bookings[i][2];}for(int i = 1;i < n;++i){ans[i] += ans[i-1];}ans.pop_back();return ans;}
};

二、等差数列差分

等差数列差分问题描述:一开始1~n范围上的数字都是0。接下来一共有m个操作,每次操作:left~right范围上依次加上首项s、末项e、公差d的数列。最终1~n范围上的每个数字都要正确得到。

等差数列差分的过程:

  1. 1.每个操作调用set方法
  2. 2.所有操作完成后在arr上生成两遍前缀和,即调用build方法
  3. 3.arr里就是最终1~n范围上的每个数字

具体代码如下所示。

void set(int left, int right, int s, int e, int d){arr[left] += s;arr[left + 1] += (d - s);arr[right + 1] -= (d + e);arr[right + 2] += e;
}void build(){for(int i = 1;i <= n;++i){arr[i] += arr[i-1];}for(int i = 1;i <= n;++i){arr[i] += arr[i-1];}
}

具体原理和一维差分,类似,这里不做推导。下面通过两个题目来加深理解。

题目二

测试链接:https://www.luogu.com.cn/problem/P4231

分析:这就是一个等差数列差分的模版题。代码如下。

#include <iostream>
using namespace std;
long long arr[10000005] = {0};
int n, m;
int main(void){int l, r, s, e, d;long long sum = 0, max_value = 0;scanf("%d%d", &n, &m);for(int i = 0;i < m;++i){scanf("%d%d%d%d", &l, &r, &s, &e);d = (e - s) / (r - l);arr[l] += s;arr[l + 1] += (d - s);arr[r + 1] -= (d + e);arr[r + 2] += e;}for(int i = 1;i <= n;++i){arr[i] += arr[i-1];}for(int i = 1;i <= n;++i){arr[i] += arr[i-1];sum ^= arr[i];max_value = max_value > arr[i] ? max_value : arr[i];}printf("%lld %lld", sum, max_value);return 0;
}

其中,用long long防止溢出。

题目三

测试链接:https://www.luogu.com.cn/problem/P5026

分析:可以发现,每一个落水的人对整体造成的影响就是四个等差数列差分,所以直接套用等差数列差分模板即可。代码如下。

#include <iostream>
#define MAXN 1000005
#define OFFSET 30001
using namespace std;
long long arr[MAXN + OFFSET + OFFSET] = {0};
int n, m;
void set(long long left, long long right, long long s, long long e, long long d)
{arr[left + OFFSET] += s;arr[left + 1 + OFFSET] += (d - s);arr[right + 1 + OFFSET] -= (d + e);arr[right + 2 + OFFSET] += e;
}
void build()
{for (int i = 1; i <= m + OFFSET; ++i){arr[i] += arr[i - 1];}for (int i = 1; i <= m + OFFSET; ++i){arr[i] += arr[i - 1];}
}
int main(void)
{int v, x;scanf("%d%d", &n, &m);for (int i = 0; i < n; ++i){scanf("%d%d", &v, &x);set(x - 3 * v + 1, x - 2 * v, 1, v, 1);set(x - 2 * v + 1, x, v - 1, -v, -1);set(x + 1, x + 2 * v, -v + 1, v, 1);set(x + 2 * v + 1, x + 3 * v - 1, v - 1, 1, -1);}build();int begin = 1 + OFFSET;printf("%lld", arr[begin++]);for (int i = 2; i <= m; ++i){printf(" %lld", arr[begin++]);}return 0;
}

其中,OFFSET是用来避免边界判断的一个手段,将所有需要操作的下标全部加上OFFSET,得到一个有效下标。这样就可以避免操作下标为负或者下标超出数组最大下标值的判断,只需要将数组的容量扩展两个OFFSET的大小就可以实现。

三、二维差分

学习二维差分首先得知道二维前缀和。假设我们有一个二维前缀和数组sum,其中sum[i][j]代表左上角(0, 0)到右下角(i, j)这个范围的累加和。sum[i][j] += sum[i][j-1] + sum[i-1][j] - sum[i-1][j-1]。如果要查询左上角(a, b)到右下角(c, d)这个范围的累加和,其值为sum[c][d] - sum[c][b-1] - sum[a-1][d] + sum[a-1][b-1]。这些计算式子可以通过画图很容易推导出来,这里不做详细讲解。实际过程中往往补第0行、第0列来减少很多条件判断,当然也可以不补,根据个人习惯决定。

在二维数组中,如果经历如下操作的过程:

1.批量的做如下的操作,每个操作都有独立的a、b、c、d、v,void add(a, b, c, d, v) : 左上角(a,b)到右下角(c,d)范围上,每个数字+v,怎么快速处理?

2.操作做完后,如何正确得到二维数组中每个位置的值?

这就是二维差分的主要工作,add时候快速处理,最后build得到每个位置的值,修改操作必须集中在一起,不能边修改边查询。

1.add方法实现,比较巧妙。

2.build方法实现,和处理前缀和类似。

3.真实数据用一圈0包裹起来,可以减少很多边界讨论。

下面给出add方法和build方法,代码如下。

void add(int a, int b, int c, int d, int v) {diff[a][b] += v;diff[c + 1][b] -= v;diff[a][d + 1] -= v;diff[c + 1][d + 1] += v;
}void build() {for (int i = 1; i <= n; i++) {for (int j = 1; j <= n; j++) {diff[i][j] += diff[i - 1][j] + diff[i][j - 1] - diff[i - 1][j - 1];}}
}

下面通过几个题目加深对二维差分的理解。

题目四

简要描述:二维前缀和模版。

测试链接:https://leetcode.cn/problems/range-sum-query-2d-immutable/

分析:这就是一个二维前缀和的模版,可以通过对最上一层和最左一层设置成0避免边界判断。代码如下所示。

class NumMatrix {
public:int arr[201][201] = {0};NumMatrix(vector<vector<int>>& matrix) {for(int i = 0;i < matrix.size();++i){for(int j = 0;j < matrix[0].size();++j){arr[i+1][j+1] = matrix[i][j] + arr[i+1][j] + arr[i][j+1] - arr[i][j];}}}int sumRegion(int row1, int col1, int row2, int col2) {return arr[row2+1][col2+1] - arr[row2+1][col1] - arr[row1][col2+1] + arr[row1][col1];}
};/*** Your NumMatrix object will be instantiated and called as such:* NumMatrix* obj = new NumMatrix(matrix);* int param_1 = obj->sumRegion(row1,col1,row2,col2);*/

题目五

测试链接:https://leetcode.cn/problems/largest-1-bordered-square/

分析:从每个点枚举边长,得到一个正方形的左上角点和右下角点,判断这个正方形是否边长全是1,可以通过这个正方形区域的累加和减去这个正方形内边长减1的正方形区域的累加和,看看值是否为边长值,是则这个正方形是否边长全是1。代码如下。

class Solution {
public:int sum[101][101] = {0};int largest1BorderedSquare(vector<vector<int>>& grid) {int ans = 1;int m = grid.size();int n = grid[0].size();for(int i = 0;i < m;++i){for(int j = 0;j < n;++j){sum[i+1][j+1] = grid[i][j] + sum[i+1][j] + sum[i][j+1] - sum[i][j];}}if(sum[m][n] == 0){return 0;}for(int i = 1;i <= m;++i){for(int j = 1;j <= n;++j){for(int k = ans+1, c = i+k-1, d=j+k-1;c <= m && d <= n;++k, ++c, ++d){if((sum[c][d] - sum[c][j-1] - sum[i-1][d] + sum[i-1][j-1]) - (sum[c-1][d-1] - sum[c-1][j] - sum[i][d-1] + sum[i][j])== ((k - 1) << 2)){ans = k;}}}}return ans * ans;}
};

题目六

简要描述:二维差分模版。

测试链接:https://www.luogu.com.cn/problem/P3397

分析:这就是一个二维差分的模版。代码如下。

#include <iostream>
#include <vector>
using namespace std;
vector<vector<int>> diff;
int n, m;
void add(int a, int b, int c, int d, int v) {diff[a][b] += v;diff[c + 1][b] -= v;diff[a][d + 1] -= v;diff[c + 1][d + 1] += v;
}
void build() {for (int i = 1; i <= n; i++) {for (int j = 1; j <= n; j++) {diff[i][j] += diff[i - 1][j] + diff[i][j - 1] - diff[i - 1][j - 1];}}
}
int main() {scanf("%d%d", &n, &m);vector<int> v;v.assign(n+2, 0);for(int i = 0;i < n+2;++i){diff.push_back(v);}for(int i = 1, x1, y1, x2, y2;i <= m;++i){scanf("%d%d%d%d", &x1, &y1, &x2, &y2);add(x1, y1, x2, y2, 1);}build();for(int i = 1;i <= n;++i){printf("%d", diff[i][1]);for(int j = 2;j <= n;++j){printf(" %d", diff[i][j]); }printf("\n");}return 0;
}

题目七

测试链接:https://leetcode.cn/problems/stamping-the-grid/

分析:我们可以对每一个0点,也就是空的点计算出,以这个点为左上角点,然后贴一张邮票的右下角点。计算这个区域的累加和,如果是0,代表这个区域可以贴上邮票,如果大于0代表有被占据的格子,不能贴上邮票。可以新建一个二维矩阵处理贴上邮票的位置,也就是差分数组,把贴上邮票的位置集体加1,。把所有的邮票贴完之后判断原来矩阵中为0的位置,在新建矩阵中是否大于0。如果是等于0代表这个格子并没有贴上邮票返回false,遍历玩整个数组如果没有返回false就返回true。代码如下。

class Solution
{
public:int m;int n;vector<vector<int>> diff;vector<vector<int>> sum;void add(int a, int b, int c, int d, int v){diff[a][b] += v;diff[c + 1][b] -= v;diff[a][d + 1] -= v;diff[c + 1][d + 1] += v;}void build(){for (int i = 1; i <= m; i++){for (int j = 1; j <= n; j++){diff[i][j] += diff[i - 1][j] + diff[i][j - 1] - diff[i - 1][j - 1];}}}int get_value(int x1, int y1, int x2, int y2){return sum[x2 + 1][y2 + 1] - sum[x2 + 1][y1] - sum[x1][y2 + 1] + sum[x1][y1];}void build_sum(vector<vector<int>> &grid){for (int i = 0; i < m; ++i){for (int j = 0; j < n; ++j){sum[i + 1][j + 1] = grid[i][j] + sum[i + 1][j] + sum[i][j + 1] - sum[i][j];}}}void clear(){vector<int> v1;vector<int> v2;v1.assign(n + 1, 0);v2.assign(n + 2, 0);for (int i = 0; i < m + 1; ++i){sum.push_back(v1);diff.push_back(v2);}diff.push_back(v2);}bool possibleToStamp(vector<vector<int>> &grid, int stampHeight, int stampWidth){m = grid.size();n = grid[0].size();clear();build_sum(grid);int c, d;for (int i = 0; i < m; ++i){for (int j = 0; j < n; ++j){if (grid[i][j] == 0){c = i + stampHeight - 1;d = j + stampWidth - 1;if (c < m && d < n && get_value(i, j, c, d) == 0){add(i + 1, j + 1, c + 1, d + 1, 1);}}}}build();for (int i = 0; i < m; ++i){for (int j = 0; j < n; ++j){if (grid[i][j] == 0 && diff[i + 1][j + 1] <= 0){return false;}}}return true;}
};

其中,sum和diff数组都是下标从1开始,sum是在最上层和最左层赋0,diff是在四周赋0,都是为了避免边界判断和越界。

题目八

测试链接:https://leetcode.cn/problems/xepqZ5/

分析:这个题需要用到离散化的技巧,因为不可能根据力场的范围大小去生成一个相应长度的数组。比如一个力场最大范围到几百万,我们不可能生成一个几百万的二维数组,所以需要将力场的真实值范围转化为下标到一个数组当中。同时,因为力场的范围可能存在小数,所以需要放大坐标消除小数的影响。数组中的下标从小到大存储着力场的边界坐标,相邻下标并不代表力场的范围是相邻的。这样就可以把所有力场转化为每个下标,可以将下标看成是新的力场边界,虽然和原来的坐标范围没有相邻关系,不过将覆盖关系保留了,比如两个力场之前不是相邻的,但在新的边界上,有可能是相邻下标;但如果两个力场之前是相互覆盖的,在新的边界上也会是覆盖的,这就保证了计算的正确。遍历每个力场,找到力场范围对应的下标,在此下标范围内进行差分。构建差分数组得到最大立场值。代码如下。

class Solution {
public:int n;vector <long> trans_x;vector <long> trans_y;vector<vector<int>> diff;int sort_getNum(vector <long>& v){int length = 1;int range = (n << 1);sort(v.begin(), v.end());for(int i = 1;i < range;++i){if(v[i] != v[i-1]){v[length++] = v[i];}}return length;}int rank(vector <long> v, long real_value, int length){int ans = 0;int left = 0, right = length-1;int middle;while (left <= right){middle = (left + ((right - left) >> 1));if(v[middle] >= real_value){ans = middle;right = middle - 1;}else{left = middle + 1;}}return ans + 1;}void add(int a, int b, int c, int d, int v){diff[a][b] += v;diff[c + 1][b] -= v;diff[a][d + 1] -= v;diff[c + 1][d + 1] += v;}int build(int m, int n){int ans = 0;for (int i = 1; i <= m; i++){for (int j = 1; j <= n; j++){diff[i][j] += diff[i - 1][j] + diff[i][j - 1] - diff[i - 1][j - 1];ans = ans > diff[i][j] ? ans : diff[i][j];}}return ans;}int fieldOfGreatestBlessing(vector<vector<int>>& forceField) {n = forceField.size();trans_x.assign(n<<1, 0);trans_y.assign(n<<1, 0);for(int i = 0, j = 0, k = 0;i < n;++i){trans_x[j++] = (long)(forceField[i][0] << 1) - (long)forceField[i][2];trans_x[j++] = (long)(forceField[i][0] << 1) + (long)forceField[i][2];trans_y[k++] = (long)(forceField[i][1] << 1) - (long)forceField[i][2];trans_y[k++] = (long)(forceField[i][1] << 1) + (long)forceField[i][2];}int length_trans_x = sort_getNum(trans_x);int length_trans_y = sort_getNum(trans_y);vector<int> temp;temp.assign(length_trans_y+2, 0);for(int i = 0;i < (length_trans_x+2);++i){diff.push_back(temp);}for(int i = 0;i < n;++i){int rank_front_x = rank(trans_x, (long)(forceField[i][0] << 1) - (long)forceField[i][2], length_trans_x);int rank_back_x = rank(trans_x, (long)(forceField[i][0] << 1) + (long)forceField[i][2], length_trans_x);int rank_front_y = rank(trans_y, (long)(forceField[i][1] << 1) - (long)forceField[i][2], length_trans_y);int rank_back_y = rank(trans_y, (long)(forceField[i][1] << 1) + (long)forceField[i][2], length_trans_y);add(rank_front_x, rank_front_y, rank_back_x, rank_back_y, 1);}return build(length_trans_x, length_trans_y);}
};

其中,trans_x和trans_y数组就是转化的数组,sort_getNum方法是在去除重复值并返回去重后的长度。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • LeNet5模型搭建
  • 华为OD-D卷小明找位置
  • 学习记录(9):Prompt提示词技巧
  • source insight 3.5快捷键合集
  • 模板方法模式(Template Method Pattern)
  • 三数之和-Leetcode
  • 深入理解 Vuex:Vue.js 应用的状态管理
  • 《最新出炉》系列小成篇-Python+Playwright自动化测试-66 - 等待元素至指定状态(出现、移除、显示和隐藏)
  • mysql数据库:SQL语言基础和基本查询
  • 黑马Java零基础视频教程精华部分_16_递归算法
  • QT下载与安装
  • 第25课 Scratch入门篇:火箭升空
  • 2024下半年国际学术会议一览表
  • 学懂C++ (十四):高级教程——C++ 动态内存管理(new和delete)详解
  • Cmake基础教程--第1章:初识cmake
  • .pyc 想到的一些问题
  • [NodeJS] 关于Buffer
  • CentOS学习笔记 - 12. Nginx搭建Centos7.5远程repo
  • Computed property XXX was assigned to but it has no setter
  • E-HPC支持多队列管理和自动伸缩
  • iOS小技巧之UIImagePickerController实现头像选择
  • jQuery(一)
  • JS变量作用域
  • laravel 用artisan创建自己的模板
  • Mybatis初体验
  • mysql中InnoDB引擎中页的概念
  • 聊聊sentinel的DegradeSlot
  • 爬虫进阶 -- 神级程序员:让你的爬虫就像人类的用户行为!
  • 区块链分支循环
  • 三栏布局总结
  • 数组大概知多少
  • 跳前端坑前,先看看这个!!
  • 系统认识JavaScript正则表达式
  • Spark2.4.0源码分析之WorldCount 默认shuffling并行度为200(九) ...
  • ​​​​​​​​​​​​​​Γ函数
  • ​软考-高级-信息系统项目管理师教程 第四版【第23章-组织通用管理-思维导图】​
  • #### golang中【堆】的使用及底层 ####
  • ###51单片机学习(1)-----单片机烧录软件的使用,以及如何建立一个工程项目
  • #使用清华镜像源 安装/更新 指定版本tensorflow
  • (1)SpringCloud 整合Python
  • (3)医疗图像处理:MRI磁共振成像-快速采集--(杨正汉)
  • (35)远程识别(又称无人机识别)(二)
  • (4)STL算法之比较
  • (带教程)商业版SEO关键词按天计费系统:关键词排名优化、代理服务、手机自适应及搭建教程
  • (第30天)二叉树阶段总结
  • (动手学习深度学习)第13章 计算机视觉---微调
  • (三)mysql_MYSQL(三)
  • (十八)三元表达式和列表解析
  • (一)Linux+Windows下安装ffmpeg
  • (一)模式识别——基于SVM的道路分割实验(附资源)
  • (原創) 人會胖會瘦,都是自我要求的結果 (日記)
  • (转)Java socket中关闭IO流后,发生什么事?(以关闭输出流为例) .
  • (转载)(官方)UE4--图像编程----着色器开发
  • .md即markdown文件的基本常用编写语法
  • .NET Framework、.NET Core 、 .NET 5、.NET 6和.NET 7 和.NET8 简介及区别