动态规划课堂4-----子数组系列

目录

引入:

例题1:最大子数组和

例题2:环形子数组的最大和

例题3:乘积最大子数组

例题4:乘积为正数的最长子数组

总结:

结语:


引入:

在动态规划(DP)子数组系列中,我们还是用前面几节所用的解题思路1. 状态表示,2.状态转移方程,3.初始化,4.填表顺序,5.返回值。在写代码时一定要把这5步考虑清楚再写代码。写代码时其步骤也比较固定分别为:1.创建 dp 表 2.初始化 3.填表 4.返回值。写代码时可以按照这4步骤写不会乱也不会把哪一部分漏掉😎。

在子数组系列问题最常用到的状态表示是:以i位置元素为结尾的所有子数组的........(题目要求).

这个非常重要,后面的题基本都是用这个模板的状态表示。

例如下图:可以将其分为两种情况长度为1和长度大于1两种情况,剩下的分析要见具体题目。

例题1:最大子数组和

链接:最大子数组和

题目简介:

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组:

是数组中的一个连续部分。

解法(动态规划):

 1. 状态表示:

dp[i] 表示:以i 位置元素为结尾的「所有⼦数组」中和的最⼤和。设个状态表示会贯穿我们这一整个系列。

 2.状态转移方程

dp[i] 的所有可能可以分为以下两种:

(1)子数组的⻓度为1 :此时dp[i] = nums[i] ;(就只有它本身)

(2)子数组的⻓度⼤于1 :此时dp[i] 应该等于以i - 1 做结尾的「所有⼦数组」中和的最⼤值再加上nums[i] ,也就是dp[i - 1] + nums[i] 。

由于我们要的是「最⼤值」,因此应该是两种情况下的最⼤值,因此可得转移⽅程: dp[i] = max(nums[i], dp[i - 1] + nums[i]) 。

 3.初始化:

辅助结点法:

注意点:(1)辅助结点⾥⾯的值要保证后续填表是正确的.(2)下标的映射关系.

在本题中,最前⾯加上⼀个格⼦,并且让dp[0] = 0 即可。之所以设为0是因为根据1.状态表示,0位置没有元素(因为dp表加了一个辅助节点故dp表的0下标对应数组的-1下标),而我们的dp里面的值是最大和,没有数组故其和为0.

 4.填表顺序:

从左往右

 5.返回值:

状态表示为以i 为结尾的所有⼦数组的最⼤值,但是最⼤⼦数组和的结尾我们是不确定的(可能前面很大最后一个数非常小导致其不是最大值)。 因此我们需要返回整个dp 表中的最大值。

代码实现如下:

class Solution {
    public int maxSubArray(int[] nums) {
        //1.创建 dp 表
        //2.初始化
        //3.填表
        //4.返回值
        int n = nums.length;
        int[] dp = new int[n + 1];
        for(int i = 1;i <= n;i++){
            dp[i] = Math.max(nums[i - 1],dp[i - 1] + nums[i - 1]);
        }
        int max = dp[1];
        for(int i = 1;i <= n;i++){//加上辅助节点后要注意下标的映射关系,还有循环的边界。
            max = Math.max(max,dp[i]);
        }
        return max;

    }
}

时间复杂度:O(n)

空间复杂度:O(n)

例题2:环形子数组的最大和

链接:环形子数组的最大和

题目简介:

给定一个长度为 n 的环形整数数组 nums ,返回 nums 的非空 子数组 的最大可能和 

环形数组 意味着数组的末端将会与开头相连呈环状。形式上, nums[i] 的下一个元素是 nums[(i + 1) % n] , nums[i] 的前一个元素是 nums[(i - 1 + n) % n] 。

子数组 最多只能包含固定缓冲区 nums 中的每个元素一次。形式上,对于子数组 nums[i], nums[i + 1], ..., nums[j] ,不存在 i <= k1, k2 <= j 其中 k1 % n == k2 % n 。

 解法(动态规划):

我们可以将最大和子数组大致划分为下面两个部分,一个是正常在中间的,另一个在头尾两边的。

这一题是例题1的升级版,如果有友友分析过的话会发现直接进行规划非常困难,那么我们能不能采用另一种思维。既然整个数组的和sum是不变的那么要求头尾两边的子数组我们能不能求正常中间数组的最小值将sum减去这个最小值不就是我们头尾两边的子数组的最大值。

 1. 状态表示:

f[i] 表⽰:以i 做结尾的「所有⼦数组」中和的最大值。

g[i] 表⽰:以i 做结尾的「所有⼦数组」中和的最小值。

 2.状态转移方程: 

