利用这个性质,如果距离(i,j)最近的点在(i-a,j+b) a>0,b>0,在第一个dp(左上开始)时(i,j)没有取得最优值,但在第二个dp(右下开始)时由于(i,j+b)的最优值已经取得(因为这个最优值在他正上方),所以(i,j)也能取得最优值。
因此我们可以这样实现本题:先把dis数组初始化为一个较大值。遍历mat数组,把为0的结点的dis位置标为0。之后分别从左上、右下开始遍历,更新。
class Solution {
public:
vector> updateMatrix(vector>& mat) {
int m = mat.size(), n = mat[0].size();
vector> dis(m,vector(n,INT_MAX/2));
for(int i=0; i=1){
dis[i][j] = min(dis[i][j],dis[i-1][j] + 1);
}
if(j>=1){
dis[i][j] = min(dis[i][j],dis[i][j-1] + 1);
}
}
}
//从右下角开始递推
for(int i=m-1;i>=0;i--){
for(int j=n-1;j>=0;j--){
//这样的写法就不需要单独先对第一行和第一列的边界进行初始赋值
if(i
6、最大正方形(221)
对于在矩形内搜索正方形或长方形的题型,常见做法是定义一个二维数组dp,dp[ i ][ j ]表示满足题目条件的,以(i, j)为右下角的正方形或长方形的属性。
因此,本题可以定义dp[ i ][ j ]表示以(i, j)为右下角的最大正方形边长值。显然dp[ i ][ j ]与dp[ i-1 ][ j ]、dp[ i ][ j-1 ]、dp[ i-1 ][ j-1 ]有关。注意到有以下性质:dp[ i ][ j ] = k的充要条件是 dp[ i-1 ][ j ]、dp[ i ][ j-1 ]、dp[ i-1 ][ j-1 ]的值均不小于k-1。可以参考下图示例理解。
class Solution {
public:
int maximalSquare(vector>& matrix) {
int m = matrix.size(), n = matrix[0].size();
vector> dp(m+1,vector(n+1,0));
int max_area = 0;
for(int i=1; i<=m; i++){
for(int j=1; j<=n; j++){
if(matrix[i-1][j-1]=='1'){
dp[i][j] = min(dp[i-1][j], min(dp[i][j-1], dp[i-1][j-1])) + 1;
max_area = max(max_area,dp[i][j]);
}
}
}
return max_area * max_area;
}
};
分割类型题
分割类型的动态规划问题的状态转移方程,通常并不依赖于相邻位置,而是以依赖于满足分割的条件的位置。
7、完全平方数(279)
本题的分割位置取决于各个完全平方数1、4、9......。设dp[ i ] 表示数字 i 最少可以用几个完全平方数相加构成,则dp[ i ] = min{ dp[ i-1 ],dp[ i -4], ... , dp[ i -j*j ] }(j*j <= i)。
class Solution {
public:
int numSquares(int n) {
vector dp(n+1,INT_MAX);
dp[0] = 0;
for(int i=1; i<=n; i++){
for(int j=1; j*j<=i; j++){
dp[i] = min(dp[i], dp[i-j*j] + 1);
}
}
return dp[n];
}
};
8、解码方法(91)
由于1-26的数字对应26个字母,所以对应方式只有1位数或者2位数,因此要考察 dp[ i ] 和 dp[ i-1 ]、dp[ i-2 ]的关系。设dp[ i ]表示从0到 i 位的数字有多少种解码方式。
当s[ i ] != ‘0’时,s[ i ]可以单独解码成字母,则dp[ i ] += dp[i-1]
当s[ i -1] != '0'时,如果s[ i -1]和s[ i ]构成的数字又小于27,则s[ i -1]和s[ i ]可以一起解码成字母,dp[ i ] += dp[i-2]。
初始条件:dp[ 0 ] = 1 ( if s[0] != ‘0’); dp[1] = 0/1/2;依情况推断。
class Solution {
public:
bool isValid(char c1, char c2){
int num = (c1 - '0')*10 + (c2-'0');
if(1<=num && num<=26) return true;
return false;
}
int numDecodings(string s) {
if(s[0]=='0') return 0;
int n = s.length();
if(n==1) return 1;
vector dp(n,0);
dp[0] = 1;
if(s[1]!='0') ++dp[1];
if(isValid(s[0],s[1])) ++dp[1];
for(int i=2; i
9、单词拆分(139)
设dp[ i ]表示[0, i]范围的子串可否进行单词拆分。则本题的分割位置是在 i 之前的可以拆分的位置上(设为idx,用一个数组存储),我们只需把这些[idx+1, i]范围的子串是否是单词列表中的单词即可,如果是则可以拆分,否则不能。
注意:由于单词列表最大长度不超过20,可以利用信息减少需要搜索的分割位置数量。
class Solution {
public:
bool wordBreak(string s, vector& wordDict) {
//把单词列表用集合存储,便于查找
unordered_set wordSet;
for(string str: wordDict){
wordSet.insert(str);
}
int n = s.length();
vector dp(n,false);
vector idx_of_True;//存储为ture的索引
idx_of_True.push_back(-1);
for(int i=0; i=0;j--){//截取从上一个为true的位置idx到i的单词查找
int idx = idx_of_True[j];
if(i-idx>20) break;//由于单词列表最长不超过20,超过20直接退出循环
string word = s.substr(idx+1,i - idx);
if(wordSet.count(word)){//子串是set中单词,把索引加入idx_of_True
dp[i] = true;
idx_of_True.push_back(i);
break;
}
}
}
return dp[n-1];
}
};
子序列问题
对子序列问题,常见的思路是定义dp[ i ]表示已 i 结尾的子序列满足性质,处理完全部位置后,选出其中最大值。
10、最长递增子序列(300)
本题定义dp[ i ]表示以nums[ i ]结尾的最长子序列长度。则dp[ i ] = max(dp[ j ]) + 1, if nums[ j ] < nums[ i ] 且0<= j <= i-1. 时间复杂度为O(n^2)。
class Solution {
public:
int lengthOfLIS(vector& nums) {
int n = nums.size();
vector dp(n,1);
int maxLength = 1;
for(int i=1; i
方法二:使用二分查找
在寻找最长子序列时,我们应该尽量让当前子序列的尾部元素尽量小,这样才更可能在后面加上新元素,生成更长子序列。我们定义dp[ k ] 表示nums数组中长度为k+1的递增子序列的尾部值,并使其值尽量小。之后开始遍历,如果nums[ i ]大于dp的全部元素,则说明更长的子序列长度出现了,nums[ i ]加入到dp数组中;否则,我们在dp中找到第一个大于等于nums[ i ]的位置pos,修改其值,这一步使长度为pos+1的子序列获得更小的尾元素,那么之后我们就更可能在此基础上增加新元素,构成更长的递增子序列。
参看下图链接理解。
这样构造的dp数组一定是递增的,可以用二分查找寻找合适位置。时间复杂度降为O(nlgn)。
class Solution {
public:
int lengthOfLIS(vector& nums) {
int n = nums.size();
vector dp;//dp[i]存储nums数组中长度为i+1的递增子序列的尾部值,并使其值尽量小
dp.push_back(nums[0]);
for(int i=1; i dp.back()){
//大于当前最大长度len的子序列尾部值,说明nums[i]可以与之构成len+1的递增子序列
dp.push_back(nums[i]);
}else{//否则,找到第一个大于等于nums[i]位置,更新对应尾部值
int low = 0, high = dp.size()-1;
while(low=nums[i]){
high = mid;
}else{
low = mid+1;
}
}
dp[low] = nums[i];
//也可以直接调用lower_bound函数
//auto itr = lower_bound(dp.begin(),dp.end(),nums[i]);
//*itr = nums[i];
}
}
return dp.size();
}
};
语法:lower_bound(a[0] , a[n], x)返回有序数组a中第一个大于等于x的下标。
11、最长公共子序列(1143)
经典最长公共子序列问题(LCS),设dp[ i ][ j ]表示第一个字符串以位置 i 结尾的前缀,和第二个字符串以位置 j 结尾的前缀的最长公共子序列长度。则dp[ i ][ j ] = dp[ i-1 ][ j-1 ], if s1[ i ] = s2[ i ];
dp[ i ][ j ] = max(dp[i-1][j],dp[i][j-1]) if s1[ i ] != s2[ i ]。
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.length(), n = text2.length();
vector> dp(m+1,vector(n+1,0));
for(int i=1; i<=m; i++){
for(int j=1; j<=n; 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[m][n];
}
};
尝试进行空间压缩,由于每次只需要相邻2行,可以使用滚动数组,可以参考背包问题的压缩思路。由于dp[ i ][ j ]取决于dp[ i-1 ][ j ]、dp[ i ][ j-1 ]、dp[ i-1 ][ j-1 ],即上部、左部、左上部元素影响。由于依赖于左部元素,因此内循环必须是顺序进行,(逆序的话,没有dp[ i ][ j-1 ]我们无法更新dp[ i ][ j ]),但这样会覆盖dp[ i-1 ][ j-1 ],因此需要事先使用一个变量pre进行保存。
当然,简单点,可以直接用两行数组保存。同时,数组长度可以取两字符串长度最小值。空间复杂度可以降为O(min(M,N))。
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.length(), n = text2.length();
vector dp(n+1,0);
for(int i=1; i<=m; i++){
int pre = dp[0];//存储dp[i][j]的左上角元素,相当于dp[i-1][j-1]
for(int j=1; j<=n; j++){
int next = dp[j];//保留修改前的元素相当于dp[i-1][j],留给下次遍历使用
if(text1[i-1] == text2[j-1]){
dp[j] = pre + 1;
}else{
dp[j] = max(dp[j],dp[j-1]);
}
pre = next;
}
}
return dp[n];
}
};
背包问题
掌握0-1背包和完全背包两个基本问题,以及空间压缩的写法。背包问题中dp[ i ][ j ]表示前 i 个物品中恰好装入容量为 j 的背包所能获得的最大价值。
12、分割等和子集(416)
本题可以转化为选取数组中的部分元素,使其和恰好等于数组总和sum的一般半(设为target)。用0-1背包问题表述:选取若干物品,能否使其重量恰好等于target。本题中由于只涉及重量,不涉及价值,因此可以只用bool类型,dp[ i ][ j ]表示前 i 个数字中是否恰好和为 j 。再采用空间压缩的写法。
class Solution {
public:
bool canPartition(vector& nums) {
int sum = accumulate(nums.begin(),nums.end(),0);
if(sum%2 != 0) return false;
int target = sum/2, n = nums.size();
vector dp(target+1,false);
dp[0] = true;//0个数的和恰好为0,故应设为true
for(int i=1; i<=n; i++){
for(int j = target; j>=nums[i-1]; j--){
dp[j] = dp[j] || dp[j-nums[i-1]];
}
}
return dp[target];
}
};
语法:accumulate函数
在头文件 #include 里,主要是用来累加容器里面的值,比如int、string之类,可以少写一个for循环。
比如直接统计 vector v 里面所有元素的和:(第三个参数的0表示sum的初始值为0)
int sum = accumulate(v.begin(), v.end(), 0);
比如直接将 vector v 里面所有元素一个个累加到string str中:(第三个元素表示str的初始值为空字符串)
string str = accumulate(v.begin(), v.end(), "");
13、一和零(474)
本问题为选取适当的字符串,在0和1不超过限值的情况下,使字符串总数尽量多。用背包问题的观点看,即0和1两个条件可以看做背包有两种容量限制,数量看作价值,把每个字符串价值看为1即可。同样的使用空间压缩,0-1背包问题使用逆序遍历,由于有2个容量条件,所以有3层循环。
class Solution {
public:
int findMaxForm(vector& strs, int m, int n) {
int slen = strs.size();
vector> dp(m+1,vector(n+1,0));
vector num0(slen,0),num1(slen,0);//统计各个字符串的0和1个数
for(int i=0; i=num0[i-1]; j--){
for(int k=n; k>=num1[i-1];k--){
dp[j][k] = max(dp[j][k], dp[j-num0[i-1]][k-num1[i-1]] + 1);
}
}
}
return dp[m][n];
}
};
14、零钱兑换(322)
完全背包问题的简单变形,硬币的面额看作是容量,数量看作价值,由于取最少数量,所以max变成min,因此初值要赋予一个较大值(实际上amount+2就足够了)。
class Solution {
public:
int coinChange(vector& coins, int amount) {
vector dp(amount+1,INT_MAX/2);
//实际上最大值amount+2就足够了,因为全部用1元硬币也只需要amount个
dp[0] = 0;
for(int i=1; i<=coins.size(); i++){
for(int j=1; j<=amount; j++){
if(j>=coins[i-1]){
dp[j] = min(dp[j], dp[j-coins[i-1]]+1);
}
}
}
if(dp[amount] == INT_MAX/2) return -1;
return dp[amount];
}
};
15、编辑距离(72)
要找出从word1到word2的最少修改数,即是在相同的LCS基础上进行增、删、替操作,因此我们可以借鉴LCS的动态规划思路。设dp[ i ][ j ]表示word1的前 i 个字符和word2的前 j 个字符所需的最少修改数,则当word1[i-1] = word2[j-1]时,dp[ i ][ j ] = dp[ i-1 ][ j-1 ](新添后缀字符一致,不需要修改);二者不等时,有如下3种修改方式:
1)对新添的不同第 i-1号字符进行替换操作,替换成word2中字符,则问题转化为在word1的前 i-1 个字符和word2的前 j-1 个字符的最小操作数,故dp[ i ][ j ] = dp[ i-1 ][ j-1 ] + 1;
2)对新添的不同第 i-1号字符进行删除操作,则问题转化为在word1的前 i-1 个字符和word2的前 j 个字符的最小操作数,故dp[ i ][ j ] = dp[ i-1 ][ j ] + 1;
3)在word1的前 i-1 个字符和word2的前 j-1 个字符的基础上,插入第i-1号字符,故dp[ i ][ j ] = dp[ i ][ j-1 ] + 1.
所以 dp[ i ][ j ] = min( dp[ i-1 ][ j ], dp[ i ][ j-1 ], dp[ i-1 ][ j-1 ] ) + 1, if word1[i-1] != word2[j-1]
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.length(), n = word2.length();
vector> dp(m+1, vector(n+1,0));
for(int i=0; i<=m; i++){
for(int j=0; j<=n; j++){
if(i==0){//长为0的w1字符串插入j次后变为空字符串w2
dp[i][j] = j;
}else if(j==0){//长为i的w1字符串删除i次后变为空字符串w2
dp[i][j] = i;
}else{
if(word1[i-1]==word2[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = min(dp[i-1][j-1], min(dp[i-1][j],dp[i][j-1]))+1;
}
}
}
return dp[m][n];
}
};
同样可以进行空间压缩,类似于LCS问题,注意保留左上角元素dp[ i-1][ j-1 ]。
class Solution {
public:
int minDistance(string word1, string word2) {
int m = word1.length(), n = word2.length();
vector dp(n+1,0);
for(int i=0; i<=m; i++){
int pre = dp[0];
for(int j=0; j<=n; j++){
int next = dp[j];
if(i==0){
dp[j] = j;
}else if(j==0){
dp[j] = i;
}else{
if(word1[i-1] == word2[j-1]) dp[j] = pre;
else dp[j] = min(pre,min(dp[j-1],dp[j])) + 1;
}
pre = next;
}
}
return dp[n];
}
};
16、只有两个键的键盘(650)
设dp[ i ]表示得到长为 i 字符串最少需要的操作数。对于质数 i,只能一个个复制得到,因此dp[ i ] = i. 对合数 i , 设 j 是 i 的因数(j>=2),则 i 可以由 j 复制得到,则dp[ i ] = dp[ j ] + dp [i/j]。dp[ j ]表示为了得到长度为 j 的字符串,需要进行的操作数,j 得到 i 过程等价于1得到i/j过程。对于所有因数,显然对最大的因数k,上式是最少的(复制长度越大粘贴次数越少)。
class Solution {
public:
int minSteps(int n) {
vector dp(n+1);
for(int i=2; i<=n; i++){
dp[i] = i;//质数只能一个个粘贴得到
for(int j=2; j*j<=i;j++){
if(i%j==0){//i是最小因数,i/j就是最大因素
dp[i] = dp[j] + dp[i/j];
break;
}
}
}
return dp[n];
}
};
17、正则表达式匹配(10)
本题的难点就在于如何建立状态转移方程。具体可以参考题解
class Solution {
public:
bool isMatch(string s, string p) {
int m = s.length(), n = p.length();
vector> dp(m+1,vector(n+1,false));
dp[0][0] = true;
for(int i=1; i
股票交易问题
股票交易类问题可以参考这篇文章。力扣
这里进行一个简单总结。
首先,问题的一般形式归结为:给定每日的股价数组prices,在最多交易k次,且至多同时持有1支股票的情况下,如何交易才能获得最大利润?
解答:股票问题最通用的情况由三个特征决定:当前的天数 i
、允许的最大交易次数 k
以及每天结束时持有的股票数。
定义: T[i][k][0] 表示在第 i 天结束时,最多进行 k 次交易且在进行操作后持有 0 份股票的情况下可以获得的最大收益;
T[i][k][1] 表示在第 i 天结束时,最多进行 k 次交易且在进行操作后持有 1 份股票的情况下可以获得的最大收益。
则状态转移方程为:
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k - 1][0] - prices[i])
T[i][k][0]要求第i天结束后持有0支股票,因此在第i天只能休息或卖出,对应max中的两种情况;
T[i][k][1]要求第i天结束后持有1支股票,因此在第i天只能休息或买入,对应max中的两种情况。
基准情况为:
T[-1][k][0] = 0, T[-1][k][1] = -Infinity
T[i][0][0] = 0, T[i][0][1] = -Infinity
两个0项表示交易还未发生(第0天或者k=0),两个-INF项表示没有股票交易时不能持有股票。
最终的解答是T[n-1][k][0]
下面的题目都是在这一基本问题的基础上进行变化。
18、买卖股票的最佳时机(121)
本题本质上是求 max(prices[ j ] - prices[ i ])( j > i )。直接的思路就是两重循环。
class Solution {
public:
int maxProfit(vector& prices) {
int n = (int)prices.size(), ans = 0;
for (int i = 0; i < n; ++i){
for (int j = i + 1; j < n; ++j) {
ans = max(ans, prices[j] - prices[i]);
}
}
return ans;
}
};
我们发现,在两重循环中,实际上进行大量重复计算。比如我们计算 j 点卖出的最大利润,是从0枚举到 j - 1作差。但其实我们可以发现,通过中间某个i点的结果,再加上 i - j-1间的数据也可以得到 j 点的答案。那最简单的情况就是利用 j 和 j-1的关系。
我们设dp[ i ] 表示在第 i 天卖出所能获得的最大利润。则对第 i天,有两种策略,一是和第 i -1天一样,在之前相同的低点买入;二是在第 i -1天买入。 所以dp[i] = max(dp[i-1]+prices[i] - prices[i-1],prices[i] - prices[i-1])。
class Solution {
public:
int maxProfit(vector& prices) {
int n = prices.size();
if(n==1) return 0;
vector dp(n);
dp[0] = 0;
int ans = 0;
for(int i=1;i
实际上可以进一步简化,我们在第 i 天卖出要获得最大利润的方法,是在 i 之前的最低点买入,那么我们用一个变量minprice记录之前的最低价。对一段时期,则是从所有 i 天的利润中选出最大值,我们再用一个变量maxprofit记录0-i天之间的最大利润。
class Solution {
public:
int maxProfit(vector& prices) {
int n = prices.size();
int maxprofit = 0, minprice = INT_MAX;
for(int i=0;i
我们再用上面说的基本情况进行分析。只能交易一次,k=1,则状态转移方程变为:
T[i][1][0] = max(T[i - 1][1][0], T[i - 1][1][1] + prices[i])
T[i][1][1] = max(T[i - 1][1][1], T[i - 1][0][0] - prices[i]) = max(T[i - 1][1][1], -prices[i])
利用T[i - 1][0][0] = 0的条件,可简化第二个状态转移方程,并节省交易次数k的维度。
T[i][0] = max(T[i-1][0], T[i-1][1] + prices[i]);
T[i][1] = max(T[i-1][1], -prices[i]);
class Solution {
public:
int maxProfit(vector& prices) {
int n = prices.size();
vector> T(n+1,vector(2));
T[0][0] = 0;
T[0][1] = -prices[0];
for(int i=1; i
由于第i天的状态只与第i-1天有关,可以用滚动数组方法减少空间复杂度。
class Solution {
public:
int maxProfit(vector& prices) {
int n = prices.size();
int profit0 = 0, profit1 = -prices[0];//分别代替T[i][0]和T[i][1]
for(int i=1; i
实际上,profit1是在更新当前遇到的股价最低值,而profit0则是在更新从当前天卖出能赚到的利润最大值,这与本问题的第3段代码思想是一致的。
19、买卖股票的最佳时机 II(122)
本题中要求进行尽量多次的交易,则k为正无穷,那么k与k-1的状态是一样的。故有
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k - 1][0] - prices[i]) = max(T[i - 1][k][1], T[i - 1][k][0] - prices[i])
因此,同样可以省略k这一维度
class Solution {
public:
int maxProfit(vector& prices) {
int n = prices.size();
vector> T(n+1,vector(2));
T[0][0] = 0;
T[0][1] = -prices[0];
for(int i=1; i
同样也可以进行空间压缩
class Solution {
public:
int maxProfit(vector& prices) {
int n = prices.size();
int profit0 = 0, profit1 = -prices[0];//分别代替T[i][0]和T[i][1]
for(int i=1; i
和股票问题1对比,区别在于profit1在更新过程中加上了已有的收益profit0,相当于在局部用问题1方法取得最大收益,在把这些收益加起来得到全局最大收益。这实际上告诉我们本题使用贪心算法的正确性。因此也可以用贪心算法,把所有的收益段加起来即可。
class Solution {
public:
int maxProfit(vector& prices) {
int ans = 0;
int n = prices.size();
for (int i = 1; i < n; ++i) {
ans += max(0, prices[i] - prices[i - 1]);
}
return ans;
}
};
20、买卖股票的最佳时机 III(123)
本题k=2,故状态转移方程为
T[i][2][0] = max(T[i - 1][2][0], T[i - 1][2][1] + prices[i])
T[i][2][1] = max(T[i - 1][2][1], T[i - 1][1][0] - prices[i])
T[i][1][0] = max(T[i - 1][1][0], T[i - 1][1][1] + prices[i])
T[i][1][1] = max(T[i - 1][1][1], T[i - 1][0][0] - prices[i]) = max(T[i - 1][1][1], -prices[i])
最后一个式子利用了T[i - 1][0][0] = 0。
class Solution {
public:
int maxProfit(vector& prices) {
int n = prices.size();
vector>> T(n+1,vector>(3,vector(2)));
T[0][2][0] = 0;
T[0][2][1] = -prices[0];
T[0][1][0] = 0;
T[0][1][1] = -prices[0];
for(int i=1; i
同样可以进行空间压缩
class Solution {
public:
int maxProfit(vector& prices) {
int n = prices.size();
int profitTwo0 = 0, profitTwo1 = -prices[0],profitOne0 = 0, profitOne1 = -prices[0];
//分别代替T[i][2][0]\T[i][2][1]\T[i][1][0]\T[i][1][1]
for(int i=1; i
21、买卖股票的最佳时机 IV(188)
本题对应的正是上面所说的一般情况。
class Solution {
public:
int maxProfit(int k, vector& prices) {
int n = prices.size();
if(n<2) return 0;
vector>> T(n,vector>(k+1,vector(2)));
for(int i=0;i
同样,注意到i只与i-1天有关,可以用滚动数组方法减少这一维的空间。
class Solution {
public:
int maxProfit(int k, vector& prices) {
int n = prices.size();
if(n<2) return 0;
vector> T(k+1,vector(2));
for(int i=0;i
当然,n天里最多进行n/2笔交易,当k>n/2时,k不再成为限制交易的条件,相当于正无穷,可以转化为k为正无穷的情况处理。这样可以减少k很大时的执行时间。
22、最佳买卖股票时机含冷冻期(309)
本题是在k为无穷的基础上,增加了一天的冷冻期。我开始的思路是设置一个标置量表示前一天是否卖出,如果卖出,那么T[i][1]只能在当天休息,而不能买入。
class Solution {
public:
int maxProfit(vector& prices) {
int n = prices.size();
vector> T(n,vector(2));
T[0][0] = 0;
T[0][1] = -prices[0];
bool flag = false;//表示前一天是否卖出股票
for(int i=1;i
但测试后,发现结果不对。思考后,发现flag可能在错误的卖出时机被修改,影响答案的正确性。
比如[1,2,3,0,2]这个例子,在prices[0] = 1时买入,在prices[1] = 2这个卖出点就会修改flag,影响后续T[i][1]的修改,而最终的结果中,我们其实应该在prices[2] = 3这个点卖出才有最大收益。
重新思考问题。K为无穷且没有冷冻期时,
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k][0] - prices[i])
现在有冷冻期,状态转移方程变为
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i])
T[i][k][1] = max(T[i - 1][k][1], T[i - 2][k][0] - prices[i])
为什么直接把T[i][k][1]中的T[i - 1][k][0] - prices[i]变成T[i - 2][k][0] - prices[i]呢?
假设我们在T[i][k][1]中执行的是买入操作,则由于冷冻期的存在,在第i-1天不能卖出股票,所以T[i-1][k][0] = T[i - 2][k][0]. 因此买入时,T[i][k][1] = T[i - 1][k][0] - prices[i] = T[i - 2][k][0] - prices[i]。
class Solution {
public:
int maxProfit(vector& prices) {
int n = prices.size();
vector> T(n+1,vector(2));
T[0][0] = 0;
T[0][1] = -prices[0];
for(int i=1; i=2? T[i-2][0]:0)-prices[i]);
}
return T[n-1][0];
}
};
同样可以压缩空间。
class Solution {
public:
int maxProfit(vector& prices) {
int n = prices.size();
int preProfit0 = 0, profit0 = 0, profit1 = -prices[0];//分别代替T[i-1][0],T[i][0]和T[i][1]
for(int i=1; i
23、买卖股票的最佳时机含手续费(714)
本题属于k为无穷加含手续费的情况,只需要在买入或卖出时的状态转移方程扣除手续费即可。
以卖出为例
T[i][k][0] = max(T[i - 1][k][0], T[i - 1][k][1] + prices[i] - fee)
T[i][k][1] = max(T[i - 1][k][1], T[i - 1][k][0] - prices[i])
class Solution {
public:
int maxProfit(vector& prices, int fee) {
int n = prices.size();
vector> T(n,vector(2));
T[0][0] = 0;
T[0][1] = -prices[0];
for(int i=1;i
空间压缩
class Solution {
public:
int maxProfit(vector& prices, int fee) {
int n = prices.size();
int profit0 = 0, profit1 = -prices[0];//分别代替T[i][0]和T[i][1]
for(int i=1; i