算法与数据结构(二十一)前缀和数组差分数组

news2024/11/24 6:04:09

前缀和技巧适用于快速、频繁地计算一个索引区间内的元素之和。

1. 一维数组中的前缀和

先看一道例题,力扣第 303 题「区域和检索 - 数组不可变」,让你计算数组区间内元素的和,这是一道标准的前缀和问题:

题目要求你实现这样一个类:

class NumArray {

    public NumArray(int[] nums) {}
    
    /* 查询闭区间 [left, right] 的累加和 */
    public int sumRange(int left, int right) {}
}

sumRange 函数需要计算并返回一个索引区间之内的元素和,没学过前缀和的人可能写出如下代码:

class NumArray {

    private int[] nums;

    public NumArray(int[] nums) {
        this.nums = nums;
    }
    
    public int sumRange(int left, int right) {
        int res = 0;
        for (int i = left; i <= right; i++) {
            res += nums[i];
        }
        return res;
    }
}

这样,可以达到效果,但是效率很差,因为 sumRange 方法会被频繁调用,而它的时间复杂度是 O(N),其中 N 代表 nums 数组的长度。

这道题的最优解法是使用前缀和技巧,将 sumRange 函数的时间复杂度降为 O(1),说白了就是不要在 sumRange 里面用 for 循环,咋整?

直接看代码实现:

class NumArray {
    // 前缀和数组
    private int[] preSum;

    /* 输入一个数组,构造前缀和 */
    public NumArray(int[] nums) {
        // preSum[0] = 0,便于计算累加和
        preSum = new int[nums.length + 1];
        // 计算 nums 的累加和
        for (int i = 1; i < preSum.length; i++) {
            preSum[i] = preSum[i - 1] + nums[i - 1];
        }
    }
    
    /* 查询闭区间 [left, right] 的累加和 */
    public int sumRange(int left, int right) {
        return preSum[right + 1] - preSum[left];
    }
}

核心思路是我们 new 一个新的数组 preSum 出来,preSum[i] 记录 nums[0…i-1] 的累加和,看图 10 = 3 + 5 + 2:

在这里插入图片描述

看这个 preSum 数组,如果我想求索引区间 [1, 4] 内的所有元素之和,就可以通过 preSum[5] - preSum[1] 得出。

这样,sumRange 函数仅仅需要做一次减法运算,避免了每次进行 for 循环调用,最坏时间复杂度为常数 O(1)。

这个技巧在生活中运用也挺广泛的,比方说,你们班上有若干同学,每个同学有一个期末考试的成绩(满分 100 分),那么请你实现一个 API,输入任意一个分数段,返回有多少同学的成绩在这个分数段内。

那么,你可以先通过计数排序的方式计算每个分数具体有多少个同学,然后利用前缀和技巧来实现分数段查询的 API:

int[] scores; // 存储着所有同学的分数
// 试卷满分 100 分
int[] count = new int[100 + 1]
// 记录每个分数有几个同学
for (int score : scores)
    count[score]++
// 构造前缀和
for (int i = 1; i < count.length; i++)
    count[i] = count[i] + count[i-1];
// 利用 count 这个前缀和数组进行分数段查询

接下来,我们看一看前缀和思路在二维数组中如何运用。

1.1 算法实践

二维矩阵中的前缀和
这是力扣第 304 题「二维区域和检索 - 矩阵不可变」,其实和上一题类似,上一题是让你计算子数组的元素之和,这道题让你计算二维矩阵中子矩阵的元素之和:
在这里插入图片描述

比如说输入的 matrix 如下图:
在这里插入图片描述

按照题目要求,矩阵左上角为坐标原点 (0, 0),那么 sumRegion([2,1,4,3]) 就是图中红色的子矩阵,你需要返回该子矩阵的元素和 8。

当然,你可以用一个嵌套 for 循环去遍历这个矩阵,但这样的话 sumRegion 函数的时间复杂度就高了,你算法的格局就低了。

注意任意子矩阵的元素和可以转化成它周边几个大矩阵的元素和的运算:
在这里插入图片描述

而这四个大矩阵有一个共同的特点,就是左上角都是 (0, 0) 原点。

