目录
动态规划的做题方式比较固定,一般就按照以下步骤进行
1.确定状态表示
2.写出状态转移方程
3.初始化与填表顺序
4.返回值
1.最大子数组和

按照题目意思,就是要找出整个数组中的某个区间,保证连续的同时总和最大,动态规划一般来说是以某个位置为结尾设置一个状态,所以我们先假定一个状态表示,先看看能不能推出状态转移方程,如果不可以就再更换
所以我们此时设置一个dp数组,表示某位置的状态,用dpi表示以i位置为结尾的所有子数组中的最大总和,简单画个图来理解一下,可以看到以i为结尾的子数组有很多个,我们可以根据长度来划分为两类情况,第一种:长度为1,也就是i位置自己构成一个子数组,第二种:长度大于1,也就是i位置和前面的数字进行结合构成子数组

那么思考一下,是不是只要取这两者的较大值就行了呢,因为总共就两种情况,不是自己一个数字就是和前面数字结合,并且结合一定是跟在i-1位置,因为子数组是连续的,而以i-1位置为结尾的子数组最大总和,是不是已经存储在dpi-1这个位置了,所以只要加上i位置的值即可
由此我们可以推测出状态转移方程,dpi=max(numsi,dpi-1+numsi)
只需要比较这两种情况谁更大即可
但是在刚接触动态规划这个类型的算法时,看着这个状态转移方程,我们可能会有疑问,那就是如果我当前位置依赖前面位置,那前面位置又依赖更前面的位置,我怎么保证我每次依赖的值存在并且合理呢,所以初始化和填表顺序就很重要
初始化,保证该表填写第一个位置的时候是正确的,才能保证后续所有位置都是正确的
填表顺序就根据状态转移方程来看,很明显i位置依赖i-1位置的话,说明右侧位置依赖左侧位置,那么填表顺序就应该是从左往右填,这样填右侧的时候能保证左侧已经填写,这样方程才是合理的
注意到i-1,在第一个位置的时候会发生越界,所以我们可以采取将dp数组多开一个位置的方式,避免越界问题,并且要保证多开的位置不影响表,因为以第一个位置为结尾的子数组最大和是它本身,所以dp0不能产生影响,我们只需要将它初始化为0即可
最后是返回值,每次填好一个位置,就判断一下大小,用一个变量ret来存储结果,每次取dpi和ret的最大值即可
后续习题讲解就没有那么多啰嗦的话了,第一道题为了将整个动态规划的做题逻辑讲清楚所以讲得比较详细
代码部分
这里注意一点,因为dp数组多开了一个位置,所以和原数组下标映射关系要减一,也就是dpi实际上对应的是numsi-1位置的数字
cpp
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int n=nums.size();
vector<int> dp(n+1,0);
int ret=INT_MIN;
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.环形子数组的最大和

环形的问题我们可以拆解成两种情况,第一种就是最大值不进循环,第二种就是最大值需要循环(红色部分表示最大值),对于第二种情况我们可以逆向思维,既然最大值循环会比较麻烦,但是剩余部分就是最小值,最小值不发生循环,只要求出最小值,然后用总和减去最大值就行
所以我们只需要用两种角度分别求一下最大值,取更大者即可

状态表示:fi表示以i为结尾的子数组的最大和,因为还要求最小和,所以要用两个表,gi表示以i位置为结尾的子数组的最小和
状态转移方程:有了第一题的经验,我们知道需要将数组按长度划分为1和大于1
fi=max(fi-1+numsi,numsi)
gi=min(gi-1+numsi,numsi)
初始化和填表顺序:同样的我们可以采取多开一个位置避免越界,因为第一个位置的最大和与最小和都是该位置的数字本身,所以全部初始化为0即可,并从左向右填表
对于最小值有个特殊处理,因为如果全部数字都是负数,也就是最小和会等于数组的总和,此时得到的第二种情况的最大值是0,但是子数组要求至少有一个元素,也就是最大和是个负数,所以这种情况特判一下,直接选择f表中最大值即可,因为按照第一种方法求最大值的时候,如果全是负数可以得到负数中的最大值,返回即可
一般情况,直接返回两种方法计算的最大值中的较大者
代码部分
同样注意动态规划表和原数组有一个减一的映射关系
cpp
class Solution {
public:
int maxSubarraySumCircular(vector<int>& nums) {
int n=nums.size();
int sum=0;
vector<int> f(n+1,0);
auto g=f;
int mmax=INT_MIN;
int mmin=INT_MAX;
for(int i=1;i<=n;i++){
sum+=nums[i-1];
f[i]=max(f[i-1]+nums[i-1],nums[i-1]);
g[i]=min(g[i-1]+nums[i-1],nums[i-1]);
mmax=max(mmax,f[i]);
mmin=min(mmin,g[i]);
}
if(mmin==sum)return mmax;
else return max(sum-mmin,mmax);
}
};
3.乘积最大子数组

