[杂记]算法:前缀和与差分数组

news2024/11/28 4:51:40

这篇讲一下前缀和与差分数组的关系


1. 前缀和

1.1 一维数组前缀和

前缀和在处理数组中的连续子数组的某一段加和的问题中很有用, 因为是拿空间换时间, 可以将线性复杂度降低为常数时间复杂度.

前缀和的道理很简单, 对于数组 a r r [ i ] , i = 0 , . . . , n − 1 arr[i], i = 0, ..., n - 1 arr[i],i=0,...,n1, 我们按照如下关系定义前缀和数组 p r e S u m [ i ] preSum[i] preSum[i]:

p r e S u m [ i ] = p r e S u m [ i − 1 ] + a r r [ i − 1 ] ,    i = 1 , . . . , n preSum[i] = preSum[i - 1] + arr[i - 1], ~~i= 1,..., n preSum[i]=preSum[i1]+arr[i1],  i=1,...,n

这时, p r e S u m [ i ] preSum[i] preSum[i]表示的是 a r r arr arr数组前 i i i个的元素和, p r e S u m preSum preSum的长度为 n + 1 n + 1 n+1. 在这种情况下, 定义 p r e S u m [ 0 ] = 0 preSum[0]=0 preSum[0]=0(因为前0个元素的和没有定义). 这样, 如果我们要计算 a r r [ l ] + a r r [ l + 1 ] + . . . + a r r [ r ] arr[l] + arr[l + 1] + ...+ arr[r] arr[l]+arr[l+1]+...+arr[r]的和, 那么显然可以用前 r + 1 r + 1 r+1个元素的和减去前 l l l个元素的和决定, 也就有:

a r r [ l ] + a r r [ l + 1 ] + . . . + a r r [ r ] = p r e S u m [ r + 1 ] − p r e S u m [ l ] arr[l] + arr[l + 1] + ...+ arr[r]=preSum[r + 1]-preSum[l] arr[l]+arr[l+1]+...+arr[r]=preSum[r+1]preSum[l]

当然, 我们也可以按照如下方式定义:

p r e S u m [ 0 ] = a r r [ 0 ] p r e S u m [ i ] = p r e S u m [ i − 1 ] + a r r [ i ] ,    i = 1 , . . . , n − 1 preSum[0] = arr[0]\\ preSum[i] = preSum[i - 1] + arr[i],~~ i= 1,..., n - 1 preSum[0]=arr[0]preSum[i]=preSum[i1]+arr[i],  i=1,...,n1

如果按照这种方式定义, 则 p r e S u m [ i ] preSum[i] preSum[i]的意义就更简单: 即 a r r [ 0 ] + . . . + a r r [ i ] arr[0] + ... + arr[i] arr[0]+...+arr[i], 也就是前 i + 1 i+1 i+1项的和. 这样的话, p r e S u m preSum preSum的长度与 a r r arr arr的一致, 都为 n n n. 这样, 如果我们要计算 a r r [ l ] + a r r [ l + 1 ] + . . . + a r r [ r ] arr[l] + arr[l + 1] + ...+ arr[r] arr[l]+arr[l+1]+...+arr[r]的和, 有:

a r r [ l ] + a r r [ l + 1 ] + . . . + a r r [ r ] = p r e S u m [ r ] − p r e S u m [ l ] arr[l] + arr[l + 1] + ...+ arr[r]=preSum[r]-preSum[l] arr[l]+arr[l+1]+...+arr[r]=preSum[r]preSum[l]

两种方式都差不多, 第一种具有更好的公式一致性.

下面用例题来展示一下一维前缀和的应用.

1.1.1 长度最小的子数组

在这里插入图片描述
这是一个经典的基本的前缀和问题. 题目要求求解最短的连续子序列, 使得子序列的和大于等于目标值.

