算法-差分, 二维前缀和, 离散化

news2024/9/23 7:21:06

文章目录

    • 本节提要
    • 1. 一维差分
      • 1.1 一维差分原理分析
      • 1.2 一维差分例题应用
    • 2. 等差数列差分
      • 2.1 等差数列差分原理分析
    • 3. 二维前缀和
      • 3.1 二维前缀和原理分析
      • 3.2 二维前缀和例题应用

本节提要

本节的主要目标是一维差分的总结, 包括一维差分, 一维等差数列差分; 二维差分和二维前缀和的相关技巧以及方法论, 还有离散化的相关技巧(其实有点类似数学里面的坐标变化), 其实关于差分跟前缀和的关系来说的话, 前缀和有点类似差分的逆运算

1. 一维差分

1.1 一维差分原理分析

其实就是在某一个特定的区间修改值, 但是我们不去用遍历的方式去修改数组, 而是仅仅修改其中的几个位置, 从而在最终返回结果的时候通过生成前缀和加工一个新的数组返回,从这里描述的也可以知道, 差分不能做到边修改边查询, 但是前缀树可以做到(以后再说)
举例:
数组的定义:
比如对于一个数组 arr = { 1 , 2 , 3 , 2 , 3 } 来说, 下标的范围是0 - 4(5个元素), 我们把对于原数组的修改变化量用一个change[ ]数组来临时接收一下, 我们的change数组目前是{ 0 , 0 , 0 , 0 ,0 }
差分过程 :
我们每次修改数组的时候, 比如我们要在left - right区间范围上, 加上a, 那我们就执行下面的操作
change[left] += a ; change[right + 1] -= a ; (这个表达式是修改的核心), 同时要注意表达式边界条件的判断
修改过程:
由于这个用文字不方便进行演示, 所以我们用一张表来进行演示
arr代表的是我们的原始数组, change是我们的增量数组, 执行结束之后的change的前缀和就是我们的真实的增量
在这里插入图片描述
原理分析:
其实原理也是比较好理解的, 比如我们我们在change[left] += a, 说明此刻的增量从这个位置开始, 然后直到 change[right + 1] 位置就会停止, 其实我们也可以用下面的图解的方式分析(待会等差数列的分析用这种方法会更好理解一点), 总结一下就是正推求和, 逆推求差
在这里插入图片描述
下面的数组是我们想要实现的, 而上面的是我们的原始数组, 我们上面的数组求和之后就会出现下面数组的样子
下面是我们实现的一个一维差分的类测试


/**
 * 下面我们写的类是为了测试线性的差分
 * 这种差分的结构不支持边修改边查询(其实也是最简单的一种差分)
 */
class Differential {
    //初始化的原始数组
    private int[] init;
    //增量数组(其实就是所有元素都是0, 然后进行sum)
    private int[] change;

    //构造方法, 创建出来原始的数组
    public Differential(int[] init) {
        this.init = init;
        //防止越界就直接用这样子
        change = new int[init.length + 1];
    }

    //进行局部位置的修改
    public void changeArr(int left, int right, int n) {
        //change[left] += n;
        //change[right + 1] -= n;
        change[left] += n;
        change[right + 1] -= n;
    }

    //进行展示(此时才正式进行数组的修改)
    public void showArray() {
        //首先计算增量数组的前缀和, 也就是增量的结果
        for (int i = 1; i < change.length; i++) {
            change[i] += change[i - 1];
        }

        //开始修改原数组(其实也就是原始数组加上增量数组, 然后清空增量数组)
        for (int i = 0; i < init.length; i++) {
            init[i] += change[i];
            System.out.print(init[i] + " ");
        }
        System.out.println();
        //不要忘记清空数组
        Arrays.fill(change, 0);
    }
}

1.2 一维差分例题应用

在这里插入图片描述
这就是一个标准的一维差分的应用题, 看懂了上面的原理解析之后相信这道题是没什么问题的, 注意的点就是编号的判断, 因为航班是从1开始的, 而且这道题的原始数组元素都是0, 所以只要一个增量数组就可以了, 代码实现如下

class Solution {
    public int[] corpFlightBookings(int[][] bookings, int n) {
        // 经典的一维线性差分板子题(加上一个位置)
        int[] temp = new int[n + 1];
        // 因为初始数组的元素也都是0, 所以增量数组的前缀和都是最终的结果
        for (int[] arr : bookings) {
            temp[arr[0] - 1] += arr[2];
            temp[arr[1]] -= arr[2];
        }
        // 遍历一下增量数组
        for (int i = 1; i < temp.length; i++) {
            temp[i] += temp[i - 1];
        }
        temp = Arrays.copyOf(temp, temp.length - 1);
        return temp;
    }
}

