一命通关前缀和


前缀和

简介

先来简单看一个场景。现在有一个公交车,第一站上了4个人,第二站上了7个人,第三站上了1个人,第四站上了5个人,问一共上了多少人?

答案很显而易见,只需要遍历这个数组,把所有元素加起来就可以了。

但是,如果我们想多次查询,中间的某几站上了多少人,如果还采用全部加起来的解法,这个时间复杂度就直接上到O(nk)去了。我们能不能让每次查询的时间复杂度都为O(1)呢?

答案也很简单。我们先将所有的数据进行预处理,把每一站公交车上还剩多少人都保存起来,这样求两站之间上了多少人,只需要将两站之间车上的人数作差就可以了。而虽然耗费了O(n)的空间,但是将单次查询的时间复杂度降为了O(1),总时间复杂度降为了O(k),典型的空间换时间的方法。

而这种思想,就是我们常说的——前缀和。


一维前缀和

一维前缀和就和刚刚所说的场景一样,我们有一段很长很长的数组,但是我们想求出任意一段数组区间的和,每次都暴力加起来自然可行,但是时间复杂度直接上到了O(n^2)。想要优化,自然想到的就是刚刚的方法:

证明方法就不多作解释了,小学生都会。

同时,我们在预处理这个前缀和数组的时候,也没有必要每一次都采用累加的方法。这里稍微用了一点点动态规划的皮毛,我们来看前缀和数组prefix[i]和prefix[i-1]的关系

所以对一个一维数组,预处理前缀和的方式:

prefix[0]=nums[0]

prefix[i]=prefix[i-1]+nums[i] 

而在预处理完前缀和数组后,查询任意一段区间的时间复杂度就变为了O(1)

一维前缀和的哈希优化

尽管这种方法看着美好,但是遇到另一个问题,又会显得有些棘手:

食德,前缀和虽然可以解决查询任意一个区间的和,但是如果要通过和来反向找区间,那时间复杂度又是暴力遍历的O(n^2),难道没有其他解决方案了吗?

当然有,我们首要想到的方法便是,把所有的和放入哈希表中,然后再去遍历整个前缀和数组,这样每个元素的查询就变成了O(1),总的时间复杂度也就降为了O(n)。

听不懂很正常,接下来详细讲一讲这个过程:

还是原来的数组,我们现在已知前缀和数组,想找到和为18的区间 j 到 i,也就是找到一对 i 和 j,使得前缀和数组prefix[i]-prefix[j]=18

但是,如果按照暴力解法,那就是遍历所有数组的时候,遍历到中间某个位置i,尝试所有的0到i-1位置上的元素,看是否能产生差为18的组合


这个查询复杂度显然是O(n),如果要降为O(1),最好的方法就是哈希表。

哈希表中的键对值<key,val>为<sum,place>,sum为出现过的前缀和,而place为按题目要求 记录该前缀和第一次出现的位置 或者 最后一次出现的位置

先用哈希表将 i 前的所有前缀和存储起来,我们想找到prefix[i]-prefix[j]=18,转换一下,也就是找到 i 前面是否有 j 使得prefix[j]=prefix[i]-18

也就是说,只需要判断元素prefix[i]-18是否在哈希表中,如果存在,则代表i之前有满足条件的子数组;如果不存在,则代表i之前没有满足条件的子数组。
如果找到了,hash[prefix[i]-18],便是需求数组的左端点,而i就是右端点

哈希表的查询时间复杂度是O(1),哈希表本身的大小是O(n),也就是在没有过多影响空间复杂度的情况下,大大提升了效率。

而通过这个方法,我们来解一道经典的题:(最好自己先尝试做一做)

经典例题

LCR 010. 和为 K 的子数组 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/QTMn0o/description/?envType=list&envId=87dIC4Q4 

和我们刚刚说的情况几乎没有什么差别,所以就不多对题目进行解释。

解题思路: 

拿到这个题目,第一想法就是先把前缀和数组撸出来。

int n=nums.size();