乘积与最大和有个很大的区别就是,每次乘上去的数字的符号,会让结果可能一下子在最大和最小之间变化,也就是累乘积变化很大,所以会出现两种情况的最大值,一个是最小的负数再乘以负数得到最大,一个是最大的正数乘以正数得到最大
也就是这道题有两种状态需要表示,分别记录正数和负数的最大最小乘积
所以fi表示以i为结尾的乘积的最大正数,gi表示以i为结尾的乘积的最小负数,也就是f表存正数,g表存负数,那么状态转移方程就应该按照numsi的正负进行分类
对于数组的划分依旧是按照长度1和大于1
如果大于0,因为f表存正数,所以需要找fi-1,取两种情况最大值,g表存负数,因为numsi是正数,所以numsi单独的这种情况就直接排除
小于0的情况下同理,注意保证每个表符号正确

初始化与填表顺序,因为要求第一个位置乘积不会影响结果,那么将规划表全部初始化为0即可,从左往右进行填表
**返回值:**注意最终结果是要找乘积最大,如果只有一个元素,不管正负直接返回就是最大值,对于一般情况,我们最终应该返回f表的最大值,因为f表一定会大于等于g表,对于0的情况,以0为结尾肯定乘积为0,在初始化两张表的时候已经为0,不用进行填写
代码部分
同样注意下标映射
cpp
class Solution {
public:
int maxProduct(vector<int>& nums) {
int n=nums.size();
if(n==1)return nums[0];
int ret=INT_MIN;
vector<int> f(n+1);
auto g=f;
for(int i=1;i<=n;i++){
if(nums[i-1]>0){
f[i]=max(f[i-1]*nums[i-1],nums[i-1]);
g[i]=g[i-1]*nums[i-1];
}
else if(nums[i-1]<0){
f[i]=g[i-1]*nums[i-1];
g[i]=min(f[i-1]*nums[i-1],nums[i-1]);
}
ret=max(ret,f[i]);
}
return ret;
}
};
4.乘积为正数的最长子数组长度
1567. 乘积为正数的最长子数组长度 - 力扣(LeetCode)

同样的,关于乘积问题,变符号的时候尤其要特殊考虑一下,要把细节注意到才能保证不错
首先是状态表示,根据经验我们猜测,要使用两个状态表
fi表示以i位置为结尾,乘积为正数的最长子数组的长度
gi表示以i位置为结尾,乘积为负数的最长子数组的长度
那么对于i位置的正负进行分类讨论,当numsi>0,此时因为要最长,所以我们试着与前面的位置进行连接,所以只需要找到前一个位置乘积为正数的最长长度,加上1即可,即使不存在这个长度,也就是fi-1==0,结果为1,也就是以当前位置一个位置作为最长子数组也是合理的
但是对于要变号的gi,如果前一个位置乘积为负数的最长长度,如果不存在我们不能直接加1,因为此时该位置本身不能作为乘积为负数的最长子数组,所以要判断一下gi-1是否为0
同理,在numsi<0时,在变号的位置要进行特判,由此推出以下的状态转移方程

初始化,将所有位置初始化为0即可,从左向右填表
返回表内最大值即可
代码部分
同样注意,多开一个位置的状态表,要改一下下标映射
cpp
class Solution {
public:
int getMaxLen(vector<int>& nums) {
int n = nums.size();
int ret = INT_MIN;
vector<int> f(n + 1);
auto g = f;
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;
}
ret = max(ret, f[i]);
}
return ret;
}
};
5.等差数列划分

依旧是根据经验,先假定一个状态表示
dpi表示以i为结尾的等差数列的个数
对于等差数列abc,显然b-a等于c-b,所以对于i位置,只需判断和前面两个位置是否构成等差数列
如图,假设蓝线部分是以g为结尾的所有等差数列的情况,那么如果X可以和fg构成等差数列,那么相当于在三条蓝线的情况下,多了一种fgX的情况,也就是dpi=dpi-1+1
条件是numsi-numsi-1==numsi-1-numsi-2

对于初始化,只需要全部取0即可,因为对于某个位置为结尾可以一个等差子数组也没有,从左向右填表,注意因为要用到i-1和i-2,所以对于原数组从下标2开始即可,因为1个和2个数字本身就不能构成等差数列
所以最终只需要遍历dp表,把所有的总和加起来即可
代码部分
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-1]-nums[i-2]==nums[i]-nums[i-1])dp[i]=dp[i-1]+1;
ret+=dp[i];
}
return ret;
}
};
6.最长湍流数组