在这里插入图片描述
这道题可以用堆做, 我们之前的题目解析里面有, 今天我们介绍的是差分的做法, 其实用差分写这道题十分的好想, 我们给定的区间的左端点其实就是我们的left, 区间右端点其实就是right + 1(从长度的角度考虑), 我们在添加一段线段的时候就让该区间的所有数值 +1 (其实就是差分, 最终遍历数组找到数值最大的位置也就是重合线段最多的地方, 代码实现如下

import java.util.*;

// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
    //根据数据量的大小给定一个数组的长度
    private static final int MAXLEN = 100001;
    private static int[] arr = new int[MAXLEN];

    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int sz = in.nextInt();
        for(int i = 0; i < sz; i++){
            //直接读取两个数作用于数组进行差分操作
            int left = in.nextInt();
            int right = in.nextInt();
            arr[left] += 1;
            arr[right] -= 1;
        }

        //生成前缀和同时生成最大值
        int resMax = 0;
        for(int i = 1; i < MAXLEN; i++){
            arr[i] += arr[i - 1];
            resMax = Math.max(resMax, arr[i]);
        }

        //清楚静态空间数组
        Arrays.fill(arr, 0);

        //输出最大值
        System.out.println(resMax);
    }
}

2. 等差数列差分

2.1 等差数列差分原理分析

其实等差数列差分跟上面的一维线性差分原理是差不多的, 理解了线性的差分之后这种等差数列差分也是很好理解的, 举例如下…
举例:
数组的定义:
对于一个数组 arr = { 1 , 2 , 3 , 2 , 3 } 来说, 下标的范围是0 - 4(5个元素), 我们把对于原数组的修改变化量用一个change[ ]数组来临时接收一下, 我们的change数组目前是{ 0 , 0 , 0 , 0 ,0 }
修改过程:
我们希望在 left - right区间上加上一个等差数列的区间, 比如 left == 1, right == 3, 首项为2, 公差为1, 末项为4 (一定确保是等差数列的区间)
此时的change数组变为 change { 0 , 2 , 3 , 4 , 0 }
差分过程:
对于差分来说, 我们仅仅需要修改下面的几个位置即可, 然后查询的时候进行两轮的前缀求和

//start是首项, end是末项, comSub是公差
change[left] += start;
change[left + 1] += comSub - start;
change[right + 1] -= comSub + end;
change[right + 2] += end;

//修改完成之后, 进行两轮前缀和就可以求出来最终的增量

原理分析:
用的还是我们的逆推分析法, 从下往上推很直观, 见下图
在这里插入图片描述

初态就是我们进行差分的时候修改的数组, 进行查询的时候只需要向下进行两次求前缀和就可以了, 我们也写了一个测试类的代码, 见下


/**
 * 下面的是一维的等差数列差分, 其实就是对一个数组的left, right的区间元素进行操作(加的不是常数而是一个等差数列)
 * 这种结构也是不支持边修改边查询
 */
class EqualsSubDifferential {
    //原始的数组
    private int[] init;
    //增量数组
    private int[] change;

    //构造方法, 传进来一个原始数组
    public EqualsSubDifferential(int[] init) {
        this.init = init;
        //创建的修改数组其实是 length + 2 长度
        change = new int[init.length + 2];
    }

    //修改数组的方法(其实修改的是增量数组, 增量数组的修改原理我们在下面说一下, 而且这里我们默认都是符合等差数列的)
    public void changeArr(int left, int right, int start, int end, int comSub) {
        //arr[left] += start
        change[left] += start;
        //arr[left + 1] += comSub - start
        change[left + 1] += comSub - start;
        //arr[right + 1] -= comSub + end;
        change[right + 1] -= comSub + end;
        //arr[right + 2] += end;
        change[right + 2] += end;
    }

    //展示原始数组的方法
    public void showArray() {
        //首先对增量数组进行两次前缀求和
        for (int i = 1; i < change.length; i++) {
            change[i] += change[i - 1];
        }
        for (int i = 1; i < change.length; i++) {
            change[i] += change[i - 1];
        }

        //现在我们的change数组才是我们真正的增量, 加上之后我们清空一下增量数组
        for (int i = 0; i < init.length; i++) {
            init[i] += change[i];
            System.out.print(init[i] + " ");
        }
        System.out.println();
        Arrays.fill(change, 0);
    }
}


