利用前缀信息解决子数组问题(上)

news2024/9/17 8:21:44

本篇文章用七个问题来详细探讨一下前缀信息来解决子数组问题的技巧

P r o b l e m 1 Problem1 Problem1 构建前缀和数组,快速解决子数组范围求和的问题

这个问题可以看专栏【前缀树】里的关于前缀和的文章,讲的很详细,定义,应用都涉及地很深。

P r o b l e m 2 Problem2 Problem2 构建前缀和最早出现的位置,返回无序数组中累加和为定值的最长子数组长度
描述:

给定一个无序数组nums,其中元素可正、可负、可0,给定一个整数 K K K,求nums所有子数组中累加和为 K K K的最长子数组长度。

输入描述:

第一行两个整数 N , K N,K N,K, N N N表示数组长度, K K K表示给定整数,第二行 N N N个数表示数组内的数

示例:

输入: 5 0
	  1 -2 1 1 1
输出: 3
问题分析:

假设定值为 K K K,也就是说子数组 n u m s [ i , . . . , j ] nums[i,...,j] nums[i,...,j]的累加和为 K K K,最先想到的就是暴力枚举,用两个for循环枚举出所有子数组,找到累加和为K的子数组,并更新最长子数组长度。

暴力枚举的时间复杂度是 O ( n 2 ) O(n^2) O(n2),我们得另寻他法。

根据前缀和的定义,我们可以清楚地知道:
n u m s [ i ] + . . . + n u m s [ j ] = p r e x [ j ] − p r e x [ i − 1 ] nums[i]+...+nums[j] = prex[j] - prex[i-1] nums[i]+...+nums[j]=prex[j]prex[i1]
如果 n u m s [ i ] + . . . + n u m s [ j ] = K nums[i] + ... + nums[j] = K nums[i]+...+nums[j]=K,说明 p r e x [ j ] − p r e x [ i − 1 ] = K prex[j] - prex[i-1] = K prex[j]prex[i1]=K,假设 p r e x [ j ] prex[j] prex[j]已知,那我们只需要找 j j j位置之前存不存在 p r e x [ i − 1 ] = p r e x [ j ] − K prex[i-1] = prex[j] - K prex[i1]=prex[j]K,如果存在说明 n u m s [ i , . . . , j ] nums[i,...,j] nums[i,...,j]的和为 K K K

刚刚讨论是存不存在的问题,如果存在,可能存在多个 i − 1 i-1 i1,题目要求的是最长子数组长度,所以我们应该取最小的那个 i − 1 i-1 i1,其实也就是前缀和 p r e x [ i − 1 ] prex[i-1] prex[i1]最早出现的位置。

我们给出上述思考所对应的代码:

int solutionProblem2(int nums[],int N,int K) {
    int prex[N + 1];
    prex[0] = 0;

    int result = 0; //存储结果
    for (int j = 0;j < N;j++) {
        prex[j + 1] = prex[j] + nums[j];
        int aim = prex[j + 1] - K;
        //开始寻找符合条件的prex[i-1] i = 0,1,2,..,j
        for(int t = 0; t <= j ;t++){
            if(prex[t] == aim){
                result = max(result, j-t);
                break;
            }
        }
    }
	return result;
}

重点提一下, n u m s [ i ] + . . . + n u m s [ j ] = p r e x [ j ] − p r e x [ i − 1 ] nums[i]+...+nums[j] = prex[j] - prex[i-1] nums[i]+...+nums[j]=prex[j]prex[i1] 公式中的 i i i是编号,从1开始,而 n u m s nums nums数组的下标从0开始,所以 p r e x [ 0 ] = 0 prex[0] = 0 prex[0]=0,无数字相加,前缀和自然为0。

再解释一下prex[j+1] = prex[j] + nums[j]这段代码,编号1 - j+1所有数字和为 p r e x [ j + 1 ] prex[j+1] prex[j+1],而编号1 - j 所有数字和为 p r e x [ j ] prex[j] prex[j],编号为j+1的数字为 n u m s [ j ] nums[j] nums[j]

