文章目录
蓝桥备赛(5)
198.打家劫舍
class Solution {
public:
int rob(vector<int>& nums) {
vector<int> dp(nums.size(), 0);
if(nums.size() == 1) return nums[0];
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
//dp[1] = nums[1];
for(int i = 2; i < nums.size(); i++)
{
dp[i] = max(dp[i-2]+nums[i], dp[i-1]);
}
return dp[nums.size()-1];
}
};
我写出了跟正确答案很相近的代码,但有几个样例是不能通过的,问题就在我最开始将dp[1]初始化成nums[1]了,dp[1]应当初始化成nums[0]和nums[1]中较大的那个数,递推公式这次推理正确了,当前dp值取决于前两个值。
121.买卖股票的最佳时机
方法一:
class Solution {
public:
int maxProfit(vector<int>& prices) {
//开辟一个二维dp数组,dp[i][0]表示第i天持股最大金额,dp[i][1]表示第i天不持股最大金额
int n = prices.size();
vector<vector<int>> dp(n, vector<int>(2));
dp[0][0] = -prices[0];
dp[0][1] = 0;
for(int i = 1; i < n; i++)
{
dp[i][0] = max(dp[i-1][0], -prices[i]);
dp[i][1] = max(dp[i-1][1], dp[i-1][0]+prices[i]);
}
return max(dp[n-1][0], dp[n-1][1]);
//return dp[n-1][1];
}
};
这道题不再是简单的背包问题了!要跳出之前写背包问题的思维,这里的需要用二维dp数组来记录数据,dp[i] [0]表示第i天持股最大金额,dp[i] [1]表示第i天不持股最大金额,这里是以自己目前有多少前来衡量,所以买股票则要-prices[i],卖股票则为+prices[i]。再就是递推公式的理解,与之前不一样,这里有两个递推式分为持股票和不持股票。在持股票的一部分中有两种选择:①保持第i天前的持股状态金额②选择当前第i天买股;题干中需要求利润最大值,那么我们需要在这两种情况中取最大值。在不持股的一部分中也有两种两种选择:①保持第i天前的不持股状态金额②选择当前第i天进行卖股票,这里需要注意我们卖股票要在第i天前选择的最优持股金额方案中加上当前的price就是最终买卖股票的最佳时机了。最后在持股和不持股中最优解中取最大值得到我们的答案,我认为直接return dp[n-1] [1]也是可以的,即便没有进行交易,最后的值也会更改到0。
方法二:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int cost = INT_MAX, profit = 0;
for(int p : prices)
{
cost = min(cost, p);
profit = max(profit, p-cost);
}
return profit;
}
};
这个写法总归来说也是动态规划,只不过是实现出来不同而已,普遍的动态规划是用dp数组不断更新最优数据,而这里就用临时变量来替代dp数组,但是都呈现出了动态更新的效果。这个写法巧妙在只用了一个for循环就达到了每一次的cost和profit都是前i天最优解,cost通过每天的取min更新最小花费,profit通过每天取max更新第i天前的最大利润,厉害,实在是厉害
122.买卖股票的最佳时机II
解法一:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<vector<int>> dp(n, vector<int>(2));
dp[0][0] = -prices[0];
dp[0][1] = 0;
for(int i = 1; i < n; i++)
{
dp[i][0] = max(dp[i-1][0], dp[i-1][1]-prices[i]);
dp[i][1] = max(dp[i-1][1], dp[i-1][0]+prices[i]);
}
return dp[n-1][1];
}
};
是不是和买卖股票很像?只改了一个地方:从dp[i] [0] = max(dp[i-1] [0], 0-prices[i])改成了;dp[i] [0] = max(dp[i-1] [0], dp[i-1] [1]-prices[i]);这也对应的题目中不同的条件,这一题不同在可以任意天数买卖股票,对一个时间段的买卖次数是没有限定的,而在上一题中在一段时间内只能买卖一次,那么我们每次如果要买第i天的股票的话对应的利润只能是0-prices[i],现在可以多次买卖,那么当第i天买股之后就要基于上一次卖股的最大利润减去当天的价格也就是dp[i-1] [1] - prices[i]
解法二:
class Solution {
public:
int maxProfit(vector<int>& prices) {
int result = 0;
for(int i = 1; i < prices.size(); i++)
{
result += max(prices[i]-prices[i-1], 0);
}
return result;
}
};
这个解法也就是贪心的解法,在之前的文章中就粘过了代码。主要思想是:只要当天的价格与前一天的价格之差是大于0的那么我就累加这个利润,遍历完这个数组得到的result就是题中所求的最大利润。
123.买卖股票的最佳时机III
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
vector<vector<int>> dp(n, vector<int>(4, 0));
dp[0][0] = -prices[0];
dp[0][2] = -prices[0];
for(int i = 1; i < n; i++)
{
dp[i][0] = max(dp[i-1][0], -prices[i]);
dp[i][1] = max(dp[i-1][1], dp[i-1][0]+prices[i]);
dp[i][2] = max(dp[i-1][2], dp[i-1][1]-prices[i]);
dp[i][3] = max(dp[i-1][3], dp[i-1][2]+prices[i]);
}
return dp[n-1][3];
}
};
相较于前面也是在交易次数上面做了改动,最多进行两笔交易那么就代表这一段时间内你只能交易一次或两次,从而我们可以想到就是状态多了2个,其他其实都没有改变,所以这里定义了n * 4的一个二维dp数组,递推公式也是大同小异,只不过需要理解的是dp[0] [2]的初始化,为什么初始化为-prices[0]呢?其实可以理解成在第一天先买再卖再买,也就是只支付了一次的第一天的价钱。
300.最长递增子序列
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int> dp(nums.size(), 1);
for(int i = 1; i < nums.size(); i++)
{
for(int j = i-1; j >= 0; j--)
{
if(nums[i] > nums[j]) dp[i] = max(dp[j]+1, dp[i]);
}
}
sort(dp.begin(), dp.end());
return dp.back();
}
};
最开始递推关系那里出错了,其实自己是发现了的,但是就是不知道怎么去表达递推公式。一个参数想不明白的问题可以尝试用两个参数去想,先把普遍性现象展现出来才是关键。假设nums[i] > nums[j]那么递推公式可以推出dp[i] = max(dp[i], dp[j]+1),首先要注意的是i一定是大于j的,然后这里要用到两层循环,外层循环枚举nums数组,内层循环寻找0-i范围内的最长递增子序列,所以max函数里有dp[i]的目的是每次都要确保是这个范围内的最长递增子序列,最后在返回dp数组的时候也不能理所应当的返回最后一个元素,最后一个元素其实不一定在最长递增子序列内,因为这里的dp[i]的定义是下标为i时,包含nums[i]的最长递增子序列。举个例子{5, 7, 8, 9, 6},dp.back()的值是2,因为包含6的最长递增子序列为{5, 6},它的长度为2,所以我们可以使用sort函数,使得最后一个元素是我们要找的最大值。
718.最长重复子数组
class Solution {
public:
int findLength(vector<int>& nums1, vector<int>& nums2) {
int n1 = nums1.size(), n2 = nums2.size();
vector<vector<int>> dp(n1+1, vector<int>(n2+1, 0));
int res = 0;
for(int i = 1; i <= n1; i++)
{
for(int j = 1; j <= n2; j++)
{
if(nums1[i-1] == nums2[j-1]) dp[i][j] = dp[i-1][j-1]+1;
res = max(res, dp[i][j]);
}
}
return res;
}
};
这一题的难点所在呢,就是如何定义dp数组,它这里给出了两个数组假设他们的长度分别为n1和n2,那么就定义一个(n+1) * (n+1)大小的一个dp数组,dp[i] [j]表示第nums[i-1]和nums[j-1]之前的最长重复子数组的长度这里的递推关系式为if(nums[i-1] == nums[j-1]) dp[i] [j] = dp[i-1] [j-1]+1,表示如果当前这两个位置相等的话就在上一个基础上加一,这里的上一个就是当前位置的左上角,所以是i-1和j-1。这也就是为什么开辟数组大小的时候是(n1+1) * (n2+1)了,这样可以将第一行和第一列直接默认初始化成0,否则如果按照n * n大小开辟数组还需要先初始化第一行和第一列,初始化的值还要根据两个数组来决定,这样十分繁琐,最后返回的也不是数组的最后一个元素,而是需要遍历二维数组中的最大值,遍历二维数组这样的重复操作就在赋值的时候一并完成,使得res一直都是符合题意的最大的长度。
1143.最长公共子序列
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int n1 = text1.size(), n2 = text2.size();
vector<vector<int>> dp(n1+1, vector<int>(n2+1, 0));
for(int i = 1; i <= n1; i++)
{
for(int j = 1; j <= n2; 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[n1][n2];
}
};
这一题和最长重复子数组很像,首先对比一下子序列和子数组的区别:子数组其实就是连续的子序列,拿数组{1,2,3,4,5,6}举例,{1,3,4}就是这个数组的一个子序列,{1,2,3}就是这个数组的一个子数组。在这一题中不同的是在递推公式上,如果text[i-1]不等于text[i-2],那么dp值是其左边和上面两个数中取最大值,这一点也是我没能想到的。
392.判断子序列
class Solution {
public:
bool isSubsequence(string s, string t) {
int n1 = s.size(), n2 = t.size();
vector<vector<int>> dp(n1+1, vector<int>(n2+1, 0));
for(int i = 1; i <= n1; i++)
{
for(int j = 1; j <= n2; j++)
{
if(s[i-1] == t[j-1]) dp[i][j] = dp[i-1][j-1]+1;
else dp[i][j] = max(dp[i][j-1], dp[i-1][j]);
}
}
return n1 == dp[n1][n2] ? true:false;
}
};
是不是又和上一题的最长公共子序列很像?仅仅只改动了最后一行代码,哎我怎么就想不到呢,思路就是定义一个dp二维数组,表示s[i-1]和t[i-1]最长公共子序列的长度,最后只需要判断dp的末尾元素是否等于s.size()就能ac本题了。
115.不同的子序列
class Solution {
public:
int numDistinct(string s, string t) {
int n1 = s.size(), n2 = t.size();
vector<vector<unsigned long long>> dp(n1+1, vector<unsigned long long>(n2+1, 0));
for(int i = 0; i <= n1; i++) dp[i][0] = 1;
for(int i = 1; i <= n1; i++)
{
for(int j = 1; j <= n2; j++)
{
if(s[i-1] == t[j-1]) dp[i][j] = dp[i-1][j-1]+dp[i-1][j];
else dp[i][j] = dp[i-1][j];
}
}
return dp[n1][n2];
}
};
dp[i] [j]数组的定义为:以s[i-1]为尾的字符串中含有t[i-1]为尾的字符串的个数,与前面几题有所不同,需要注意。然后就是递推公式,当s[i-1]和t[j-1]相等的时候只需要模拟都减去这个字符后原有的dp值加上只减去s[i-1]字符的dp值,举个例子s=bagg和t=bag当遍历到s的第二个g和t的g时,这时的dp值就应当是1+1=2了;当s[i-1]和t[j-1]不匹配的时候,dp值就等于减去s的当前字符时含有t的个数。初始化的时候也是需要注意的:dp[i] [0]表示t为空,那么s中删除若干个字符之后会有几个空字符串呢?答案就是1个,所以dp[i] [0]这一列要初始化为1。dp[0] [j]表示s为空,空字符串中有多少个字符串t呢?答案是0,所以dp[0] [j]那一行要初始化成0,最后还有一个特殊情况就是s和t都为空的情况下,子序列为1。这里是为编辑距离题型做铺垫,在动态规划中涉及到删除添加替换的操作。
583.两个字符的删除操作
解法一:
class Solution {
public:
int minDistance(string word1, string word2) {
int n1 = word1.size(), n2 = word2.size();
vector<vector<int>> dp(n1+1, vector<int>(n2+1, 0));
for(int i = 0; i <= n1; i++) dp[i][0] = i;
for(int j = 0; j <= n2; j++) dp[0][j] = j;
for(int i = 1; i <= n1; i++)
{
for(int j = 1; j <= n2; j++)
{
if(word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1];
else dp[i][j] = min(min(dp[i-1][j]+1, dp[i][j-1]+1), dp[i-1][j-1]+2);
}
}
return dp[n1][n2];
}
};
dp[i] [j]的定义为:以word1[i-1]为结尾和以word[j-1]为结尾需要的最少删除次数,递推公式如果当前两个字符相等那么就不需要进行删除操作,当前的dp值就为各自减去当前字符时所需的删除次数,如果不同的话,那么当前的dp值为左值和上值取小的那个(在二维dp数组的网格中)。初始化步骤:如果word1长度为n1,word2长度为0,那么就需要进行n1个删除操作(对应dp[i] [0] = i);同理word1长度为0,word2长度为n2,那么就需要进行n2个删除操作(对应dp[0] [j] = j)。
解法二:
class Solution {
public:
int minDistance(string word1, string word2) {
int n1 = word1.size(), n2 = word2.size();
vector<vector<int>> dp(n1+1, vector<int>(n2+1, 0));
for(int i = 1; i <= n1; i++)
{
for(int j = 1; j <= n2; j++)
{
if(word1[i-1] == word2[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 n1+n2-2*dp[n1][n2];
}
};
这也是我自己通过上面的做题经验想出来的思路,可惜没能用代码实现出来,先是照搬最长公共子序列的代码,然后返回n1+n2-2*dp[n1] [n2]即可。当我求出了两个字符串的最长公共子序列,那么要求出最少的删除次数就只需要用两个长度和减去两倍的公共长度就行了。
当然两个不同的解法最大的区别在于dp数组的定义,这样就导致了递推公式的不同和初始化的形式不同,所以正确的定义dp数组并推出递推关系式十分重要。
72.编辑距离
class Solution {
public:
int minDistance(string word1, string word2) {
int n1 = word1.size(), n2 = word2.size();
vector<vector<int>> dp(n1+1, vector<int>(n2+1, 0));
for(int i = 1; i <= n1; i++) dp[i][0] = i;
for(int j = 1; j <= n2; j++) dp[0][j] = j;
for(int i = 1; i <= n1; i++)
{
for(int j = 1; j <= n2; j++)
{
if(word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1];
else dp[i][j] = min(min(dp[i-1][j]+1, dp[i][j-1]+1), dp[i-1][j-1]+1);//分别对应删除word1、添加word1(删除word2)、替换
}
}
return dp[n1][n2];
}
};
经过上面题目的铺垫,知道了删除应该怎么写,但是不知道添加和替换怎么写,看完卡尔的视频之后,我真tm是傻子,写了这么多道动态规划,递推公式一定要以dp数组的定义来推,怎么就是想不出来。再说一遍:递推公式一定要以dp数组的定义来推!对word1的添加操作实际上就是对word2的删除操作,修改操作就是在dp值左上角的基础上+1,因为dp数组的定义就是操作数,我压根不需要管你修改的是什么,删除添加的什么,我只需要管我的dp数组值+1就行了,还是不能以惯性思维去做题!初始化操作还是和前面几题相同,分为两个极端word1长度为0或word2长度为0时编辑距离的最小次数。
647.回文字串
class Solution {
public:
int countSubstrings(string s) {
int n = s.size();
int result = 0;
vector<vector<bool>> dp(n, vector<bool>(n, false));
for(int i = n-1; i >= 0; i--)
{
for(int j = i; j < n; j++)
{
if(s[i] == s[j])
{
if(j-i <= 1)
{
dp[i][j] = true;
result++;
}
else if(dp[i+1][j-1])
{
dp[i][j] = true;
result++;
}
}
}
}
return result;
}
};
这个题给我的启发是动态规划题型需要合理定义dp数组并推出递推关系式。为什么这样说呢?这个题如果你按照题目的问题定义dp数组的话我们是无法推出dp表达式的。这道题题解dp[i] [j]的含义是[i,j]长度的字串是否为回文串,类型是bool类型。递推关系式是:如果当前两个字符相等那么只需要判断字串[i+1,j-1]是否为回文字串。这里的遍历顺序与以往的也有所不同,从递推关系式可以看出dp[i] [j]的值由左下角的值来确定,所以这里的i需要从后往前遍历,然后j的起始位置要从i开始,因为我们定义的dp数组是[i,j]的字串,所以j一定是大于等于i的。
516.最长回文子序列
class Solution {
public:
int longestPalindromeSubseq(string s) {
int n = s.size();
vector<vector<int>> dp(n, vector<int>(n, 0));
for(int i = 0; i < n; i++) dp[i][i] = 1;
for(int i = n-1; i >= 0; i--)
{
for(int j = i+1; j < n; j++)
{
if(s[i] == s[j]) dp[i][j] = dp[i+1][j-1]+2;
else dp[i][j] = max(dp[i+1][j], dp[i][j-1]);
}
}
return dp[0][n-1];
}
};
这一题的dp[i] [j]定义为长度为[i,j]子串的最长回文序列长度,递推关系式为:如果当前两个字符相等,那么由左下角的dp长度值+2,不相等的话就在[i+1,j]长度的dp值和[i,j+1]长度的dp值中取较大的那个。这里需要初始化的地方是二维数组中对角线的位置,所以在循环中j直接等于i+1,如果j=i的话会导致数组越界错误,最后返回的是dp[0] [n-1]即表示[0,n-1]子串的最长回文子序列长度。