class Test1 {
    public static void main(String[] args) {
        int[] arr = {4, 5, 6, 2, 1, 3, 7, 5};
        EqualsSubDifferential eq = new EqualsSubDifferential(arr);
        eq.changeArr(0, 4, 2, 6, 1);
        eq.changeArr(4, 7, 3, 6, 1);
        eq.showArray();
        eq.showArray();
    }
}


```二维前缀和原理分析
### 2.2 等差数列差分例题应用
![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/5e4be3f67e944b43a6c7e90ef33c44e7.png#pic_center)

```java
package class047;

// 一开始1~n范围上的数字都是0,一共有m个操作,每次操作为(l,r,s,e,d)
// 表示在l~r范围上依次加上首项为s、末项为e、公差为d的数列
// m个操作做完之后,统计1~n范围上所有数字的最大值和异或和
// 测试链接 : https://www.luogu.com.cn/problem/P4231
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的code,提交时请把类名改成"Main",可以直接通过

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;

public class Code02_ArithmeticSequenceDifference {

	public static int MAXN = 10000005;

	public static long[] arr = new long[MAXN];

	public static int n, m;

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		StreamTokenizer in = new StreamTokenizer(br);
		PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
		while (in.nextToken() != StreamTokenizer.TT_EOF) {
			n = (int) in.nval;
			in.nextToken();
			m = (int) in.nval;
			for (int i = 0, l, r, s, e; i < m; i++) {
				in.nextToken(); l = (int) in.nval;
				in.nextToken(); r = (int) in.nval;
				in.nextToken(); s = (int) in.nval;
				in.nextToken(); e = (int) in.nval;
				set(l, r, s, e, (e - s) / (r - l));
			}
			build();
			long max = 0, xor = 0;
			for (int i = 1; i <= n; i++) {
				max = Math.max(max, arr[i]);
				xor ^= arr[i];
			}
			out.println(xor + " " + max);
		}
		out.flush();
		out.close();
		br.close();
	}

	public static void set(int l, int r, int s, int e, int d) {
		arr[l] += s;
		arr[l + 1] += d - s;
		arr[r + 1] -= d + e;
		arr[r + 2] += e;
	}

	public static 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];
		}
	}

}

3. 二维前缀和

3.1 二维前缀和原理分析

与一维前缀和(我们在构建前缀信息那一节已经详细的分析过了)类似, 二维前缀和也是为了解决在某一个区间的二维求和问题
比如求左上角点为 ( 0 , 0 ) , 右下角点为 ( a , b ) 的矩形空间的内部求和
我们利用的核心公式为
前缀和 = 左前缀和 + 上前缀和 - 左上前缀和 + 自己(注意边界讨论)
这个公式的原理就是一个简单的容斥原理, 下面我们简单图解一下
在这里插入图片描述
用上面的方法可以很容易的生成一个二维的前缀和数组, 那我们如何计算左上角点为(a,b), 右下角点为(c,d)的矩形的范围求和呢, 其实还是一个简单的容斥原理, 图解如下
在这里插入图片描述
我们通过上面的图解转化 :
待求区间的和 = 右下角坐标的前缀和 - 左下角左侧元素的前缀和 - 右上角上方元素的前缀和 - 左上角坐标的左上角元素的前缀和

3.2 二维前缀和例题应用

在这里插入图片描述
这就是一道标准的二维前缀和的板子题, 根据之前我们说的, 因为二维前缀和需要边界情况的讨论, 我们一般的处理做法是

  1. 在左侧和上侧填上一圈0, 避开边界情况的讨论
  2. 直接在原数组进行讨论(多数情况可以复用原数组)

第一种写法(避开边界讨论) :

class NumMatrix {

    //新增的sum数组(多增加一个半环的写法)
    private int sum[][];

    public NumMatrix(int[][] matrix) {
        int row = matrix.length;
        int col = matrix[0].length;
        sum = new int[row + 1][col + 1];

        //进行前缀和构造
        for(int i = 1; i <= row; i++){
            for(int j = 1; j <= col; j++){
                sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + matrix[i - 1][j - 1];
            }
        }
    }
    
    public int sumRegion(int row1, int col1, int row2, int col2) {
        return sum[row2 + 1][col2 + 1] - sum[row2 + 1][col1] - sum[row1][col2 + 1] + sum[row1][col1]; 
    }
}

第二种写法(直接复用原数组的结构, 进行边界的讨论)

class NumMatrix {