vector<int> prefix(n);
prefix[0]=nums[0];

for(int i=1;i<n;i++)
    prefix[i]=prefix[i-1]+nums[i];

食德,你无敌了孩子,求前缀和数组就是公式。

然后再来看题目,要求子数组和为k的个数,甚至比刚刚所讲的求出长度更加简单。
我们在遍历的时候,实际上是要求前缀和为sum的子数组个数,于是哈希表的键对值<key,val>为<sum,time>,sum代表出现过的前缀和,time代表该前缀和出现过的次数。

 同时,我们一边插入,一边搜索,如果有满足条件的就让结果加上哈希表中key对应的值

但是,我们明显漏掉了一种情况:当遍历到2时,就要找哈希表中出现过几个2-2=0。虽然哈希表中出现过0次,但是子数组[1,1]显然也是满足条件的。所以,其实我们漏掉的情况便是,当数组中没有元素时的前缀和。故而我们初始化的时候要加上hash[0]=1

unordered_map<int,int> hash;//键对值为前缀和sum和出现的次数time
hash[0]=1;//初始化0
int ret=0;

for(int i=0;i<prefix.size();i++)
{
     if(hash.find(prefix[i]-k)!=hash.end())//看前缀和数组中是否有
         ret+=hash[prefix[i]-k];//如果有返回值就加上

    hash.insert(prefix[i]);
}

 解题代码:
class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        unordered_map<int,int> hash;
        hash[0]=1;
        int sum=0;//把前缀和数组也优化掉了,但是优不优化都不会太影响空间复杂度
        int ret=0;

        for(int i=0;i<nums.size();i++)
        {
            sum+=nums[i];
            if(hash.find(sum-k)!=hash.end())
                ret+=hash[sum-k];
            hash[sum]++;
        }

        return ret;
    }
};

公式

看到一维前缀和的题,不管那么多,直接先把前缀和数组撸出来。
然后,根据题目要求构建哈希表。

在遍历的时候,一边对之前的元素进行查找,一边在哈希表中插入当前的元素。一定要一边查找一边插入,如果全部插入完再查找,后面才会出现的前缀和也会在前面的计算中加入,最终导致结果增多。

而哈希表的键对值一般来说只有两种:

  1. <sum,place> 存储前缀和与其对应的第一次或最后一次出现的位置
  2. <sum,time>  存储前缀和与其出现的次数

按照题目要求构建哈希表,就可以解决大部分问题。 


二维前缀和 

二维前缀和,就是在一维前缀和的基础上,将数组变为二维数组,然后求二维数组中某个区间的和。

不要被这个样子吓到,其实解决问题的方法还是一样的。

我们知道,一维数组的前缀和,是从0到i中所有元素的和。同理,到了二维,那么就是从(0,0)到(i,j)中,所有元素的和。无论是三维还是四维,分析方式都是一样的,以一个点为基准点,计算出每一个点到他所形成的区间的前缀和。 

既然这样,那么计算前缀和的方式自然不可能累加。我们还是稍微使用一下动态规划的思想,找到每一个位置和其相邻位置之间的关系。 

虽然这个图有点抽象,但是我们不难发现

  • left黑色的区域+up蓝色的区域,中间重叠了above绿色的区域
  • 红色的区域,正好包括了left,up,above三个区域,同时多出来了一小块区域
  • 多出来的区域正好是nums[i][j] 

于是,我们便可以通过数学关系推出来:

但是很明显,当i=0或者j=0时,这个表达式会越界。在一维的时候,我们的解决方案是手动初始化,但是二维手动初始化不免有些麻烦。所以我们采用动态规划最常用的一种方法:多开一行一列

//假设原二维数组为vector<vector<int>> nums
int row=nums.size();//行
int col=nums[0].size();//列

vector<vector<int>> prefix(row+1,vector<int>(col+1));

for(int i=1;i<row+1;i++)
{
    for(int j=1;j<col+1;j++)
    {
        prefix[i][j]=prefix[i-1][j]+prefix[i][j-1]-prefix[i-1][j-1]+nums[i-1][j-1];
        //注意多开了一行一列,所以前缀和数组和原数组的映射发生了变化
    }
}
//预处理完毕

