数组(二)-- LeetCode[303][304] 区域和检索 - 数组不可变

news2024/9/30 21:19:12

1 区域和检索 - 数组不可变

1.1 题目描述

        题目链接:https://leetcode.cn/problems/range-sum-query-immutable/

1.2 思路分析

        最朴素的想法是存储数组 nums 的值,每次调用 sumRange 时,通过循环的方法计算数组 nums 从下标 i i i 到下标 j j j 范围内的元素和,需要计算 j − i + 1 j−i+1 ji+1 个元素的和。由于每次检索的时间和检索的下标范围有关,因此检索的时间复杂度较高,如果检索次数较多,则会超出时间限制。

        由于会进行多次检索,即多次调用 sumRange,因此为了降低检索的总时间,应该降低 sumRange 的时间复杂度,最理想的情况是时间复杂度 O ( 1 ) O(1) O(1)。为了将检索的时间复杂度降到 O ( 1 ) O(1) O(1),需要在初始化的时候进行预处理。

        注意到当 i ≤ j i≤j ij 时,sumRange(i,j) 可以写成如下形式:

sum ⁡ Range ⁡ ( i , j ) = ∑ k = i j n u m s [ k ] = ∑ k = 0 j n u m s [ k ] − ∑ k = 0 i − 1 n u m s [ k ] \begin{aligned} & \operatorname{sum} \operatorname{Range}(i, j) \\ = & \sum_{k=i}^j n u m s[k] \\ = & \sum_{k=0}^j n u m s[k]-\sum_{k=0}^{i-1} n u m s[k]\end{aligned} ==sumRange(i,j)k=ijnums[k]k=0jnums[k]k=0i1nums[k]

        由此可知,要计算 sumRange(i,j),则需要计算数组 nums 在下标 j j j 和下标 i − 1 i−1 i1 的前缀和,然后计算两个前缀和的差。

        如果可以在初始化的时候计算出数组 nums 在每个下标处的前缀和 pre_sum,即可满足每次调用 sumRange 的时间复杂度都是 O ( 1 ) O(1) O(1)

示例代码:

class NumArray:

    def __init__(self, nums: List[int]):
        self.pre_sum = [0]            # 便于计算累加和,若直接分配数组空间,计算效率更高
        for i in range(len(nums)):
            self.pre_sum.append(self.pre_sum[i] + nums[i])  # 计算nums累加和

    def sumRange(self, left: int, right: int) -> int:
        return self.pre_sum[right+1] - self.pre_sum[left]

        下面以数组 [1, 12, -5, -6, 50, 3] 为例,展示了求 pre_sum 的过程。

复杂度分析

  • 时间复杂度:初始化 O ( n ) O(n) O(n),每次检索 O ( 1 ) O(1) O(1),其中 n n n 是数组 nums 的长度。初始化需要遍历数组 nums 计算前缀和,时间复杂度是 O ( n ) O(n) O(n)。每次检索只需要得到两个下标处的前缀和,然后计算差值,时间复杂度是 O ( 1 ) O(1) O(1)
  • 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 nums 的长度。需要创建一个长度为 n + 1 n+1 n+1 的前缀和数组。

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

2.1 题目描述

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

2.2 思路分析

        这部分借鉴自:笨猪爆破组的题解————从暴力法开始优化 「二维前缀和」做了什么事 | leetcode.304

1. 暴力法

        对二维矩阵,求子矩阵 ( n ∗ m ) (n*m) (nm) 的和。两重循环,累加求和。

        每次查询时间复杂度 O ( n ∗ m ) O(n∗m) O(nm),n和m是子矩阵的行数和列数。查询的代价大。