    //定义一个数组的引用, 等会直接指向老的数组然后进行复用
    private int[][] sum;
    public NumMatrix(int[][] matrix) {
        sum = matrix; //直接指向老数组
        int row = matrix.length;
        int col = matrix[0].length;
        //直接在原数组上复用前缀和数组(空间复杂度为o(1))
        for(int i = 0; i < row; i++){
            for(int j = 0; j < col; j++){
                matrix[i][j] += (get(matrix, i, j -1) + get(matrix, i - 1, j) - get(matrix, i - 1, j - 1));
            }
        }
    }
    
    public int sumRegion(int row1, int col1, int row2, int col2) {
        return sum[row2][col2] - get(sum, row2, col1 - 1) - get(sum, row1 - 1, col2) + get(sum, row1 - 1, col1 - 1);
    }

    //进行边界讨论的get方法
    private int get(int[][] arr, int i, int j){
        return (i < 0 || j < 0) ? 0 : arr[i][j];
    }
}

/**
 * Your NumMatrix object will be instantiated and called as such:
 * NumMatrix obj = new NumMatrix(matrix);
 * int param_1 = obj.sumRegion(row1,col1,row2,col2);
 */

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2036778.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

唐诡探案外传-MySQL误操作之“寻凶”

序幕&#xff1a;字符集被修改之谜 立秋后的第一天下午&#xff0c;太阳仍不知疲倦地炙烤着大地&#xff0c;办公室内的中央空调不停歇地鼓吹着冷风。某办公楼内&#xff0c;IT部门的阿祖被同事急匆匆地找来&#xff0c;说是系统出现了奇怪的bug&#xff0c;追查之下发现测试环…

搭建高可用OpenStack(Queen版)集群(十二)之启动一个实例

一、搭建高可用OpenStack&#xff08;Queen版&#xff09;集群之启动一个实例 创建实例之前&#xff0c;首先要创建虚拟网络 一、创建网络 建议命令行和web管理协同进行配置&#xff0c;这样不容易出错 在控制节点执行 1、创建公有网络部分 1、在命令行执行 . admin-openrc ne…

【SQL】平均售价

目录 题目 分析 代码 题目 表&#xff1a;Prices ------------------------ | Column Name | Type | ------------------------ | product_id | int | | start_date | date | | end_date | date | | price | int | ---------------…

技术研究:Redis 数据结构与 I/O 模型

数据结构 Redis之所以“快”&#xff0c;一方面因为它是内存数据库&#xff0c;所有操作都在内存上完成&#xff0c;内存的访问速度本来就快。另一方面则是因为高效的数据结构&#xff0c;使得操作键值效率较高。总体来说&#xff0c;Redis使用了一个用来保存每个Key/Value的全…

【Story】如何高效记录并整理编程学习笔记?

目录 一、为何笔记在编程学习中如此重要&#xff1f;1.1 知识的捕捉1.2 理解和消化1.3 知识的复习1.4 知识的分享 二、建立高效的笔记系统2.1 确定笔记的目标2.2 选择合适的工具2.3 笔记的结构化2.4 记录有效的内容2.5 定期回顾和更新 三、保持笔记条理性的技巧3.1 使用一致的格…

【数据结构】堆排序与TOP-K问题

&#x1f308;个人主页&#xff1a;Yui_ &#x1f308;Linux专栏&#xff1a;Linux &#x1f308;C语言笔记专栏&#xff1a;C语言笔记 &#x1f308;数据结构专栏&#xff1a;数据结构 文章目录 1.堆排序1.1 建堆1.2 利用堆删除思想来进行排序1.3 堆排序的时间复杂度 2.TOP-K问…

Uniapp之微信小程序计算器

UI仿的iOS手机计算器&#xff0c;基本功能已经实现&#xff0c;如下效果图 具体使用可以参考微信小程序&#xff1a;日常记一记--我的---计算器 第一步&#xff1a;UI界面设计 1&#xff0c;strClass模块是计算过程代码展示 2&#xff0c;result-view模块是结果展示 3&#xff…

嵌入式学习---DAY24:进程--二

一、exec函数族----启动一个新程序 用fork创建子进程后执行的是和父进程相同的程序&#xff08;但有可能执行不同的代码分支&#xff09;&#xff0c; 子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时&#xff0c;该进程的 用户空间代码和数据完全被…

SHT30温湿度传感器全解析——概况,性能,MCU连接,样例代码

常见温湿度传感器测量范围&#xff1a;(价格仅供参考&#xff0c;具体性能要看折线图) 型号DHT11DHT20AHT10AHT20AHT30SHT20价格&#xffe5; 2.49&#xffe5;3.04&#xffe5; 1.9&#xffe5;1.4&#xffe5; 1.3&#xffe5;5.5温度测量范围20—90%RH0—100%RH0—100%RH0—…