优化分析:

同学们写完代码之后最好再计算一下代码的时间复杂度,我们发现,利用了前缀和的代码的时间复杂度也是 O ( n 2 ) O(n^2) O(n2),并没有起到太大的优化作用,哪个地方还能进行优化呢?

回顾代码我们发现,我们在寻找符合条件的 p r e x [ i − 1 ] prex[i-1] prex[i1]时竟然还在用循环来寻找, p r e x [ i − 1 ] prex[i-1] prex[i1]有很多个【比如 p r e x [ i − 1 ] = M , M prex[i-1] = M, M prex[i1]=M,M 会在前缀和数组中出现多次】,我们只要出现最早的那一个,也就是说只要用到 p r e x [ i − 1 ] prex[i-1] prex[i1],就要出现最早的那一个。所以我们可以利用哈希表来存储出现最早的 p r e x [ i − 1 ] prex[i-1] prex[i1],哈希表的形式为:{ p r e x [ i − 1 ] prex[i-1] prex[i1]的值:此值出现的最早位置}

最终优化后的代码:
int solutionProblem2(int nums[],int N,int K) {
    int prex[N + 1];
    prex[0] = 0;
	unordered_map<int,int> map;
    map[0] = 0;	//编号0不存在数字,其值自然为0
    
    int result = 0; //存储结果
    for (int j = 0;j < N;j++) {
        prex[j + 1] = prex[j] + nums[j];
        if(mp.count(prex[j+1]) == 0){
            mp[prex[j+1]] = j+1; //值为prex[j+1]的前缀和出现在编号为j+1的位置
        }
        int aim = prex[j + 1] - K;
        //开始寻找符合条件的prex[i-1] i = 0,1,2,..,j
        if(mp.count(aim)){
            result = max(result, j+1 - mp[aim]);
        }
    }
	return result;
}
P r o b l e m 3 Problem3 Problem3 构建前缀和出现的次数,返回无序数组中累加和为定值的子数组数量

Leetcode 560

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数

子数组是数组中元素的连续非空序列。

示例 1:

输入:nums = [1,1,1], k = 2
输出:2

示例 2:

输入:nums = [1,2,3], k = 3
输出:2

提示:

  • 1 <= nums.length <= 2 * 104
  • -1000 <= nums[i] <= 1000
  • -107 <= k <= 107
问题分析:

其实这道题目我在【前缀树】专栏里的一篇前缀和文章中详细讲过,现在我大致讲一下思路。

累加和为定值,那我们就要用到:
p r e x [ i − 1 ] = p r e x [ j ] − K prex[i-1] = prex[j] - K prex[i1]=prex[j]K
前缀和数组构建完毕后,只需要遍历 j j j即可,同时利用哈希表**{ p r e x [ i − 1 ] prex[i-1] prex[i1] : 出现次数 }**,在遍历每个 j j j时对最终结果进行更新。解决代码和上一题很相似,同学们看一遍就懂了。

解决代码:
int solutionProblem3(int nums[],int N,int K) {
	int prex[N + 1];
	prex[0] = 0;
	unordered_map<int,int> map;  // prex[i-1] : 出现次数
	map[0] = 1; //无数字相加为0,算出现一次
    
	int result = 0; //存储结果
    for (int j = 0;j < N;j++) {
        //构建prex[]与map{:}
        prex[j + 1] = prex[j] + nums[j];
        map[prex[j + 1]]++;

        int aim = prex[j + 1] - K;
        if (map.count(aim) != 0) {
            result += map[aim];
        }
    }
    return result;
}
P r o b l e m 4 Problem4 Problem4 构建前缀和最早出现的位置,返回无序数组中正数和负数个数相等的最长子数组长度

