今天又是新的一周,我们的动态规划章节还没有讲解完成,因为动态规划的题型着实是比较多,我们已经讲完了几种比较重要且经典的题型,比如背包问题,包含0-1背包与完全背包,打家劫舍问题,买卖股票的最佳时期问题,上一次讲解我们还讲解了几道子序列的问题,那我们今天就继续我们以前的进度继续讲解动态规划。
第一题对应力扣编号为1143的题目最长公共子序列
我们以前是有讲过几道子序列的题目,比如最长上升子序列,那我们这道题目与前面的有何不同呢?我们首先来看一下今天的题目:

这道题目还是涉及到两个字符串,当然我们所找的公共子序列其实在每一个字符串里面未必是连续的,而且在每一个字符串的相对位置也未必一样,其实就是因为我们的两个字符串长度不一定一样,那很明显我们还是需要使用动态规划的思路来解决这道题目,我们当然还是需要使用动规五部曲来解决:
第一步还是确定dp数组以及下标的含义,dpij:长度为0, i - 1的字符串text1与长度为0, j - 1的字符串text2的最长公共子序列为dpij,这个我们要注意没有算上我们最后的元素,我们这样做是为了初始化方便,原因其实前面解释过了我可以再给大家解释一遍:如果定义 dpij为 以下标i为结尾的A,和以下标j 为结尾的B,那么 第一行和第一列毕竟要进行初始化,如果nums1i 与 nums20 相同的话,对应的 dpi0就要初始为1, 因为此时最长重复子数组为1。 nums2j 与 nums10相同的话,同理。
第二步就是确定递推公式,其实就是两种可能text1i - 1 与 text2j - 1相同,text1i - 1 与 text2j - 1不相同,如果text1i - 1 与 text2j - 1相同,那么找到了一个公共元素,所以dpij = dpi - 1j - 1 + 1;但是如果text1i - 1 与 text2j - 1不相同,那就看看text10, i - 2与text20, j - 1的最长公共子序列 和 text10, i - 1与text20, j - 2的最长公共子序列,取最大的。这个大家一定要注意,这也是本题目与前面不一样的地方,我们当前位置的元素不一样,我们就要取前面的两个最长公共子序列的最大值。
第三步是初始化dp数组,先看看dpi0应该是多少呢?text10, i-1和空串的最长公共子序列自然是0,所以dpi0 = 0;同理dp0j也是0,剩下的就看我们的递推公式了。
第四步就是确定遍历顺序,这个很明显我们根据上面的递推公式我们就可以知道dpij是由三个方向推导而来,

下面是举一个例子来理解我们的递推过程,我先比较当前位置的元素,还是看看是否相等,如果相等的花就说明我们是从dpi-1j-1传递过来的,如果不相等我们就取左面和上面的最大值作为我们的最长上升子序列的长度即可。大家看下面的示意图,我的行与列都空了一行或者一列,其实这就对应了我们dp数组的含义。