简单解释一下题目意思,所谓湍流数组其实就是像下面这张图片一样,把每个尖端当作数组的元素,某元素两边比该元素都大或者都小

可以看到对于湍流数组,可以是向下走为结尾,也可以是向上走为结尾,所以我们设置两个状态
fi表示以i为结尾且向上走的最长湍流数组的长度
gi表示以i为结尾且向下走的最长湍流数组的长度
那么对于fg的选择,要看当前位置和前一个位置的关系,如果numsi>numsi-1,那么相当于这个结尾是向上走的,那么前一个位置应该是向下走的,也就是fi=gi-1+1
同样的,如果是这个结尾是向下走的,那么gi=fi-1+1
初始化,全部都赋值为1,因为一个数字本身可以构成向上或者向下的湍流数组,从左向右填表
返回值,取fg表中的最大值即可
代码部分
cpp
class Solution {
public:
int maxTurbulenceSize(vector<int>& nums) {
int n=nums.size();
vector<int> f(n,1);
auto g=f;
int ret=1;
for(int i=1;i<n;i++){
if(nums[i]>nums[i-1])f[i]=g[i-1]+1;
else if(nums[i]<nums[i-1])g[i]=f[i-1]+1;
ret=max(ret,max(f[i],g[i]));
}
return ret;
}
};
7.单词拆分

这道题因为涉及字符串是否存在于给出的字典中,要用到一点哈希表的知识,也就是将字典中的字符串提前存储在哈希表中,这样可以直接用来判断某个字符串是否在字典中
因为要查找字符串,显然如果我们遍历到某个位置i时,要多开一个变量j从i位置向前搜寻,开辟出长度为i-j+1的一个字符串,当这j,i区间的字符串如果存在于字符串中,并且0,j-1区间内的字符串可以由字典中字符串拼接起来,那么此时我们可以认为以0为起点,以i为结尾的字符串可以由字典中的字符串拼接而成

根据上面的操作过程,我们可以假定一个状态表示
dpi表示以0为开头i为结尾的字符串,是否可以被字典中的字符串拼接而成
如果刚刚的操作成立,也就是dpj-1==true且s.substr(j,i-j+1)这一串字符串在字典中,dpi=true
如果遍历到开头都不存在这种情况,那么dpi=false
所以我们将所有位置初始化为false,从左向右填表,但是因为第一个单词的前面没有true存在,所以我们额外开一个位置,让dp0=true,让第一个单词能顺利查找到,因为下标映射导致截取字符串的书写比较麻烦,所以我们可以让给定字符串前面加上一个空格,这样让下标映射保持一致
返回值:判断dp表的最后一个位置是否为true即可
代码部分
cpp
class Solution {
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> st;
for (auto& e : wordDict) {
st.insert(e);
}
string tmp = " " + s;
s = tmp;
int n = s.size();
vector<bool> dp(n, false);
dp[0] = true;
for (int i = 1; i < n; i++) {
for (int j = i; j >= 1; j--) {
if (dp[j - 1] == true && st.count(s.substr(j, i - j + 1))) {
dp[i] = true;
break;
}
}
}
return dp[n-1];
}
};
8.环绕字符串中唯一的子字符串
467. 环绕字符串中唯一的子字符串 - 力扣(LeetCode)

题目意思就是,在26个字母按顺序无限循环的字符串中,给定字符串中有多少个子字符串可以出现在无限循环的字符串中,比如abc,cde,zab都是按照顺序的一串子字符串
我们先注意一点,也就是对于某个字符为结尾的所有子字符串中,它的顺序是固定的
比如abcdecde这个字符串,很明显cde包含在abcde这个字符串中,也就是说abcde计算字符串出现个数时已经包含了cde的情况,也就是说对于以某个字符为结尾的子字符串来说,我们只需要取最大值即可
所以我们猜测一个状态表示
dpi表示以i为结尾共有多少个子字符串出现在base中,然后每次更新以si这个字符为结尾的个数,取最大值,所以还要用到一个哈希表,存储以某个字符为结尾的子字符串出现在base中的最大个数
状态转移方程,当满足si-1+1==si||si-1=='z'&&si=='a'这个条件时,dpi=dpi-1+1,也就是与前面字符结合加上当前字符本身的情况
因为每个字符都会出现在base中,所以所有值初始化为1,从左向右填表
最终遍历一次哈希表,取出所有最大值的总和即可
代码部分
cpp
class Solution {
public:
int findSubstringInWraproundString(string s) {
unordered_map<char,int> hash;
int n=s.size();
vector<int> dp(n,1);
hash[s[0]]=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]+1;
}
hash[s[i]]=max(hash[s[i]],dp[i]);
}
int ret=0;
for(auto& [x,y]:hash){
ret+=y;
}
return ret;
}
};