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

【差分】详解一维前缀和和差分问题

文章目录

  • 1. 一维前缀和
  • 2. 一维差分
  • 3. 航班预定统计
  • 4. 等差队列差分
  • 5. 洛谷-P4231 三步必杀
  • 6. 洛谷-P5026 Lycanthropy
  • 6. 参考资料

1. 一维前缀和

首先来看下一维前缀和,下面以一道例题开始,现在给你一个数组 [1,5,8,5,2],现在需要询问 Q 次,每一次询问都会给出 [L,R] 范围,要你求数组在 [L,R] 这个范围的总和。

首先看到上面的题,最简单的思路就是直接遍历 L 到 R,然后求总和。

public int sum(int[] arr, int L, int R){
    int sum = 0;
    for(int i = L; i <= R; i++){
        sum += arr[i];
    }
    return  sum;
}

但是这样时间复杂度就去到 O(n * m),n 是询问次数,m 是 arr 的长度,如果是数组长度在 1 0 5 10^5 105 次方,这样就会超时了,所以需要一种方法在 O(1) 时间就能直接获得 [L,R] 这个范围总和,就是前缀和了。
在这里插入图片描述
pre[i] 表示从 arr[0] … arr[i-1] 的总和,比如:

  • pre[1] = arr[0]
  • pre[2] = arr[0] + arr[1]
  • pre[3] = arr[0] + arr[1] + arr[2]
  • pre[4] = arr[0] + arr[1] + arr[2] + arr[3]
  • pre[5] = arr[0] + arr[1] + arr[2] + arr[3] + arr[4]

这样定义好了之后,如果说要求 [2,4] 这个范围的总和,那么只需要用 pre[5] - pre[2] 就可以了,pre[0] = 0 是为了方便计算,有了前缀和,我们就可以把上面的 sum 方法改写成这样,这里顺便把 queries 也给出来,经过前缀和改造,下面的时间复杂度就变成了 O(m + n)。

public class Test {

    public static void main(String[] args) {
        Test main = new Test();
        main.sum(new int[]{1,5,8,5,2}, new int[][]{{2, 4}, {0, 4}, {1, 3}});
        // 15
        // 21
        // 18
    }

    /**
     * @param arr 数组
     * @param queries 查询数组
     * @return
     */
    public void sum(int[] arr, int[][] queries){
        int[] preSum = new int[arr.length + 1];
        for (int i = 1; i <= arr.length; i++) {
            preSum[i] = arr[i - 1] + preSum[i - 1];
        }
        for (int[] query : queries) {
            System.out.println(sum(preSum, query[0], query[1]));
        }
    }

    public int sum(int[] preSum, int L, int R){
        return preSum[R + 1] - preSum[L];
    }
}

2. 一维差分

那有了前缀和的基础,现在我们就要来看下差分问题了。现在我们还是给定一个数组 arr = [1,5,8,5,2],然后给 m 次操作,这些操作包含 3 个值 [L,R,V],意思是对数组 arr 的 [L,R] 范围里面的数字都减去 V,问经过这 m 次操作之后,arr 数组变成什么样了。

最简单的做法就是遍历这 m 次操作,然后再遍历 arr 的 [L,R] 范围,再减去 V,最终的时间复杂度是 O(m * n)

假设现在 m = 3,分别是 [2,4,-3],[1,3,5],[0,2,-3],我们先来看下最后的结果:
在这里插入图片描述
最终结果就是 [2,7,7,7,-1],那下面我们就来看下差分数组,首先定义数组 d,d[i] 表示 arr[i] 和 arr[i-1] 的差值。
在这里插入图片描述
那么 d 数组有什么特点呢?d 的前缀和就是 arr,如下图所示。
在这里插入图片描述
有了 d 数组之后,针对每个操作 [L,R,V],我们在 d 上做两个动作:d[L] + V,d[R + 1] - V,最终再对 d 数组求前缀和,看下面图。
在这里插入图片描述
可以看到最终前缀和的结果就是要求的结果,那么整个过程时间复杂度是多少呢?由于每一次操作都只是对两个下标做了标注,所以时间复杂度就是 O(n),n 是数组长度。