由于f[i]在上一题已经分析过故这里只分析g[i].

(1)子数组的⻓度为1 :此时g[i] = nums[i] ;

(2)子数组的⻓度⼤于1 :此时g[i] 应该等于以i - 1 做结尾的所有⼦数组中和的最⼩值再加上nums[i] ,也就是g[i - 1] + nums[i] 。

由于我们要的是最小子数组和,因此应该是两种情况下的最⼩值,因此可得转移⽅程:g[i] = min(nums[i], g[i - 1] + nums[i]) 。

 3.初始化:

辅助节点法.

前面加上⼀个格子,并且让g[0] = 0,f[0] = 0,根据状态表示为0,没有元素那么和显然为0.

 4.填表顺序:

从左往右

 5.返回值:

按正常情况来我们应该返回f()里面的最大值,和g表里面的最小值-sum,但是有一种特殊的情况如下图,就是如果数组的数全为负数的情况下,g表会把所有的负数都加入,然后sum-g表就会等于0,下面的正确最大和为-1sum-g表最小值等于0的情况就相当于一个子数组里面一个数都没有这是题目不允许的

故我们返回sum == gmin ? fmax : max(fmax, sum - gmin)。这样就可以排除全为负数的情况。

代码实现如下:

class Solution {
    public int maxSubarraySumCircular(int[] nums) {
    //1.创建 dp 表
    //2.初始化
    //3.填表
    //4.返回值
    int n = nums.length;
    int sum = 0;
    for(int i = 0;i < n;i++){
        sum += nums[i];
    }
    int[] f = new int[n + 1];
    int[] g = new int[n + 1];
    for(int i = 1;i <= n;i++){
        f[i] = Math.max(nums[i - 1],nums[i - 1] + f[i - 1]);
        g[i] = Math.min(nums[i - 1],nums[i - 1] + g[i - 1]);
    }
    int max1 = f[1];
    for(int i = 1;i <= n;i++){
        max1 = Math.max(max1,f[i]);
    }
    int max2 = -0x3f3f3f3f;
    for(int i = 1;i <= n;i++){
        if(sum == g[i]){
            max2 = Math.max(max2,-0x3f3f3f3f);
        }else{
            max2 = Math.max(sum - g[i],max2);
        }
    }
    return Math.max(max1,max2);

    }
}

max2初始化为-0x3f3f3f3f是因为求最大值初始化的值要够小,之所以不初始化为Integer.MIN_VALUE,这是因为这个数如果再发生减法的话可能会变成无穷大,0x3f3f3f3f这个数够小且稳定。

时间复杂度:O(n)

空间复杂度:O(n)

例题3:乘积最大子数组

链接:乘积最大子数组

题目简介:

给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

测试用例的答案是一个 32-位 整数。

解法(动态规划):

这题乍一看好像和最大子数组和好像差不多,但是乘法要考虑正负号的因素,如果只有一个dp表存储表示乘积最大子数组那么下面这种情况的最大值只能是5,可是结果是30.所以只有一个dp表是不够的,我们还需要一个表来存储最小值(乘于一个负数就变成最大值)。

 1. 状态表示:

f[i] 表⽰:以i 结尾的所有⼦数组的最大乘积。

g[i] 表⽰:以i 结尾的所有⼦数组的最小乘积。

 2.状态转移方程:

对于f[i] ,也就是「以i 为结尾的所有⼦数组的最⼤乘积」,对于所有⼦数组,可以分为下面三种形式:

(1)子数组的⻓度为1 ,也就是nums[i]。

(2)子数组的⻓度⼤于1 ,但nums[i] > 0 ,此时需要的是i - 1 为结尾的所有⼦数组的最⼤乘积f[i - 1] ,再乘上nums[i] ,也就是nums[i] * f[i - 1] 。

(3)子数组的⻓度⼤于1 ,但nums[i] < 0 ,此时需要的是i - 1 为结尾的所有⼦数组的最⼩乘积g[i - 1] ,再乘上nums[i] ,也就是nums[i] * g[i - 1] 。

如果nums[i] = 0 ,所有⼦数组的乘积均为0 ,三种情况其实都包含了 。

综上所述, f[i] = max(nums[i], max(nums[i] * f[i - 1], nums[i] * g[i - 1]) )。

对于g[i] ,也就是「以i 为结尾的所有子数组的最⼩乘积」,对于所有⼦数组,可以分为下面三种形式:

(1)子数组的⻓度为1 ,也就是nums[i] 。

(2)子数组的⻓度⼤于1 ,但nums[i] > 0 ,此时需要的是i - 1 为结尾的所有⼦数组的最⼩乘积g[i - 1] ,再乘上nums[i] ,也就是nums[i] * g[i - 1] 。