pycharm最新专业版激活码

pycharm最新专业版激活码 Pycharm下载地址&#xff1a;pycharm下载 首先&#xff0c;我们打开下载的 pycharm 专业版并安装。 按照下图所示先点击上方的 Activation code&#xff0c;再将激活码粘贴至输入框&#xff0c;最后点击 Activate 激活。 激活码&#xff1a; OS2AN…

旋转关系介绍

目录 旋转矩阵与轴角 旋转矩阵与欧拉角 旋转矩阵与四元数 轴角与四元数 轴角与欧拉角 欧拉角与四元数 欧拉角与四元数 旋转矩阵与轴角 设旋转矩阵R[■8(r_11&r_12&r_13r_21&r_22&r_23r_31&r_32&r_33)]&#xff0c;轴角使用一个单位向量n和一个角…

Go--GMP调度模型

目录 GMP模型G、M、P简介P和M的个数**P和M何时会被创建**goroutine创建流程goroutine什么时候会被挂起 GMP的调度调度流程调度策略调度时机同时启动了一万个goroutine&#xff0c;会如何调度&#xff1f; GMP模型 G、M、P简介 GMP是Go运行时调度层面的实现&#xff0c;包含4个…

质量对中国开发商提升游戏品牌信誉和信任度的影响

随着全球游戏产业的持续增长&#xff0c;中国开发商正在大举进军国际市场。然而&#xff0c;他们面临的关键挑战之一是建立和维护与全球参与者的品牌信誉和信任。他们的游戏质量在实现这一目标方面起着至关重要的作用。从技术性能到故事讲述和本地化&#xff0c;高质量的游戏对…

OpenGL3.3_C++_Windows(35)

PBR_IBL漫反射 IBL图像的光照(Image based lighting&#xff09;&#xff1a;非直接光源&#xff0c;它是一种更精确的环境光照输入格式&#xff0c;甚至也可以说是一种全局光照的粗略近似。环境光照&#xff1a;获取每个wi光源辐射率&#xff0c;求辐照度&#xff1a;将周围环…

Linux学习笔记11(计算机网络)

目录 网络七层模型/五层模型 IP地址分类 CIDR Centos7的网卡IP配置 RockyLinux9的网卡IP配置 网络七层模型/五层模型 自下到上 物理层&#xff1a; 建立物理连接&#xff0c;传输 0 和 1 的比特流 数据链路层&#xff1a; 物理地址寻址&#xff0c;流量控制&#xff0c;差错…

基于vue框架的SSM基于B_S的毕业设计题目管理系统的设计与实现ij0q7(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;学生,教师,毕设题目,毕设选题,毕设任务书,开题报告,中期检查,毕业论文,论文成绩,答辩成绩,答辩通知,班级 开题报告内容 基于Vue框架的SSM&#xff08;SpringSpring MVCMyBatis&#xff09;的毕业设计题目管理系统设计与实现 开题报告…

刷题记录第109天-K个一组反转链表

解题思路&#xff1a; 第一步&#xff1a;实现一个数组&#xff0c;给定一段链表的头结点和尾节点&#xff0c;反转该链表&#xff0c;并返回新的头结点和尾结点。 第二步&#xff1a;初始化一个虚拟头结点&#xff0c;用于记录最终头结点和规范操作。 第三步&#xff1a;给定一…

​产品经理-​你如何理解“互联网思维(35)

在产品规划和功能改版中&#xff0c;确实非常重视用户需求和体验。产品需求是互联网产品的核心 用户体验是互联网产品的重点。在互联网新产品规划中&#xff0c;会非常重视用户验证环节 确保做出来的东西确实是用户想要的&#xff1b;而在已经上线的产品中&#xff0c;往往会有…

Raspberry Pi Pico 家族的进化 —— RP2040、RP2350与RP2354性能比较

随着树莓派Pico系列的不断扩展&#xff0c;其背后的芯片也得到了升级和改进。从最初的RP2040到最新的RP2354&#xff0c;每一次迭代都带来了新的功能和性能提升。本文将详细对比RP2040、RP2350和RP2354三款芯片的关键特性&#xff0c;帮助开发者了解它们的差异&#xff0c;并选…

Windows File Recovery卡在99%怎么解决?实用指南!

为什么会出现“Windows File Recovery卡在99%”的问题&#xff1f; Windows File Recovery&#xff08;Windows文件恢复&#xff09;是微软设计的命令行应用程序。它可以帮助用户从健康/损坏/格式化的存储设备中恢复已删除/丢失的文件。 通过输入相关命令&#xff0c;设置源/…