买卖股票的最佳时机
题目
版本1 正确 交易次数为一次 模板写法
public static int maxProfit(int[] prices) {
// 买卖股票的最佳时机, 只能进行一次交易
// 状态有三种, 第i天, 最大交易次数K, 是否持有股票, dp[i][k][j] = x; 表示第i天交易k次, 且持不持有股票的最大利润为x
// 注意这里一定要认为买入股票就算就算交易一次 不然结果不对
int K = 1;
int [][][] dp = new int[prices.length + 1][K + 1][2];
// base case
// 第0天, 股票还没开始, 任何交易次数下, 不持有股票的利润都为0, dp[0][k][0] = 0;
// 第0天, 股票还没开始, 任何交易次数下, 持有股票都不可能, 为了让这种不可能不影响结果, dp[0][k][1] = Integer.MIN_VALUE
for (int k = 0; k <= K; k ++) {
dp[0][k][1] = Integer.MIN_VALUE;
}
// 任何一天, 交易次数为0, 表示没有交易过, 此时不持有股票的最大利润为0, 即dp[i][0][0] = 0;
// 任何一天, 交易次数为0, 表示没有交易过, 此时持有股票的最大利润为负无穷, 即dp[i][0][1] = Integer.MIN_VALUE;
for (int i = 0; i <= prices.length; i ++) {
dp[i][0][1] = Integer.MIN_VALUE;
}
// 状态转移
for (int i = 1; i <= prices.length; i ++) {
for (int k = 1; k <= K; k ++) {
// 第i天, 当前交易次数为k次, 目前不持有股票.
dp[i][k][0] = Math.max(dp[i - 1][k][1] + prices[i - 1], dp[i - 1][k][0]);
// 第i天, 当前交易次数为k次, 目前持有股票
// 注意这里一定要认为买入股票就算就算交易一次 不然结果不对
dp[i][k][1] = Math.max(dp[i - 1][k - 1][0] - prices[i - 1], dp[i - 1][k][1]);
}
}
// 最大值应该是最后一天, 交易次数为K次, 并且不持有股票的时候
return dp[prices.length][K][0];
}
正确的原因
(1) 注意base case, 什么时候该赋值负无穷
(2) 注意一定要在买入股票的时候认为发生了一次交易, 不然结果不对
如何限定交易次数k的?
以K最大值为2来进行理解.
(1) 首先第一次交易可以在任何天发生, 第二次交易也是, 但是第二次交易一定是发生在第一次交易之后的.
(2) 不断枚举第一次交易发生的时机, 然后得到一个利润, 但是实际保存的都是当前天数之前, 第一次交易后, 获利的最大值. 因此最大值产生后, 后面天枚举得到的依旧是这个值
(3) 然后第二次交易依赖第一次交易的结果, 不同时机发生第二次交易依赖得到的第一次结果, 可能不同, 也可能相同, 那么最后第二次交易后的最大值就传递到了最后.
(4) 其实就相当于先枚举了一遍只发生一次交易的最大值的情况, 然后再从头遍历, 枚举一遍第二次交易发生的情况. 通过数组大小的限制, 就一定只会发生两次交易, 只不过是发生在不同时机而已, 永远不可能会有第三次交易.
举个例子来说
枚举完第一次交易后最大值分别是[1, 2, 2, 2, 4, 5, 5]
然后枚举第二次交易[-inf, 3, 3, 4, 5, 7, 7]
因此记住, 枚举的其实是交易的时机, 交易次数和遍历的次数有关, 是限定死的
版本2 正确 单纯的为了解题, 非模板写法
public int maxProfit(int[] prices) {
if (prices.length == 0) {
return 0;
}
// 如果交易次数限制为一次的话, 考虑不用动态规划, 而是寻找数组中差值最大即可
// 记录数组i位置之前的最小值, 然后不断更新最大差值, 最后结果就是答案
int minBeforeI = prices[0];
int ans = 0;
for (int i = 1; i < prices.length; i ++) {
if (prices[i] >= minBeforeI) {
// 尝试更新一次答案
ans = Math.max(prices[i] - minBeforeI, ans);
} else {
minBeforeI = prices[i];
}
}
return ans;
}
正确的原因
(1) 这道题限制只交易一次, 没必要那么复杂用动态规划来解决
买卖股票的最佳时机II
题目
版本1 正确 交易次数无限
public int maxProfit(int[] prices) {
// 交易次数不限制
// 状态只剩两个 一个是第i天, 一个是是否持有股票, dp数组的值是最大利润
int [][] dp = new int[prices.length + 1][2];
// base case
dp[0][1] = Integer.MIN_VALUE;
// 状态转移
for (int i = 1; i <= prices.length; i ++) {
// 第i天不持有股票
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1]);
// 第i天持有股票
dp[i][0] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i - 1]);
}
// 最后返回的就是第i天, 不持有股票的时候
return dp[prices.length][0];
}
正确的原因
(1) 无需考虑交易次数的问题
买卖股票的最佳时机III
题目
版本1 正确 k = 2的情况
public static int maxProfit(int[] prices) {
// 买卖股票的最佳时机, 只能进行一次交易
// 状态有三种, 第i天, 最大交易次数K, 是否持有股票, dp[i][k][j] = x; 表示第i天交易k次, 且持不持有股票的最大利润为x
// 注意这里一定要认为买入股票就算就算交易一次 不然结果不对
int K = 2;
int [][][] dp = new int[prices.length + 1][K + 1][2];
// base case
// 第0天, 股票还没开始, 任何交易次数下, 不持有股票的利润都为0, dp[0][k][0] = 0;
// 第0天, 股票还没开始, 任何交易次数下, 持有股票都不可能, 为了让这种不可能不影响结果, dp[0][k][1] = Integer.MIN_VALUE
for (int k = 0; k <= K; k ++) {
dp[0][k][1] = Integer.MIN_VALUE;
}
// 任何一天, 交易次数为0, 表示没有交易过, 此时不持有股票的最大利润为0, 即dp[i][0][0] = 0;
// 任何一天, 交易次数为0, 表示没有交易过, 此时持有股票的最大利润为负无穷, 即dp[i][0][1] = Integer.MIN_VALUE;
for (int i = 0; i <= prices.length; i ++) {
dp[i][0][1] = Integer.MIN_VALUE;
}
// 状态转移
for (int i = 1; i <= prices.length; i ++) {
for (int k = 1; k <= K; k ++) {
// 第i天, 当前交易次数为k次, 目前不持有股票.
dp[i][k][0] = Math.max(dp[i - 1][k][1] + prices[i - 1], dp[i - 1][k][0]);
// 第i天, 当前交易次数为k次, 目前持有股票
// 注意这里一定要认为买入股票就算就算交易一次 不然结果不对
dp[i][k][1] = Math.max(dp[i - 1][k - 1][0] - prices[i - 1], dp[i - 1][k][1]);
}
}
// 最大值应该是最后一天, 交易次数为K次, 并且不持有股票的时候
return dp[prices.length][K][0];
}
正确的原因
(1) 在模板的基础上, 令k = 2即可.
买卖股票的最佳时机IV
题目
版本1 正确 但是没必要记录每次交易后的最大值
public static int maxProfit(int k, int[] prices) {
// 最多完成k比交易
// 买卖都需要一天都话, 最多也就只能进行 prices.length / 2 次交易, 当k大于这个数字时, 都是无效的
if (k > prices.length / 2) {
k = prices.length / 2;
}
// 有三种状态, 天数, 交易次数, 是否持有股票
// dp[i][j][1]表示第i天, 交易次数为j次, 并且持有股票的最大利润
// dp[i][j][0]表示第i天, 交易次数为j次, 不持有股票的最大利润
int [][][] dp = new int [prices.length + 1][k + 1][2];
// base case
// dp[0][j][0] = 0
// dp[0][j][1] = Integer.MIN_VALUE, 第0天还没开始, 不可能持有股票
for (int j = 0; j <= k; j ++) {
dp[0][j][1] = Integer.MIN_VALUE;
}
// dp[i][0][0] = 0
// dp[i][0][1] = Integer.MIN_VALUE, 交易次数为0的时候, 不可能持有股票
for (int i = 0; i <= prices.length; i ++) {
dp[i][0][1] = Integer.MIN_VALUE;
}
// 状态转移
int max = 0;
for (int i = 1; i <= prices.length; i ++) {
for (int j = 1; j <= k; j ++) {
// 第i天 交易次数为j 且不持有股票
dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i - 1]);
max = Math.max(dp[i][j][0], max);
// 第i天 交易次数为j 且持有股票
dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i - 1]);
}
}
// 交易次数为k次, 但是可能并不是完成所有的交易次数, 然后最大利润最大
// 因此需要寻找最大利润, 一定是至少完成了一次的才有利润
return max;
}
正确的原因
(1) 记录合适的k的次数, 当k过大的时候, 进行合理的缩小
(2) 没必要记录每次交易后的最大值, 因为根据定义dp[][][]记录的一直都是最大值, 已经进行过择优了, 因此dp[prices.length][k][0]就一定是最大值, 即使可能k只用一次就达到了最优, 后续的交易次数其实并没有真正的将结果结算进去, 因为被择优了
版本2 正确 最优写法
public static int maxProfit(int k, int[] prices) {
// 最多完成k比交易
// 买卖都需要一天都话, 最多也就只能进行 prices.length / 2 次交易, 当k大于这个数字时, 都是无效的
if (k > prices.length / 2) {
k = prices.length / 2;
}
// 有三种状态, 天数, 交易次数, 是否持有股票
// dp[i][j][1]表示第i天, 交易次数为j次, 并且持有股票的最大利润
// dp[i][j][0]表示第i天, 交易次数为j次, 不持有股票的最大利润
int [][][] dp = new int [prices.length + 1][k + 1][2];
// base case
// dp[0][j][0] = 0
// dp[0][j][1] = Integer.MIN_VALUE, 第0天还没开始, 不可能持有股票
for (int j = 0; j <= k; j ++) {
dp[0][j][1] = Integer.MIN_VALUE;
}
// dp[i][0][0] = 0
// dp[i][0][1] = Integer.MIN_VALUE, 交易次数为0的时候, 不可能持有股票
for (int i = 0; i <= prices.length; i ++) {
dp[i][0][1] = Integer.MIN_VALUE;
}
// 状态转移
for (int i = 1; i <= prices.length; i ++) {
for (int j = 1; j <= k; j ++) {
// 第i天 交易次数为j 且不持有股票
dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i - 1]);
// 第i天 交易次数为j 且持有股票
dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i - 1]);
}
}
// 根据dp数组的定义, 最大值是不断向后传递的, 因为每次取值的时候都择优了的, 因此即使最大交易次数为5
// 实际发生2次交易就能得到利润最大值, 后面三次交易是不会真正发生的(即后三次交易的结果在择优的时候被忽略了)
return dp[prices.length][k][0];
}
买卖股票时机含冷冻期
题目
版本1 正确 交易次数无限 含冷冻期
public int maxProfit(int[] prices) {
// 包含冷冻期的股票交易的最大利润
// 可以多次买卖股票, 即不限制交易次数.
// 冷冻期即买入股票的时候, 只能考虑前天卖出了股票今天买入的情况, 不能考虑昨天
// 状态就只剩第i天, 以及第i天股票的持有状态了
int [][] dp = new int[prices.length + 1][2];
// base case
// 第0天, 股票还没开始, 不持有股票的利润都为0, dp[0][0] = 0;
// 第0天, 股票还没开始, 持有股票都不可能, 为了让这种不可能不影响结果, dp[0][1] = Integer.MIN_VALUE
dp[0][1] = Integer.MIN_VALUE;
// 状态转移
for (int i = 1; i <= prices.length; i ++) {
// 第i天不持有股票的情况
dp[i][0] = Math.max(dp[i - 1][1] + prices[i - 1], dp[i - 1][0]);
// 第i天持有股票的情况
if (i >= 2) {
// 当天数大于等于2的时候, 才开始考虑dp[i - 2][0]的情况
dp[i][1] = Math.max(dp[i - 2][0] - prices[i - 1] , dp[i - 1][1]);
} else {
dp[i][1] = Math.max(dp[i - 1][0] - prices[i - 1] , dp[i - 1][1]);
}
}
// 最大值应该是最后一天, 并且不持有股票的时候
return dp[prices.length][0];
}
正确的原因
(1) 交易次数无限, 就不用考虑交易次数了, 冷冻期只影响当i >= 2的时候, 持有股票的情况. 注意边界即可