文章目录
股票问题
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
股票问题为什么使用动态规划?
因为求得某一天的最大利润,一定和前一天的最大利润有关,因为股票是按照天数连续增长的。针对每一天,前一天的情况都在考虑范围内。
二维dp-常规股票问题
121. 买卖股票的最佳时机
使用五步法分析:
- 确定dp数组以及下标含义:dp[i][0]表示第i天持有股票所得最多现金。dp[i][1]表示第i天不持有股票所得最多现金。
- 以上两种情况,每一种情况都有两个状态推导。
dp[i][0]可以由两个状态推导而来:
a. 第i - 1天就持有股票,保持现状,所得金额为:dp[i - 1][0]。
b. 第i天才买入股票,所得金额为:-price[i]。
dp[i][1]也可以由两个状态推导而来:
a. 第i - 1天不持有股票,保持现状,所得金额为:dp[i - 1][1]。
b. 第i天卖出股票,所得金额为:dp[i - 1][0] + prices[i]。
3、 dp数组初始化:dp[0][0]为第0天持有股票,dp[0][0] -= prices[i]。dp[0][1]表示第0天不持有股票,dp[0][1] = 0。
4、 遍历顺序:从前往后。
5、 举例推导dp。
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int [][] dp = new int [n][2];
//dp[i][0]:表示第i天不持有时的最大利润
//dp[i][1]:表示第i天持有时的最大利润
dp[0][0] = 0;
dp[0][1] = - prices[0];
//举例每种情况
for(int i = 1 ; i < n ; i ++){
//不持有的两种状态推导
//1. 今天刚卖出去的。dp[i][0] = dp[i - 1][1] + price[i]
//2. 之前卖出去的。 dp[i][0] = dp[i - 1][0]
dp[i][0] = Math.max(dp[i - 1][1] + prices[i] , dp[i - 1][0]);
//持有的两种状态推导
//1. 今天刚持有的。dp[i][1] = - price[i]
//2. 之前就持有的。 dp[i][1] = dp[i - 1][1]
dp[i][1] = Math.max(-prices[i] , dp[i - 1][1]);
}
return dp[n - 1][0];
}
}
122. 买卖股票的最佳时机 II - 力扣(LeetCode)
现在变换问题:股票可以买卖多次,该怎么算?
本题的股票可以买卖多次,当第i天买入股票的时候,所得现金可能有之前买过的利润。
递推公式:dp[i][0]表示第i天持有股票的最多现金。
● 第i- 1天持有股票,dp[i - 1][0]。
● 第i天买入股票,(可能包含之前的利润)dp[i - 1][1] - prices[i]
dp[i][1]表示第i天不持有股票的最多现金。
● 第i天不持有股票,dp[i - 1][1]。
● 第i天卖出股票prices[i] + dp[i - 1][0]
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int [][] dp = new int [n][2];
dp[0][0] = 0;
dp[0][1] = - prices[0];
for(int i = 1 ; i < n ; i ++){
dp[i][0] = Math.max(dp[i - 1][1] + prices[i] , dp[i - 1][0]);
//唯一变化:持有的两种状态推导
//1. 今天刚持有的。dp[i][1] =dp[i - 1][0] - price[i]
dp[i][1] = Math.max(dp[i - 1][0]-prices[i] , dp[i - 1][1]);
}
return dp[n - 1][0];
}
}
714. 买卖股票的最佳时机含手续费 - 力扣(LeetCode)
可以无限次买卖股票,但是包含手续费?
相对于动态规划:122.买卖股票的最佳时机II (opens new window),本题只需要在计算卖出操作的时候减去手续费就可以了,代码几乎是一样的。
class Solution {
public int maxProfit(int[] prices, int fee) {
int n = prices.length;
int [][] dp= new int [n + 1][2];
dp[0][1] = -prices[0];
dp[0][0] = 0;
for(int i = 1 ; i < n ; i ++){
dp[i][0] = Math.max(dp[i - 1][0] , prices[i] + dp[i - 1][1] - fee);
dp[i][1] = Math.max(dp[i - 1][1] , dp[i - 1][0] - prices[i]);
}
return dp[n - 1][0];
}
}
多维dp-其他股票问题
123. 买卖股票的最佳时机 III - 力扣(LeetCode)
a. 确定dp数组下标及其含义:
dp[i][j]中i表示第i天,j为[0 - 4]五个状态,dp[i][j]为第i天状态j所剩的最大现金。
五个状态为:
ⅰ. 没有操作 (其实我们也可以不设置这个状态)
ⅱ. 第一次持有股票
ⅲ. 第一次不持有股票
ⅳ. 第二次持有股票
ⅴ. 第二次不持有股票
b. 确定递推公式:
达到dp[i][1]状态,有两个具体操作:
■ 操作一:第i天买入股票了,那么dp[i][1] = dp[i-1][0] - prices[i]
■ 操作二:第i天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][1]
一定是选最大的,所以 dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]);同理可推出剩下状态部分:
dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2])dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
c. dp数组如何初始化
第0天没有操作,dp[0][0] = 0;
第0天做第一次买入的操作,dp[0][1] = -prices[0];
第0天做第一次卖出的操作,(当天买入当天卖出)dp[0][2] = 0;
第0天做第二次买入的操作,dp[0][3] = - prices[0];
第0天做第二次卖出的操作,dp[0][4] = 0。
d. 举例推导dp数组。
最后返回最后一次不持有的dp数组,因为如果第一次卖出已经是最大值了,那么我们可以在当天立刻买入再立刻卖出。所以dp[4][4]已经包含了dp[4][2]的情况。也就是说第二次卖出手里所剩的钱一定是最多的。
class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
// 0:不操作
// 1:第一次持有
// 2:第一次不持有
// 3:第二次持有
// 4:第二次不持有
int [][] dp = new int [len][5];
dp[0][0] = 0;
dp[0][1] = -prices[0];
dp[0][2] = 0;
dp[0][3] = -prices[0];
dp[0][4] = 0;
for(int i = 1 ; i < len ; i ++){
// 不操作
dp[i][0] = dp[i - 1][0];
// 第一次持有
dp[i][1] = Math.max(dp[i - 1][1] ,dp[i - 1][0] - prices[i]);
// 第一次不持有
dp[i][2] = Math.max(dp[i - 1][2] , dp[i - 1][1] + prices[i]);
// 第二次持有
dp[i][3] = Math.max(dp[i - 1][3] , dp[i - 1][2] - prices[i]);
// 第二次不持有
dp[i][4] = Math.max(dp[i - 1][4] , dp[i - 1][3] + prices[i]);
}
return dp[len - 1][4];
}
}
188. 买卖股票的最佳时机 IV
最多可以完成k笔交易,该怎么算?
与最多完成两次交易类似,我们先找到完成两次交易的规律:
发现:2次交易我们确定了五种状态,那么k次交易,我们可以确定2 * k + 1次状态。
- dp数组含义:
dp[i][0]:第i天不做操作的最大金额。
dp[i][1]:第i天持有股票的最大金额。
dp[i][2]:第i天不持有股票的最大金额。
······
规律:j为奇数表示持有,j为偶数表示不持有。 - 递推公式:
dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1]);
dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2])
规律:j为偶数时:dp[i][j] = max(dp[i - 1][j - 1] + prices[i] , dp[i - 1][j])。
j为奇数时:dp[i][j] = max(dp[i - 1][j - 1] - prices[i] , dp[i - 1][j]) - dp数组初始化:
规律:j为奇数时,初始化为 - prices[0]。 - 遍历顺序:顺序遍历
- 举例推导。
class Solution {
public int maxProfit(int k, int[] prices) {
// 按照2次的规律来写
// 买入都是单数 卖出都是双数
int len = prices.length;
// 2次dp[5]
int [][] dp = new int [len][2 * k + 1];
// 初始化dp
for(int j = 1 ; j <= 2 * k ; j +=2){
dp[0][j] = -prices[0];
}
for(int i = 1 ; i < len ; i ++){
for(int j = 0 ; j <= 2 * k ; j ++){
if(j == 0) dp[i][0] = dp[i - 1][0];
// 全部是持有状态 之前就持有了 || 之前没持有,今天才持有
else if(j % 2 != 0) dp[i][j] = Math.max(dp[i - 1][j],dp[i - 1][j - 1] - prices[i]);
else if(j % 2 == 0) dp[i][j] = Math.max(dp[i - 1][j],dp[i - 1][j - 1] + prices[i]);
}
}
// 不持有状态
return dp[len - 1][2 * k];
}
}
309. 最佳买卖股票时机含冷冻期 - 力扣(LeetCode)
可以买卖多次,但是股票包含冷冻期,该怎么算?
最开始可以找到三个状态:卖出(不包含冷冻期)、买入、冷冻期。
// 状态流转
// 卖出, 今天卖出 || 之前卖出 || 之前是冷冻期
dp[i][0] = Math.max(dp[i - 1][2] + prices[i],Math.max(dp[i - 1][0],dp[i - 1][1]));
// 今天是冷冻期 前一天卖出了
dp[i][1] = dp[i - 1][0];
// 今天买入 前一天就买入了 冷冻期买入了
dp[i][2] = Math.max(dp[i - 1][2],dp[i - 1][1] - prices[i]);
也可以考虑把今天卖出状态单独列出:
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int [][] dp = new int [n][4];
//卖出状态
dp[0][0] = 0;
//持有状态
dp[0][1] = - prices[0];
//今天卖出状态
dp[0][2] = 0;
//今天为冷冻期
dp[0][3] = 0;
for(int i = 1 ; i < n ; i++){
//流转到买入
dp[i][1] = Math.max(Math.max(dp[i - 1][1] , dp[i - 1][0] - prices[i]) , dp[i - 1][3] - prices[i]);
//流转到卖出
dp[i][0] = Math.max(dp[i - 1][0] , dp[i - 1][3]);
//流转到今天卖出
dp[i][2] = dp[i - 1][1] + prices[i];
//流转到冷冻
dp[i][3] = dp[i - 1][2];
}
return Math.max(Math.max(dp[n - 1][0] , dp[n - 1][2]) , dp[n - 1][3]);
}
}