那有人可能就要问了,海老师海老师,预处理完这个数组之后怎么求区间和呢?

和求前缀和一样的思想,还是看区间的关系。最终可以得到表达式:

实在抱歉,因为图太不好画, 所以贴了一个手写版

用这个思想,这个题目便不在话下1314. 矩阵区域和 - 力扣(LeetCode) 


抽象前缀和

如果前缀和只能解决这些求和的问题,那研究前缀和就真没什么意义了。前缀和还在其他场景中,将一个其他的问题转换为前缀和的问题,然后用前缀和的方法公式解决。 

1124. 表现良好的最长时间段 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/longest-well-performing-interval/description/

如果说,这个一看就是动态规划,与前缀和八竿子打不着的题目,要用前缀和的方法来解决,你信吗? 

题目解析

这是一道及其抽象的题目。给一个数组,如果某一个元素大于8,那么这个元素就是good;否则元素就是bad。对一个子数组,其中good的数量大于bad的数量,求满足条件的子数组最长的长度为多少。

 一看子数组,oh,dp问题。但是,我们可以将其巧妙转换为前缀和的问题:

问题转化

我们发现,不管题目描述多花里胡哨,但是最终只想说一句话:数组里的元素只有两个状态。 
这里的状态不是动态规划里所说的状态,而就是我们日常里所说的状态——两种元素具有不同的性质。

我们不妨就按照题目的意思,把两个状态分别定义为1和-1,1代表大于8的元素,-1代表不大于8的元素,然后把整个数组修改一下:

for(auto& e:hours)
   e=e>8?1:-1;

有什么用?我们再来看整个数组:

而题目意思,要求good大于bad的最长子数组,我们修改完数组以后,问题是不是就变成了,和大于0的最长子数组 

此刻,这个问题就转换为了一个前缀和的问题,而通过一维前缀和的哈希优化那里的知识,我们便能完美解决这个问题:

  1. 哈希表:sum,place型,place为sum第一次出现的位置
  2. 重置数组种的所有元素为1和-1
  3. 撸出前缀和数组
  4. 根据题目要求,遍历一次,over 

解题代码 

class Solution {
public:
    int longestWPI(vector<int>& hours) {
        unordered_map<int,int> hash;//sum,place型
        hash[0]=-1;
        for(auto& e:hours)
        {
            if(e>8) e=1;
            else e=-1;
        }

        int sum=0;//优化了前缀和数组,优不优化一样
        int ret=0;//返回值
        for(int i=0;i<hours.size();i++)
        {
            sum+=hours[i];
            if(sum>0)//如果sum大于0,表示从0到现在所有元素都满足条件,直接就为到i为止的所有元素
                ret=max(ret,i+1);
            else if(hash.count(sum-1))//如果sum小于0,找到极限情况sum-1的位置
                ret=max(ret,i-hash[sum-1]);
                
            if(hash.find(sum)==hash.end())
                hash[sum]=i;
            //如果哈希表中已经有了该元素,因为要求第一个出现的元素,所以不能修改值
            //只有在哈希表中没有该元素的时候,再去插入这个值
        }

        return ret;
    }
};

总结和公式

很多看似不是前缀和的问题,但是如果数组中只有两个状态,要求某个满足某条件区间的长度,完全可以将两个状态定义为1和-1,然后再去用前缀和的思想来解决。在一些个别情况,有三个状态,也可以转换成1,0,-1,再来用前缀和解决。

  1. 观察是否只有两个状态的元素数组,是否求某个区间的长度或个数
  2. 将两个状态分别转化成1和-1
  3. 用一维前缀和的哈希优化那里的方法来解决问题

懂了?再把这题解决到掉,LCR 011. 连续数组 - 力扣(LeetCode)


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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

DevEco-Studio 3.1.1 Release和DevEco Studio NEXT Developer Preview1同时安装在mac m2上