我们可以很容易地想到用前缀和快速在 O ( 1 ) O(1) O(1)的复杂度内计算某个区间的和. 为了求出最短的子序列, 我们对前缀和数组枚举左右端点, 也就是滑动窗口的方法. 对于每一个右端点, 枚举左端点, 只要合法(和大于target), 那么就右移左端点, 并更新答案即可. 整体的复杂度为 O ( n ) O(n) O(n).

代码:

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int length = nums.size();
        vector<int> preSum (length + 1, 0);  // preSum[i] 前i个元素的和
        int result = INT_MAX;

        for (int idx = 0; idx < length; idx ++)
            preSum[idx + 1] = preSum[idx] + nums[idx];

        // 找到最小的j - i使得preSum[j + 1] - preSum[i] >= target
        // 用滑动窗口
        int left = 0, right = 1;
        while (right < length + 1) {
            // 对于每个right 找到尽量靠近right 的left
            while (preSum[right] - preSum[left] >= target) {
                result = min(result, right - left);
                left ++;
            }
            right ++;
        }

        return result == INT_MAX ? 0 : result;
    }
};

1.1.2 除自身以外数组的乘积

在这里插入图片描述
这个题让我们返回一个新数组, 新数组的每个元素表示的是元素组除了对应索引外的这个元素其余元素的乘积. 我们可以借鉴前缀和的思想, 对于原数组的第i个元素, 除它之外的乘积是前i个元素的积, 与后length - (i + 1)个元素的积. 因此我们定义两个前缀数组, 一个left表示从前面数元素的累积, 一个right表示从后面数元素的累积, 最后第i个元素, 除它之外的乘积是left[i] * right[length - (i + 1)]:

class Solution {
public:
    vector<int> productExceptSelf(vector<int>& nums) {
        int length = nums.size();
        // 用两个数组记录前缀乘积与后缀乘积 则
        // nums[i]对应答案为preMulLeft[i] * preMulRight[length - 1 - i]
        vector<int> preMulLeft (length + 1, 1);  // 前i个元素的乘积
        vector<int> preMulRight (length + 1, 1);  // 后i个元素的乘积

        for (int idx = 0; idx < length; idx ++) 
            preMulLeft[idx + 1] = preMulLeft[idx] * nums[idx];
        for (int idx = 0; idx < length; idx ++) 
            preMulRight[idx + 1] = preMulRight[idx] * nums[length - 1 - idx];

    
        vector<int> result (length, 1);
        for (int idx = 0; idx < length; idx ++)
            result[idx] = preMulLeft[idx] * preMulRight[length - 1 - idx];

        return result; 

    }
};

1.2 二维数组前缀和

二维数组前缀和与一维的一致, 只需要理解下面的图就可以了:

在这里插入图片描述
也就是说, 二维数组的某处i, j的前缀和是i, j - 1处的(黄色部分)加上i - 1, j处的(蓝色部分)减去i-1, j-1处的(黄色与蓝色重叠部分)再加上对应元素值的即可.
如果我们采用第一种方式定义, 即 p r e S u m [ i ] [ j ] preSum[i][j] preSum[i][j]表示前 i i i行前 j j j列所有元素的和, 那么有如下关系:

p r e S u m [ i ] [ j ] = p r e S u m [ i − 1 ] [ j ] + p r e S u m [ i ] [ j − 1 ] − p r e S u m [ i − 1 ] [ j − 1 ] + a r r [ i − 1 ] [ j − 1 ] ,    i , j = 1 , . . . , n preSum[i][j] = preSum[i-1][j] + preSum[i][j-1]-preSum[i-1][j-1]+arr[i-1][j-1], ~~i,j=1, ...,n preSum[i][j]=preSum[i1][j]+preSum[i][j1]preSum[i1][j1]+arr[i1][j1],  i,j=1,...,n

1.2.1 二维区域和检索 - 矩阵不可变

在这里插入图片描述
这个题让我们返回某个子矩阵的元素和. 可以利用上面的二维前缀和来做, 代码如下:

class NumMatrix {
public:
    vector<vector<int>> sums;