(3)子数组的⻓度⼤于1 ,但nums[i] < 0 ,此时需要的是i - 1 为结尾的所有⼦数组的最⼤乘积f[i - 1] ,再乘上nums[i] ,也就是nums[i] * f[i - 1] 。

综上所述, g[i] = min(nums[i], min(nums[i] * f[i - 1], nums[i] * g[i - 1])) 。

 3.初始化:

辅助节点:

f[0] = g[0] = 1。

 4.填表顺序:

从左往右,两个表⼀起填。

 5.返回值:

返回f 表中的最⼤值

代码如下:

class Solution {
    public int maxProduct(int[] nums) {
        //1.创建 dp 表
        //2.初始化
        //3.填表
        //4.返回值
        int n = nums.length;
        int[] f = new int[n + 1];
        int[] g = new int[n + 1];
        g[0] = f[0] = 1;
        for(int i = 1;i <= n;i++){
            f[i] = nums[i - 1];
            g[i] = nums[i - 1];
            if(nums[i - 1] >= 0){
                f[i] = Math.max(f[i],nums[i - 1] * f[i - 1]);
                g[i] = Math.min(nums[i - 1] * g[i - 1],g[i]);
            }else{
                f[i] = Math.max(f[i],nums[i - 1] * g[i - 1]);
                g[i] = Math.min(g[i],nums[i - 1] * f[i - 1]);
            }
        }
        int max = f[1];
        for(int i = 1;i <= n;i++){
            max = Math.max(max,f[i]);
        }
        return max;
    }
}

时间复杂度:O(n)

空间复杂度:O(n)

例题4:乘积为正数的最长子数组

链接:乘积为正数的最长子数组

题目简介:

解法(动态规划):

但是,如果我们知道「以i - 1 为结尾的所有子数组,乘积为负数的最长子数组的⻓度」neg[i - 1] ,那么此时的dp[i] 是不是就等于neg[i - 1] + 1呢?

通过上⾯的分析,我们可以得出,需要两个dp 表,才能推导出最终的结果。不仅需要⼀个乘积为正数的最长子数组,还需要⼀个乘积为负数的最长子数组。

 1. 状态表示:

f[i] 表⽰:以i 结尾的所有⼦数组中,乘积为正数的最长子数组的⻓度。

g[i] 表⽰:以i 结尾的所有⼦数组中,乘积为负数的最长子数组的⻓度。

 2.状态转移方程:

推状态转移方程时最好画图来帮助我们理解。

对于f[i] ,也就是以i 为结尾的乘积为正数的最⻓⼦数组,根据nums[i] 的值,可以分为三种情况:

(1)nums[i] = 0 时,所有以i 为结尾的⼦数组的乘积都不可能是正数,此时f[i] = 0 .

(2)nums[i] > 0 时,那么直接找到f[i - 1] 的值(这⾥请再读⼀遍f[i - 1] 代表的意义,并且考虑如果f[i - 1] 的结值是0的话,影不影响结果),然后加⼀即可, 此时f[i] = f[i - 1] + 1.

(3)nums[i] < 0 时,此时我们要看g[i - 1] 的值(这⾥请再读⼀遍g[i - 1] 代表的意义。因为负负得正,如果我们知道以i - 1 为结尾的乘积为负数的最⻓⼦数组的⻓度,加上1即可),根据g[i - 1] 的值,⼜要分两种情况:

1.g[i - 1] = 0 ,说明以i - 1 为结尾的乘积为负数的最长子数组是不存在的,⼜因为nums[i] < 0,所以以i 结尾的乘积为正数的最长子数组也是不存在的,此时f[i] = 0。

2.g[i - 1] != 0 ,说明以i - 1 为结尾的乘积为负数的最长子数组是存在的,⼜因为nums[i] < 0 ,所以以i 结尾的乘积为正数的最长子数组就等于g[i - 1] + 1。

综上所述, nums[i] < 0 时, f[i] = g[i - 1] == 0 ? 0 : g[i - 1] + 1;

 3.初始化:

辅助节点法:

f[0] = g[0] = 0。

 4.填表顺序:

从左往右,两个表⼀起填。

 5.返回值: 

根据状态表⽰,我们要返回f 表中的最⼤值。

代码如下:

class Solution {
    public int getMaxLen(int[] nums) {
        //1.创建 dp 表
        //2.初始化
        //3.填表
        //4.返回值
        int n = nums.length;
        int[] f = new int[n + 1];
        int[] g = new int[n + 1];
        for(int i = 1;i <= n;i++){
            if(nums[i - 1] > 0){
                f[i] = f[i - 1] + 1;
                g[i] = (g[i - 1] == 0) ? 0 : g[i - 1] + 1;

            }else if(nums[i - 1] < 0){
                f[i] = (g[i - 1] == 0) ? 0 : g[i - 1] + 1;
                g[i] = f[i - 1] + 1;
            }else{
                f[i] = 0;
                g[i] = 0;
            }
        }
        int max = f[1];
        for(int i = 1;i <= n;i++){
            max = Math.max(max,f[i]);
        }
        return max;

    }
}

时间复杂度:O(n)

空间复杂度:O(n)

总结:

由于文章篇幅有限我就挑选了经典的关于子数组dp问题来讲解,还有一些题我觉得也很好但写不下了😭,希望大家做完四题后还要多练练,下面我再给出两题有兴趣的友友可以AK。

题目一:最长湍流子数组

题目二:单词拆分

结语:

其实写博客不仅仅是为了教大家,同时这也有利于我巩固知识点,和做一个学习的总结,由于作者水平有限,对文章有任何问题还请指出,非常感谢。如果大家有所收获的话还请不要吝啬你们的点赞收藏和关注,这可以激励我写出更加优秀的文章。

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

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

相关文章

kibana 上dashbord 和discover 时间快 or 慢 8小时,处理方案

今天遇到了一个问题。在es库中的数据的时间是正确的。但是在kibana的discover展示页面上是错误的&#xff0c;错了8个小时。我这里是快了8个小时。这个问题非常难受&#xff0c;因为看起来&#xff0c;总是差8个小时&#xff0c;特别是查看日志的时候&#xff0c;总有一种错觉&…

AHU 汇编 实验一

一、实验名称&#xff1a;实验1 实验1 用Debug命令查看寄存器和内存中的内容 实验目的:求掌握使用Debug命令查看寄存器和内存的方法。 通过第2章两个简单实例认识汇编语言程序&#xff0c;初步了解程序格式&#xff1b;段定义&#xff1b;标号&#xff1b;DOS系统功能&#xf…

Anaconda prompt运行打开jupyter notebook 指令出错解决方案

一、打不开jupyter notebook网页 报错如下&#xff1a; Traceback (most recent call last): File “D:\anaconda3\lib\site-packages\notebook\traittypes.py”, line 235, in _resolve_classes klass self._resolve_string(klass) File “C:\Users\DELL\AppData\Roaming\Py…

Imagination:RISC-V CPU的重要力量

根据SHD集团最近发布的报告显示&#xff0c;RISC-V正全速发展中。通过分析从2021年到2030年这十年间RISC-V核在不同应用和功能领域的潜在市场&#xff0c;作者Rich Wawrzyniak得出结论称&#xff0c;到2030年&#xff0c;22.3%的SoC将包含RISC-V CPU&#xff0c;RISC-V的收入预…

【工具使用-VScode】VScode如何设置空格和tab键显示

一&#xff0c;简介 在提交代码的时候&#xff0c;行末尾的tab和空格不符合规范&#xff0c;但是如果在vscode中不显示tab和空格的话&#xff0c;不能及时的查看到并改正&#xff0c;导致提交代码之后还需要再次进行修改&#xff0c;效率比较低。 代码编辑界面如图所示&#…

【Node.js】-闲聊:前端框架发展史

前端框架的发展史是一个不断演进和创新的过程&#xff0c;旨在提高开发效率、优化用户体验&#xff0c;并推动前端技术的不断发展。以下是前端框架发展的主要阶段和关键里程碑&#xff1a; 早期阶段&#xff1a; 在这个阶段&#xff0c;前端主要由HTML、CSS和JavaScript等基础技…

修改简化docker命令

修改|简化docker命令 使用命令打开 .bashrc 文件&#xff1a; vim ~/.bashrc在文件中添加类似以下行来创建别名&#xff1a; # 查看所有容器 alias disdocker images # 查看运行容器 alias dpsdocker ps # 查看所有容器 alias dpsadocker ps -a # 停止容器 alias dsdocker s…

NLP 算法实战项目:使用 BERT 进行模型微调,进行文本情感分析

本篇我们使用公开的微博数据集(weibo_senti_100k)进行训练&#xff0c;此数据集已经进行标注&#xff0c;0: 负面情绪&#xff0c;1:正面情绪。数据集共计82718条(包含标题)。如下图&#xff1a; 下面我们使用bert-base-chinese预训练模型进行微调并进行测试。 技术交流&#x…

基于Springboot的招生宣传管理系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的招生宣传管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构…

python爬虫(4)

