动态规划课堂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。

题目一:最长湍流子数组

题目二:单词拆分

结语:

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

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

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

相关文章

农场管理小程序|基于微信小程序的农场管理系统设计与实现(源码+数据库+文档)

农场管理小程序目录 目录 基于微信小程序的农场管理系统设计与实现 一、前言 二、系统设计 三、系统功能设计 1、用户信息管理 2、农场信息管理 3、公告信息管理 4、论坛信息管理 四、数据库设计 五、核心代码 七、最新计算机毕设选题推荐 八、源码获取&#x…

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

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

【大厂AI课学习笔记NO.68】开源和开源发展情况

开源即源代码公开&#xff0c;任何人能获取源代码&#xff0c;查看、修改、分发他们认为合适的代码。 依托同行评审和社区生成&#xff0c;旨在以分散、协作的方式开发。 我们曾经很详细的讨论过开源协议的问题&#xff0c;详细可以参考我的文章&#xff1a; https://giszz.…

LeetCode-1004. 最大连续1的个数 III

每日一题系列&#xff08;day 20&#xff09; 前言&#xff1a; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f308; &#x1f50…

docker部署在线聊天室平台Fiora

Fiora 是一款开源免费的在线聊天系统 https://github.com/yinxin630/fiora 部署 创建docker网络 docker network create fiora-networkdocker-compose部署 vim docker-compose.yml version: 3 services:fiora_redis:image: rediscontainer_name: fiora_redisrestart: alway…

电脑解锁后黑屏有鼠标--亲测!!不需要重装系统!!

问题&#xff1a;上周电脑黑屏&#xff0c;只有鼠标&#xff0c;鼠标还不能右键&#xff01;&#xff01; 中招&#xff1a;win10系统最新版火绒安全 &#xff0c;那你有概率获得开机黑屏套餐一份。 原因是&#xff1a;火绒把我们的explorer删除了导致黑屏&#xff0c;这个文…

【OpenGL手册11】材质的模型

目录 一、说明二、材质表面和光照三、设置材质四、光的属性五、不同的光源颜色练习 一、说明 在现实世界里&#xff0c;每个物体会对光产生不同的反应。比如&#xff0c;钢制物体看起来通常会比陶土花瓶更闪闪发光&#xff0c;一个木头箱子也不会与一个钢制箱子反射同样程度的…

1分钟带你学会使用装饰器编写Python函数

1.需求 向 test() 函数中&#xff0c;新增一个功能&#xff0c;多输出一句话"给他补铁" def test():print("水中放吸铁石") # test()# 第一种方式&#xff1a;重写函数 def test():print("水中放吸铁石")print("给他补铁") test()# …

py脚本模拟json数据,StructuredStreaming接收数据存储HDFS一些小细节 ERROR:‘path‘ is not specified

很多初次接触到StructuredStreaming 应该会写一个这样的案例 - py脚本不断产生数据写入linux本地&#xff0c; 通过hdfs dfs 建目录文件来实时存储到HDFS中 1. 指定数据schema&#xff1a; 实时json数据 2. 数据源地址&#xff1a;HDFS 3. 结果落地位置&#xff1a; HDFS …

高级语言讲义2010软专(仅高级语言部分)

1.编写一程序&#xff0c;对输入的正整数&#xff0c;求他的约数和。 如&#xff1a;18的约数和为1236939 #include <stdio.h>int getsum(int n){int i,sum0;for(i1;i<n;i)if(n%i0)sumi;return sum; } int main(){int sum getsum(18);printf("%d",sum); …

PCB检测,基于YOLOV8NANO

PCB检测&#xff0c;基于YOLOV8NANO&#xff0c;训练得到PT模型&#xff0c;转换成ONNX&#xff0c;只需要OPENCV&#xff0c;支持C/PYTHON/ANDROID开发PCB检测&#xff0c;基于YOLOV8NANO&#xff0c;只需要OPENCV

每日一题leetcode第2834:找出美丽数组的最小和

目录 一.题目描述 二.思路及优化 三.C代码 一.题目描述 二.思路及优化 首先我们看到这个题&#xff0c;就是根据给出的数组元素个数N&#xff0c;从[1&#xff0c;N]找出N个元素&#xff0c;使得N个元素的和最小&#xff0c;其中随便抽两个数出来&#xff0c;两个数之和不能为…

BC134 蛇形矩阵

一&#xff1a;题目 二&#xff1a;思路分析 2.1 蛇形矩阵含义 首先&#xff0c;这道题我们要根据这个示例&#xff0c;找到蛇形矩阵是怎么移动的 这是&#xff0c;我们可以标记一下每次移动到方向 我们根据上图可以看出&#xff0c;蛇形矩阵一共有两种方向&#xff0c;橙色…

【Pytorch】新手入门:基于sklearn实现鸢尾花数据集的加载

【Pytorch】新手入门&#xff1a;基于sklearn实现鸢尾花数据集的加载 &#x1f308; 个人主页&#xff1a;高斯小哥 &#x1f525; 高质量专栏&#xff1a;Matplotlib之旅&#xff1a;零基础精通数据可视化、Python基础【高质量合集】、PyTorch零基础入门教程&#x1f448; 希望…

Hadoop运行搭建——系统配置和Hadoop的安装

Hadoop运行搭建 前言&#xff1a; 本文原文发在我自己的博客小站&#xff0c;直接复制文本过来&#xff0c;所以图片不显示(我还是太懒啦&#xff01;)想看带图版的请移步我的博客小站~ Linux镜像&#xff1a;CentOS7 系统安装&#xff1a;CentOS安装参考教程 系统网卡设置…

C语言——函数指针——函数指针变量详解

函数指针变量 函数指针变量的作用 函数指针变量是指向函数的指针&#xff0c;它可以用来存储函数的地址&#xff0c;并且可以通过该指针调用相应的函数。函数指针变量的作用主要有以下几个方面&#xff1a; 回调函数&#xff1a;函数指针变量可以作为参数传递给其他函数&…

三菱PLC基础指令

LD指令(a触点的逻辑运算开 指令表程序 0000 LD X000 0001 OUT Y000 LDI指令(b触点的逻辑运算开 指令表程序 0000 LDI X000 0001 OUT Y000 3.数据寄存器(D)的位指定*1(仅对应FX3u&#xff0c;FX3uc可编程控制器) 指令表程序 0000 LD D0.3 0001 OUT Y000 4.定时器 0000 LDI X00…

Objects类 --java学习笔记

Objects类 Objects是一个工具类&#xff0c;提供了很多操作对象的静态方法给我们使用 Objects类常用的三个方法 Objects.equals 比直接equals更安全&#xff0c;因为Objects.equals里面做了非空校验 Objects.isNull&#xff08;A&#xff09; 等价于 A null Objects.non…

Redisson学习

简介 Redisson 是一个在 Redis 的基础上实现的 Java 驻留内存数据网格&#xff08;In-Memory Data Grid&#xff09;。它提供了许多分布式 Java 对象和服务&#xff0c;包括分布式锁、分布式集合、分布式执行服务、分布式调度任务等。 使用 依赖 相关依赖&#xff0c;注意版…

【兔子机器人】修改GO电机id(软件方法、硬件方法)

一、硬件方法 利用上位机直接修改GO电机的id号&#xff1a; 打开调试助手&#xff0c;点击“调试”&#xff0c;查询电机&#xff0c;修改id号&#xff0c;即可。 但先将四个GO电机连接线拔掉&#xff0c;不然会将连接的电机一并修改。 利用24V电源给GO电机供电。 二、软件方…