    NumMatrix(vector<vector<int>>& matrix) {
        int m = matrix.size();
        if (m > 0) {
            int n = matrix[0].size();
            sums.resize(m + 1, vector<int>(n + 1));
            for (int i = 0; i < m; i++) {
                for (int j = 0; j < n; j++) {
                    sums[i + 1][j + 1] = sums[i][j + 1] + sums[i + 1][j] - sums[i][j] + matrix[i][j];
                }
            }
        }
    }

    int sumRegion(int row1, int col1, int row2, int col2) {
        return sums[row2 + 1][col2 + 1] - sums[row1][col2 + 1] - sums[row2 + 1][col1] + sums[row1][col1];
    }
};

2. 差分数组

差分可以看作是前缀和的逆运算(相当于积分与求导的关系). 为了便于理解, 对于数组 a r r [ i ] , i = 0 , . . . , n − 1 arr[i], i=0,...,n-1 arr[i],i=0,...,n1, 按照如下方式定义差分数组 d i f f diff diff:

d i f f [ 0 ] = a r r [ 0 ] d i f f [ i ] = a r r [ i ] − a r r [ i − 1 ] ,    i = 1 , . . . , n − 1 diff[0] = arr[0] \\ diff[i] = arr[i] - arr[i - 1], ~~i = 1,...,n-1 diff[0]=arr[0]diff[i]=arr[i]arr[i1],  i=1,...,n1

我们对差分数组求前缀和, 按照第二种前缀和定义的方式(这样差分数组与前缀和数组长度都为 n n n, 便于理解), 结果恰为原数组. 例如:

在这里插入图片描述
差分数组可以干什么呢? 如果我们想批量对某个区间进行加减, 则是很有用的. 例如, 我要对数组1,2,3,4,5的3,4都加1, 那么可以将差分数组3对应的位置加1, 5对应的位置减1, 如下图:

在这里插入图片描述

简单来讲, 如果想对数组的[l, r]区间(闭区间)同时加r, 则等效于将diff[l] += r, diff[r + 1] -= r, 再求diff的前缀和. 为了不让数组越界, 防止r为右端点的情形, 我们可以多开一个元素.

二维的差分也是一样的道理, 如果想对[x1, y1][x2, y2]都加r, 则等效于做一下操作:

diff[x1][y1] += r
diff[x1 + 1][y1] -= r
diff[x1][y1 + 1] -= r
diff[x1 + 1][y1 + 1] += r

按照下图就好理解了:(牢记要对差分数组求前缀和)(图的来源: LeetCode题解)

在这里插入图片描述
有了以上的基础, 可以很容易做出来下面的题了:

子矩阵元素加 1

在这里插入图片描述
代码:


class Solution {
public:
    vector<vector<int>> rangeAddQueries(int n, vector<vector<int>>& queries) {
        vector<vector<int>> result (n, vector<int> (n, 0));  // 结果数组
        vector<vector<int>> diff (n + 1, vector<int> (n + 1, 0));  // 差分数组
        // 因为可能对边界操作 故多开一个

        // 计算出差分数组 再对差分数组求前缀和就是结果
        for (auto& q : queries) {
            int row0 = q[0], row1 = q[2], col0 = q[1], col1 = q[3];

            diff[row0][col0] += 1;
            diff[row1 + 1][col0] -= 1;
            diff[row0][col1 + 1] -= 1;
            diff[row1 + 1][col1 + 1] += 1;
        }

        // result 即为diff的前缀和
        // 先将左上角与边缘的计算出来
        result[0][0] = diff[0][0];
        for (int i = 1; i < n; i ++) {
            result[0][i] = result[0][i - 1] + diff[0][i];
            result[i][0] = result[i - 1][0] + diff[i][0];
        }

        // 其余位置 二维前缀和
        for (int row = 1; row < n; row ++) {
            for (int col = 1; col < n; col ++) 
                result[row][col] = result[row - 1][col] + result[row][col - 1] - result[row - 1][col - 1] + diff[row][col];
        }

        return result;

    }
};

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

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

