动态规划入门见:【Java数据结构与算法】动态规划从入门到入坟,思路、方法、技巧(一)
动态规划中等见:【Java数据结构与算法】动态规划从入门到入坟,思路、方法、技巧(二)
多状态动态规划要点就是,逻辑严密,让程序自己选择最优方案。
买卖股票的最佳时机Ⅱ
题目:给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你一天可以多次买卖。但是买之前必须卖掉手里的股票
输入: prices = [7,1,5,3,6,4]
输出: 7 ,第 2 天买,第3天卖,第4天买,第5天卖 5-1+6-3=7
题目分析:
对于第 i 天结束, 只有两种状态: 手里有股票,手里没股票。
对于第i + 1天结束, 如果手里没股票,只有两种可能,①第 i 天手里没有股票②第 i 天手里有股票 i + 1天卖掉了。
可以判别,今天卖了股票好,还是之前就没买好
如果手里有股票,也有两种可能,①第i天没股票,i + 1天买了股票, ②第i天有股票, 第i + 1天没操作。
可以判别手里有股票来自于今天买好,还是之前买了没卖好
解题思路:
- dp[i][0] 表示第i天交易后,手里没有股票的收益
- dp[i][1] 表示第 i 天交易后,手里有股票的收益。
- dp[i][0] = max(dp[i - 1][0], dp[i- 1][1] + price[i])
- dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - price[i])
- 初始化:第一天,手里没票只能是没操作,利润为0 dp[0][0] = 0, 手里有票只能是今天买了dp[0][1] = -price[0]
- 遍历顺序: 从左到右
- 举例dp prices = [7,1,5,3,6,4]
dp[5][0] = [ 0, 0, 4, 4 , 7 , 7 ]
dp[5][1] = [-7,-1, -1, 1 , 1 , 3 ]
java代码
class Solution {
public int maxProfit(int[] prices) {
//记录手里没股票, 是因为卖了还是因为没买导致赚钱多
//记录手里有股票, 是因为买了,还是没操作导致剩钱多,
int[][] dp = new int[prices.length][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for(int i = 1; i < dp.length; 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[prices.length - 1][0];
}
}
贪心算法:7 1 5 6 只要今天比昨天大就赚钱,例如这个可以第2天买入第三天卖出赚 4, 第三天再买入,第四天卖出赚1 总赚5.
class Solution {
public int maxProfit(int[] prices) {
//记录手里没股票, 是因为卖了还是因为没买 剩钱多
//记录手里有股票, 是因为买了,还是没操作,剩钱多,
int[][] dp = new int[prices.length][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for(int i = 1; i < dp.length; 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[prices.length - 1][0];
}
}
买卖股票的最佳时机 III
题目:给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。且买的时候手里不能有股票
题目分析: 这道题较上上道题,买卖股票的最佳时机Ⅱ,多了最多完成两笔交易的限制。也就是可以不做交易,做一笔交易,做两笔交易。
1.dp数组及下标含义
状态也不能再简单的分为第i天后手里有股票和手里没有股票了。
可以分为 第 i 天后,有这几种状态, dp[i][j]的值表示账户余额
- dp[i][0]无操作 – 没股票
- dp[i][1]第一次有股票(进行过一次买股票) – 有股票
- dp[i][2]第一次卖出了导致没股票(完成了第一次交易) – 没股票
- dp[i][3]第二次有股票(进行过两次买股票) – 有股票
- dp[i][4]第二次卖出了导致没股票 (完成了第二次交易)-- 没股票
2.递推公式
第一次有股票可由之前无操作,今天第一次买 入 或 之前就买了 推出
dp[i][1] = dp[i - 1][1] 或 dp[i - 1][0] - prices[i - 1];
自动选看之前买更好还是现在买更好, dp[i][1]是负值,值大说明买第一个股票花费少,下面也同理自动选之前卖好还是当天卖好,当然也是选大值
dp[i][2] = Math.max(dp[i - 1][1] + prices[i], dp[i - 1][2])
第二次有股票,可由第一次卖出导致没股票+今天买入 或 之前就买入了推出
dp[i][3] = Math.max(dp[i - 1][3] , dp[i - 1][2] - prices[i - 1])
第二次没股票可由当天卖了第二次有的那个股票 或 之前就没股票
dp[i][4] = Math.max(dp[i - 1][3] + prices[i], dp[i - 1][4])
最终结果在 第二次卖出股票,或第一次卖出股票,或不操作中。
如果不操作更优得话,第一次没有股票和第二次没有股票都会自动选择之前就没股票即不操作。所以前两个包括了不操作。
3.初始化
dp[0][0] = 0
dp[0][1] = -prices[0]
dp[0][2] = 0
dp[0][3] = -prices[0] 可以理解为在第0天买了立马卖了又买了
dp[0][4] = 0
java代码
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][5];
//初始化
dp[0][1] = -prices[0];
dp[0][3] = -prices[0];
for(int i = 1; i < n; 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]);
//第二次买入股票 来自于之前就买了第二次,或者第一次卖完没买但当天买
//i = 1 时两者相同 i > 2 时 如果之前能进行一次赚钱的交易,则必定选择后者当天买入
//然后再自动选择是第一次交易完立马当天买入好,还是找到一个更好的买入时间买入好
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[n - 1][4];
}
}
代码优化, 由于每个状态只用了前一个状态,可由变量代替dp数组。
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int firstHas = -prices[0];
int secondHas = -prices[0];
int firstSale = 0;
int secondSale = 0;
for(int i = 1; i < n; i++) {
//第一次有股票由
firstHas = Math.max(firstHas, -prices[i]);
firstSale = Math.max(firstSale, firstHas + prices[i]);
secondHas = Math.max(secondHas, firstSale-prices[i]);
secondSale = Math.max(secondHas + prices[i], secondSale);
}
return secondSale;
}
}
买卖股票的最佳时机 IV
题目:给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 K 笔 交易。且买的时候手里不能有股票
上一题是最多完成两笔交易, 而此题是最多完成K笔交易
所以有 2 * k + 1 个状态。用循环的方式表示就行了,需要注意的是,状态需要连续的更新,不能先更新买的再更新卖的。
Java代码
class Solution {
public int maxProfit(int k, int[] prices) {
//k 笔交易 2 * k + 1 个状态
if(prices == null || prices.length == 0) return 0;
int n = prices.length;
int[][] dp = new int[n][2 * k + 1];
//初始化
for(int i = 1; i < 2 * k + 1; i+=2) {
dp[0][i] = -prices[0];
}
int ret = 0;
for(int i = 1; i < n; i++) {
//第l次有股票, 要么是之前买了, 要么是之前没股票
//完成第l次交易, 要么之前就完成了,要么之前没完成(手里有股票)现在完成
for(int j = 1; j < 2 * k + 1; j+=2) {
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - 1] - prices[i]);
dp[i][j + 1] = Math.max(dp[i - 1][j + 1], dp[i - 1][j] + prices[i]);
ret = Math.max(ret, dp[i][j + 1]);
}
}
return dp[n - 1][2 * k];
}
}
最佳买卖股票时机含冷冻期
题目:给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
输入: [1,2,3,0,2]
输出: 3 解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
题目分析: 这道题比最佳买卖股票时机Ⅱ,多了一个限定条件,冷冻期,卖出股票第二天不能买入。
之前没有冷冻期的时候, 手里有股票可以从之前手里有股票和今天现买股票推出。
手里没有股票可以从之前就没有股票和今天卖了股票推出
//之前有股票,或今天现买 推得手里有股票的状态
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]);
1.dp数组及下标含义
而加入冷冻期之后, 今天没股票依然可以由之前就没股票和今天卖了股票推出。
但是,今天有股票不能简单的由今天买了、或之前就有推出,因为可能存在今天是冷冻期不能买的情况。
所以可以将今天没股票情况拆分成: ①今天卖了股票, ②昨天卖了股票,今天为冷冻期。③前天或更早卖了股票。
所以总共有四个状态:
1.dp[i][0] 今天有股票 (今天买的,或者之前就有) —>有股票
2.dp[i][1]今天卖了股票 —>没股票
3.dp[i][2] 昨天卖了股票,今天冷冻期—> 没股票
4.dp[i][3] 前天或更早卖了股票,今天没限制 ---->没股票
2.递推公式
这样就可以得出正确的递推公式:
//今天卖了股票, 可由之前有股票推出
dp[i][1] = dp[i - 1][0] + prices[i];
//昨天卖了股票,今天冷冻期
dp[i][2] = dp[i - 1][1];
//前天或更早卖了股票 可由 前天卖了股票、大前天或更早卖了股票 推出
dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2])
//今天有股票 可由两天前就卖出了股票 + 今天买股票 和 之前就有股票推出
//由于dp[i][0] 得由dp[i][3]推出,所以得先得出dp[i][3]
dp[i][0] = Math.max(dp[i][3] - prices[i], dp[i - 1][0]);
最终在手里没有股票的状态中得到最大利润
3. 初始化
由于dp[i][x]是由dp[i - 1][x] 推出的,所以先初始化dp[0][x],从i = 1位置开始遍历数组。
dp[0][0] 表示今天买了股票或之前买了,由于没有之前只能是今天买了,所以dp[0][0]= -prices[0];
其他手中无股票的状态,第0天过后依然为0。初始化值和默认值相同。
4.遍历顺序
从左到右, 注意dp[i][0] = Math.max(dp[i][3] - prices[i], dp[i - 1][0]);dp[i][0] 由dp[i][3]推出,所以得先求出dp[i][3]
Java代码
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][4];
dp[0][0]= -prices[0];
for(int i = 1; i < n; i++) {
//今天卖了股票, 可由之前有股票推出
dp[i][1] = dp[i - 1][0] + prices[i];
//昨天卖了股票,今天冷冻期
dp[i][2] = dp[i - 1][1];
//前天或更早卖了股票 可由 前天卖了股票、大前天或更早卖了股票 推出
dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2]);
//今天有股票 可由两天前就卖出了股票 + 今天买股票 和 之前就有股票推出
//由于dp[i][0] 得由dp[i][3]推出,所以得先得出dp[i][3]
dp[i][0] = Math.max(dp[i][3] - prices[i], dp[i - 1][0]);
}
return Math.max(dp[n - 1][1], Math.max(dp[n - 1][2], dp[n - 1][3]));
}
}
买卖股票的最佳时机含手续费
题目:每笔交易都要付fee的手续费,可以无限次交易
解题思路
可以假设卖的时候扣手续费,
状态分为
1.有股票—之前买了股票、之前没有股票今天买了股票
2.没股票—之前卖了股票(含一直没买)、之前有股票今天卖了
Java代码
class Solution {
public int maxProfit(int[] prices, int fee) {
//两个状态 没股票、有股票
int n = prices.length;
int[][] dp = new int[n][2];
//初始化
dp[0][1] = -prices[0];
for(int i = 1; i < n; 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[n - 1][0];
}
}