题目来源
题目描述
通解:最多买卖k次
状态定义的各种优化问题
首先我们来确定一天会有几个状态,最容易想到的有四种:
- 当天买入了股票
- 当天卖出了股票
- 当天没有进行任何操作,但是之前是买入股票的状态
- 当天没有进行任何操作,但是之前是卖出股票的状态
这是四种大家最容易想到的状态, 但是同时操作四个状态确实有点复杂了, 通过分析后我们可以将状态优化为两个状态:当天持有股票和当天不持有股票。每个状态又是由两种情况组合而成,本质上还是四个状态, 只不过通过优化后我们就只需要操作两个状态了.。
下面我们考虑如何表示这两个状态。
- 最常见的方法是使用dp[0]和dp[1]来表示这两种状态,这种方式不仅不方便识别dp数组的定义, 而且还增加了dp数组的一个维度,就本题而言, 如果这样定义的话, 那我们最终就会得到一个三维的dp数组, 操作起来可以说是相当麻烦了.
- 所以建议直接用两个能明确表示dp数组含义的名称来命名,比如用have表示持有股票的状态, 用no表示不持有股票的状态,这样定义之后, 不仅含义清晰明了, 还省下了一个维度.
最终的步骤是确定dp数组有几个维度。
- 首先必须有表示天数i的一个维度
- 本题还必须有表示买卖k次股票的j这个维度
所以最终就得到了两个二维的dp数组定义如下图:
确定递推公式
在确定递推公式时, 我们必须先明确一个概念:k何时进行变化。这点很重要, 直接就导致之后递推公式的不同. 官方题解是默认一买一卖才算完整交易一次, 买入不算交易, 卖出才算一次交易, 所以只有卖出时k变化. 当然也有人定义的是买入时k变化, 这种就会产生不同的递推公式, 但都是正确的.
确定了这个条件之后, 递推公式就没太大难度了, 就是每种状态都由两种情况组合而来, 最终取最大值:
根据上面我们定义的k何时变化, 体现在递推公式中就是在推导no[i][j]的第二种情况时用到的have[i-1][j-1], 因为卖出股票时k会变化, 所以上一个持有股票的状态就是j-1了. 这点一定要注意, 它不止在递推公式中很重要, 也在之后的空间优化时很重要! 这也是推导递推公式的唯一难点了!
dp数组的初始化
本题的初始化过程还是有点复杂的, 但是并不难理解, 这里就不多解释了:
最终返回结果
由于在所有的n天结束之后,手上不持有股票对应的最大利润一定是严格大于手上持有股票对应的最大利润的,然而完成的交易数并不是越多越好(例如数组prices 单调递减,我们不进行任何交易才是最优的), 因此最终的答案即为**no[n−1][0..k]
中的最大值**.
当我们将上的k用具体的数固定住的时候, 就衍生出了下面的几种情况, 这里有一点需要说明: k=1或者+∞对于这个维度来说都是没有意义的, 可以直接忽略掉这个维度, 变成一维数组.
注意
(1)k的正序和倒序
- 为了避免状态压缩造成的数据覆盖, 应该采用k倒序(本题采用正序也是可以得到相同的结果的),本题没有影响, 不代表其他动态规划的题目也没有影响
(2)k的范围
- 如果交易次数大于n/2,必然存在有一天交易了两次,然而这是毫无意义的, 因为 n 天最多只能进行⌊n/2⌋ 笔交易, 其中⌊x⌋ 表示对x向下取整。因此我们可以将 k 对⌊n/2⌋ 取较小值之后再进行动态规划.
(3)have数组的维数
- k+1维是没问题的, 如果维数只有k, 就表示不了交易0次了, 要是数组一直是递减的, 一交易就是亏损.
(4)j的循环
- no[i][j]的状态转移方程中包含have[i−1][j−1], 在j=0 时其表示不合法的状态, 因此在 j=0 时, 我们无需对 no[i][j] 进行转移, 让其保持值为 0 即可, 所以在j=0时, 我们需要单独对have[i][0]进行处理.
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int n = prices.size();
if(n == 0){
return 0;
}
k = std::min(k, n / 2); // 最多k次操作
std::vector<std::vector<int>> have(n, std::vector<int>(k + 1)); // 1...n ,0... k
std::vector<std::vector<int>> no(n, std::vector<int>(k + 1)); // 1...n ,0... k
have[0][0] = -prices[0]; // 第 0 天持有股票的利润: 以 prices[0] 的价格买入股票
no[0][0] = 0; // 第 0 天不持有股票的利润:(也就是不买入也不卖出),当前利润一定为0
for (int i = 1; i <= k; ++i) { //
have[0][i] = no[0][i] = INT_MIN / 2; //第0天,对于第k次操作
}
for (int i = 1; i < n; ++i) { //第i天
have[i][0] = std::max(have[i - 1][0], no[i - 1][0] - prices[i]); // j=0时, no[i][0]不合法
for (int j = 1; j <= k; ++j) {
have[i][j] = std::max(have[i - 1][j], no[i - 1][j] - prices[i]);
no[i][j] = std::max(no[i - 1][j], have[i - 1][j - 1] + prices[i]); // j=0时, no[i][0]不合法
}
}
return *std::max_element(no[n - 1].begin(), no[n - 1].end());
}
};