1.最大子数组和
1 题目分析
给你一个整数数组 nums
,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组是数组中的一个连续部分。
2.讲解算法原理
1.状态表示:
dp[i] 表示为 i 位置的元素为结尾的所有子数组中最大和;
2.状态转移方程;
我们从上题可以看出,当数组的数据为1时,此时的最大和就为数组的这个值;
于是我们可以得出:dp[i] 的两种表示形式
1. 当数组为1时,dp[i] = nums[i];
2.当数组不为1时,我们只需要求出每个位置的 dp(i),然后返回 dp数组中的最大值即可。
0 | 1 | ...... | i-1 | i |
nums[0] | nums[1] | ...... | nums[i-1] | nums[i] |
dp[i] = nums[i -1] + dp[i -1];
因为我们要最大值,所以比较这两个结果的最大值即可,
dp[i] = Math.max(nums[i],nums[i -1] + dp[i -1]);
此时我们dp[i]中储藏的是数组中不同位置到达 i 的值,所以我们还需要返回数组中最大值即可,
3.初始化:
1.下表的映射关系
2.里面的值保证在之后的填表中是正确的,
4.填表顺序:
从左往右,两个表一起填
5.返回值:
整个dp表中的最大值即可:
3.编写代码
2.环形子数组的最大和
1.题目解析
给定一个长度为 n
的环形整数数组 nums
,返回 nums
的非空 子数组 的最大可能和 。
环形数组 意味着数组的末端将会与开头相连呈环状。
子数组 最多只能包含固定缓冲区 nums
中的每个元素一次。
2.讲解算法原理
1.状态表示:
f[i] 表示以 i 位置的元素为结尾的所有子数组中最大和;
g[i]表示:以 i 位置的元素为结尾的所有子数组中最小和;
2.状态转移方程:
能遇到环形。数组问题是我们都可以将整个数组切割成两部分。第一个就是他们首尾连接的部分,还有中间这一部分。让我们切割完之后就可以按照以下步骤来分析。
所以我们按照定义的这两个状态是分别求出这个数组中最大值和最小值即可。因为上题已经分析过如何求这个数组中一段数据的最大值,所以这里就不带给大家分析,直接上状态转移方程。因为此时我们还求了整个数组中的和。所以说我们还要定一个sum等于零去加上数组中的每一个元素。让他求得数组中的和。
f[i] = Math.max(num[i - 1],f[i - 1 ]+num[i - 1]);
fmax = Math.max(fmax,f[i]);
g[i] = Math.min(num[i - 1],g[i - 1 ]+num[i - 1]);
gmin = Math.min(gmin,g[i]);
res +=num[i -1];
3.初始化:
1.下表的映射关系
2.里面的值保证在之后的填表中是正确的,
4.填表顺序:
从左往右,两个表一起填
5.返回值:
在处理返回值时,我们要先判定,这个数组最终的和是否和我们求的最小值一样,如果一样的话我们就去返回,F(i)里面我们所求得的那一段数组的最大和,如果他俩不一样,我们就要去找这两个(fmax,res - gmin)当中的最大值。
return res == gmin ? fmax : Math.max(fmax, res - gmin);
3.编写代码
3.乘积最大子数组
1.题目分析:
给你一个整数数组 nums
,请你找出数组中乘积最大的非空连续
子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
2.讲解算法原理
1.状态表示:
第一次分析:
f[i] 表示为 i 位置的元素为结尾的所有子数组中最大乘积;
第二次添加:
g[i] 表示为 i 位置的元素为结尾的所有子数组中最小乘积;
2.状态转移方程;
错误:
我们从上题可以看出,当数组的数据为1时,此时的最大乘积就为数组的这个值;
于是我们可以得出:f[i] 的两种表示形式
1. 当数组为1时,f[i] = nums[i];
2.当数组不为1时,我们只需要求出每个位置的 dp(i),然后返回 dp数组中的最大值乘积:
0 | 1 | ...... | i-1 | i |
nums[0] | nums[1] | ...... | nums[i-1] | nums[i] |
f[i] = nums[i -1] * f[i -1];
此时我们就会发现当数组中num的值小于零时,我们是得不出这段数组中所有子数组乘积的最大值。因此我们光定义一个状态转移方式是不对的。所以我们还要再定义一个状态表示。
正确:
我们继续按上述步骤进行分析f[i] 的两种表示形式:
1. 当数组为1时,f[i] = nums[i];
2.当数组不为1时,我们只需要求出每个位置的 dp(i),然后返回 dp数组中的最大值乘积:
在乘积这里我们就可以分析num[i -1]的情况:
1. 当num[i -1] > 0 时:nums[i -1] * f[i -1];
2. 当num[i -1] <= 0 时:nums[i -1] * g[i -1];
因为我们要最大值,所以比较这三个结果的最大值即可,
f[i] = Math.max(num[i -1],Math.max(nums[i -1] * f[i -1],nums[i -1] * g[i -1]));
分析g[i] 的两种表示形式:
1. 当数组为1时,g[i] = nums[i];
2.当数组不为1时,我们只需要求出每个位置的 dp(i),然后返回 dp数组中的最小值乘积:
在乘积这里我们就可以分析num[i -1]的情况:
1. 当num[i -1] > 0 时:nums[i -1] * g[i -1];
2. 当num[i -1] <= 0 时:nums[i -1] * f[i -1];
因为我们要最小值,所以比较这三个结果的最小值即可,
g[i] = Math.min(num[i -1],Math.min(nums[i -1] * g[i -1],nums[i -1] * f[i -1]));
3.初始化:
1.下表的映射关系,
2.里面的值保证在之后的填表中是正确的 ——> f[0] = g[0] = 1;
4.填表顺序:
从左往右,两个表一起填
5.返回值:
F表里的最大值.
3.代码编写
4.乘积为正数的最长子数组长度
1.题目解析
给你一个整数数组 nums
,请你求出乘积为正数的最长子数组的长度。
一个数组的子数组是由原数组中零个或者更多个连续数字组成的数组。
请你返回乘积为正数的最长子数组长度。
2.讲解算法原理
1.状态表示:
由上题我们得出num[i]有大于0和小于0的两种状态
f[i] 表示为 i 位置的元素为结尾的所有子数组中乘积为正数的个数;
g[i] 表示为 i 位置的元素为结尾的所有子数组中乘积为负数的个数;
2.状态转移方程;
1.乘积为正数的个数分析:
当f[i] 的长度为1时 : num[i] > 0 时 ,1;
num[i] < 0 时 , 0;
当f[i] 的长度不为1时 : num[i] > 0 时 , f[i] = f[i - 1] +1;
num[i] < 0 时 , 因为不算0 所以f[i] =g[i -1] == 0?0: g[i -1] + 1;
当g[i] 的长度为1时 : num[i] > 0 时 ,0;
num[i] < 0 时 , 1;
当g[i] 的长度不为1时 : num[i] < 0 时 , g[i] = f[i - 1] +1;
num[i] > 0 时 , 因为不算0 所以g[i] =g[i -1] == 0?0: g[i -1] + 1;
3.初始化:
1.下表的映射关系,
2.里面的值保证在之后的填表中是正确的 ——> f[0] = g[0] = 1;
4.填表顺序:
从左往右,两个表一起填
5.返回值:
F表里的最大值.
3.代码编写
5.等差数列划分
1.题目解析
如果一个数列 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该数列为等差数列。
- 例如,
[1,3,5,7,9]
、[7,7,7,7]
和[3,-1,-5,-9]
都是等差数列。
给你一个整数数组 nums
,返回数组 nums
中所有为等差数组的 子数组 个数。
子数组 是数组中的一个连续序列。
2.讲解算法原理
1.状态表示:
dp[i] 表示为 i 位置的元素为结尾的所有子数组中等差数组的最大个数
2.状态转移方程;
0 | 1 | 2 | ...... | i-2 | i-1 | i |
a | b | c |
由等差数列的性质可知:
c - b == b - a
于是我们可以得出dp状态
当c - b == b - a 时 dp[ i ] = dp[i - 1] + 1;
当c - b =/= b - a 时 dp[ i ] = 0;
我们可以用三目运算符简化一下:
dp[ i ] = c - b == b - a ? dp[i - 1] + 1 : 0;
3.初始化:
1.下表的映射关系,
2.里面的值保证在之后的填表中是正确的 ——> dp[0] = dp[1] = 0;
4.填表顺序:
从左往右,两个表一起填
5.返回值:
F表里的最大值.
3.代码编写
6.最长湍流子数组
1.题目解析
给定一个整数数组 arr
,返回 arr
的 最大湍流子数组的长度 。
如果比较符号在子数组中的每个相邻元素对之间翻转,则该子数组是 湍流子数组 。
更正式地来说,当 arr
的子数组 A[i], A[i+1], ..., A[j]
满足仅满足下列条件时,我们称其为湍流子数组:
- 若
i <= k < j
:- 当
k
为奇数时,A[k] > A[k+1]
,且 - 当
k
为偶数时,A[k] < A[k+1]
;
- 当
- 或 若
i <= k < j
:- 当
k
为偶数时,A[k] > A[k+1]
,且 - 当
k
为奇数时,A[k] < A[k+1]
。
- 当
2.讲解算法原理
1.状态表示:
第一次分析:
dp[i] 表示为 i 位置的元素为结尾的最大湍流子数组的长度;
当我们只定义一个状态表示时,我们会发现用它表示时dp[i -1]的数字有两种状态
1.比后一位大 2.比前一位小 所以不行
第二次分析:
因为dp[i]的前一位有两种表现形式,所以我们要定义两种状态表示
f[i] 表示为 i 位置的元素为结尾的,最后呈现“上升”状态的最大湍流子数组的长度 ;
g[i]表示为 i 位置的元素为结尾的,最后呈现“下降”状态的最大湍流子数组的长度;
2.状态转移方程;
0 | 1 | 2 | ...... | i-2 | i-1 | i |
b | a |
1.当最后一位呈现“上升”状态的 情况分析:
当 a > b时 f[i] = g[i -1] +1;
当 a < b时 f[i] = 1;
当 a = b 时 ,0;
2.当最后一位呈现“下降”状态的 情况分析:
当 a > b时 1;
当 a < b时 g[i] = f[i -1] +1;
当 a = b 时 0;
3.初始化:
1.下表的映射关系
2.因为只有一个元素时,长度为一,所以我们把初始值都设为一,即可省略为1的这种情况,
4.填表顺序:
从左往右,两个表一起填
5.返回值:
F表和G表里的最大值.
3.编写代码
7. 单词拆分
给你一个字符串 s
和一个字符串列表 wordDict
作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s
则返回 true
。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
2.讲解算法原理
1.状态表示:
-
- 使用一个布尔数组 dp,其中 dp[i] 表示字符串 s[0..i−1]是否可以用字典中的单词完全拼接而成。
- 初始状态为 dp[0]=true 表示空字符串可以被认为是拼接成功的。
2.状态转移方程:
遍历字符串 s的每个位置 i,尝试找到一个 j(其中 0≤j<i),使得:
dp[j]=true: 即子字符串 s[0..j−1]可以被拼接。
s[j..i−1]∈wordDict: 即从位置 j 到位置 i−1的子字符串在字典中存在。
如果满足上述条件,就可以拼接出 s[0..i−1],因此 dp[i]=true;
if(dp[j - 1] && hash.contains(s.substring(j,i+1))){
dp[i] = true;
优化细节:
- 使用字典wordDict构建一个哈希集合 hash,加速对子字符串的存在性检查 (O(1) 时间复杂度)。
- 使用嵌套循环:
- 外层循环遍历字符串 s 的末尾位置 i。
- 内层循环倒序检查可能的拆分点 jjj,从 iii 开始尝试向前扩展。如果发现满足条件的 dp[j−1] 和 s[j..i−1],则可以直接确定 dp[i]=true,并退出内层循环(剪枝优化)。
3.初始化
1.下表的映射关系,---> s = ' ' + s;
2.里面的值保证在之后的填表中是正确的 ——> dp[0] = true;
4.填表顺序:
从左往右,两个表一起填
5.返回值:
f[n];