给定一个无序数组nums,其中元素可正、可负、可0,给定一个整数 K K K,求arr所有子数组中正数和负数个数相等的最长子数组长度。

要求】时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)

示例:

输入:5
	 1 -2 1 1 1
输出:2
问题分析:

这个问题乍一看无从下手,因为正数和负数个数相等并不等价于累加和为某个值,前缀和自然也就用不了了。所以我们要适当地对原数组做一些变换,既然正数和负数的个数相等,记正数的个数为 a a a,那负数的个数也为 a a a,我们发现 a + − a = 0 a+ -a = 0 a+a=0

也就是说,我们把所有正数变为1,所有负数变为-1,0照样还是0,原数组变换之后,题目就可以简化成下面这种描述:

​ 返回无序数组中累加和为0的最长子数组长度。

有同学会问,累加和为0可以吗?如果数组里的元素全是0 呢?

其实也没问题,因为0既不是正数也不是负数,正数、负数的个数都为0,也符合正数和负数个数相等这个条件。

解决代码:
int solutionProblem4(int nums[],int N) {
    int newNums[N];
    for (int i = 0;i < N;i++) {
        if (nums[i] > 0) newNums[i] = 1;
        else if (nums[i] < 0)    newNums[i] = -1;
        else newNums[i] = 0;
    }
    
    int prex[N + 1];
	prex[0] = 0;
	unordered_map<int,int> map;  // prex[i-1] : 最早出现位置
    map[0] = 0;
    
	int result = 0; //存储结果
    for (int j = 0;j < N;j++) {
        //构建prex[]与map{:}
        prex[j + 1] = prex[j] + newNums[j];
        if (map.count(prex[j + 1]) == 0) {
            map[prex[j + 1]] = j + 1;
        }

        int aim = prex[j + 1] - 0;
        if (map.count(aim) != 0) {
            result = max(result, j + 1 - map[aim]);
        }
    }
    return result;
}
P r o b l e m 5 Problem5 Problem5 构建前缀和最早出现的位置,表现良好的最长时间段问题

LeetCode1124 表现良好时间段

给你一份工作表 h o u r s hours hours,上面记录某一位员工的每天的工作小时数。我们认为当员工一天中的工作小时数大于8小时的时候,那么这一天就是劳累的一天。所谓表现良好的时间段,意味着在这段时间内,劳累的天数是严格大于不劳累的天数,请你返回表现良好时间段的最大长度。

示例1:

输入: hours = [9,9,6,0,6,6]
输出: 3
问题分析:

我们对原数组做以下变换:

  • 大于8的数字转换为1
  • 小于或等于8的数字转换为-1

做变换之后,如果一个子数组 n u m s [ i , . . . , j ] nums[i,...,j] nums[i,...,j]是表现良好时间段,那么
n u m s [ i ] + n u m s [ i + 1 ] + . . . + n u m s [ j ] > 0 nums[i] + nums[i+1]+...+nums[j] > 0 nums[i]+nums[i+1]+...+nums[j]>0
转换为前缀和形式:
p r e x [ j ] − p r e x [ i − 1 ] > = 1 p r e x [ i − 1 ] < = p r e x [ j ] − 1 prex[j] - prex[i-1] >= 1 \\ prex[i-1] <= prex[j] - 1 prex[j]prex[i1]>=1prex[i1]<=prex[j]1
【注意】:这里的i,j都是编号,从1开始。

如果有 p r e x [ j ] > 0 prex[j] > 0 prex[j]>0,那么说明 n u m s [ 1 ] + . . . + n u m s [ j ] nums[1]+...+nums[j] nums[1]+...+nums[j]是表现良好时间段,长度为 j j j

如果 p r e x [ j ] < = 0 prex[j] <= 0 prex[j]<=0,那我们得借助前缀和来分析:

