目录
[1. 最⼤⼦数组和(medium)](#1. 最⼤⼦数组和(medium))
[2. 环形⼦数组的最⼤和(medium)](#2. 环形⼦数组的最⼤和(medium))
[3. 乘积最⼤⼦数组(medium)](#3. 乘积最⼤⼦数组(medium))
[4. 乘积为正数的最⻓⼦数组(medium)](#4. 乘积为正数的最⻓⼦数组(medium))
[5. 等差数列划分(medium)](#5. 等差数列划分(medium))
[6. 乘积为正数的最⻓⼦数组(medium)](#6. 乘积为正数的最⻓⼦数组(medium))
[7. 单词拆分(medium)](#7. 单词拆分(medium))
[8. 环绕字符串中唯⼀的⼦字符串(medium)](#8. 环绕字符串中唯⼀的⼦字符串(medium))
在经过前面多节动态规划的学习之后,我们对多状态动态规划问题有了更深入的体会。现在,让我们进入新的章节 ------ 子数组和子序列问题。将这两小节放在一起进行对比,通过学会总结和对比,我们能够更深刻地理解其中的含义。
子数组系列问题:
1. 最⼤⼦数组和(medium)
题目意思很简单,就是要求出数组内和最大的一段子数组
解析:
1.状态表达式:
dp[i]表示:以i位置为结尾的所有子数组和里面的最大值
2.状态转移方程:
画出dp表,可以发现所有子数组被分成了两类,一类是只以自己结尾,另一类是自己+前面的最大和子数组,那么由这两类构成一个状态转移方程:
cpp
dp[i]=max(dp[i-1]+nums[i-1],nums[i-1]);
3.初始化:
这里添加一个虚拟节点,为了避免访问越界i-1,添加一个虚拟节点初始化为0即可,这样就算nums[0]<0是一个负值,最后dp[1]取值都是等于nums[0]
max(dp[i-1]+nums[i-1],nums[i-1]);
4.填表顺序:
由i-1 -> i 所以填表顺序就是从左往右填
5.返回值:
设置一个ret=nums[0] 返回最大值dp表内的最大值即可;
代码编写:
cpp
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n=nums.size();
int ret=nums[0];
vector<int> dp(n+1);
for(int i=1;i<=n;i++)
{
dp[i]=max(dp[i-1]+nums[i-1],nums[i-1]);
ret=max(dp[i],ret);
}
return ret;
}
};
总结:
最大子数组和是一个十分经典的问题,一定一定要完全弄懂弄透;
2. 环形⼦数组的最⼤和(medium)
求出环形子数组里面的最大和
解析:
画图:
所以分别设置两个dp表来分别求出这个数组内的最大和最小值,然后进行判断max(sum-_min,_max) 谁最大,就返回谁
1.状态表达式:
dp[i]表示:以i位置为结尾的所有子数组的最大和
_dp[i]表示:以i位置为结尾的所有子树组的最小和
2.状态转移方程:
cpp
_dp[i]=min(_dp[i-1]+nums[i-1],nums[i-1]);
dp[i]=max(dp[i-1]+nums[i-1],nums[i-1]);
3.初始化:
dp[0]为添加的一个虚拟节点,初始化为0即可
4.填表顺序:
从左往右
5.返回值:
返回值有点讲究,返回数组总和-_min 和 _max的最大值,如果_min==总和就说明整个数组都是负数,返回最小的负数
cpp
return _max==_min?ret:max(ret,_max-_min);
代码编写:
cpp
class Solution {
public:
int maxSubarraySumCircular(vector<int>& nums) {
int n=nums.size();
vector<int> dp(n+1);
int ret=nums[0];
int sum=0;
int f=0;
for(auto e : nums)
{
if(e>0) f=1;
sum+=e;
}
for(int i=1;i<=n;i++)
{
dp[i]=min(dp[i-1]+nums[i-1],nums[i-1]);
ret=min(ret,dp[i]);
}
vector<int> _dp(n+1);
int _ret=nums[0];
for(int i=1;i<=n;i++)
{
dp[i]=max(dp[i-1]+nums[i-1],nums[i-1]);
_ret=max(_ret,dp[i]);
}
cout<<ret<<" "<<_ret<<endl;
if(f==0) return _ret;
return max(sum-=ret,_ret);
}
};
总结:
这一题环形数组是一体很经典的题目,注意要分别对最大值进行分类讨论
3. 乘积最⼤⼦数组(medium)
求最大乘积连续子数组
解析:
这个题目其实还是很恶心人的,很多测试用例都贼恶心
1.状态表达式:
那么遇到eg:1 2 3 4 5 像这种连续的正数可以很顺利的通过
但是遇到eg:[-2,3,-4] 取最大值就只能取到3,可是最大值应该是 -2 * 3 *-4 才对
所以一个dp表只用来存最大值是不够的,还应该设置一个_dp表专门来存最小值(小于0)
就可以让dp取最大的空间变大dp[i]=max(dp[i-1]*nums[i-1],max(nums[i-1],_dp[i-1]*nums[i-1]));
dp[i]表示:以i为结尾,所有子数组最大乘积的值
_dp[i]表示:以i为结尾,所有子数组中乘积最小的值
2.状态转移方程就是:
cpp
dp[i]=max(dp[i-1]*nums[i-1],max(nums[i-1],_dp[i-1]*nums[i-1]));
_dp[i]=min(dp[i-1]*nums[i-1],min(_dp[i-1]*nums[i-1],nums[i-1]));
3.初始化:
dp[0]一定要初始化为1,否则后面的所有值都是0,不能影响dp表后面的值
cpp
dp[0]=1,_dp[0]=1;
4.填表顺序:
从左往右
5.返回值:
返回最大的dp值,因为dp[i]表示:以i为结尾,所有子数组最大乘积的值
代码编写:
cpp
class Solution {
public:
int maxProduct(vector<int>& nums) {
int n=nums.size();
vector<int> dp(n+1);
vector<int> _dp(n+1);
dp[0]=1,_dp[0]=1;
int ret=nums[0];
for(int i=1;i<=n;i++)
{
dp[i]=max(dp[i-1]*nums[i-1],max(nums[i-1],_dp[i-1]*nums[i-1]));
_dp[i]=min(dp[i-1]*nums[i-1],min(_dp[i-1]*nums[i-1],nums[i-1]));
ret=max(dp[i],ret);
}
return ret;
}
};
总结:
求关于子数组乘积,从上面可以看出,一个最大dp表是不够的,还需要一个最小的dp表来表示负数,然后来扩大最大dp表的范围
4. 乘积为正数的最⻓⼦数组(medium)
题目意思很简单,就是求出最长的乘积为正数的子数组长度
解析:
1.状态表达式:
dp[i]表示:以i位置为结尾的所有子数组中乘积为正数的最长长度
但是这一题跟上一题一样,如果只有关于正数乘积的最长长度,不记录负数乘积的最长长度,就无法得出关于当前nums[i] < 0 的时候求出 _dp[i-1] * nums[i] 更长的正数乘积长度
所以还要单独设置一个_dp[i]来表示负数乘积的更长长度,这样dp[i] 的选择返回会变的更大:
_dp[i]表示:以i位置为结尾的所有子数组中乘积为负数的最长长度
2.状态转移方程:
当前位置就有两种情况可以考虑:
nums[i] > 0 --> dp[i] = dp[i-1] + 1
但是不能保证所有情况下,_dp[i-1]都是存在的,所以每次判断_dp[i]都要有一个前提:
_dp[i]=_dp[i-1]==0?0:_dp[i-1]+1;
nums[i] < 0 --> _dp[i] = dp[i-1] + 1
但是不能保证所有情况下,_dp[i-1]都是存在的,所以每次判断_dp[i]都要有一个提:
dp[i]=_dp[i-1]==0?0:_dp[i-1]+1;
3.初始化:
因为要返回的是乘积为正数的最大长度,所以我们多开一个虚拟节点,为了不会越界访问,在dp[1]这个位置跟dp[0]这个位置没有必然联系,因为dp[0]是一个多开的虚拟节点,所以dp[1]等于0 还是等于1取决于nums[1-1]本身,所以dp[0]初始化为0即可
4.填表顺序:
从左往右填
5.返回值:
返回最大的dp[i] 即可
代码编写:
cpp
class Solution {
public:
int getMaxLen(vector<int>& nums) {
int n=nums.size();
vector<int> dp(n+1);
auto _dp=dp;
int ret=0;
for(int i=1;i<=n;i++)
{
if(nums[i-1]>0)
{
dp[i]=dp[i-1]+1;
_dp[i]=_dp[i-1]==0?0:_dp[i-1]+1;
}
else if(nums[i-1]<0)
{
dp[i]=_dp[i-1]==0?0:_dp[i-1]+1;
_dp[i]=dp[i-1]+1;
}
else dp[i]=_dp[i]=0;
ret=max(ret,dp[i]);
}
return ret;
}
};
总结:
这一题跟上一题很类似,具体还是要单独来分析每一种情况,只要在草稿纸上分析透彻了就大差不差了,首先就是分析当前dp[i]格子可以遇到几种情况,会遇到前一个值的乘积可能大于0 可能小于0,在考虑当前格子的nums[i]是大于0 还是小于0 ,这样一排列组合 就有4种情况
5. 等差数列划分(medium)
题意很简单,就是求出这个数组内所有的等差数列的个数
解析:
1.状态表达式:
dp[i]表示:以i位置为结尾的所有子数组满足等差数列的数组的个数
2.状态转移方程:
条件:nums[i] + nums[i-2] == 2*nums[i-1]
才能满足是等差数列
eg: 1 2 3 4 5
下标:0 1 2 3 4
dp[2] = 1 , dp[3] = 2 , dp[4] = 3
也就是说dp[i] = dp[i-1] + 1;
3.初始化:
因为是求关于等差数列的个数,所以只有满足条件才能 +1 所以这一题不用多开虚拟节点,并且全部都初始化为0即可
4.填表顺序:
从左往右填
5.返回值:
因为每一个i位置为结尾都是一个独立的情况,所以要设置ret += 所有的dp[i]
代码编写:
cpp
class Solution {
public:
int numberOfArithmeticSlices(vector<int>& nums) {
int n=nums.size();
if(n<3) return 0;
vector<int> dp(n);
int ret=0;
for(int i=2;i<n;i++)
{
if(nums[i-2] + nums[i] == 2*nums[i-1]) dp[i] = dp[i-1] + 1;
ret+=dp[i];
}
return ret;
}
};
总结:
这一题只用设置一个dp表,只用分析当前位置的3个数字是否满足等差的情况,如果满足就可以跟dp[i-1]联动起来,因为dp[i-1]满足的,dp[i]也会满足
6. 乘积为正数的最⻓⼦数组(medium)
题目意思就是要满足:
(nums[i]>nums[i-1]&&nums[i-2]>nums[i-1]) || (nums[i]<nums[i-1]&&nums[i-2]<nums[i-1])
这样交错式的就是一个湍流子数组,要返回最长的这种子数组的长度
解析:
1.状态表达式:
dp[i]表示:以i位置为结尾的湍流子数组的最长长度
2.状态转移方程:
因为返回的是最长长度,所以每次都只是在满足dp[i-1]条件下,长度+1,所以这一题主要就是要分析在什么情况下+1
条件:nums[i] < nums[i-1]&&nums[i-2] < nums[i-1] 下满足先升后降
条件:nums[i] > nums[i-1]&&nums[i-2] > nums[i-1] 下满足先降后升
只有在这种条件下才能满足dp[i] = dp[i-1] + 1
细节问题:
最后就是不满足任何一种情况,那么dp[i]就只能置为1本身的长度了
3.初始化:
这一题不用多开空间,但是要访问dp[i] dp[i-1] dp[i-2] 三个位置的值,其中最远的就是i-2,所以为了避免越界,要提前处理好nums.size()==2的情况,开始都将dp[0] = dp[1] =1
如果有nums[0] != nums[1] 就说明dp[1]=2,初始化长度为2
4.填表顺序:
从左往右填表
5.返回值:
设置一个ret,返回dp[i] 里面的最大值
编写代码:
cpp
class Solution {
public:
int maxTurbulenceSize(vector<int>& nums) {
int n=nums.size();
if(n==1) return 1;
vector<int> dp(n);
dp[0]=dp[1]=1;
if(nums[1]!=nums[0]) dp[1]=2;
if(n==2) return dp[1];
int ret=0;
for(int i=2;i<n;i++)
{
if((nums[i]>nums[i-1]&&nums[i-2]>nums[i-1]) || (nums[i]<nums[i-1]&&nums[i-2]<nums[i-1]))
dp[i] = dp[i-1] + 1;
else if((nums[i]>nums[i-1]&&nums[i-2]<=nums[i-1]) || (nums[i]<nums[i-1]&&nums[i-2]>=nums[i-1]))
dp[i] = 2;
else dp[i] = 1;
cout<<dp[i]<<endl;
ret=max(ret,dp[i]);
}
return ret;
}
};
总结:
这一题的细节情况较多,只要能在草稿纸上画图耐心分析,一定可以考虑到所有的情况的!
7. 单词拆分(medium)
题目意思很简单,就是给了一个字典,然后可以多次使用里面的单词,返回能否拼接成字符串s
解析:
1.状态表达式:
dp[i]表示:以i位置为结尾的[0,i]区间的字符串,能否被拼接,所以dp是一个bool类型的数组
2.状态转移方程:
if(dp[j-1]&&hash.count(str)) dp[i]=true;
判断条件:
在保证前面的字符串能够被拼接的条件下[0,j-1],还要保证最后一个单词在字典里面存在[j,i]
3.初始化:
由于最后dp[i] 跟 dp[j-1] 有关,j --> [0,i] 所以为了防止越界访问,可以多开一个虚拟节点
然后填入true,dp[0]=true,为了不影响后续的填表正确
4.填表顺序:
从左往右
5.返回值:
返回dp[n],表示以n位置为结尾的字符串能否被拼接
代码编写:
cpp
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
int n=s.size();
unordered_map<string,int> hash;
for(auto e : wordDict) hash[e]++;
vector<bool> dp(n+1);
dp[0]=true;//保证后续填表是正确的
for(int i=1;i<=n;i++)
{
for(int j=1;j<=i;j++)
{
string str=s.substr(j-1,i-j+1);
if(dp[j-1]&&hash.count(str)) dp[i]=true;
}
}
return dp[n];
}
};
总结:
这一题真的有点难度,一定要吃透才行,如果没见过这个体型,那这次记住了就直接套模板了
8. 环绕字符串中唯⼀的⼦字符串(medium)
题目意思很简单,就是在[a-z-a]这个连续的字符串内找到字符串s有多少个字串在这里面存在
解析:
1.状态表达式:
dp[i]表示:以i位置为结尾的字符串中的子串在环绕字符串中存在的个数
2.状态转移方程:
判断条件:
要满足s[i-1] + 1 =s[i] || s[i-1] == 'z' && s[i] == 'a' 即可dp[i] += dp[i-1]
因为此时的dp[i]满足条件只是满足的子串长度变长了,并不是个数变多了
3.初始化:
所有dp[i]=1 都初始化为1,表示每一个字符都是可以单独在环绕字符里面存在的
4.填表顺序:
从左往右填
5.返回值:
eg:cbc这个字符串,就会存在 'c' 'cb' 'b' 三种情况,而多出来的一个'c'则不记录
创建一个26大小的hash表白,里面只存放以某一个字符结尾的子串的最多存在的子串个数,因为长子串必定包含短子串
最后返回hash表内的所有和
代码编写:
cpp
class Solution {
public:
int findSubstringInWraproundString(string s) {
int n=s.size();
vector<int> dp(n,1);
for(int i=1;i<n;i++)
if(s[i-1]+1==s[i] || s[i-1]=='z'&&s[i]=='a') dp[i]+=dp[i-1];
int hash[26]={0};
for(int i=0;i<n;i++)
hash[s[i]-'a']=max(hash[s[i]-'a'],dp[i]);
int ret=0;
for(auto e : hash) ret+=e;
return ret;
}
};
总结:
这一题的细节问题很多,还需要自己下去多思考多总结,总之动态规划问题千变万变,兜离不开以i位置为结尾的元素个数/长度,然后考虑判断条件,来确定状态转移方程,最后就思考细节问题即可,这一题就是要考虑长子串 遇到短子串会有包含关系