>_<
给定一个数组,它的第 i 个元素是股票第 i 天的价格。
你要怎样交♂易才能获得最多的利润?
A. 不限买卖次数
一共两个状态【不持股】【持股】
dp[i][0]的含义是 第i天不持股的最大资金
dp[i][1]的含义是 第i天持股的最大资金
这里的不持股[0],持股[1]表示是该天的状态而非动作——dp[i][0]的含义是第i天处于不持股的状态,而不是说第i天发生卖股票这个动作 —— 理解这句话是解决所有股票问题的关键
class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
int[][] dp = new int[len][2];
// 初始化
dp[0][0] = 0;
dp[0][1] = -prices[0];
// 状态转移
for(int i = 1; i < len; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
}
return dp[len - 1][0];
}
}
B. 限制一次买卖
还是一共两个状态【不持股】【持股】
但注意,由于只能买卖一次,两种状态不能像上题那样随意转化——只能从【持股】转化为【不持股】,而不能反向转化
class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
int[][] dp = new int[len][2];
// 初始化
dp[0][0] = 0;
dp[0][1] = -prices[0];
// 状态转移
for(int i = 1; i < len; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], -prices[i]);
}
return dp[len - 1][0];
}
}
C. 限制两次买卖
法一:多状态
由于买卖次数的限制,一共有5种状态:
dp[i][0]的状态是 什么都不做(固定为0)
dp[i][1]的状态是 第1次持股
dp[i][2]的状态是 第1次不持股
dp[i][3]的状态是 第2次持股
dp[i][4]的状态是 第2次不持股
这时的状态转移也不是第一个例题那样的两个状态双向转化,而是一个单向的过程
class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
int[][] dp = new int[len][5];
// 初始化
dp[0][1] = -prices[0];
dp[0][3] = -prices[0];
// 状态转移
for(int i = 1; i < len; i++) {
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];
}
}
法二:规整的三维动态规划
dp[i][j][1]的含义是:处于第i天,第j次交易,持股的状态
dp[i][j][0]的含义是:处于第i天,第j次交易,不持股的状态
从图中直观观察出:
(1) 不管是第几次交易(j=whatever),持股(k=1)初值取负,不持股(k=0)初值取0——初始化时会用到
(2) 当从不持股跳到持股状态时,所处的交易次数一定也跳了1;当从持股状态跳到不持股状态时,一定没有跳出此次交易中——写dp转移时会用到
class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
int[][][] dp = new int[len][3][2];
// 初始化
dp[0][1][1] = -prices[0];
dp[0][2][1] = -prices[0];
// 状态转移
for(int i = 1; i < len; i++) {
for(int j = 1; j < 3; 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[len - 1][2][0];
}
}
D. 限制k次买卖
此时企图使用【多状态】的思路表示出所有状态是不可能的,因为交易的次数不能写死——本题只能使用三维dp
其实…这就是上一题的第二种写法。记得看上题的图理解 。
class Solution {
public int maxProfit(int k, int[] prices) {
int len = prices.length;
int[][][] dp = new int[len][k + 1][2];
// 初始化
for(int j = 1; j < k + 1; j++) {
dp[0][j][0] = 0;
dp[0][j][1] = -prices[0];
}
// 状态转移
for(int i = 1; i < len; i++) {
for(int j = 1; j < k + 1; 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[len - 1][k][0];
}
}
E. 手续费
最简单母题的变式,在卖出股票时收取手续费即可
class Solution {
public int maxProfit(int[] prices, int fee) {
int len = prices.length;
int[][] dp = new int[len][2];
// 初始化
dp[0][0] = 0;
dp[0][1] = -prices[0];
// 状态转移
for(int i = 1; i < len; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i] - fee);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
}
return dp[len - 1][0];
}
}
F. 冷冻期
如果你真正理解了【多状态】解法,那么本题也很简单——一共3个状态
dp[i][0]表示 不持股(非冷冻期)
dp[i][1]表示 持股
dp[i][2]表示 不持股(冷冻期)
关键点在于(1)弄清楚这三种状态的转化关系(2)最后处于冷冻期/非冷冻期都有可能
class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
int[][] dp = new int[len][3];
// 初始化
dp[0][0] = 0;
dp[0][1] = -prices[0];
dp[0][2] = 0;
// 状态转移
for(int i = 1; i < len; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][2]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
dp[i][2] = dp[i - 1][1] + prices[i];
}
return Math.max(dp[len - 1][0], dp[len - 1][2]);
}
}
理解与补充
- 想一想,为什么「不限制买卖次数」是2个状态的交叉转化,「限制2次买卖」是5个状态的单向转化呢?其实,「不限制买卖次数」的交叉转化可以看成无限个状态的单向转化(不做任何事,第一次1,第一次0,第二次1,第二次0,第三次1,第三次0…)
- 对于最简单的母题「不限制买卖次数」,还有一种更高效的思路—— 贪心。即利润的叠加,只要股票价格相对于前一天有所上涨,就累加上这个利润,最终结果即为全局最优的总利润(
int diff = prices[i] - prices[i - 1]; if(diff > 0) { res += diff; }
) - 在「限制2次买卖」的代码中,返回值都是dp表最右下角的值,含义为【最后一天、第二次交易、不持股状态】,但是因为可能只交易了1次,最终结果也可能是【最后一天、第一次交易、不持股状态】呀?其实,在初始化时,【第一天、第一次交易、持股状态】【第一天、第二次交易、持股状态】的初始值都是-prices[0],而且第一、二次交易向下转化的代码是一样的——也就是说,当交易次数不满2次时,“第二次交易”本身没有意义,是对“第一次交易”的完全复制 ——所以我们放心地将dp右下角作为返回值
- 在多个变式中,都使用了 多状态 的方法,我们发现,找出所有状态往往不难,可找出状态的转化关系却不简单,对此有什么技巧呢?对于某一个状态,思考它可以由哪些状态转化而来
- 上面的dp都可以做滚动数组优化——直接在原先代码上删去一个维度,很简单,不再给出代码
E N D END END
B Y A L O L I C O N BY A LOLICON BYALOLICON