leetcode-股票系列的题目(动态规划)
一.买卖股票的最佳时机(一次交易)
题目
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。 如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。 注意:你不能在买入股票前卖出股票。 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题解
一次交易的最大收益=最低价格买进&最高价格卖出
所以第i天如果卖出的最大收益:第i-1天前完成交易的最大收益,或者是第i天卖出&i-1天以来最低价格买入
遍历每天的操作,记录如果是第i天卖出的收益,&第i-1天的最低价格(因为第i-1天以来的最低价格买入,第i天卖出,就是第i天卖出能够得到的最大的收入)
class Solution { public int maxProfit(int[] prices) {s if(prices.length == 0){ return 0; } //记录前i-1天的最低价格,选择那一天买入 int min = prices[0]; //记录最大收益 int max = 0; for(int i=1; i<prices.length; i++){ //最大收益是 今天卖出,之前 最低价格买进(所以更新完max再更新min)。还是原来的 max = Math.max(max, prices[i]-min); min = Math.min(min, prices[i]); } return max; } }
二.买卖股票的最佳时机II(无限次交易)
题目
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。 设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题解
贪心算法:无限次交易的话,就可以不用看何时达到最优,而是只要今天价格比昨天高,就可以昨天买入,今天卖出。这样每一次价格上涨的收益都可以赚到,就是股神的操作了...
贪心算法是在每一步总是做出在当前看来最好的选择,贪心在于“今天股价-昨天的股价”=正数/负数/0,那么只取正数,即只在有收益的时候购买就是当前看来最好的选择,就是该贪心算法的决策。
动态规划:(一般可以用贪心算法解决的问题,都可以考虑用动态规划)
1)定义状态dp[i][j]:第i天(具有前缀性质,考虑了之前天数的收益,算了max)是否持有股票(j=0代表不持有股票,j=1代表持有股票)能获得的最大收益;
2)状态转移方程:从不持有股票开始,最后的状态还是不持有股票。每一天的状态可以转移(买卖股票),也可以不操作;
dp[i][0]=Math.max(dp[i-1][0], dp[i-1][1]+prices[i]);//第i天不持有股票,可能是第i-1天就不持有,第i天不操作&第i-1天持有,第i天卖出
dp[i][1]=Math.max(dp[i-1][1], dp[i-1][0]-prices[i]);//第i天持有股票,可能是第i-1天就持有,第i天不操作&第i-1天不持有,第i天买入
3)确定起始状态,初始化一些值,不然从什么值转移呢...:dp[0][0]=0,即第一天什么都不做,那么就是0;dp[0][1]=-prices[0],即买入股票,收益就是-prices[0];
4)确定终止状态:dp[len-1][0],即最后一天不持有股票的最大收益,因为dp[len-1][0]>dp[len-1][1],因为不持股,就会将股票转为现金
class Solution { public int maxProfit(int[] prices) { if(prices.length == 0){ return 0; } //贪心算法 int ret = 0; for(int i=1; i<prices.length; i++){ if(prices[i]>prices[i-1]){ ret += (prices[i]-prices[i-1]); } } return ret; //动态规划 /*int len = prices.length; //dp[i][0]表示第i天不持有股票,dp[i][1]表示第i天持有股票 的收益 int[][] dp = new int[len][2]; dp[0][0] = 0; //如果第0天买入,那么当前收益为-price[0] dp[0][1] = -prices[0]; for(int i=1; i<len; i++){ //第i天不持有股票,可能是i-1天就不持有,或者i-1天持有,但是第i天卖出 dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]); //第i天持有,可能是i-1天就持有,或者i-1天买入 dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0]-prices[i]); } return dp[len-1][0];*/ } }
三.买卖股票的最佳时机含冷冻期
题目
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。 设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票): 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题解
考虑冷冻期:第i-1天卖出股票后,无法在第i天买入。因此是对买入时间有限制,所以会对第i天持有股票的收益有影响。
第i天持有股票的来源:第i-1天就持有/第i天买入,但是这里有一个前提是冷冻期,如果是第i天买入,必须是i-2天以前卖出了,即该状态的来源是i-2天以前不持有股票的最大收益。而不是之前没有冷冻期的i-1天不持有股票的最大收益。
如果i<2,即第一天或者第二天买入。那么只能是前两天都没有任何操作,因为一次买卖加上冷冻期是超过2天的,所以这种情况下之前的最大收益=0
class Solution { public int maxProfit(int[] prices) { int len = prices.length; if(len<1){ return 0; } //dp[i][0]第i天不持有股票,dp[i][1]第i天持有 int[][] dp = tiannew int[len][2]; dp[0][1] = -prices[0]; for(int i=1; i<len; i++){ //第i天不持有:i-1天就不持有,今天不操作/i-1天持有,今天卖出 dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]); //第i天持有:i-1天就持有/i-1不持有,今天买入,只能是i-2天以前卖的(如果i<2,那么只能是前两天都没有任何操作,因为一次买卖加上冷冻期是超过2天的,所以收益=0) dp[i][1] = Math.max(dp[i-1][1], (i>=2 ? dp[i-2][0] : 0)-prices[i]); } return dp[len-1][0]; } }
四.买卖股票的最佳时机含手续费
题目
给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。 你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。 返回获得利润的最大值。 注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题解
如果没有手续费,其实问题又转化回了之前的,无限次交易求最大收益的场景,使用贪心算法即可。
如果有手续费,因为一次交易只需要缴纳一次手续费。所以可以选择在卖股票的时候,把手续费给减掉/在买股票的时候,把手续费给减掉。(下面的解法是卖股票的时候,在收益里把手续费减掉)
class Solution { public int maxProfit(int[] prices, int fee) { if(prices.length<1){ return 0; } //如果没有手续费,那么就化为k次交易,之前的贪心算法 if(fee==0){ int ret = 0; for(int i=1; i<prices.length; i++){ if(prices[i]>prices[i-1]){ ret += (prices[i]-prices[i-1]); } } } //dp[i][0]表示第i天不持有股票 的最大收益,dp[i][1]表示第i天持有股票 int[][] dp = new int[prices.length][2]; //初始化 dp[0][0] = 0; dp[0][1] = -prices[0]; for(int i=1; i<prices.length; i++){ //第i天不持有股票的最大收益:前一天就不持有,不操作/今天卖出 dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]+prices[i]-fee); //第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 天的价格。 设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。 注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题解
最多两次交易==一次/两次/不交易
类似于只进行一次交易的思路。那么就是再多加一次交易,第二次交易的买入值应该是在减去第一次收益的基础上的,这样第二次交易结束的收益就是两次交易的总收益了。
public int maxProfit(int[] prices) { int buy1 = Integer.MAX_VALUE; int buy2 = Integer.MAX_VALUE; int sell1 = 0; int sell2 = 0; for(int price : prices) {
//第一次买入的价格,应该是之前的最低价 buy1 = Math.min(buy1, price);
//第一次的收益,这个今天不持有股票的状态可以来自于可能来自于今天卖出之前的/不操作 sell1 = Math.max(sell1, price - buy1);
//第二次买入的价格,因为要结合两次交易的收益,那么就需要减去第一次的收益 buy2 = Math.min(buy2, price - sell1);
//两次买卖的收益,可能来自于今天卖出之前的/不操作 sell2 = Math.max(sell2, price - buy2); } return sell2; }
六.买卖股票的最佳时机IV(最多k笔交易)
题目
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。 设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。 注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 来源:力扣(LeetCode) 链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
题解
这个题是上一个题的泛化版,当k=2时,就是上一个题了。
当k>=prices.length/2时,因为买卖算一天,那么就是每天都可以交易,那其实相当于可以交易无限次,使用贪心算法。
否则就是还是使用动态规划。
遍历每一天,把每一天模拟k次交易,不断更新最大收益值。第二次买的时候,价格考虑了用第一次赚的钱去补贴一部分。那么交易次数必须作为一个新的维度被考虑进来,这个时候仅仅二维就不够了
1)定义状态dp[i][k][j]:第i天(具有前缀性质,考虑了之前天数的收益,算了max),第k次买卖股票,是否持有股票(j=0代表不持有股票,j=1代表持有股票)能获得的最大收益;
2)状态转移方程:从不持有股票开始,最后的状态还是不持有股票。每一天的状态可以转移(买卖股票),也可以不操作;
dp[i][j][0]=Math.max(dp[i-1][j][0], dp[i-1][j][1]+prices[i]);//第i天不持有股票,可能是第i-1天就不持有,第i天不操作&第i-1天持有,第i天卖出,所以两天买入股票的次数都是j
dp[i][j][1]=Math.max(dp[i-1][j][1], dp[i-1][j-1][0]-prices[i]);//第i天持有股票,可能是第i-1天就持有,第i天不操作&第i-1天不持有,第i天买入,此时昨天买入的次数是j-1
3)确定起始状态:dp[i][0][0]=0,即什么都不做,没有买入过股票,手头也没有持股,那么就是0;dp[i][0][1]=Integer.MIN_VALUE,即没有买入过股票,那么不可能持股,使用整型的最小值表示这种不可能性;dp[0][1..k][0]=0,dp[0][1...k][1]=-prices[0],即第一天至多可以持有一笔交易,如果持有那么当前收益就是-prices[0],如果不持有当前收益是0
4)确定终止状态:dp[len-1][k][0],即最后一天不持有股票的最大收益
class Solution {
public int maxProfit(int k, int[] prices) {
if(prices.length<1){
return 0;
}
if(k >= prices.length/2){
return greedy(prices);
}
int[][][] dp = new int[prices.length][k+1][2];
//初始化
for(int i=0; i<prices.length; i++){
dp[i][0][0] = 0;
dp[i][0][1] = Integer.MIN_VALUE;
}
for(int i=1; i<=k; i++){
dp[0][i][0] = 0;
dp[0][i][1] = -prices[0];
}
//状态转移
for(int i=1; i<prices.length; i++){
for(int j=k; j>=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[prices.length-1][k][0];
}
public static int greedy(int[] prices){
int sum = 0;
for(int i=1; i<prices.length; i++){
if(prices[i]>prices[i-1]){
sum += (prices[i]-prices[i-1]);
}
}
return sum;
}
}
对三维数组进行压缩,去掉最高维度的天数,用dp[k][2]来代替.
因为初始状态为dp[0][0],所以第一次买入dp[0][1],第一次卖出dp[1][0];第二次买入dp[1][1],第二次卖出dp[2][0];第k次买入dp[k-1][1],第k次卖出dp[k][0]
状态转移公式为:dp[j-1][1]=max(dp[j-1][1], dp[j-1][0]-prices[i]),即第j次买入状态来源于本次不操作/第j-1次不持有股票,本次买入
dp[j][0]=max(dp[j][0], dp[j-1][1]+prices[i]),即第j次卖出状态来源于本次不操作/第j-1次持有股票,本次卖出
为什么逆序?因为对dp进行压缩后,少了一个维度,正序遍历会出现状态覆盖的情况。
class Solution {
public int maxProfit(int[] prices) {
if(prices.length<1){
return 0;
}
int k = 2;
//dp[i][0]表示第i次不持有股票,dp[i][1]表示第i次持有股票。去掉天数这个维度
int[][] dp = new int[k+1][2];
//初始化
for(int i=0; i<=k; i++) {
dp[i][0] = 0;
dp[i][1] = -prices[0];
}
//动规
for(int i=1; i<prices.length; i++) {
for(int j=k; j>0; j--) {
//第j次持有股票的最大收益:不做操作/买入(价格是基于前一天的收益 的差价)
dp[j-1][1] = Math.max(dp[j-1][1], dp[j-1][0]-prices[i]);
//第j次不持有的最大收益:不做操作/卖出
dp[j][0] = Math.max(dp[j][0], dp[j-1][1]+prices[i]);
}
}
return dp[k][0];
}
}