1. 题目
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
示例 2:
输入:k = 2, prices = [3,2,6,5,0,3]
输出:7
解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3
。
提示:
0 <= k <= 100
0 <= prices.length <= 1000
0 <= prices[i] <= 1000
Related Topics 数组 动态规划
👍 530 👎 0
2. 题解
2.1 解法1: 动态规划
1.状态定义
对于股票交易问题, 动态规划的思想主要是 穷举状态, 然后再找出每个 状态 对应的 选择 , 对于该题, 问题的 状态 有三个, 第一个是天数,第二个是 允许交易的最大次数,第三个是 当前的股票持有状态; 所以有以下状态定义:
dp[i][k][0 or 1]
0 <= i <= n-1, 1 <= k <= K
n 为天数,大 K 为最多交易数
此问题共 n × K × 2 种状态,全部穷举就能搞定。
for 0 <= i < n:
for 1 <= k <= K:
for s in {0, 1}:
dp[i][k][s] = max(buy, sell, rest)
2.状态转移方程
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
max( 选择 rest , 选择 sell )
解释:今天我没有持有股票,有两种可能:
要么是我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有;
要么是我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
max( 选择 rest , 选择 buy )
解释:今天我持有着股票,有两种可能:
要么我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票;
要么我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了。
注意 k 的限制,我们在选择 buy 的时候,把 k 减小了 1,当然你也可以在 sell 的时候减 1, 两者不重复即可
3.状态初始化(定义 base case)
dp[-1][k][0] = 0
解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0 。
dp[-1][k][1] = -infinity
解释:还没开始的时候,是不可能持有股票的,用负无穷表示这种不可能。
dp[i][0][0] = 0
解释:因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0 。
dp[i][0][1] = -infinity
解释:不允许交易的情况下,是不可能持有股票的,用负无穷表示这种不可能。
对于状态的初始化, 可根据具体问题, 来具体的考虑
4.具体题目解答
1. 本题 188.买卖股票的最佳时机 IV(困难)
k = 任意整数
由于进行一次交易需要两天, 一天买入一天卖出, 所以 最大交易次数 k <= n/2
, 在初始时可以进行判断, 若 k>n/2
, 问题转化为 不限次交易, 相当于问题: 122.买卖股票的最佳时机 II(简单)
初始化:
主要需要初始化 第 0 天的情况
dp[i][0][0]:对应于初始状态,第i天0次交易卖出,既然都没交易,那何来卖出呢,所以只能是0。这里不用进行初始化, 因为默认为 0
dp[0][k][0]: 第1天 未持有股票, 所得最大收益 为 0
dp[0][k][1]: 第1天, 持有股票, 最大收益为 -price[0]
代码:
class Solution {
public int maxProfit(int k, int[] prices) {
int n = prices.length;
if (n == 0) {
return 0;
}
if (k > n / 2) {
// 转化为不限次交易
int dp_i_0 = 0, dp_i_1 = -prices[0];
for (int i = 1; i < n; i++) {
int temp = dp_i_0;
dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
dp_i_1 = Math.max(dp_i_1, temp - prices[i]);
}
return dp_i_0;
}
int[][][] dp = new int[n][k + 1][2];
// 初始化第 0 天的情况
for (int i = 0; i <= k; i++) {
dp[0][i][0] = 0;
dp[0][i][1] = -prices[0];
}
for (int i = 1; i < n; i++) {
for (int j = 1; j <= k; j++) {
dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]);
dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
}
}
return dp[n - 1][k][0];
}
}
代码参考: 四种解法+图解 188.买卖股票的最佳时机 IV
2. 121. 买卖股票的最佳时机 (k=1)
转移方程
套上面分析状态转移方程,根据 base case,可以做一些化简
dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0] - prices[i])
= max(dp[i-1][1][1], -prices[i])
解释:k = 0 的 base case,所以 dp[i-1][0][0] = 0。
现在发现 k 都是 1,不会改变,即 k 对状态转移已经没有影响了。
可以进行进一步化简去掉所有 k:
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 天的状态
dp[0][0] = 0
dp[0][1] = -prices[0]
代码:
class Solution {
public int maxProfit(int[] prices) {
if (prices.length == 0) {
return 0;
}
int dp_i_0 = 0, dp_i_1 = -prices[0];
for (int i = 1; i < prices.length; i++) {
dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
dp_i_1 = Math.max(dp_i_1, -prices[i]);
}
return dp_i_0;
}
}
3. 122. 买卖股票的最佳时机 II (k 不限次数)
转移方程
套上面分析状态转移方程,根据 base case,可以直接删去 k
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
= max(dp[i-1][k][1], dp[i-1][k][0] - prices[i])
我们发现数组中的 k 已经不会改变了,也就是说不需要记录 k 这个状态了:
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])
初始化
初始化第 0 天的状态
dp[0][0] = 0
dp[0][1] = -prices[0]
代码:
class Solution {
public int maxProfit(int[] prices) {
if (prices.length == 0) {
return 0;
}
int dp_i_0 = 0, dp_i_1 = -prices[0];
for (int i = 1; i < prices.length; i++) {
int temp = dp_i_0;
dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
dp_i_1 = Math.max(dp_i_1, temp - prices[i]);
}
return dp_i_0;
}
}
4. 123. 买卖股票的最佳时机 III (k=2)
这里与 188.买卖股票的最佳时机 IV(困难) 题目基本没有区别, 只是 k 指定了为 2, 所以直接令 k=2, 复用 188题的代码即可
转移方程
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
初始化
初始化第 0 天的状态
dp[0][k][0] = 0
dp[0][k][1] = -prices[0]
代码:
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
if (n == 0) {
return 0;
}
int k = 2;
int[][][] dp = new int[n][k + 1][2];
// 初始化第 0 天的情况
for (int i = 0; i <= k; i++) {
dp[0][i][0] = 0;
dp[0][i][1] = -prices[0];
}
for (int i = 1; i < n; i++) {
for (int j = 1; j <= k; j++) {
dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]);
dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
}
}
return dp[n - 1][k][0];
}
}
以上代码复杂度较高, 由于这里 k 取值范围比较小,所以可以不用 for 循环,直接把 k = 1 和 2 的情况全部列举出来也可以:
状态转移方程:
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][1][0] - prices[i])
dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
dp[i][1][1] = max(dp[i-1][1][1], -prices[i])
代码:
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
if (n == 0) {
return 0;
}
int dp_i10 = 0;
int dp_i11 = -prices[0];
int dp_i20 = 0;
int dp_i21 = -prices[0];
for (int i = 1; i < n; i++) {
// 今天未持有: 1.昨天就未持有,今天无操作 2.昨天持有今天卖出
dp_i20 = Math.max(dp_i20, dp_i21 + prices[i]);
// 今天持有: 1.昨天就持有, 今天无操作 2.昨天未持有, 今天买入,交易次数+1
dp_i21 = Math.max(dp_i21, dp_i10 - prices[i]);
// 下面推交易次数为 1 的情况
dp_i10 = Math.max(dp_i10, dp_i11 + prices[i]);
// 交易记录为1 ,那么如果昨天未持有, 今天买入, 那么此次为第一次交易, 原收益为 0 , 直接等于 -prices[i]
dp_i11 = Math.max(dp_i11, -prices[i]);
}
return dp_i20;
}
}
5. 309. 最佳买卖股票时机含冷冻期 (k 不限次数, 只是含有冷冻期)
转移方程
每次 sell 之后要等一天才能继续交易。只要把这个特点融入上一题的状态转移方程即可:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])
解释:第 i 天选择 buy 的时候,要从 i-2 的状态转移,而不是 i-1 。
初始化
初始化第 0 天的状态
dp[0][0] = 0
dp[0][1] = -prices[0]
代码:
这个注意需要额外定义一个用于表示 dp[i-2][0]
的变量, 初始时值也为 0 , 具体在循环体内的赋值, 可以先弄清楚各个变量的含义, 然后在草稿纸上演算一下, 是否代表其正确的值, 避免出错
class Solution {
public int maxProfit(int[] prices) {
if (prices.length == 0) {
return 0;
}
// 初始化第 0 天的情况
int dp_i_0 = 0;
int dp_i_1 = -prices[0];
// 代表 dp[i-2][0], 初始时为 0
int dp_pre_0 = 0;
for (int i = 1; i < prices.length; i++) {
int temp = dp_i_0;
dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
dp_i_1 = Math.max(dp_i_1, dp_pre_0 - prices[i]);
dp_pre_0 = temp;
}
return dp_i_0;
}
}
6. 714. 买卖股票的最佳时机含手续费 (k 不限次数, 每次买入需要减手续费, 与 122 题类似)
转移方程
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i][1], dp[i-1][0] - prices[i] - fee)
初始化
初始化第 0 天的状态
dp[0][0] = 0
dp[0][1] = -prices[0] - fee
代码:
class Solution {
public int maxProfit(int[] prices, int fee) {
if (prices.length == 0) {
return 0;
}
int dp_i0 = 0;
int dp_i1 = -prices[0] - fee;
for (int i = 1; i < prices.length; i++) {
int temp = dp_i0;
dp_i0 = Math.max(dp_i0, dp_i1 + prices[i]);
dp_i1 = Math.max(dp_i1, temp - prices[i] - fee);
}
return dp_i0;
}
}