这时候有一个问题,我们最终只能得到 p r e x [ i − 1 ] < p r e x [ j ] prex[i-1] < prex[j] prex[i1]<prex[j],在对 j j j遍历时,我们可以知道 p r e x [ j ] prex[j] prex[j],但 p r e x [ i − 1 ] prex[i-1] prex[i1]怎么拿到呢?难道要对哈希表**{ p r e x [ i − 1 ] prex[i-1] prex[i1]:出现的最早位置}**里面的键“ p r e x [ i − 1 ] prex[i-1] prex[i1]”进行遍历吗?遍历的时间复杂度是 O ( n ) O(n) O(n),显然不能对哈希表进行遍历。

只要满足 p r e x [ i − 1 ] < = p r e x [ j ] − 1 prex[i-1] <= prex[j]-1 prex[i1]<=prex[j]1,那 n u m s [ i , . . . , j ] nums[i,...,j] nums[i,...,j]就是表现良好时间段,并且,我可以笃定地告诉你,当 p r e x [ i − 1 ] = p r e x [ j ] − 1 prex[i-1] = prex[j]-1 prex[i1]=prex[j]1时, n u m s [ i , . . . , j ] nums[i,...,j] nums[i,...,j]是以 j j j结尾最长的表现良好时间段。

举个例子,比如 n u m s [ 1 ] + n u m s [ 2 ] + . . . + n u m s [ j ] = − 3 nums[1]+nums[2]+...+nums[j] = -3 nums[1]+nums[2]+...+nums[j]=3,也就是 p r e x [ j ] = − 3 prex[j] = -3 prex[j]=3,那我们就要寻找前缀和数组中有没有 p r e x [ i − 1 ] < = − 4 prex[i-1] <= -4 prex[i1]<=4

我们假设有 p r e x [ m 1 ] = − 5 prex[m_1] = -5 prex[m1]=5,并且此时的 n u m s [ m 1 , . . . , j ] nums[m_1,...,j] nums[m1,...,j]是最长的表现良好的时间段。那么请问, p r e x [ m 1 ] prex[m_1] prex[m1]的值是怎么来的呢?是不是有许多个 1 , − 1 , 1 , − 1 1,-1,1,-1 1,1,1,1加来的?也就是说前缀和数组中的值是 + 1 , − 1 , − 1 , + 1 +1,-1,-1,+1 +1,1,1,+1这么变化的,每次都是做差值为 1 1 1或者 − 1 -1 1的运算。所以 − 5 -5 5的变化过程一定是这样的:
p r e x [ 0 ] = 0   − > . . . − >    − 1    − > . . . − >    − 2    − > . . . − >    − 3    − > . . . − >    − 4    − > . . . − >    − 5 prex[0] = 0 ~-> ...->~~-1~~ -> ...->~~-2~~->...->~~-3~~->...->~~-4~~->...->~~-5 prex[0]=0 >...>  1  >...>  2  >...>  3  >...>  4  >...>  5
所以 − 4 -4 4一定会比 − 5 -5 5出现的早,更比 − 6 , − 7 , . . . -6,-7,... 6,7,...出现的早。

所以我们直接找 p r e x [ i − 1 ] = p r e x [ j ] − 1 prex[i-1] = prex[j]-1 prex[i1]=prex[j]1就好了。

解决代码:
int solutionProblem5(int nums[], int N) {
    //对原数组进行处理
    int newNums[N];
    for (int i = 0; i < N;i++) {
        if (nums[i] > 8) newNums[i] = 1;
        else    newNums[i] = -1;
    }

    int prex[N + 1];
    prex[0] = 0;
    unordered_map<int, int> map;
    map[0] = 0;

    int result = 0; //存储结果
    for (int j = 0;j < N;j++) {
        //构建prex[]与map{:}
        prex[j + 1] = prex[j] + newNums[j];
        if (prex[j + 1] > 0)   result = max(result, j + 1);
        if (map.count(prex[j + 1]) == 0) {
            map[prex[j + 1]] = j + 1;
        }

        int aim = prex[j + 1] - 1;
        if (map.count(aim) != 0) {
            result = max(result, j + 1 - map[aim]);
        }
    }
    return result;
}

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

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