2. 第一步优化

        上面的暴力法其实也分了 n 步:第一行的求和,到第 n 行的求和,它们是 n 个一维数组。

        昨天我们学习了一维前缀和,我们可以对这n个一维数组求前缀和,得到n个一维pre_sum数组。

        为了节省查询的时间,我们求出整个矩阵每一行的一维pre_sum数组

        根据前缀和定义: p r e _ s u m [ i ] = n u m s [ 0 ] + n u m s [ 1 ] + ⋯ + n u m s [ i ] {pre}\_{sum}[i]=nums[0]+nums[1]+\cdots+nums[i] pre_sum[i]=nums[0]+nums[1]++nums[i],求出前缀和(下图红字):

        然后套用通式: n u m s [ i ] + ⋯ + n u m s [ j ] = p r e _ s u m [ j ] − p r e _ s u m [ i − 1 ] nums[i]+\cdots+nums[j]=pre\_sum[j]-pre\_sum[i-1] nums[i]++nums[j]=pre_sum[j]pre_sum[i1]

        即可求出粉色子阵列的和,计算情况如下图。

        可见,如果想多次查询子阵列的和,我们可以提前求出每一行数组的一维前缀和。

        那么查询阶段,求出一行子数组的求和,就只是 O ( 1 ) O(1) O(1),查询 n 行的子阵列,每次就查询花费 O ( n ) O(n) O(n),比 O ( n 2 ) O(n^2) O(n2)

3. 示例代码:

class NumMatrix:

    def __init__(self, matrix: List[List[int]]):
        m, n = len(matrix), len(matrix[0])          # 矩阵的行和列
        self.pre_sum = [[0] for _ in range(m)]      # 构造一维前缀和矩阵
        for i in range(m):
            for j in range(n):
                self.pre_sum[i].append(self.pre_sum[i][j]+matrix[i][j])

    def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
        return sum([self.pre_sum[i][col2+1]-self.pre_sum[i][col1] for i in range(row1, row2+1)])

4. 第二步优化
        还可以继续优化吗?

        我们引入一个概念:二维前缀和,定义式如下

        pre_sum[i][j] 表示:左上角为 arr[0][0],右下角为 arr[i][j] 的阵列的求和.

        我们把这个阵列拆分成四个部分,如图中的色块。

        要想求出 pre_sum[i][j],根据上图,由容斥原理,有:

        移项后:

a r r [ i ] [ j ] = p r e _ s u m [ i ] [ j ] + p r e _ s u m [ i − 1 ] [ j − 1 ] − p r e _ s u m [ i − 1 ] [ j ] − p r e _ s u m [ i ] [ j − 1 ] arr[i][j] = pre\_sum[i][j] + pre\_sum[i-1][j-1] - pre\_sum[i-1][j] - pre\_sum[i][j-1] arr[i][j]=pre_sum[i][j]+pre_sum[i1][j1]pre_sum[i1][j]pre_sum[i][j1]

        现在想求:行 从 a 到 A,列 从 b 到 B 的子阵列的和。叠加上式,各种相消后。得:

        回到粉色子阵列,求她的和,就是如下图的 4 个 pre_sum 矩阵元素相加减。

        问题来了,怎么求出 pre_sum 二维阵列的每一项?

        就是用遍历原矩阵,两层循环,套下图的公式。

        注意到上图黄字,在 -1 位置上预置了 0,只是为了让处于边界的 preSum 元素,也能套用下面的通式。

        两个关键式 pre_sum[i][j] 的定义式如下,并且预置 pre-sum[-1][j]pre_sum[i][-1] 为 0:

preSum ⁡ [ i ] [ j ] = ∑ x = 0 i ∑ y = 0 j arr ⁡ [ x ] [ y ] \operatorname{preSum}[i][j]=\sum_{x=0}^i \sum_{y=0}^j \operatorname{arr}[x][y] preSum[i][j]=x=0iy=0jarr[x][y]

        求:行从 a 到 A,列从 b 到 B 的子阵列的和的通式:
∑ i = a A ∑ i = b B arr ⁡ [ i ] [ j ] = pre_sum ⁡ [ A ] [ B ] + pre_sum ⁡ [ a − 1 ] [ b − 1 ] − pre_sum ⁡ [ A ] [ b − 1 ] − pre_sum ⁡ [ a − 1 ] [ B ] \sum_{i=a}^A \sum_{i=b}^B \operatorname{arr}[i][j]=\operatorname{pre\_sum}[A][B]+\operatorname{pre\_sum}[a-1][b-1]-\operatorname{pre\_sum}[A][b-1]-\operatorname{pre\_sum}[a-1][B] i=aAi=bBarr[i][j]=pre_sum[A][B]+pre_sum[a1][b1]pre_sum[A][b1]pre_sum[a1][B]

        查询的时间复杂度降下来了
        因此子阵列的求和,都只需要访问二维 pre_sum 数组的四个值。

        预处理阶段,求出二维 pre_sum 数组,需要花费 O ( n ∗ m ) O(n∗m) O(nm),n和m是子矩阵的行数和列数。

        但之后每次查询,就都是 O ( 1 ) O(1) O(1) 的时间复杂度

5. 调整 pre_sum 矩阵

        为了减少特判的代码,我们调整一下 pre_sum 矩阵,原先 arr[i][j] 对应 pre_sum[i][j]

        现在错开,arr[i][j] 对应 pre_sum[i+1][j+1]

        如下图所示,pre_sum 阵列会比原矩阵多一行一列,为了让 pre_sum 的 -1 列 -1 行变成 0 行 0 列

        现在 preSum[i][j] 的定义式,改一下

pre_sum ⁡ [ i + 1 ] [ j + 1 ] = ∑ x = 0 i ∑ y = 0 j arr ⁡ [ x ] [ y ] \operatorname{pre\_sum}[i+1][j+1]=\sum_{x=0}^i \sum_{y=0}^j \operatorname{arr}[x][y] pre_sum[i+1][j+1]=x=0iy=0jarr[x][y]

        并且预置 pre_sum[0][j]pre_sum[i][0] 为 0

        求:行从 a 到 A,列从 b 到 B 的子阵列的和,的通式,改一下:
∑ i = a A ∑ i = b B arr ⁡ [ i ] [ j ] = pre_sum ⁡ [ A + 1 ] [ B + 1 ] + pre_sum ⁡ [ a ] [ b ] − pre_sum ⁡ [ A + 1 ] [ b ] − pre_sum ⁡ [ a ] [ B + 1 ] \sum_{i=a}^A \sum_{i=b}^B \operatorname{arr}[i][j]=\operatorname{pre\_sum}[A+1][B+1]+\operatorname{pre\_sum}[a][b]-\operatorname{pre\_sum}[A+1][b]-\operatorname{pre\_sum}[a][B+1] i=aAi=bBarr[i][j]=pre_sum[A+1][B+1]+pre_sum[a][b]pre_sum[A+1][b]pre_sum[a][B+1]

6. 示例代码:

class NumMatrix:

    def __init__(self, matrix: List[List[int]]):
        m, n = len(matrix), len(matrix[0])          # 矩阵的行和列
        self.pre_sum = [[0]*(n+1) for _ in range(m+1)]      # 构造一维前缀和矩阵
        for i in range(m):
            for j in range(n):
                self.pre_sum[i+1][j+1] = self.pre_sum[i+1][j] + self.pre_sum[i][j+1] - self.pre_sum[i][j] + matrix[i][j]

    def sumRegion(self, row1: int, col1: int, row2: int, col2: int) -> int:
        return (self.pre_sum[row2+1][col2+1] - self.pre_sum[row1][col2+1] - self.pre_sum[row2+1][col1] + self.pre_sum[row1][col1])

7. 复杂度分析

  • 时间复杂度:初始化 O ( m n ) O(mn) O(mn),每次检索 O ( 1 ) O(1) O(1),其中 m 和 n 分别是矩阵 matrix 的行数和列数。初始化需要遍历矩阵 matrix 计算二维前缀和,时间复杂度是 O ( m n ) O(mn) O(mn)。每次检索的时间复杂度是 O ( 1 ) O(1) O(1)
  • 空间复杂度: O ( m n ) O(mn) O(mn),其中m和n分别是矩阵 matrix 的行数和列数。需要创建一个 m+1 行 n+1 列的二维前缀和数组 pre_sum。

