一)连续子数组的最大和
53. 最大子数组和 - 力扣(Leetcode)
思路1:列举出当前数组中所有元素组成的子数组,再进行针对每一个子数组求和即可
思路2:动态规划
1)定义一个状态表示:根据经验+题目要求
以i位置为结尾,巴拉巴拉,当前是以i位置为结尾,所以还需要求出来从0号位置开始到i号位置所有的子数组的和
dp[i]表示以i位置为元素结尾(包含i位置的元素)的所有子数组中的最大和
2)根据状态表示推导状态转移方程
单独nums[i]自己构成一个子数组,nums[i]和nums[i-1]构成子数组,要么nums[i]和前面两个数构成子数组,要么和前面三个数构成子数组
题目的状态表示是以i位置为结尾的所有子数组的最大和,这个子数组根据长度具体分为两种情况:
1)以i为结尾的子数组长度是1:当前i位置就是以i位置为结尾子数组的最大和此时和就是array[i]
这种情况是i前面的数的子数组的最大和是一个负数此时array[i]加上了前面的负数还不如不加
2)以i为结尾的子数组长度大于1:此时的array[i]子数组中必须要有的,当前的值array[i]再加上到i-1位置的子数组的最大和即可,这种情况是i前面的以i-1位置为结尾的子数组的最大和是一个正数,此时以i位置为结尾的子数组的最大和当然要加上前面的正数;
3)初始化:保证在进行填表的时候数组不会发生越界
可以还是采用虚拟节点的方式,让整个dp表从下标是1的位置开始进行计数
1)虚拟节点里面的值要保证后面再进行填表的时候值是正确的
2)加上虚拟节点之后要保证现在的dp表和原来的dp表的映射关系是相同的
4)填表顺序:从左向右
5)返回值:子数组的最大和不一定是以最后一个位置为结尾,还有可能是以数组中的某一个位置为结尾的最大和,所以返回的是整个dp表里面的最大值
class Solution { public int maxSubArray(int[] nums) { //1.定义一个状态表示dp[i]表示以i位置为结尾的子数组的最大和 int[] dp=new int[nums.length]; dp[0]=nums[0]; int max=dp[0]; for(int i=1;i
class Solution { public int maxSubArray(int[] array) { //1.定义一个状态表示dp[i]表示以i位置为结尾的子数组的最大和 int[] dp=new int[array.length+1]; int max=Integer.MIN_VALUE; dp[0]=0; for(int i=1;i<=array.length;i++){//从dp表中开始进行遍历 dp[i]=Math.max(array[i-1],dp[i-1]+array[i-1]);//注意下表映射关系 max=Math.max(dp[i],max); } return max; } }
二)环形子数组的最大和:
918. 环形子数组的最大和 - 力扣(Leetcode)
题目思路:相比于连续最大子数组和多了一种情况,这道题也是子数组的一类题,但是第一个元素和最后一个元素进行组合也是属于子数组的一种情况
1)还是相当于求的是连续最大子数组的和:
连续最大子数组的左右区间恰好是连续的,就和上一道题是一样的
2)求出第一种情况的最大值和第二种情况的最大值然后求出他们之间的最大值就是最终我们要求出来的结果
第一种情况:连续最大子数组和这段区间是在数组中是连续的
第二种情况:连续最大子数组和在数组中是不连续的,后面选择一部分数,前面选择一部分数
1)状态表示:根据经验+题目要求,根据状态标识推导状态转移方程
f[i]表示以i为结尾的所有子数组的最大和
g[i]表示以i为结尾的所有子数组的最小和
要么是以i为结尾的最大和或者是最小和要么单独是自己要么是加上i-1的最大和或者是最小和
1)既然是求以i位置为结尾的最小和,那么必须包含i位置的元素
2)拿下面这个图来进行举例,要么是单独的nums[i]作为以i位置为结尾的最大值,要么就是nums[i]加上以虚线前面的最大值,刚好就是以i-1为结尾,子数组的最大值
2)初始化+填表顺序(从左到右)
初始化的时候可以在dp数组相比于原来的数组的前面加上一个虚拟的节点
1)在虚拟节点的值要保证后续再进行填表的时候是正确的
2)注意新创建的dp数组和原来的数组的下标映射关系
3)返回值
1)找到f表里面的最大值
2)找到g标里面的最小值(考虑数组中全部是负数)
3)假设数组中全是负数[-5,-5,-5,-5,-5]
此时min=-25,sum=-25,max=-5
class Solution { public int maxSubarraySumCircular(int[] nums) { //f表示以i为结尾子数组的最大和 //g表示以i为结尾子数组的最小和 int[] f=new int[nums.length+1]; int[] g=new int[nums.length+1]; f[0]=0; g[0]=0; int sum=0; int max=Integer.MIN_VALUE; int min=Integer.MAX_VALUE; for(int i=1;i<=nums.length;i++){ f[i]=Math.max(nums[i-1],nums[i-1]+f[i-1]); max=Math.max(f[i],max); g[i]=Math.min(nums[i-1],nums[i-1]+g[i-1]); min=Math.min(g[i],min); sum=sum+nums[i-1]; } //考虑数组中的数全部是负数的情况 if(sum-min==0){ return max; } return Math.max(sum-min,max); } }
三)乘积最大的子数组:
152. 乘积最大子数组 - 力扣(Leetcode)
1)定义一个状态表示:
f[i]表示以i位置为结尾的所有子数组的最大乘积
下面这个题也是分成两种情况,但是和之前的连续最大的子数组和又有一些不同
1)假设nums[i]是一个正数,但是dp[i-1]是负数的话,那么只是mus[i]是正数,以i为结尾的子数组的最大乘积就是nums[i],但是如果nums[i]是一个正数,那么dp[i-1]是一个正数,那么这种情况就是nums[i]*dp[i-1]
2)但是假设如果nums[i]是负数,那么上面的推论就是错误的,如果此时nums[i]是负数,那么如果你nums[i]*f[i-1]此时就不是最大的数了,如果f[i-1]越大,说明nums[i]*f[i-1]的乘积就越小,所以此时nums[i]应该和以i-1位置为结尾的最小的子数组的乘积进行相乘;
所以经过分析,一个状态表示是远远不够的,所以此题的状态表示就是:
上面的代码是错误的,因为在求最小值的时候
1)如果nums[i]是负数,那么最小值应该是nums[i]或者是nums[i]*f[i-1]
2)如果nums[i]是正数,那么最小值应该是nums[i]或者是nums[i]*g[i-1]
所以说上面的错误在求最小值的时候也没有进行分类讨论
假设这个数组中全部是正数,那么g表中的值全部都是nums[i],所以说不要将nums[i]的值全部忘记了
class Solution { public int maxProduct(int[] nums) { int[] f=new int[nums.length+1];//以i为结尾子数组乘积的最大值 int[] g=new int[nums.length+1];//以i为结尾子数组乘积的最小值 f[0]=1; g[0]=1; int max=Integer.MIN_VALUE; for(int i=1;i<=nums.length;i++){ //这是在进行遍历dp表 if(nums[i-1]>0) { g[i]=Math.min(nums[i-1],g[i-1]*nums[i-1]); //注意下标的映射关系,虚拟节点的值要保证后续的位置是正确的 f[i]=Math.max(nums[i-1],f[i-1]*nums[i-1]); } else { g[i]=Math.min(nums[i-1],f[i-1]*nums[i-1]); f[i]=Math.max(nums[i-1],g[i-1]*nums[i-1]); } max=Math.max(f[i],max); } return max; } }
四)乘积为正数的最长的子数组的长度:
1567. 乘积为正数的最长子数组长度 - 力扣(Leetcode)
1)定义一个状态表示:
1.1)f[i]表示以i位置元素为结尾的所有子数组的中乘积为正数的最长长度
1.2)像这种子数组的题划分方式是非常固定的,当前元素划分主要是两种情况,答案中的一种就是就是以i为结尾的位置的元素,单独一个构成子数组,要么就是以i位置为结尾的元素拼接上以i-1位置为结尾的元素巴拉巴拉,多个元素进行结合构成一个子数组;
我们要依据子数组的长度来进行划分问题
但是在这里面还是要注意一个特殊的情况,如果当前元素也就是nums[i]是小于0的,那么此时nums[i]只要和以i-1位置为结尾的长度为负数的子数组的最大长度进行组合就可以了,此时f[i-1]=g[i-1]+1即可,但是如果此时出现了这种情况,如果nums[i]是小于0的,此时nums[i]前面的数都是正数,那么此时前面都没有负数也就是g[i-1]=0,此时的f[i-1]=g[i-1]+1就不成立了
所以说最终的状态转移方程就是
if(i>0) f[i]=f[i-1]+1
if(i<0) f[i]=0或者是g[i-1]+1
就是类似于上面这种情况的分析,会涉及到很多if else 语句所以先进行分析f[i]再来分析g[i]
class Solution { public int getMaxLen(int[] nums) { //f[i]表示以i为结尾,所有子数组乘积为正数的最长长度 //g[i]表示以i为结尾,所有子数组乘积为负数的最长长度 int[] f=new int[nums.length+1]; int[] g=new int[nums.length+1]; f[0]=0; g[0]=0; int maxlen=0; for(int i=1;i<=nums.length;i++){ if(nums[i-1]>0){ f[i]=f[i-1]+1; if(g[i-1]==0){ g[i]=0; }else{ g[i]=g[i-1]+1; } }else if(nums[i-1]<0){ if(f[i-1]==0){ g[i]=1; }else{ g[i]=f[i-1]+1; } if(g[i-1]==0){ f[i]=0; }else{ //前面一个负数都没有,只有当前的数是负数,那么f[i]==0; f[i]=g[i-1]+1; } }else{ f[i]=0; g[i]=0; } maxlen=Math.max(maxlen,f[i]); } return maxlen; } }
1)如果说我们在计算f[i]以i位置为结尾的所有子数组中乘积为正数的最长长度,如果nums[i]是负数, 要想要求f[i]就要进行计算以i-1位置为结尾的的子数组中乘积是负数的最长长度,但是假设这个数前面的数都是正数或者是0,也就是说g[i-1]=0,也就是说这个负数和前面以i-1为结尾的子数组无法拼接成正数,所以if(nums[i]<0) f[i]=g[i-1]==0?0:g[i-1]+1
2)如果我们再进行计算g[i]以i位置为结尾的所有子数组中乘积是负数的最长长度,如果nums[i]是正数,要想计算g[i]就要计算以i-1位置为结尾的子数组中乘积是负数的最长长度,假设如果前面的数都是正数或者是0,那么就说明这个正数和前面i-1为结尾的子数组无法拼接成负数,if(nums[i]>0) g[i]=g[i-1]==0?0:g[i-1]+1
五)等差数列划分:
413. 等差数列划分 - 力扣(LeetCode)
解法1:动态规划:
1)定义一个状态表示:根据经验+题目要求
dp[i]表示以i位置为结尾的所有子数组的中有多少个等差数列
假设在上面的a b c d数组中,如果a b c d是等差数列,那么a b c d e是不是等差数列呢,答案肯定是,先找到以i位置为结尾的所有子数组然后从这些子数组里面找到有多少个等差数列
2)根据状态标识推导状态转移方程:
因为子数组还是连续的,所以以i位置为结尾的元素想要构成等差数列,那么在这个i位置的元素至少和i-1位置的元素和i-2位置的元素构成等差数列
a b元素是不在以i-1位置为结尾的所有子数组中等差数列的个数中,因为想要狗成等差数列,必须包含三个元素以上,但是再进行计算dp[i]的时候,a,b,c是可以构成等差数列的
但是再进行计算dp[i]的时候,新出现的a b c是一种新的情况,所以dp[i]=dp[i-1]+1
如果不能构成等差数列,那么dp[i]=0
3)进行初始化操作:
dp[0],dp[1]分别表示以0元素为结尾,以1元素为结尾的等差数列的个数,此时只是需要全部初始化成0就可以了
4)填表顺序:从左到右进行填表
5)返回值:dp[i]仅仅是表示以i位置为结尾的等差数列的个数,而题目求的是所有等差数列的个数,所以返回值是dp表内所有元素的和
class Solution { public int numberOfArithmeticSlices(int[] nums) { if(nums==null||nums.length==1||nums.length==2) return 0; //1.定义一个状态标识dp[i]表示以i位置元素为结尾等差数列的个数 int[] dp=new int[nums.length]; dp[0]=0; dp[1]=0; for(int i=2;i
解法2:暴力破解:
因为题干已经说了,数组中至少包含三个元素:所以至少需要三个元素才可以构成等差数列
class Solution { public int numberOfArithmeticSlices(int[] nums) { int count=0; for(int i=0;i
class Solution { public int numberOfArithmeticSlices(int[] array) { int count=0; for(int i=0;i
六)最长湍流子数组:
题目解析:
978. 最长湍流子数组 - 力扣(Leetcode)
意思就是从给定数组中选i个数,使得这i个数一高一低
(画成折线图就是↗↘↗↘,或者↘↗↘↗↘)
一)定义一个状态表示:
经验+题目表示,dp[i]表示以i元素为结尾的所有子数组中,最长的湍流子数组的长度
二)根据状态表示推到状态转移方程
根据最近的一步来划分问题,因为dp[i]表示的是以i为结尾,最长的湍流子数组的长度,但是以当前i来进行划分问题,一共是分成三种情况的:
因为到i位置的时候,数组可能是划分出三种形态的,所以一个单独的状态表示肯定是不够的
f(i)表示以i元素为结尾的所有子数组中,最后呈现上升状态的最长湍流子数组的长度
g(i)表示以i元素为结尾的所有子数组中,最后呈现下降状态的最长湍流子数组的长度
三)初始化:
f[i]和g[i]两个函数值最差的情况下也就是1,可以将f表和g表全部初始化成1
四)填表顺序(从左向右填写)+返回值:返回两个表中的最大值
class Solution { public int maxTurbulenceSize(int[] array) { //1.搞一个dp表 //2.进行初始化操作 //3.进行填表 //f[i]表示以i为结尾的子数组湍流子数组的最长长度况且是呈现上升趋势的 int[] f=new int[array.length]; //g[i]表示以i为结尾的子数组湍流子数组的最长长度款况且是呈现下降趋势的 int[] g=new int[array.length]; f[0]=1; g[0]=1; int maxlen=Integer.MIN_VALUE; for(int i=1;i
array[i-1]){ //如果当前i位置的元素比i-1位置的元素大 f[i]=g[i-1]+1; g[i]=1; }else if(array[i]==array[i-1]){ f[i]=1; g[i]=1; }else{ //如果当前位置的元素比i-1位置的元素小 f[i]=1; g[i]=f[i-1]+1; } maxlen=Math.max(maxlen,f[i]); maxlen=Math.max(maxlen,g[i]); } return maxlen==Integer.MIN_VALUE?1:maxlen; } }
七)单词拆分:
139. 单词拆分 - 力扣(Leetcode)
1)状态表示:是根据经验+题目要求
dp[i]表示[0,i]区间内的字符串,是否能够被字典中的单词拼接而成
如果是由字典中的单词拼接而成,那么dp[i]里面存放的就是true
如果不是由字典中的单词拼接而成,那么dp[i]里面存放的就是false
2)根据状态表示推导状态转移方程:根据最后一个位置的情况dp[i]来划分问题
2.1)题目要求是这个字符串是否能够被字典中的单词拼接而成,最后一个位置相当于是最后一个单词,要么是最后一个字符来单独构成一个单词,或者是最后两个字符来构成最后一个单词,或者是最后三个字符构成最后一个单词,或者是整个从0到i-1位置的字符串来构成最后一个单词,做题思路是将0-i区间内的字符串划分成字符串+最后一个单词
整个字符串=前面的字符串+"最后一个位置的单词"
2.2)如果我们能够确定前面的那个字符串能够被字典中的单词拼接而成,况且最后一个单词在字典中,那么就可以判断0-i位置的字符串能够被字典中的单词组成
2.2)设j是最后一个单词起始位置的下标,那么前一个字符串的结束位置是j-1,j>=0&&j<=i,如果j=0,那么说明从0-i就是作为一个单词,如果j等于i,那么从0-j-1是作为一个字符串,最后一个字符就是一个单词,但是j从0号位置向后走的过程中,只有出现一种情况里面的表达式是true,那么整个dp[i]的值就是true
3.3)首先要判断一下,从0-j-1的这段区间的字符串是否能够被字典拼接而成,但是dp[j-1]恰好表示的是0-j-1这段区间的位置能否恰好被字典拼接而成,第二部分就用来判断从j到i之间的子串是否存在于字典中,当第一部分况且第二部分都是true的时候,整个从0-i位置的字符串才能被字典中的单词拼接而成;
所以最终的状态转移方程是:
1)dp[i]=dp[j-1]==true&&s(j,i)==在字典中
2)dp[i]=false
3)初始化:
1)里面的值要保证后续的填表是正确的:如果需要使用到dp[0]里面的值,那么说明把[0,i]区间内字符串当作是最后一个单词,第-1个字符当成最后一个字符串,所以它的值必须是true,如果是dp[0]是false,那么后续的值都是false
2)下标的映射关系:在原始字符串里面加上一个辅助位置的字符,是原始字符串从1位置开始进行计数
4)填表顺序+返回值:从左到右,返回dp[n]
class Solution { public boolean wordBreak(String s, List
list) { HashSet words=new HashSet<>(); for(String str:list){ words.add(str); } boolean[] dp=new boolean[s.length()+1]; dp[0]=true; s="-"+s; int n=s.length(); for(int i=1;i
class Solution { public boolean wordBreak(String s, List
wordDict) { HashSet set=new HashSet<>(); for(String str:wordDict){ set.add(str); } char[] array=s.toCharArray(); int n=array.length; boolean[] dp=new boolean[n]; for(int i=0;i
八)环绕字符串中唯一的子字符串
467. 环绕字符串中唯一的子字符串 - 力扣(Leetcode)
1.定义一个状态表示:
dp[i]表示以i位置为元素的结尾的所有子串里面,找到有多少个在base中出现
2.根据状态表示推导状态转移方程:
1)像这样的子数组,子序列问题,推到状态转移方程的时候一般分成两类
2)i位置的字符单独来构成子串,因为题干中说道整个字符串都是小写字母,那么dp[i]=1
3)如果以i位置为结尾的在base中出现过,况且长度要大于1,那么必定i-1和i位置的元素进行组合也是在base中出现过的
if(array[i-1]+1==array[i]||s[i-1]=='z'&&s[i]=='a') dp[i]=dp[i-1]+1
else dp[i]=1
3.初始化
dp[0]=0,因为单独一个字符一定是可以在base中出现过的,所以可以在dp表中将所有值初始化成1
4.填表顺序:从左向右
5.返回值:
正常情况下应该返回的是dp表里面所有的值的和,这样做是错误的,因为我们多计算了重复的子串,所以我们需要进行去重:相同类型的字串,只需要进行统计一次即可
class Solution { public int findSubstringInWraproundString(String s) { //1.首先创建一个大小是s.length()的dp表,并将形参转化成字符串 char[] array=s.toCharArray(); int[] dp=new int[array.length]; int[] result=new int[26]; dp[0]=1; int sum=0; for(int i=1;i