相关文章

《Linux Shell脚本攻略》学习笔记-第四章

4.1 简介 本章主要介绍sed、awk、grep、cut等命令&#xff0c;这些工具可以相互结合以满足文本处理需求。 正则表达式是一种基础的模式匹配技术。 4.2 使用正则表达式 正则表达式是由字面文本和具有特殊意义的符号组成的。 1&#xff09;位置标记 位置标记锚点是标识字符串位置…

Anaconda安装、opencv环境配置、jupyter notebook使用虚拟环境

目录一、Anaconda 的安装二、opencv 3.4.1.15版本安装三、jupyter notebook使用虚拟环境四、运行报错-缺库一、Anaconda 的安装 Anaconda官网&#xff1a;Anaconda Installers Anaconda历史版本&#xff1a;Anaconda Index of 这边建议和我装一样anaconda3 python3.7&#xf…

opencv的图像基本操作(基于jupyter Notebook)

opencv的基本操作cv2是opencv在python中的缩写&#xff0c;函数开头用cv2cv2.imread(cat.jpg) #读入图片cat.jpgcv2.imwrite(mycat.png,img) #图片img保存为mycat.pngcv2.imshow(image,img) #创建窗口&#xff0c;显示图像cv2.waitKey(10000) #等待时间&#xff0c;以 毫秒为单…

整数分解

问题描述 将 3 分解成两个正整数的和, 有两种分解方法, 分别是 312312 和 321321 。注意顺序不同算不同的方法。 将 5 分解成三个正整数的和, 有 6 种分解方法, 它们是 113122113122 131212221311131212221311 。 请问, 将 2021 分解成五个正整数的和, 有多少种分解方法? …

Android大厂面试100题,涵盖测试技术、环境搭建、人力资源

测试技术面试题 1、什么是兼容性测试&#xff1f;兼容性测试侧重哪些方面&#xff1f; 2、我现在有个程序&#xff0c;发现在Windows上运行得很慢&#xff0c;怎么判别是程序存在问题还是软硬件系统存在问题&#xff1f; 3、测试的策略有哪些&#xff1f; 4、正交表测试用例…

Sinutrain下载安装与开启OPC UA---kalrry

Sinumerik下载安装与开启OPC UA---kalrry前言一、安装前准备二、Win7安装1、软件安装2、开启授权3、文件配置4、客户端连接三、Win10/11安装四、启动后使用前言 本教程只适用于 Sinutrain-v4.7 版本&#xff0c;其他版本配置目录有所改变建议安装到默认路径&#xff0c;否则后…

【云原生】k8s安全机制

内容预知 前言 1. 认证&#xff08;Authentication&#xff09; 1.1 k8s集群内的三种认证方式 1.2 k8s集群内的认证说明 &#xff08;1&#xff09;需要被认证的访问类型 &#xff08;2&#xff09;安全性说明 &#xff08;3&#xff09;证书颁发的方式 &#xff08;4&a…

Qt中使用qt自带的函数实现各种进制间的相互转换,easy.

文章目录一.十进制转各种进制第一种&#xff1a;使用QString的静态函数number第二种&#xff1a;使用QString的拼接函数arg二.各种进制相互转换一.十进制转各种进制 第一种&#xff1a;使用QString的静态函数number ①使用QString的静态函数number即可&#xff0c;如我把字符…

嵌入式linux-进程状态与进程关系

1. 进程状态 1.1什么是进程状态 Linux 系统下进程通常存在 6 种不同的状态&#xff0c;分为&#xff1a;就绪态、运行态、僵尸态、可中断睡眠状态&#xff08;浅度 睡眠&#xff09;、不可中断睡眠状态&#xff08;深度睡眠&#xff09;以及暂停态。 下面我们来一一总结一下&…

数据湖之Hudi基础:入门介绍和编译部署