那么一维差分的原理是什么呢?由于最终需要通过前缀和来计算,所以这里使用的就是前缀和的特性,假设现在需要在 [2,4] 范围 - 3,那么我们可以在 d 数组 d[2] -3,然后 d[5] + 3,这样前缀和的时候 d[2] … d[4] 都会 -3,而 d[5] 由于 + 3 了就会和前面的 -3 抵消掉,由于数组没有 d[5],所以只对 d[2] - 3 就行了。
在这里插入图片描述


3. 航班预定统计

1109.航班预定统计

这里有 n 个航班,它们分别从 1n 进行编号。

有一份航班预订表 bookings,表中第 i 条预订记录 bookings[i] = [fisti, lasti, seatsi] 意味着在从 firstilasti包含 firstilasti )的 每个航班 上预订了 seatsi 个座位。

请你返回一个长度为 n 的数组 answer,里面的元素是每个航班预定的座位总数。

示例什么的就不给出了,格式有点难搞,下面直接给出代码,这里的 d 数组因为没有初始值,所以直接初始化为 0 就行了。

public int[] corpFlightBookings(int[][] bookings, int n) {
    int[] d = new int[n];
    for(int i = 0; i < bookings.length; i++){
        int L = bookings[i][0];
        int R = bookings[i][1];
        int v = bookings[i][2];
        d[L-1] += v;
        if(R < n){
            d[R] -= v;
        }
    }
    for(int i = 1; i < n; i++){
        d[i] += d[i-1];
    }
    return d;
}

上面是一种写法,如果不想判断越界,就把数组设置成 n + 2,因为编号是从 1 开始,我们惯用写法是从 0 开始,所以上面的代码其实就是从 0 开始算,所以才会有 d[L-1] += v,下面就是不需要判断边界的版本。

public int[] corpFlightBookings(int[][] bookings, int n) {
    int[] d = new int[n + 2];
    for(int i = 0; i < bookings.length; i++){
        int L = bookings[i][0];
        int R = bookings[i][1];
        int v = bookings[i][2];
        d[L] += v;
        d[R + 1] -= v;
    }
    for(int i = 1; i < d.length; i++){
        d[i] += d[i-1];
    }
    int[] ans = new int[n];
    for(int i = 0; i < n; i++){
        ans[i] = d[i+1];
    }
    return ans;
}

4. 等差队列差分

等差队列差分的题目就是给你一个长度为 n 的数组,接下来再给你 m 个操作,每次操作就是在 [L,R] 范围上去依次加上 首项s末项e公差d 的队列,问你最终这个数组的结果是什么?

比如说现在给你一个队列 [0,1,2,3,4,5,6,7],然后给你 m 个操作,分别是

  • [2,4],s = 1,e = 7,d = 3
  • [1,5],s = 3,e = 7,d = 1
  • [3,7],s = 4,e = 20,d = 4

好了,下面我们就来看下这个例子,也就是说第一个操作需要在下标 2、3、4 分别加上 1、4、7,第二个操作需要在下标 1、2、3、4、5 分别加上 3、4、5、6、7,第三个操作需要在下标 3,4,5,6,7 分别加上 4,8,12,16,20,然后最终结果就是:

在这里插入图片描述

对于这种差分,需要做以下步骤,假设现在求出了数组 d,那么

  1. d[l] += s
  2. d[l + 1] += d - s
  3. d[r + 1] -= d + e
  4. d[r + 2] += e

最后将 d 数组进行两次前缀和,结果如下:
在这里插入图片描述
可以看到这样算起来就是最终的结果,注意这里直接用的是 0 数组而不是 d 数组,那为什么会这样呢?看一下过程就知道了,比如下面的例子,假设我需要在 [L,R] 直接加上 s,d,e 的等差数列。

在这里插入图片描述


5. 洛谷-P4231 三步必杀

P4231 三步必杀

里面就是最简单的等差队列差分,下面直接给出代码,注意下面如果输入输出使用 Scanner 来读取,那么结果会超时。

import java.io.*;

public class Main {

    public static long[] arr;
    public static int N = 0;
    public static int M = 0;

    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;
            arr = new long[N + 3];
            for (int i = 0; i < M; i++) {
                in.nextToken();
                int l = (int) in.nval;
                in.nextToken();
                int r = (int) in.nval;
                in.nextToken();
                int s = (int) in.nval;
                in.nextToken();
                int e = (int) in.nval;
                int d = (e - s) / (r - l);
                set(l, r, s, e, d);
            }
            build();
            long xor = 0, max = 0;
            for (int i = 1; i <= N; i++) {
                max = Math.max(max, arr[i]);
                xor ^= arr[i];
            }

