目录
子数组系列问题:
1. 最⼤⼦数组和(medium)
解析:
1.状态表达式:
2.状态转移方程:
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.填表顺序:
5.返回值:
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[i]表示:以i位置为结尾的所有子数组的最大和_dp[i]表示:以i位置为结尾的所有子树组的最小和
_dp[i]=min(_dp[i-1]+nums[i-1],nums[i-1]);
dp[i]=max(dp[i-1]+nums[i-1],nums[i-1]);
return _max==_min?ret:max(ret,_max-_min);
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)
解析:
但是遇到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为结尾,所有子数组中乘积最小的值
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表后面的值
dp[0]=1,_dp[0]=1;
4.填表顺序:
从左往右
5.返回值:
返回最大的dp值,因为dp[i]表示:以i为结尾,所有子数组最大乘积的值
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)
题目意思很简单,就是求出最长的乘积为正数的子数组长度
解析:
dp[i]表示:以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] 即可
代码编写:
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]
代码编写:
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])
解析:
dp[i]表示:以i位置为结尾的湍流子数组的最长长度
条件: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
如果有nums[0] != nums[1] 就说明dp[1]=2,初始化长度为2
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)
解析:
if(dp[j-1]&&hash.count(str)) dp[i]=true;
在保证前面的字符串能够被拼接的条件下[0,j-1],还要保证最后一个单词在字典里面存在[j,i]
代码编写:
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)
解析:
1.状态表达式:
dp[i]表示:以i位置为结尾的字符串中的子串在环绕字符串中存在的个数
从左往右填
创建一个26大小的hash表白,里面只存放以某一个字符结尾的子串的最多存在的子串个数,因为长子串必定包含短子串
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位置为结尾的元素个数/长度,然后考虑判断条件,来确定状态转移方程,最后就思考细节问题即可,这一题就是要考虑长子串 遇到短子串会有包含关系