那么做这道题更好的思路和一维数组中的前缀和是非常类似的,我们可以维护一个二维 preSum 数组,专门记录以原点为顶点的矩阵的元素之和,就可以用几次加减运算算出任何一个子矩阵的元素和:

class NumMatrix {
    // 定义:preSum[i][j] 记录 matrix 中子矩阵 [0, 0, i-1, j-1] 的元素和
    private int[][] preSum;
    
    public NumMatrix(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        if (m == 0 || n == 0) return;
        // 构造前缀和矩阵
        preSum = new int[m + 1][n + 1];
        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] 的元素和
    public 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];
    }
}

这样,sumRegion 函数的时间复杂度也用前缀和技巧优化到了 O(1),这是典型的「空间换时间」思路。

前缀和技巧就讲到这里,应该说这个算法技巧是会者不难难者不会,实际运用中还是要多培养自己的思维灵活性,做到一眼看出题目是一个前缀和问题。

除了本文举例的基本用法,前缀和数组经常和其他数据结构或算法技巧相结合,我会在 前缀和技巧高频习题 中举例讲解。

1.2 前缀和数组总结

  1. 前缀和数组适用场景:原始数组不会被修改的情况下,频繁查询某个区间的累加和;
  2. 前缀和数组优势:前缀和数组是典型的空间换时间的解决方案,将 前缀和 进行便利存储(O(n) 时空复杂度),然后 O(1) 时间复杂度得到最终数组区间和;

2. 差分数组

本文讲一个和前缀和思想非常类似的算法技巧「差分数组」,差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减。

比如说,我给你输入一个数组 nums,然后又要求给区间 nums[2…6] 全部加 1,再给 nums[3…9] 全部减 3,再给 nums[0…4] 全部加 2,再给…

一通操作猛如虎,然后问你,最后 nums 数组的值是什么?

常规的思路很容易,你让我给区间 nums[i…j] 加上 val,那我就一个 for 循环给它们都加上呗,还能咋样?这种思路的时间复杂度是 O(N),由于这个场景下对 nums 的修改非常频繁,所以效率会很低下。

这里就需要差分数组的技巧,类似前缀和技巧构造的 preSum 数组,我们先对 nums 数组构造一个 diff 差分数组,diff[i] 就是 nums[i] 和 nums[i-1] 之差:

int[] diff = new int[nums.length];
// 构造差分数组
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
    diff[i] = nums[i] - nums[i - 1];
}

在这里插入图片描述

通过这个 diff 差分数组是可以反推出原始数组 nums 的,代码逻辑如下:


int[] res = new int[diff.length];
// 根据差分数组构造结果数组
res[0] = diff[0];
for (int i = 1; i < diff.length; i++) {
    res[i] = res[i - 1] + diff[i];
}

这样构造差分数组 diff,就可以快速进行区间增减的操作,如果你想对区间 nums[i…j] 的元素全部加 3,那么只需要让 diff[i] += 3,然后再让 diff[j+1] -= 3 即可:

在这里插入图片描述

原理很简单,回想 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 修改后的结果。

现在我们把差分数组抽象成一个类,包含 increment 方法和 result 方法:

// 差分数组工具类
class Difference {
    // 差分数组
    private int[] diff;
    
    /* 输入一个初始数组,区间操作将在这个数组上进行 */
    public Difference(int[] nums) {
        assert nums.length > 0;
        diff = new int[nums.length];
        // 根据初始数组构造差分数组
        diff[0] = nums[0];
        for (int i = 1; i < nums.length; i++) {
            diff[i] = nums[i] - nums[i - 1];
        }
    }

    /* 给闭区间 [i, j] 增加 val(可以是负数)*/
    public void increment(int i, int j, int val) {
        diff[i] += val;
        if (j + 1 < diff.length) {
            diff[j + 1] -= val;
        }
    }

    /* 返回结果数组 */
    public int[] result() {
        int[] res = new int[diff.length];
        // 根据差分数组构造结果数组
        res[0] = diff[0];
        for (int i = 1; i < diff.length; i++) {
            res[i] = res[i - 1] + diff[i];
        }
        return res;
    }
}

这里注意一下 increment 方法中的 if 语句:


public void increment(int i, int j, int val) {
    diff[i] += val;
    if (j + 1 < diff.length) {
        diff[j + 1] -= val;
    }
}

当 j+1 >= diff.length 时,说明是对 nums[i] 及以后的整个数组都进行修改,那么就不需要再给 diff 数组减 val 了。

2.1 算法实践

首先,力扣第 370 题「区间加法」 就直接考察了差分数组技巧:
在这里插入图片描述

那么我们直接复用刚才实现的 Difference 类就能把这道题解决掉:

int[] getModifiedArray(int length, int[][] updates) {
    // nums 初始化为全 0
    int[] nums = new int[length];
    // 构造差分解法
    Difference df = new Difference(nums);
    
    for (int[] update : updates) {
        int i = update[0];
        int j = update[1];
        int val = update[2];
        df.increment(i, j, val);
    }
    
    return df.result();
}

当然,实际的算法题可能需要我们对题目进行联想和抽象,不会这么直接地让你看出来要用差分数组技巧,这里看一下力扣第 1109 题「航班预订统计」:
在这里插入图片描述

函数签名如下:


int[] corpFlightBookings(int[][] bookings, int n)

这个题目就在那绕弯弯,其实它就是个差分数组的题,我给你翻译一下:

给你输入一个长度为 n 的数组 nums,其中所有元素都是 0。再给你输入一个 bookings,里面是若干三元组 (i, j, k),每个三元组的含义就是要求你给 nums 数组的闭区间 [i-1,j-1] 中所有元素都加上 k。请你返回最后的 nums 数组是多少?

Note

因为题目说的 n 是从 1 开始计数的,而数组索引从 0 开始,所以对于输入的三元组 (i, j, k),数组区间应该对应 [i-1,j-1]。

这么一看,不就是一道标准的差分数组题嘛?我们可以直接复用刚才写的类:

int[] corpFlightBookings(int[][] bookings, int n) {
    // nums 初始化为全 0
    int[] nums = new int[n];
    // 构造差分解法
    Difference df = new Difference(nums);

    for (int[] booking : bookings) {
        // 注意转成数组索引要减一哦
        int i = booking[0] - 1;
        int j = booking[1] - 1;
        int val = booking[2];
        // 对区间 nums[i..j] 增加 val
        df.increment(i, j, val);
    }
    // 返回最终的结果数组
    return df.result();
}

这道题就解决了。

还有一道很类似的题目是力扣第 1094 题「拼车」,我简单描述下题目:

你是一个开公交车的司机,公交车的最大载客量为 capacity,沿途要经过若干车站,给你一份乘客行程表 int[][] trips,其中 trips[i] = [num, start, end] 代表着有 num 个旅客要从站点 start 上车,到站点 end 下车,请你计算是否能够一次把所有旅客运送完毕(不能超过最大载客量 capacity)。

函数签名如下:

boolean carPooling(int[][] trips, int capacity);

比如输入:

trips = [[2,1,5],[3,3,7]], capacity = 4

这就不能一次运完,因为 trips[1] 最多只能上 2 人,否则车就会超载。

相信你已经能够联想到差分数组技巧了:trips[i] 代表着一组区间操作,旅客的上车和下车就相当于数组的区间加减;只要结果数组中的元素都小于 capacity,就说明可以不超载运输所有旅客。

但问题是,差分数组的长度(车站的个数)应该是多少呢?题目没有直接给,但给出了数据取值范围:

0 <= trips[i][1] < trips[i][2] <= 1000

车站编号从 0 开始,最多到 1000,也就是最多有 1001 个车站,那么我们的差分数组长度可以直接设置为 1001,这样索引刚好能够涵盖所有车站的编号:


boolean carPooling(int[][] trips, int capacity) {
    // 最多有 1001 个车站
    int[] nums = new int[1001];
    // 构造差分解法
    Difference df = new Difference(nums);
    
    for (int[] trip : trips) {
        // 乘客数量
        int val = trip[0];
        // 第 trip[1] 站乘客上车
        int i = trip[1];
        // 第 trip[2] 站乘客已经下车,
        // 即乘客在车上的区间是 [trip[1], trip[2] - 1]
        int j = trip[2] - 1;
        // 进行区间操作
        df.increment(i, j, val);
    }
    
    int[] res = df.result();
    
    // 客车自始至终都不应该超载
    for (int i = 0; i < res.length; i++) {
        if (capacity < res[i]) {
            return false;
        }
    }
    return true;
}