其实mas上支持同一个app的多个版本安装的&#xff0c;需要注意的是SDK的目录需要区分开&#xff0c;不能被覆盖。 HarmonyOS和OpenHarmony SDK配置 3.1.1 Release&#xff0c;HarmonyOS的SDK目录&#xff1a;/Users/zhongyili/Library/Huawei/Sdk_3.1 NEXT Developer Previ…

【2】SLoRa: A Systematic Framework for Synergic Interference Resilience In LPWAN

这一篇基于上一篇文章,仅记录我遗忘的知识点&#xff1b; 以较低的成本提供可靠的符号恢复&#xff1b; 符号恢复【振幅和频率】 为了避免环境噪声并提高可靠性&#xff0c;我们采用信号预处理和窗口滑动算法来检测幅度变化点&#xff1b; 信号处理&#xff1a;归一化和指数化…

如何查看前端的vue项目是vue2还是vue3项目

1. 检查package.json文件 在项目的根目录下&#xff0c;打开package.json文件&#xff0c;查找dependencies或devDependencies部分中的vue条目。版本号将告诉你是Vue 2还是Vue 3。例如&#xff1a; Vue 2.x: "vue": "^2.x.x"Vue 3.x: "vue": &…

Rust入门:C++和Rust动态库(dll)的相互调用

无论是C调用Rust动态库还是Rust调用C动态库&#xff0c;其操作基本都是一样地简单&#xff0c;基本和C调用C的动态库没什么区别&#xff0c;只需要列出所需要导入的函数&#xff0c;并链接到相应的lib文件即可。 这里&#xff0c;在windows中&#xff0c;我们以dll动态库为例说…

虽说主业搞前端,看到如此漂亮的网页UI,也是挪不开眼呀。

漂亮的网页UI能够吸引人的眼球&#xff0c;给人留下深刻的印象。作为前端开发人员&#xff0c;可以通过不断学习和掌握设计技巧和工具&#xff0c;提升自己的UI设计能力&#xff0c;为用户提供更好的视觉体验。 以下是一些提升网页UI设计能力的建议&#xff1a; 学习设计基础知…

外包干了一周,技术明显倒退。。。。。

先说一下自己的情况&#xff0c;本科生&#xff0c;2019年我通过校招踏入了南京一家软件公司&#xff0c;开始了我的职业生涯。那时的我&#xff0c;满怀热血和憧憬&#xff0c;期待着在这个行业中闯出一片天地。然而&#xff0c;随着时间的推移&#xff0c;我发现自己逐渐陷入…

【鸿蒙 HarmonyOS 4.0】解决:搜索无效问题

一、背景 页面包含搜索框和列表&#xff0c;列表默认展示所有数据并具有分页功能。然而&#xff0c;在输入关键字到搜索框时&#xff0c;列表未正确展示搜索结果。 二、功能实现 2.1、原代码及实现效果 import ChargeType from ../../viewModel/ChargeType import ChargeMo…

devc++8x8取模软件

这几天在搞arduino nano和单个max7219模块&#xff0c;涉及到16进制的取模&#xff0c;在网上转了一圈&#xff0c;没找到合适的取模软件&#xff0c;于是自己做了一个&#xff0c;试过&#xff0c;可以用&#xff0c;按esc退出并生成16进制的取模结果 源代码&#xff1a; #i…

liunx操作系统 环境变量

环境变量 main函数参数 命令行参数环境变量 环境变量的查看环境变量的获取 main函数参数 命令行参数 main函数是有参数的&#xff0c;只是我们一般不适用 这是main函数从bash中读取进程数据使用的一个基本入口。 下面进行简单演示。 o 好oo都是我们输入的命令行参数。其实&a…

20240304-使用VS2022编译blender3.6.2源代码

20240304-使用VS2022编译blender3.6.2源代码 一、软件环境 Win10 x64 22h2 JuneVS2022 v17.9.0CMake v3.24.4SVN v1.14.3GIT v2.29.2标签&#xff1a;win10 22h2 vs2022 blender 63335分栏&#xff1a;C 二、硬件环境 Win10 x64的PC台式机 三、获取源码 方法一 网盘下载…