做题心得:以后会不会延伸到张量呢,更高维数的也是总结通式,就比如三维是一个立方体,依然是计算每个小立方体的和。

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

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

相关文章

javaEE 初阶 — 关于 IPv4、IPv6 协议、NAT(网络地址转换)、动态分配 IP 地址 的介绍

文章目录1. IPv42. IPv63. NAT4. 动态分配 IP 地址1. IPv4 在互联网的世界中只有 0 和1 ,所以每个人都有一个由 0 和 1 组成的地址来让别人找到你。 这段由 0 和 1 组成的地址叫 IP 地址,这是互联网的基础资源,可以简单的理解为互联网的土地。…

《Qt 6 C++开发指南》简介

我们编写的新书《Qt 6 C开发指南》在2月份终于正式发行销售了,这本书是对2018年5月出版的《Qt 5.9 C开发指南》的重磅升级。以下是本书前言的部分内容,算是对《Qt 6 C开发指南》的一个简介。1.编写本书的目的《Qt 5.9C开发指南》是我写的第一…

(三)代表性物质点邻域的变形分析

本文主要内容如下:1. 伸长张量与Cauchy-Green 张量2. 线元长度的改变2.1. 初始/当前构型下的长度比2.2. 主长度比与 Lagrange/Euler 主方向2.3. 初始/当前构型下任意方向的长度比3. 线元夹角的改变4. 面元的改变5. 体元的改变1. 伸长张量与Cauchy-Green 张量 由于变…

新C++(10):Map\Set的封装