至此,这道题也解决了。

最后,差分数组和前缀和数组都是比较常见且巧妙的算法技巧,分别适用不同的场景,而且是会者不难,难者不会。所以,关于差分数组的使用,你学会了吗?

更多经典的数组/链表技巧习题见 数组链表解题技巧精讲。

2.2 差分数组总结

  1. 差分数组适用场景:差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减;
  2. 个人体感,差分数组的适用范围更广一些,尤其是针对问题的抽象如公交车&航班问题;

参考文献

[1] 小而美的算法技巧:前缀和数组
[1] 小而美的算法技巧:差分数组

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

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

相关文章

多线程(JavaEE初阶系列2)

目录 前言&#xff1a; 1.什么是线程 2.为什么要有线程 3.进程与线程的区别与联系 4.Java的线程和操作系统线程的关系 5.多线程编程示例 6.创建线程 6.1继承Thread类 6.2实现Runnable接口 6.3继承Thread&#xff0c;使用匿名内部类 6.4实现Runnable接口&#xff0c;使…

nodejs+vue+elementui学生选课系统awwm9

前端技术&#xff1a;nodejsvueelementui,视图层其实质就是vue页面&#xff0c;通过编写vue页面从而展示在浏览器中&#xff0c;编写完成的vue页面要能够和控制器类进行交互&#xff0c;从而使得用户在点击网页进行操作时能够正常。 可以设置中间件来响应 HTTP 请求。 (3) 教…

详细总结Webpack5的配置和使用

打包工具 使用框架&#xff08;React、Vue&#xff09;&#xff0c;ES6 模块化语法&#xff0c;Less/Sass 等 CSS预处理器等语法进行开发的代码要想在浏览器运行必须经过编译成浏览器能识别的 JS、CSS 等语法&#xff0c;才能运行。 所以需要打包工具帮我们做完这些事。除此之…

微信小程序学习笔记(三)——视图与逻辑

页面导航 什么是页面导航 页面导航指的是页面之间的相互跳转。例如&#xff0c;浏览器中实现页面导航的方式有如下两种&#xff1a; <a> 链接location.href 小程序中实现页面导航的两种方式 声明式导航、 在页面上声明一个 <navigator> 导航组件通过点击 <…

R语言手动安装包

包安装问题解决方案 1. Biocmanager安装 2. 手动安装 文章目录 包安装问题解决方案前言一、Rstudio手动安装二、conda手动安装R包2.1 在Linux环境中使用R2.2 anaconda官网包下载 总结 前言 大家好✨&#xff0c;这里是bio&#x1f996;。点赞关注不迷路。在上一篇blog中为大家…

Tabby - 本地化AI代码自动补全 - Linux Debian

参考&#xff1a; https://github.com/TabbyML/tabby Docker | Tabby Linux Debian上快速安装Docker并运行_Entropy-Go的博客-CSDN博客 Tabby - 本地化AI代码自动补全 - Windows10_Entropy-Go的博客-CSDN博客 为什么选择Tabby 已经有好几款类似强劲的代码补全工具&#xf…

自动驾驶感知系统-摄像头

车载摄像头的工作原理&#xff0c;首先是采集图像&#xff0c;将图像转换为二维数据&#xff1b;然后&#xff0c;对采集的图像进行模式识别&#xff0c;通过图像匹配算法识别行驶过程中的车辆、行人、交通标识等&#xff1b;最后&#xff0c;依据目标物体的运动模式或使用双目…

(202307)wonderful-sql:基础查询与排序(task2)

教程链接&#xff1a;Datawhale - 一个热爱学习的社区 知识学习 前提&#xff1a; 上一次任务中提出了本课程的用表&#xff0c;但是我并没有加入这个表&#xff0c;这次学习前先对这个表进行插入。 INSERT INTO product VALUES(0001, T恤衫, 衣服, 1000, 500, 2009-09-20)…

[Linux笔记]gdb调试器常用指令

