3.动态规划.题目3
- 题目
- 23.买卖股票的最佳时机3-困难
- 24.买卖股票的最佳时机4
- 25.买卖股票的最佳时机含冷冻期
- 26.买卖股票的最佳时机含手续费
- 27.最长递增子序列
- 28.最长连续递增序列
- 29.最长重复子数组
- 30.最长公共子序列
- 31.不相交的线
- 编辑距离总结
题目
23.买卖股票的最佳时机3-困难
题目链接
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)
1.dp[i][j]数组定义:0没有操作 (其实我们也可以不设置这个状态);1第一次持有股票;2第一次不持有股票;3第二次持有股票;4第二次不持有股票
2.递推关系:
1)达到dp[i][1]
状态,有两个具体操作:操作一:第i天买入股票了,那么dp[i][1] = dp[i-1][0] - prices[i]
;操作二:第i天没有操作,而是沿用前一天买入的状态dp[i][1] = dp[i - 1][1]
;
2)dp[i][2]
也有两个操作:第i天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i]
;第i天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2]
;
3)达到dp[i][3]
状态:dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i])
;
4)达到dp[i][4]
状态:dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i])
;
3.初始化:对于0下标dp[0][0] = 0
;p[0][1] = -prices[0]
;dp[0][3] = -prices[0]
;dp[0][4] = 0
;
4.遍历顺序:从左往右
// 改进使用内存版本
int maxProfit(vector<int>& prices) {
if(prices.size()==0) return 0;
std::vector dp(5,0);
dp[1] = -prices[0];
dp[3] = -prices[0];
for(int i=1; i<prices.size(); i++){
dp[1] = max(dp[1], dp[0]-prices[i]);
dp[2] = max(dp[2], dp[1]+prices[i]);
dp[3] = max(dp[3], dp[2]-prices[i]);
dp[4] = max(dp[4], dp[3]+prices[i]);
}
return dp[4];
}
时间复杂度:O(n);空间复杂度:O(1)
24.买卖股票的最佳时机4
题目链接
给你一个整数数组 prices 和一个整数 k ,其中 prices[i] 是某支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
这题与23题的区别在于可完成k笔交易,每一次交易需要两个状态表示,因此k步即可用2k个状态量表示,因此可以在外环遍历每日的股票价格,在内环使遍历k个交易状态。
// 改进使用内存版本
int maxProfit(int k, vector<int>& prices) {
if(prices.size()==0) return 0;
std::vector<int> dp(2*k+1, 0);
for(int i=1; i<2*k+1; i +=2) dp[i]=-prices[0];
for(int i=1; i<prices.size(); i++){
for(int j=1; j<2*k+1; j +=2){
dp[j] =max(dp[j], dp[j-1]-prices[i]);
dp[j+1] =max(dp[j+1], dp[j]+prices[i]);
}
}
return dp[2*k];
}
时间复杂度: O(n x k),其中 n 为 prices 的长度;空间复杂度: O(k)
25.买卖股票的最佳时机含冷冻期
题目链接
给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票)。卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
这题相当于状态除了持有,不持有,还多了一个冷冻期的状态
1.dp[i][j]数组定义:1)状态一:持有股票状态或买入 2)不持有股票状态,这里就有两种卖出股票状态:状态二:保持卖出股票的状态(两天前就卖出了股票,度过一天冷冻期。或者是前一天就是卖出股票状态,状态三:今天卖出股票;3)状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!
2.递推关系:状态一:dp[i][0] = max(dp[i - 1][0], dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i])
;状态二:dp[i][1] = max(dp[i - 1][1], dp[i - 1][3])
;状态三:dp[i][2] = dp[i - 1][0] + prices[i]
;状态四:dp[i][3] = dp[i - 1][2]
3.初始化:dp[0][0] = -prices[0]
,其余0下标状态下均初始化为0;
4.遍历顺序:从前往后遍历
int maxProfit(vector<int>& prices) {
int len = prices.size();
if(len==0) return 0;
std::vector<std::vector<int>> dp(2, std::vector<int>(4,0));
dp[0][0] = -prices[0];
for(int i=1; i<len; i++){
dp[i%2][0] = max(dp[(i-1)%2][0], max(dp[(i-1)%2][1]-prices[i], dp[(i-1)%2][3]-prices[i]));
dp[i%2][1] = max(dp[(i-1)%2][1], dp[(i-1)%2][3]);
dp[i%2][2] = dp[(i-1)%2][0]+prices[i];
dp[i%2][3] = dp[(i-1)%2][2];
}
// 卖出状态,保持卖出状态,冷冻期状态
return max(dp[(len-1)%2][3], max(dp[(len-1)%2][2], dp[(len-1)%2][1]));
}
时间复杂度:O(n);空间复杂度:O(n)
26.买卖股票的最佳时机含手续费
题目链接
给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
1.dp[i]数组定义:分为持有股票,不持有股票两种状态
2.递推关系:只有在卖出(不持有股票)的状态下有差别,多了一个fee手续费:dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i])
;
3.初始化:dp[0][0] = -prices[0]
,其余0下标状态下均初始化为0;
4.遍历顺序:从前往后遍历
int maxProfit(vector<int>& prices, int fee) {
int len = prices.size();
if(len==0) return 0;
std::vector<std::vector<int>> dp(2, std::vector<int>(2,0));
dp[0][0]=-prices[0];
for(int i=1; i<len; i++){
dp[i%2][0] = max(dp[(i-1)%2][0], dp[(i-1)%2][1]-prices[i]);
dp[i%2][1] = max(dp[(i-1)%2][1], dp[(i-1)%2][0]+prices[i]-fee);
}
return max(dp[(len-1)%2][0], dp[(len-1)%2][1]);
}
27.最长递增子序列
题目链接
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。示例 :输入:nums = [10,9,2,5,3,7,101,18];输出:4;解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
1.dp[i]数组定义:表示i之前包括i的以nums[i]结尾的最长递增子序列的长度。
2.递推关系:只保留最大值if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1)
;
3.初始化:每一个i,对应的dp[i](即最长递增子序列)起始大小至少都是1;
4.遍历顺序:从前往后遍历;
int lengthOfLIS(vector<int>& nums) {
int len=nums.size();
if(len<=1) return len;
std::vector<int> dp(len, 1);
int res = 0;
for(int i=1; i<len; i++){
for(int j=0; j<i; j++){
if(nums[i]>nums[j]) dp[i]=max(dp[i], dp[j]+1);
}
if(dp[i]>res) res = dp[i];
}
return res;
}
时间复杂度: O(n^2);空间复杂度: O(n)
28.最长连续递增序列
题目链接
给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r
,都有 nums[i] < nums[i + 1]
,那么子序列 [nums[l], nums[l + 1], …, nums[r - 1], nums[r]] 就是连续递增子序列。
本题与27题最大的区别在于数组连续,
1.dp[i]数组定义:以下标i为结尾的连续递增的子序列长度为dp[i]。
int findLengthOfLCIS(vector<int>& nums) {
int len = nums.size();
if(len<=1) return len;
int res=0;
std::vector<int> dp(len, 1);
for(int i=1; i<len; i++){
if(nums[i]>nums[i-1]){
dp[i] = dp[i-1]+1;
}
if(dp[i]>res) res = dp[i];
}
return res;
}
时间复杂度:O(n);空间复杂度:O(n)
理解27,28题的区别:概括来说,不连续递增子序列的跟前0-i 个状态有关,连续递增的子序列只跟前一个状态有关
29.最长重复子数组
题目链接
给两个整数数组 nums1 和 nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度 。示例 1:输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7];输出:3;解释:长度最长的公共子数组是 [3,2,1] 。这里要求子数组是连续的。
暴力解法:只需要先两层for循环确定两个数组起始位置,然后再来一个循环可以是for或者while,来从两个起始位置开始比较,取得重复子数组的长度——三层for循环
动态规划:
1.dp[i][j]数组定义:以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j];这样-1处理是为了方便初始化
2.递推关系:当A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1
;
3.初始化:dp[i][0] 和dp[0][j]其实都是没有意义的,因此均初始化为0;
4.遍历顺序:根据递推公式可以看出,遍历i 和 j 要从1开始;
int findLength(vector<int>& nums1, vector<int>& nums2) {
int len1 = nums1.size();
int len2 = nums2.size();
std::vector<std::vector<int>> dp(len1+1, std::vector<int>(len2+1, 0));
int res = 0;
for(int i=1; i<=len1; i++){
for(int j=1; j<=len2; j++){
if(nums1[i-1]==nums2[j-1]) dp[i][j]=dp[i-1][j-1]+1;
if(dp[i][j]>res) res = dp[i][j];
}
}
return res;
}
// 改进内存版
int findLength(vector<int>& nums1, vector<int>& nums2) {
int len1 = nums1.size();
int len2 = nums2.size();
std::vector<std::vector<int>> dp(2, std::vector<int>(len2+1, 0));
int res = 0;
for(int i=1; i<=len1; i++){
for(int j=1; j<=len2; j++){
if(nums1[i-1]==nums2[j-1]) dp[i%2][j]=dp[(i-1)%2][j-1]+1;
else dp[i%2][j] = 0;
if(dp[i%2][j]>res) res = dp[i%2][j];
}
}
return res;
}
30.最长公共子序列
题目链接
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。示例 1:输入:text1 = “abcde”, text2 = “ace” ;输出:3 ;解释:最长公共子序列是 “ace” ,它的长度为 3 。
这与29题的区别在于不要求是连续的了,但要有相对顺序,即:“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
1.dp[i][j]数组定义:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列长度为dp[i][j];这样-1处理是为了方便初始化
2.递推关系:如果text1[i - 1] 与 text2[j - 1]相同
,那么找到了一个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1
;如果text1[i - 1] 与 text2[j - 1]不相同
,那就看看text1[0, i - 2]与text2[0, j - 1]
的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]
的最长公共子序列,取最大的dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
3.初始化:test1[0, i-1]和空串的最长公共子序列自然是0,所以dp[i][0] = 0
,同理dp[0][j]=0
4.遍历顺序:递推公式,可以看出,有三个方向可以推出dp[i][j]
;都必须从前往后遍历
int longestCommonSubsequence(string text1, string text2) {
int len1 = text1.size();
int len2 = text2.size();
std::vector<std::vector<int>> dp(2, std::vector<int>(len2+1, 0));
for(int i=1; i<=len1; i++){
for(int j=1; j<=len2; j++){
if(text1[i-1]==text2[j-1]) dp[i%2][j]=dp[(i-1)%2][j-1]+1;
else dp[i%2][j] = max(dp[(i-1)%2][j], dp[i%2][j-1]);
}
}
return dp[(len1)%2][len2];
}
时间复杂度: O(nm),其中 n 和 m 分别为 text1 和 text2 的长度;空间复杂度: O(nm)
31.不相交的线
题目链接
在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足: nums1[i] == nums2[j]
;且绘制的直线不与任何其他连线(非水平线)相交。请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。以这种方法绘制线条,并返回可以绘制的最大连线数。
不相交其实可以抽象为在nums1[i]-[j], nums2[i]-j,在来两个数组这范围划定了子数组,然后要求子数组的最大个数;另外一种思路是寻找两个数组中的公共子序列(相对顺序不变,例如4在1后面),发现这些数字连线就可以得到不相交的线。题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!那么这题就和30题求最长公共子序列就是一样的。
int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
int len1 = nums1.size();
int len2 = nums2.size();
std::vector<std::vector<int>> dp(2, std::vector<int>(len2+1, 0));
for(int i=1; i<=len1; i++){
for(int j=1; j<=len2; j++){
if(nums1[i-1]==nums2[j-1]) dp[i%2][j] = dp[(i-1)%2][j-1]+1;
else dp[i%2][j] = max(dp[(i-1)%2][j], dp[i%2][j-1]);
}
}
return dp[len1%2][len2];
}
时间复杂度: O(nm);空间复杂度: O(2m)
编辑距离总结
- 判断子序列
基本题目是给定字符串 s 和 t ,判断 s 是否为 t 的子序列(子序列没有连续性要求,只要保证字母之间的相对顺序一致即可),这题可以使用双指针或贪心法解决。dp[i][j]数组表示以s[i-1]结尾
的序列在以t[j-1]结尾
所匹配的序列长度。
if (s[i - 1] == t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = dp[i][j - 1];
- 不同的子序列
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数,本题虽然也只有删除操作,不用考虑替换增加之类的,双指针就无法解决了。dp[i][j]数组表示以i-1为结尾
的s子序列中出现以j-1为结尾
的t的个数为dp[i][j]。而当s[i - 1] 与 t[j - 1]相等时,子序列数量有两部分组成。
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];
}
- 两个字符串的删除操作
给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最少步数,每步可以删除任意一个字符串中的一个字符。dp[i][j]数组表示以i-1为结尾
的字符串word1,和以j-1位结尾
的字符串word2,想要达到相等,所需要删除元素的最少次数。当word1[i - 1] 与 word2[j - 1]相同的时候,不需要进行操作;当当word1[i - 1] 与 word2[j - 1]不相同的时候,
if (word1[i - 1] == word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1});
// 等效于 dp[i][j] = min({dp[i - 1][j] + 1, dp[i][j - 1] + 1});
}
- 编辑距离
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。操作包括删除,添加,替换(其实删除和添加可以相互替换)。dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。if (word1[i - 1] == word2[j - 1])
那么说明不用任何编辑;if (word1[i - 1] != word2[j - 1])
,此时就需要编辑,(理解删,增可以归于一类删除的操作),替换就是进行一次替换令word1[i - 1] == word2[j - 1]
。
if (word1[i - 1] == word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
}
else {
dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;
}