今天的2道题是一个套路,两者唯一的区别就是“是否可以多次买卖”。这一区别最终体现在了状态转移方程上买入的情况,是从0开始买入还是继承之前的利润再买入。所以在做题的时候要仔细看下题目要求,是只能一次,还是可以多次买卖。
第1题(LeetCode 121. 买卖股票的最佳时机)看似是很简单的一道题,但自己除了双重循环的暴力解法外,没有想到其他可行的方法。题解中除了暴力解法外,还有两种方法。第1种是贪心方法,需要在遍历prices时不断更新最小价格,并根据“当前价格”与“最小价格”的差来更新答案。因为最小价格在更新时只会变得更小,所以如果“当前价格”等于或大于之前结果对应的卖出价格时,“当前价格”与“最小价格”的差只会更大。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int priceMin = prices[0], res = 0;
for (int price : prices) {
priceMin = min(priceMin, price);
res = max(res, price - priceMin);
}
return res;
}
};
第2种是DP解法。dp数组的第i天有两个数值,dp[i][0]表示第i天“不持有”股票对应的最大所得现金,dp[i][1]表示第i天“持有”股票对应的最大所得现金。
- 这里的持有就是字面意思,第i天持有分2种情况。有可能之前就买入,即第(i - 1)天就持有了(dp[i - 1][1]);也有可能是之前未买入,第i天买入(-prices[i])。
- 不持有也分两种情况。有可能前一天就不持有(dp[i - 1][0]);也有可能前一天持有,但第i天卖出(dp[i - 1][1] + prices[i])。
注意“第i天持有”的第2种情况,因为这一题中只能买卖一次,所以“之前未买入”就代表“之前没有买也没有卖”,相当于之前的存款数额为0,所以应该对应-prices[i],而不是dp[i - 1][0] - prices[i]。所以状态转移方程就可以总结为dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i])和dp[i][1] = max(dp[i - 1][1], -prices[i])。
初始化方面,第0天“不持有”对应的现金数额为0,“持有”则对应买入第1天的股票,现金数额为-prices[0]。由于每个dp[i]的取值都依赖于其前一个,所以遍历方向为正向遍历。最终是要卖出的,对应“不持有”,所以最后返回dp的最后一行第0列数值作为结果。
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<vector<int>> dp(prices.size(), vector<int>(2, 0));
dp[0][0] = 0; // [i][0]表示第i天“不持有”股票对应的最大所得现金
dp[0][1] = -prices[0]; // [i][1]表示第i天“持有”股票对应的最大所得现金
for (int i = 1; i < prices.size(); ++i) {
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = max(dp[i - 1][1], -prices[i]);
}
return dp[prices.size() - 1][0];
}
};
也正是由于每个dp[i]的取值都依赖于其前一个,所以dp矩阵没必要设置行数与prices保持一致,只需要2行即可。在实现上,可以用“下标对2取余”的“滚动数组”方式。
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<vector<int>> dp(2, vector<int>(2, 0));
dp[0][0] = 0; // [i][0]表示第i天“不持有”股票对应的最大所得现金
dp[0][1] = -prices[0]; // [i][1]表示第i天“持有”股票对应的最大所得现金
for (int i = 1; i < prices.size(); ++i) {
dp[i % 2][0] = max(dp[(i - 1) % 2][0], dp[(i - 1) % 2][1] + prices[i]); // prices下标不对2取余
dp[i % 2][1] = max(dp[(i - 1) % 2][1], -prices[i]); // prices下标不对2取余
}
return dp[(prices.size() - 1) % 2][0];
}
};
第2题(LeetCode 122.买卖股票的最佳时机II)在day 37中用贪心方法解决过,这里再来尝试用DP方法。只有一处要求与上一题不同,就是股票可以买卖多次(但任意时刻还是最多只能持有一个股票)。所以“第i天持有”的第2种情况,也就是“之前未买入,第i天买入”需要改为“第(i - 1)天未持有,第i天买入”。也就是说,第i天就不再是在“从未买卖过”的基础上进行了,而是可以在“获得之前买卖利润”的基础上来进行“第i天的买入”。所以这一部分的取值对应dp[i - 1][0] - prices[i]。
下面直接写出“滚动数组”方式实现的代码。
class Solution {
public:
int maxProfit(vector<int>& prices) {
vector<vector<int>> dp(2, vector<int>(2, 0));
dp[0][0] = 0; // [i][0]表示第i天“不持有”股票对应的最大所得现金
dp[0][1] = -prices[0]; // [i][1]表示第i天“持有”股票对应的最大所得现金
for (int i = 1; i < prices.size(); ++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]);
}
return dp[(prices.size() - 1) % 2][0];
}
};