主要记录下Hudi的概述和打包编译等内容&#xff0c;方便参考 文章目录简介官网发展历史Hudi特性使用场景安装部署编译环境准备编译hudi1.源码包上传到服务器2.修改pom文件3.修改源码兼容hadoop34.手动安装kafka依赖&#xff08;非必须&#xff09;5.解决spark模块依赖冲突6.执行…

【基础篇】4 # 链表(上):如何实现LRU缓存淘汰算法?

说明 【数据结构与算法之美】专栏学习笔记 链表结构 数组需要一块连续的内存空间来存储&#xff0c;对内存的要求比较高&#xff0c; 而链表并不需要一块连续的内存空间&#xff0c;它通过指针将一组零散的内存块串联起来使用。 结点&#xff1a;指的是内存块后继指针 next…

Postgresql源码(98)lex与yacc的定制交互方式

1 背景知识一&#xff1a;LEX %option prefix Postgresql中使用%option prefix"core_yy"&#xff0c;影响范围&#xff1a;yy_create_buffer,yy_delete_buffer,yy_flex_debug,yy_init_buffer,yy_flush_buffer,yy_load_buffer_state,yy_switch_to_buffer,yyin,yyleng…

【并发编程十一】c++线程同步——future

【并发编程十一】c线程同步——future一、互斥二、条件变量三、future1、promise1.1、子线程设值&#xff0c;主线程获取1.2、主线程设置值&#xff0c;子线程获取1.3、shared_future2、async2.1、不开新线程的async2.2、开新线程的async3、packaged_task3.1、不使用bind3.2、提…

Kafka-概述

一、Kafka是什么 1.定义 Apache Kafka 是一款开源的消息引擎系统。 消息引擎系统是一组规范。企业利用这组规范在不同系统之间传递语义准确的消息&#xff0c;实现松耦合的异步式数据传递。 二、消息队列的使用场景 传统消息队列的应用场景包括 缓存/削峰、解耦、异步通信 …

vue(透传属性,$attrs)

官方文档 https://cn.vuejs.org/guide/components/attrs.html 案例 <FirstLevel class"attr-test-class" name"zs" age"18"></FirstLevel>FirstLevel组件没有用props去申明name和age&#xff0c;所以这两个属性会透视传递。 <…

RT-Thread系列--组件初始化

一、目的RT-Thread里面有个特别有意思的软件设计叫做组件自动初始化。有些小伙伴可能是第一次听说&#xff0c;所以这边我解释一下&#xff0c;请看下面的代码片段static void clock_init() {// 时钟初始化 } static void uart_init() {// 串口初始化 } static void i2c_init()…

SpringBoot自定义拦截器

&#x1f341;博客主页&#xff1a;&#x1f449;不会压弯的小飞侠 ✨欢迎关注&#xff1a;&#x1f449;点赞&#x1f44d;收藏⭐留言✒ ✨系列专栏&#xff1a;&#x1f449;SpringBoot专栏 &#x1f525;欢迎大佬指正&#xff0c;一起学习&#xff01;一起加油&#xff01; …

JavaScript 浏览器的重排和重绘

文章目录JavaScript 浏览器的重排和重绘概述浏览器解析过程重排重绘优化将多次改变样式的属性操作合并为一次需要多次重排的元素设置为绝对定位减少DOM操作复杂元素处理先设置display为none处理完后再显示缓存频繁操作的属性减少使用table布局使用事件委托绑定事件处理程序利用…

上海亚商投顾:沪指重返3200点 牛市旗手回归!

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。市场情绪三大指数今日继续走强&#xff0c;沪指重返3200点上方&#xff0c;创业板指午后一度涨近3%&#xff0c;随后涨幅有所…

2023.1. Stimulsoft 报告和仪表板的新版本:Crack

2023.1. Stimulsoft 报告和仪表板的新版本。 发布时间&#xff1a;2022 年 12 月 9 日 我们很高兴地宣布发布 Stimulsoft Reports and Dashboards 2023.1 版&#xff01;我们为 .NET Core 组件添加了对Razor Pages的支持&#xff0c;为PHP和Blazor平台更新了组件。此外&#x…