相关文章

VMware安装Ubuntu虚拟机

Ubuntu镜像下载 https://ubuntu.com/download/desktop 创建虚拟机 1.典型配置 2.稍后安装操作系统 3.选择操作系统&#xff0c;Linux&#xff0c;ubuntu64位 3.设置虚拟机名称和安装位置 4.磁盘大小&#xff0c;存储为单个文件 安装系统 1.选择镜像 2.开启虚拟机 2.安装Ub…

CTFHub技能树-Git泄漏-Log

目录 一、前提知识 1.git泄漏原理 ​编辑 2.git文件泄漏造成后果 3.利用方法 (1) GitHack是一个.git泄露利用脚本&#xff0c;通过泄露的.git文件夹下的文件&#xff0c;还原重建工程源代码。渗透测试人员、攻击者&#xff0c;可以进一步审计代码&#xff0c;挖掘&#x…

【重学 MySQL】十七、比较运算符的使用

【重学 MySQL】十七、比较运算符的使用 **等于&#xff08;&#xff09;**基本用法示例注意事项结论 **安全等于运算符&#xff08;<>&#xff09;****不等于&#xff08;<> 或 !&#xff09;**示例注意事项 **大于&#xff08;>&#xff09;、大于等于&#xf…

傅里叶级数,傅里叶变换

先读文章&#xff1a;傅里叶分析之掐死教程&#xff08;完整版&#xff09;更新于2014.06.06 - 知乎 (zhihu.com) 傅里叶级数 一、内容&#xff1a;每个周期性函数都可以表示为无穷多个不同频率的正弦函数的叠加。 二、公式&#xff1a; 三、从时域到频域所保留的三点信息&…

修改Anaconda中JupterLab的默认文件存储路径

安装好Anaconda后&#xff0c;使用JupterLab创建的文件会默认存储在C盘&#xff0c;每次打开业都是打开C盘&#xff0c;可以按照如下步骤修改默认存储路径 1.点击开始然后打开anconda prompt。 2.打开命令行输入 jupyter notebook --generate-config &#xff0c;点击回车&…

MySQL——表操作

目录 一、创建表 二、查看表 2.1 查看表中某成员的数据 2.2 查看整个表中的表成员 2.3 查看创建表时的句柄 三、修改表 alter 3.1 重命名 rename 3.2 新增一列 add 3.3 更改列属性 modify 3.4 更改列名称 change 3.5 删除某列 上一篇博客介绍了库的操作&#xff0c;…

零基础快速上手HarmonyOS ArkTS开发5---从简单的页面开始2---使用List组件构建列表、Grid组件构建网格布局

接着零基础快速上手HarmonyOS ArkTS开发4---从简单的页面开始继续往下学习页面布局的知识。最近发现之前学习这一章节的内容在官方已经被下了&#xff0c;替换成了另外一个案例了&#xff08;华为开发者学堂&#xff09;&#xff1a; 而且整个视频的风格也不一样了&#xff0c;…

DBeaver 24.0 高阶用法

DBeaver 24.0 高阶用法 文章目录 DBeaver 24.0 高阶用法DBeaver 介绍功能一、元数据搜索功能二、仪表盘显示功能三、ER图功能四、导出数据最后 DBeaver 介绍 DBeaver 确实是一款功能强大的通用数据库管理工具&#xff0c;适合所有需要以专业方式处理数据的用户。它不仅提供了直…

【笔记篇】Davinci Configurator BswM模块

目录 1 简介1.1 架构概览2 功能描述2.1 特性2.2 初始化2.3 状态机2.3.1 BSWM_INIT2.3.2 BSWM_WAIT_IMMEDIATE_REQUEST2.3.3 BSWM_MAIN_FUNCTION2.3.4 BSWM_MODE_ARBITRATION_AND_CONTROL2.3.5 BSWM_EMPTY_QUEUE2.3.6 BSWM_DEINIT2.4 模式管理2.4.1 即时模式处理2.4.2 强制即时模…