综合上述分析我们可以给出解题代码:
cpp
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
//dp数组的初始化注意我们的含义我们就要开text1.size()+1与text2.size()+1的长度
vector<vector<int>> dp(text1.size() + 1, vector<int>(text2.size() + 1, 0));
for (int i = 1; i <= text1.size(); ++i)
{
for (int j = 1; j <= text2.size(); ++j)
{
if (text1[i - 1] == text2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
return dp[text1.size()][text2.size()];
}
};
我还是要跟大家强调几句,注意我们dp数组的含义大家就不会出错,这里索引和定义dp数组很容易出错,长度永远比索引大1,因此最后我们输出的结果应该是dptext1.size()text2.size()才对。
第二题对应力扣1035的题目不相交的线
这是我们今天的第二道题目,这道题目没见过这样的背景,我们就直接看一下题目要求:

题目看似很抽象其实并不难理解,就是我们能连线的位置元素应该是相等的而且还要保证不与其他的线相交才可以,那我其实感觉与上一道题目有点相似都是一旦发现元素相等最大连线数会加1,但是大家要注意的是我们还需要保证不相交,大家要知道什么情况下会相交,只有新的两个元素在两个数组中的位置都大于前面两个元素在两个数组中的位置才可以不相交,还有一点大家需要明白其实就是说nums1和nums2的最长公共子序列是1,4,长度为2。 这个公共子序列指的是相对顺序不变(即数字4在字符串nums1中数字1的后面,那么数字4也应该在字符串nums2数字1的后面),这不与我们上面的题目一模一样,我就不再给大家重复动规五部曲了,我就直接给出大家解题代码:
cpp
class Solution {
public:
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
//定义dp数组
vector<vector<int>> dp(nums1.size() + 1, vector<int>(nums2.size() + 1, 0));
for (int i = 1; i <= nums1.size(); ++i)
{
for (int j = 1; j <= nums2.size(); ++j)
{
if (nums1[i - 1] == nums2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
return dp[nums1.size()][nums2.size()];
}
};
第三题对应力扣编号为53的题目最大子数组和
这道题目我们以前应该是做过,我们就直接看一下题目要求:

看到题目大家估计就有思路了,不就是连续相加嘛?我就可以给出一种暴力的解决方法:
cpp
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int result = INT32_MIN;
int count = 0;//存储连续和
for (int i = 0; i < nums.size(); ++i)
{
count += nums[i];
if (count > result) result = count;
if (count <= 0) count = 0;
}
return result;
}
};
这种思路很简单也很好理解,但是我们现在在动态规划章节我当然是要考虑使用动规五部曲来给大家讲解了,
第一步确定dp数组及其含义,dpi:包括下标i(以numsi为结尾)的最大连续子序列和为dpi。这个题目使用一个一维数组就可以了。
第二步是确定递推数组,我们不难发现其实dpi只有两个方向可以推出来:dpi - 1 + numsi,即:numsi加入当前连续子序列和,还有其中情况就是重新计算子数组的和因为目前的和一定不会最大的和。当然我们一定是取最大的,所以dpi = max(dpi - 1 + numsi, numsi)。
第三步dp数组的初始化,我们可以看到整个递推公式都是依赖dp0来继续往后递推的,因此我们就要初始化dp0,因此根据dpi的定义,很明显dp0应为nums0即dp0 = nums0。
第四步就是确定遍历顺序,这个很明显应该是从前往后遍历。
经过以上的分析我们就可以尝试给出解题代码:
cpp
class Solution {
public:
int maxSubArray(vector<int>& nums) {
if (nums.size() == 0) return 0;
//定义dp数组
vector<int> dp(nums.size(), 0);
dp[0] = nums[0];
int result = dp[0];
for (int i = 1; i < nums.size(); ++i)
{
//如果相加越来越小那我们就从头开始
dp[i] = max(dp[i - 1] + nums[i], nums[i]);
if (dp[i] > result) result = dp[i];
}
return result;
}
};
这个就不难理解,大家主要还是得了解一点我们最后的结果可不是dpnums.size() - 1而是里面的最大值,最后的和不一定是最大的,这点大家清楚这道题目就不难了。
第四题对应力扣编号为392的题目判断子序列
这是我们今天的最后一道题目,判断是不是子序列好像是不难的,我们还是先看一下题目要求:

读懂题目,此题不难,其实不适用动态规划也是可以的,
cpp
class Solution {
public:
bool isSubsequence(string s, string t) {
int i = 0, j = 0;
while(i < s.length() && j < t.length())
{
if (s[i] == t[j])i++;
j++;
}
return i == s.length();
}
};
我们就看看s里面的所有字符是否在t里面都出现过就可以了,如果最后i=s.length()的话就说明这就是子序列,但是我们应该还是可以使用动态规划的思路去解决的,我们来看一下动规五部曲如何解决:
第一步确定dp数组(dp table)以及下标的含义,dpij 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dpij。这个其实与我们今天第一题的思路是类似的。
第二步确定递推公式,这个应该与第一题也是类似的,if (si - 1 == tj - 1),这就说明t中找到了一个字符在s中也出现了,if (si - 1 != tj - 1,相当于t要删除元素,继续匹配,这样我们就可以给出我们的递推公式,if (si - 1 == tj - 1),那么dpij = dpi - 1j - 1 + 1;,因为找到了一个相同的字符,相同子序列长度自然要在dpi-1j-1的基础上加1,if (si - 1 != tj - 1),此时相当于t要删除元素,t如果把当前元素tj - 1删除,那么dpij 的数值就是 看si - 1与 tj - 2的比较结果了,即:dpij = dpij - 1;因为我是在t中寻找s如果当前不是的话我们就继续看下一个是不是,我是在t里面寻找s的每一个字符。
第三步就是dp数组的初始化,从递推公式可以看出dpij都是依赖于dpi - 1j - 1 和 dpij - 1,所以dp00和dpi0是一定要初始化的。这个也比较好理解。
第四步就是遍历顺序,同理从递推公式可以看出dpij都是依赖于dpi - 1j - 1 和 dpij - 1,那么遍历顺序也应该是从上到下,从左到右,这个与第一题是完全类似的。
这样经过上述分析我们就可以尝试给出本题的解题代码:
cpp
class Solution {
public:
bool isSubsequence(string s, string t) {
//初始化dp数组
vector<vector<int>> dp(s.size() + 1, vector<int>(t.size() + 1, 0));
for (int i = 1; i <= s.size(); ++i)
{
for (int j = 1; j <= t.size(); ++j)
{
if (s[i - 1] == t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = dp[i][j - 1];
}
}
return (dp[s.size()][t.size()] == s.size()) ? true : false;
}
};
其实大家只要理解了我们第一题的思路这道题其实非常类似。我们这道题就分享到这里。
今日总结
今天的题目其实都是子序列问题的变式题,大家只要明白一个其实不同的题目不过是递推公式会由变化,大家要注意类比学习,其实动态规划章节的题目类型比较多且杂,我们要对动规五部曲熟记于心,慢慢地大家就会发现动态规划其实是完全可以拿下的。我们今天就分享到这里,我们明天见!