"湖人总冠军"一、Map\Set的介绍Set是C标准库中的一种关联容器。所谓关联容器就是通过键(key)来读取和修改元素。与map关联容器不同,它只是单纯键的集合。取自这里Map是STL 的一个关联容器,它提供一对一(其中…

第二回:艺术画笔见乾坤

import numpy as np import pandas as pd import re import matplotlib import matplotlib.pyplot as plt from matplotlib.lines import Line2D from matplotlib.patches import Circle, Wedge from matplotlib.collections import PatchCollection一、概述 1. matplotlib…

软件测试:用“bug”来表示“在电脑程序里的错误”

计算机基础知识计算机(personal computer)俗称电脑(pc),是现代一种用于高速计算的电子机器,可以进行数值计算,又可以进行逻辑判断,还具有存储记忆功能,且能够按照程序的运…

【模拟集成电路】频率综合器(Frequency Synthesizer,FS)设计

应用于无线局域网的频率综合器设计前言频率综合器简介各部分链接链接:前言 本文主要内容是对频率综合器或称为PLL 做出简单介绍,为课程设计部分章节内容,后需给出各部分的设计方案,以及测试结果。 频率综合器简介 无线收发系统中…

跳槽进字节跳动了,面试真的很简单

前言: 最近金三银四跳槽季,相信很多小伙伴都在面试找工作, 怎样才能拿到大厂的offer,没有掌握绝对的技术,那么就要不断的学习 如何拿下阿里等大厂的offer的呢,今天分享一个秘密武器,资深测试工程师整理的…

Elasticsearch7.8.0版本进阶——持久化变更

目录一、持久化变更的概述二、事务日志(translog)三、持久化变更完整流程四、事务日志(translog)的作用五、事务日志(translog)的目的一、持久化变更的概述 没有用 fsync 把数据从文件系统缓存刷&#xff…

随机森林算法(Random Forest)R语言实现

随机森林1. 使用Boston数据集进行随机森林模型构建2. 数据集划分3.构建自变量与因变量之间的公式4. 模型训练5. 寻找合适的ntree6. 查看变量重要性并绘图展示7. 偏依赖图:Partial Dependence Plot(PDP图)8. 训练集预测结果1. 使用Boston数据集进行随机森…

【华为OD机试模拟题】用 C++ 实现 - 分糖果(2023.Q1)

最近更新的博客 华为OD机试 - 入栈出栈(C++) | 附带编码思路 【2023】 华为OD机试 - 箱子之形摆放(C++) | 附带编码思路 【2023】 华为OD机试 - 简易内存池 2(C++) | 附带编码思路 【2023】 华为OD机试 - 第 N 个排列(C++) | 附带编码思路 【2023】 华为OD机试 - 考古…

【华为OD机试模拟题】用 C++ 实现 - 时间格式化(2023.Q1)

最近更新的博客 华为OD机试 - 入栈出栈(C++) | 附带编码思路 【2023】 华为OD机试 - 箱子之形摆放(C++) | 附带编码思路 【2023】 华为OD机试 - 简易内存池 2(C++) | 附带编码思路 【2023】 华为OD机试 - 第 N 个排列(C++) | 附带编码思路 【2023】 华为OD机试 - 考古…

匈牙利算法与KM算法的区别

前记 在学习过程中,发现很多博客将匈牙利算法和KM算法混为一谈,当时只管用不管分析区别,所以现在来分析一下两个算法之间的区别。 匈牙利算法在二分图匹配的求解过程中共两个原则: 1.最大匹配数原则 2.先到先得原则 而KM算法求…

Linux centos升级nodejs,解决升级NodeJS遇到的问题,升级GLIBC、GLIBCXX、gcc(含资源包下载)

公司网站用的Nuxt开发的,本地开发环境NodeJS已经升级到16.14.2版本,服务器也要从12版本升级到16.14.2 如需本次安装的资源,请下滑到文章下面下载整套资源 NodeJS版本下载地址:https://nodejs.org/dist/v16.14.2 解压安装node后…

Docker 应用实践-仓库篇

目前 Docker 官方维护了一个公共仓库 Docker Hub,用于查找和与团队共享容器镜像,界上最大的容器镜像存储库,拥有一系列内容源,包括容器社区开发人员、开放源代码项目和独立软件供应商(ISV)在容器中构建和分…

【涨薪技术】0到1学会性能测试 —— 分类及应用领域

上一次推文我们分享了性能测试相关的专业术语,今天我们来看下性能测试的分类及应用领域!后续文章都会系统分享干货,带大家从0到1学会性能测试,另外还有教程等同步资料,文末获取~ 性能测试划分 01、负载测试 负载测试是指服务器最…

【华为OD机试模拟题】用 C++ 实现 - 商人买卖(2023.Q1)

最近更新的博客 华为OD机试 - 入栈出栈(C++) | 附带编码思路 【2023】 华为OD机试 - 箱子之形摆放(C++) | 附带编码思路 【2023】 华为OD机试 - 简易内存池 2(C++) | 附带编码思路 【2023】 华为OD机试 - 第 N 个排列(C++) | 附带编码思路 【2023】 华为OD机试 - 考古…

黑客网站大全!都在这了!速看被删就没了

我们学习网络安全,很多学习路线都有提到多逛论坛,阅读他人的技术分析帖,学习其挖洞思路和技巧。但是往往对于初学者来说,不知道去哪里寻找技术分析帖,也不知道网络安全有哪些相关论坛或网站,所以在这里给大…

java延时队列

二、延时队列使用场景 那么什么时候需要用延时队列呢?常见的延时任务场景 举栗子: 订单在30分钟之内未支付则自动取消。重试机制实现,把调用失败的接口放入一个固定延时的队列,到期后再重试。新创建的店铺,如果在十天内都没有上传过商品&…

XML调用 CAPL Test Function

🍅 我是蚂蚁小兵,专注于车载诊断领域,尤其擅长于对CANoe工具的使用🍅 寻找组织 ,答疑解惑,摸鱼聊天,博客源码,点击加入👉【相亲相爱一家人】🍅 玩转CANoe&…