gcc/g形成的可执行程序默认是release版的。若要生成debug版&#xff0c;应使用-g选项。 如&#xff1a;gcc -o mytest test.c -g -stdc99 可以看到debug版包含了调试信息&#xff1a; 使用&#xff1a;gdb 文件名&#xff1a; 补充&#xff1a; 以下所有的查看指令都不会影…

CMU 15-445 -- Parallel Execution - 11

CMU 15-445 -- Join Algorithms - 11 引言Parallel & DistributedInter-query vs. Intra-query Parallelism Process ModelApproach #1: Process per DBMS WorkerApproach #2: Process PoolApproach #3: Thread per DBMS Worker Execution ParallelismInter-query Parallel…

基于SpringBoot+vue的滴答拍摄影项目设计与实现

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架…

JAVA设计模式——23种设计模式详解

一、什么是设计模式&#x1f349; 设计模式&#xff08;Design pattern&#xff09; 是解决软件开发某些特定问题而提出的一些解决方案也可以理解成解决问题的一些思路。通过设计模式可以帮助我们增强代码的可重用性、可扩充性、 可维护性、灵活性好。我们使用设计模式最终的目…

基于物联网网关的工业数据可视化平台有什么功能?

随着数字化浪潮的不断发展&#xff0c;工业数据的价值越来越重要。在企业利用数据的过程中&#xff0c;数据可视化是数字化系统中十分重要的一部分。然而&#xff0c;工厂多种设备、多种协议影响到系统的搭建使得企业无法获得全面的数据视图&#xff0c;也无法对整个生产流程进…

Ubuntu22.04 安装深度学习服务器全纪录

文章目录 Ubuntu 22.04 安装深度学习服务器全纪录1. 制作启动盘2. 安装 Ubuntu 22.043.配置国内镜像软件源4. Python 相关设置5. 配置 SSH6. 配置远程桌面6. 安装 CUDA7. 安装 docker8. 安装 Anaconda9. 安装 ChatGLM210. 使用 fastllm 推理加速 Ubuntu 22.04 安装深度学习服务…

【100天精通python】Day8:数据结构_元组Tuple的创建、删除、访问、修改、推导系列操作

目录 1 创建元组 2 删除元组 3 访问元组元素 4 多个值的同时赋值和交换 5 修改元组元素 6 元组推导式 7 元组运算符 8 元组常用场景 9 元组&#xff08;Tuple&#xff09;和列表&#xff08;List&#xff09;的区别 元组&#xff08;tuple&#xff09;是 Python 中的…

Codeforces Round 886 (Div. 4)

目录 A. To My Critics B. Ten Words of Wisdom C. Word on the Paper D. Balanced Round E. Cardboard for Pictures F. We Were Both Children G. The Morning Star A. To My Critics time limit per test1 second m…

《qt quick核心编程》笔记一

1.基础HelloWorld代码 import QtQuick 2.15 import QtQuick.Window 2.15 import QtQuick.Controls 2.15Window {width: 400height: 300visible: truetitle: qsTr("Hello 1World")Rectangle {width: parent.widthheight: parent.heightcolor: "gray"Text {…

RabbitMQ惰性队列使用

说明&#xff1a;惰性队列是为了解决消息堆积问题&#xff0c;当生产者生产消息的速度远高于消费者消费消息的速度时&#xff0c;消息会大量的堆积在队列中&#xff0c;而队列中存放的消息数量是有限的&#xff0c;当超出数量时&#xff0c;会造成消息的丢失。而扩容队列&#…

Homography单应性矩阵

1. Homography 单应性概念 考虑 同一个平面(比如书皮)的两张图片&#xff0c;红点表示同一个物理坐标点在两张图片上的各自位置。在 CV 术语中&#xff0c;我们称之为对应点。 Homography 就是将一张图像上的点映射到另一张图像上对应点的3x3变换矩阵. 因为 Homography 是一个 …

AtcoderABC237场

A - Not OverflowA - Not Overflow 题目大意 题目要求判断给定的整数N是否在范围[-231, 231-1]内&#xff0c;如果是则输出"Yes"&#xff0c;否则输出"No"。 思路分析 位运算&#xff1a;由于题目中的范围是2的幂次方&#xff0c;可以使用位运算来进行快…