            System.out.println(xor + " " + max);

        }
        out.flush();
        out.close();
        br.close();
    }

    public static void build() {
        for (int i = 2; i <= N; i++) {
            arr[i] += arr[i - 1];
        }
        for (int i = 2; i <= N; i++) {
            arr[i] += arr[i - 1];
        }
    }

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

6. 洛谷-P5026 Lycanthropy

P5026 Lycanthropy

题目描述:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这道题就是等差队列差分,题目的意思可以理解成现在在湖面上扔下一颗石子,会荡起波浪,扔石子的位置是 x,波浪的形状如下。
在这里插入图片描述
可以看到,x 的范围可以去到 [x - 3 * v,x + 3 * v],为了解决边界问题,我们让生成的数组大小设置成 30001 + 1000001 + 30001,这样是为了不超过边界,因为 x 的范围是 [0,1000000],对于输入的每一行都调用四次设置等差队列的逻辑,整体代码如下。

import java.io.*;

public class Main {

    public static int n = 0;
    public static int m = 0;
    public static int offset = 30001;
    public static int[] arr = new int[offset + 1000001 + offset];

    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; i < n; i++) {
                in.nextToken();
                int v = (int) in.nval;
                in.nextToken();
                int x = (int) in.nval;
                set(v, x);
            }
            // 两次前缀和
            build();

            // 输出数组
            out.print(arr[offset + 1]);
            for(int i = offset + 2; i <= m + offset; i++){
                out.print(" " + arr[i]);
            }
            out.println();
        }
        out.flush();
        out.close();
        br.close();
    }

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

    // v 就是体积
    // x 就是入水点
    public static void set(int v, int 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);
    }

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

}

当然了,这里的逻辑是不涉及到边界 x - 3v 和 x + 3v 的,下面也可以设置边界,看起来更清晰点。

// v 就是体积
// x 就是入水点
public static void set(int v, int x) {
    // 四个等差队列
    set(x - 3 * v, x - 2 * v, 0, 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, v - 1, 0, -1);
}

在这里插入图片描述


6. 参考资料

资料来自左神的一维差分讲解:算法讲解047【必备】一维差分与等差数列差分





如有错误,欢迎指出!!!

http://www.dtcms.com/a/99451.html

相关文章:

  • Java高级JVM知识点记录,内存结构,垃圾回收,类文件结构,类加载器
  • 无人机进行航空数据收集对于分析道路状况非常有用-使用无人机勘测高速公路而不阻碍交通-
  • BurpSuit抓包失败-基础配置
  • 用war解压缩.7zip文件解压缩正在进行但是结束后文件消失了
  • 计算机二级考前急救(Word篇)
  • python:将mp4视频快进播放,并保存新的视频
  • OpenHarmony子系统开发 - 安全(二)
  • Redisson分布式锁深度解析:原理与实现机制
  • STM32F4单片机SDIO驱动SD卡
  • NLP语言模型训练里的特殊向量
  • Spring Boot整合Kafka详细指南(JDK 1.8)
  • Flutter环境搭建
  • JDK1.8和Maven、Git安装教程自用成功
  • 【MySQL基础】函数之字符串函数详解
  • JVM Java类加载 isInstance instanceof 的区别
  • 洛谷题单1-P5703 【深基2.例5】苹果采购-python-流程图重构
  • JDBC的详细使用
  • 【零基础入门unity游戏开发——2D篇】2D物理关节 —— Joint2D相关组件
  • [Lc4_dfs] 解数独 | 单词搜索
  • PyQt6实例_批量下载pdf工具_界面开发
  • MDK中结构体的对齐、位域、配合联合体等用法说明
  • C#:第一性原理拆解属性(property)
  • 分享一个Pyside6实现web数据展示界面的效果图
  • Springboot学习笔记3.20
  • SmolDocling文档处理模型介绍
  • Python 循环全解析:从语法到实战的进阶之路
  • 人工智能之数学基础:矩阵的相似变换的本质是什么?
  • DeepSeek网络拓扑设计解密:如何支撑千卡级AI训练的高效通信?
  • 缓存 vs 分布式锁:高并发场景下的并发控制之道
  • 【C++】类和对象(二)默认成员函数之拷贝构造函数、运算符重载、赋值运算符重载