计算机毕业设计选题推荐-剧本杀创作与预约管理系统-Java/Python项目实战(亮点:数据可视化分析)

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

maven配置文件常用模板

注释很详细&#xff0c;直接上代码 项目结构 内容 父项目 pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi…

系统架构师-ERP+集成

ERP 集成平台end&#xff1a;就懒得画新的页

快手的视频素材去哪找?快手视频素材在哪里找啊

要在快手这类短视频平台上脱颖而出&#xff0c;创意与剪辑技巧固然重要&#xff0c;但拥有高质量的视频素材同样关键。今天&#xff0c;我将推荐一些优秀的素材网站&#xff0c;它们能为你的快手视频增色不少。从动人的自然风景到日常的城市生活&#xff0c;这些网站提供的素材…

经济管理专业数据库介绍

本文介绍了四个经济管理专业数据库&#xff1a;国研网全文数据库、EPS数据平台、中经网、Emerald全文期刊库&#xff08;管理学&#xff09;。 一、国研网全文数据库 国研网是国务院发展研究中心主管、北京国研网信息有限公司承办的大型经济类专业网站。国研网教育版”是国研…

通信工程学习:什么是PCM脉冲编码调制、DPCM差分脉冲编码调制、ADPCM自适应差分脉冲编码调制

PCM脉冲编码调制、DPCM差分脉冲编码调制、ADPCM自适应差分脉冲编码调制 PCM、DPCM、ADPCM是音频编码技术中的三种重要方式&#xff0c;它们在音频信号的数字化、压缩和传输中起着关键作用。以下是对这三种技术的详细解释&#xff1a; 一、PCM&#xff08;Pulse Code Modulatio…

2024年全新deepfacelive如何对应使用直播伴侣-腾讯会议等第三方软件

# 2024年全新deepfacelive如何对应使用直播伴侣-腾讯会议等第三方软件 前提按照之前的步骤打开deepfacelive正确配置并且在窗口已经输出了换脸后的视频&#xff0c;不懂步骤可以移步 https://doc.youyacao.com/88/2225 ## 首先下载obs并配置 https://obsproject.com/ 通过…

C语言 | Leetcode C语言题解之第394题字符串解码

题目&#xff1a; 题解&#xff1a; #define N 2000typedef struct {int data[30];;int top; } Stack;void push(Stack *s, int e) { s->data[(s->top)] e; }int pop(Stack *s) { return s->data[--(s->top)]; }//多位数字串转换成int int strToInt(char *s) {cha…

C++【适配器】【仿函数】【deque结构了解】【反向迭代器】学习

目录 priority_queue 适配器 适配器初认识 模板参数的缺省值 仿函数 priority_queue 向上调整算法&&向下调整算法 为什么需要使用仿函数 函数指针方式 仿函数方式 仿函数较于函数指针的优点 函数指针的调用实现 仿函数的调用实现 使用仿函数 deq…

Java 入门指南:Java 并发编程 —— 线程隔离技术 ThreadLocal

线程隔离技术 线程隔离是一种多线程编程技术&#xff0c;它可以将数据或资源在不同线程之间进行隔离&#xff0c;保证每个线程使用的数据或资源是独立的&#xff0c;不会互相干扰。线程隔离通常应用于高并发场景下&#xff0c;可以有效提升系统性能并提高并发能力。 实现方式…

MySQL record 03 part

插入表数据&#xff1a; 一般情况下&#xff0c;向表中添加新的记录&#xff0c;应该包含此表的所有字段&#xff0c;也就是应该给表的所有字段添加值&#xff0c; 1.使用insert into语句&#xff0c;指定字段名&#xff08;可以是所有的字段&#xff0c;也可以是某几个字段&am…