#前期先说明一下为啥爬虫需要学习数组的存储和处理&#xff0c;只是说在你后期接触到最简单的爬虫后有一个地方可以存放你的数据# 下面为大家带来一个我在做excel表整理时的代码以及上次代码的结果 上次代码的结果&#xff1a; 新的代码&#xff1a; import numpy as np im…

mysql | 查询数据的过程|优化-->索引 |存储引擎

查询的过程 首先确认mysql 服务器是否启动 systemctl mysqld status 登录连接 mysql -h i p − u ip -u ip−uuser -p (-h 指定服务器ip -u 指定用户名 -p 指定密码) mysql 数据包 经过抓包分析&#xff08;mysql包其实就是基于tcp协议 3306端口) 传输采用mysql 协议&#xff0…

【操作系统概念】第12章:大容量存储阶段

文章目录 0.前言12.1 概述12.2磁盘结构12.3 磁盘调度12.3.1 FCFS调度12.3.2 SSTF调度12.3.3 SCAN调度12.3.4 C-SCAN调度12.3.5 如何选择磁盘调度 0.前言 文件系统从逻辑上来看包括三部分。第10章讨论了文件系统的用户和程序员的接口。第11章描述了操作系统实现这种接口的内部数…

【脚本玩漆黑的魅影】全自动丢球

文章目录 原理全部代码 原理 启动后截图。 丢球以后再截图。 如果两图一致&#xff0c;说明没成功&#xff0c;读档重来。 如果两图不一致&#xff0c;说明成功了。 while True:press(A)time.sleep(2)if is_same_img(ImageGrab.grab(), data_img):press(save2)else:break全部…

基于java+springboot+vue实现的农产品智慧物流系统(文末源码+Lw)23-239

课题意义 现如今&#xff0c;信息种类变得越来越多&#xff0c;信息的容量也变得越来越大&#xff0c;这就是信息时代的标志。近些年&#xff0c;计算机科学发展得也越来越快&#xff0c;而且软件开发技术也越来越成熟&#xff0c;因此&#xff0c;在生活中的各个领域&#x…

【stm32】hal库学习笔记--定时器输出PWM波

【stm32】hal库学习笔记–定时器输出PWM波 PWM波原理 输出比较 输入捕获 驱动函数 定时器驱动函数 PWM波驱动函数 定时器基本不使用DMA方式 定时器中断处理通用函数 HAL_TIM_IRQHandler实验一:输出固定占空比PWM波 时钟树配置 PF9 改为tim14CH1 tim14配置 开启tim14全局中…

求递归算法时间复杂性

递推方法 求n&#xff01;的递归算法&#xff1a; 该算法的时间复杂性&#xff1a; 递推过程&#xff1a; 主定理方法 要求&#xff1a;a>1,b>1 求解步骤&#xff1a; f(n)的渐进上界是以n的log以b为底的e次幂 判断关系后一定要满足这三个对应规则 例题&#xff1a;…

Java中常用的集合及方法(2)

在Java&#xff08;JDK8&#xff09;中&#xff0c;集合&#xff08;Collection&#xff09;是数据结构的实现&#xff0c;用于存储和操作对象集合。 集合&#xff08;Collection&#xff09;中包含的一般类或接口&#xff1a; 在这其中呢&#xff0c;我们经常使用的其实就是L…

nginx 学习总结

1.nginx 是什么以及nginx 的用途&#xff1f; Nginx 是一种高性能的 Web 和反向代理服务器&#xff0c;以及邮件&#xff08;IMAP/POP3&#xff09;代理服务器。它最初是由俄罗斯程序员 Igor Sysoev 使用 C 语言开发的开源项目。Nginx 以其占用内存少、并发能力强而闻名&…

【Leetcode】299. 猜数字游戏

文章目录 题目思路代码结果 题目 题目链接 你在和朋友一起玩 猜数字&#xff08;Bulls and Cows&#xff09;游戏&#xff0c;该游戏规则如下&#xff1a; 写出一个秘密数字&#xff0c;并请朋友猜这个数字是多少。朋友每猜测一次&#xff0c;你就会给他一个包含下述信息的提…

专题二 -滑动窗口 - leetcode 209. 长度最小的子数组 | 中等难度

leetcode 209. 长度最小的子数组 leetcode 209. 长度最小的子数组 | 中等难度1. 题目详情1. 原题链接2. 基础框架 2. 解题思路1. 题目分析2. 算法原理3. 时间复杂度 3. 代码实现4. 知识与收获 leetcode 209. 长度最小的子数组 | 中等难度 1. 题目详情 给定一个含有 n 个正整数…