安装sqlserver2022最新版只能使用.\SQLEXPRESS登录数据库怎么修改成.

.\SQLEXPRESS “服务器名称 localhost\SQLEXPRESS”中的 “SQLEXPRESS”就是数据库的实例名称/数据库名/服务器名&#xff0c; “localhost”即登录本计算机安装的数据库 安装sqlserver2022最新版只能使用.\SQLEXPRESS登录数据库怎么修改成. 2、查看SQL Server数据库的实例名…

Redis实战—验证码登录功能实现

本博客为个人学习笔记&#xff0c;学习网站&#xff1a;黑马程序员Redis入门到实战 实战篇之验证码登录 目录 基于Session实现登录流程 发送短信验证码 短信验证码登录、注册 校验登录状态 session共享问题 Redis代替session的业务流程 设计Key的结构 设置Key的细节 整…

FreeRTOS操作系统学习——任务管理

任务概念 在FreeRTOS中&#xff0c;一个任务相当于一个线程&#xff0c;可以有很多的任务&#xff0c;每个人任务可以设置不同的优先级。相同优先级的任务轮流使用CPU&#xff0c;高优先级的任务可以一直使用CPU&#xff0c;直到主动放弃&#xff0c;低级的任务才有被执行的机…

Stable Diffusion 模型分享:DucHaiten-AIart-SDXL(动漫、3D、逼真)

本文收录于《AI绘画从入门到精通》专栏&#xff0c;专栏总目录&#xff1a;点这里。 文章目录 模型介绍生成案例案例一案例二案例三案例四案例五案例六案例七案例八 下载地址 模型介绍 这是一个丰富多彩的 SDXL 模型&#xff0c;可以绘制动漫、3D、科幻、真实等类型的图片。 …

响应式编程五股票订阅系统实现

响应式编程五 使用StepVerifier测试响应式流StepVerifier要点 使用StepVerifier进行高级测试股票订阅系统数据库表 使用StepVerifier测试响应式流 出于测试目的&#xff0c;Reactor 提供了额外的 reactor-test 模块&#xff0c;该模块提供了 StepVerifier。StepVerifier 提供了…

Go的安装

一. 下载地址 Go官方下载地址&#xff1a;https://golang.org/dl/ Go中文网&#xff1a;https://go.p2hp.com/go.dev/dl/ 根据不同系统下载不同的包。 二. 配置GOPATH GOPATH是一个环境变量&#xff0c;用来表明你写的go项目的存放路径。 GOPATH路径最好只设置一个&#xff0…

【JavaEE初阶】 JVM 运行时数据区简介

文章目录 &#x1f343;前言&#x1f332;堆&#xff08;线程共享&#xff09;&#x1f384;Java虚拟机栈&#xff08;线程私有&#xff09;&#x1f38b;本地方法栈&#xff08;线程私有&#xff09;&#x1f333;程序计数器&#xff08;线程私有&#xff09;&#x1f334;方法…

CentOS上安装MySQL 5.7和MySQL 8.0教程

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

Hive的性能优化

1.调优概述 Hive 作为大数据领域常用的数据仓库组件&#xff0c;在设计和查询时要特别注意效率。影响 Hive 效率的几乎从不是数据量过大&#xff0c;而是数据倾斜、数据冗余、Job或I/O过多、MapReduce分配不合理等等。对 Hive 的调优既包含 Hive 的建表设计方面&#xff0c;对H…

如何学习I2C协议

文章目录 学习I2C协议0 懒人直达1 了解协议开发者2 从恩智浦半导体公司下载官方技术文档3 翻译成中文4 资源下载 学习I2C协议 0 懒人直达 点击直达 1 了解协议开发者 I2C&#xff08;Inter-Integrated Circuit&#xff09;协议是由荷兰皇家飞利浦电子公司&#xff08;现恩智…