[編程題] lk [股票類買賣問題(多個情況)--動態規划問題的綜合提升]
題目:lk:121 122 123 188 309 714 LeetCode 上拿下如下題目:
買賣股票的最佳時機
買賣股票的最佳時機 II
買賣股票的最佳時機 III
買賣股票的最佳時機 IV
最佳買賣股票時機含冷凍期
買賣股票的最佳時機含手續費
一、股票類動態規划問題探究
本類動態規划的核心點記錄
① 狀態定義
我們涉及到天數,涉及到最大的交易次數,涉及到是否今天是買入還是賣出;故需要三維的dp數組
才能很好的解決此問題。
這個問題的「狀態」有三個,第一個是第i天獲得的利潤(0~i-1),第二個是允許交易的最大次數(1~k),第三個是當前的持有狀態(0:未持有,1:持有)
//注意默認大小要生成n,k+1的大小的。都是n能取到n-1是最后一個數組索引和k值表示買賣次數,能取到k索引
int[][][] dp = new int[n][k+1][2];
② 初始條件
含義
dp[-1][k][0] = 0
解釋:因為 i 是從 0 開始的,所以 i = -1 意味着還沒有開始,這時候的利潤當然是 0 。
dp[-1][k][1] = -infinity
解釋:還沒開始的時候,是不可能持有股票的,用負無窮表示這種不可能。
dp[i][0][0] = 0
解釋:因為 k 是從 1 開始的,所以 k = 0 意味着根本不允許交易,這時候利潤當然是 0 。
dp[i][0][1] = -infinity
解釋:不允許交易的情況下,是不可能持有股票的,用負無窮表示這種不可能。
總結一下初始條件:
//當第0天的話
if(i==0){
//沒持股,肯定利潤為0
dp[i][j][0] = 0;
//持有股,那么肯定是買了第一股,利潤為負
dp[i][j][1] = -prices[0];
}else{
...
}
③ 狀態轉移方程
本人經過在第一次思考的時候,主要發現問題在於交易次數j的定義問題
這里有不同的思路:
寫法1:題目規定完整交易最多2次(買和賣算1次),那么定義 j=2
/*特別注意:這里在dp[i][j][0]中的參數2(dp[i-1][j][1]+prices[i] )為什么不是dp[i-1][j-1][1]+prices[i],是因為我們把k值跟買關聯,只要一買進,就代表着k值+1交易一次了,(即昨天買進一股,代表着新一輪交易開始啦)也代表着上一輪的完整交易結束,如果在買的時候j就變化,賣的時候j也變化,那么,買和賣這么一次,j已經達到2了,停止交易了,而題目給的是兩次完整的交易,如果偏要買賣都把j變化的話,初始化值j的時候必須設置為4(本題而言)*/
dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1]+prices[i]); //第i天手里沒有持有股
dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0]-prices[i]); //第i天手里持有股
即:j就設置為題目給定的買賣數。交易兩次,把k和買入做關聯,只要是前一天買入了,那么后一天的買賣數就加1;而前一天賣出的話,還沒買,交易數不變。
特點:時間復雜度小,即買賣2次,j也就只在買入的2次里發生了變化 (time:6ms)
寫法1:題目規定完整交易最多2次(買和賣算1次),那么定義 j=2*交易次數=4
//解釋:j設置為買賣交易的次數的2倍,即交易兩次,j設置為4;前一天買入 j就增加,前一天賣出,j也增加。總共兩次買賣4次操作,j變化4
dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j-1][1]+prices[i]); //第i天手里沒有持有股
dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0]-prices[i]); //第i天手里持有股
即:交易兩次,指定k=2*2 (原因是在狀態方程中買一次加j變化1,賣一次也j變化1。故買賣兩次,即j變化4次)
特點:白白的增加了2倍的時間復雜度,多了很多操作。 (time:10ms))
④ 返回值
我們返回第n天的手里沒有出游股的最大交易次數時候的利潤值即可,如下:
return dp[n-1][k][0];
⑤ 上述以最多2次交易
的題目案例為例,代碼參考如下
<1>使用上述的寫法1寫的代碼:
好理解,時間復雜度大,循環次數多
//方法1:指定k = 買賣數*2
//交易兩次,指定k=2*2 (原因是在狀態方程中買一次加j變化1,賣一次也j變化1。故買賣兩次,即j變化4次)
//特點:白白的增加了2倍的時間復雜度,多了很多操作。 (time:10ms))
public int maxProfit1(int[] prices) {
if(prices.length<=1){return 0;}
int n = prices.length;
int k = 2*2;
//new int[n][k][2]; 參數1:是第i天為止的利潤,參數2:表示最多可以完成幾筆交易,參數3:0表示未持有股票,1表示持有股票
int[][][] dp = new int[n][k+1][2];
for(int i=0;i<n;i++){
for(int j=1;j<=k;j++){
if(i==0){
dp[i][j][0] = 0;
dp[i][j][1] = -prices[0];
}else{
//解釋:j設置為買賣交易的次數的2倍,即交易兩次,j設置為4;前一天買入 j就增加,前一天賣出,j也增加。總共兩次買賣4次操作,j變化4
dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j-1][1]+prices[i]); //第i天手里沒有持有股
dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0]-prices[i]); //第i天手里持有股
}
}
}
//我們返回第n天的手里沒有出游股的最大交易次數時候的利潤值即可,如下:
return dp[n-1][k][0];
}
輸出:時間復雜度高
<1>使用上述的寫法2寫的代碼:
時間復雜度小,循環次數少
//方法2:指定k = 買賣數
//交易兩次,把k和買入做關聯,只要是前一天買入了,那么后一天的買賣數就加1;而前一天賣出的話,還沒買,交易數不變。
//特點:時間復雜度小,即買賣2次,j也就只在買入的2次里發生了變化 (time:6ms)
public int maxProfit1(int[] prices) {
if(prices.length<=1){return 0;}
int n = prices.length;
int k = 2;
//new int[n][k][2]; 參數1:是第i天為止的利潤,參數2:表示最多可以完成幾筆交易,參數3:0表示未持有股票,1表示持有股票
int[][][] dp = new int[n][k+1][2];
for(int i=0;i<n;i++){
for(int j=1;j<=k;j++){
if(i==0){
dp[i][j][0] = 0;
dp[i][j][1] = -prices[0];
}else{
/*特別注意:這里在dp[i][j][0]中的參數2(dp[i-1][j][1]+prices[i] )為什么不是dp[i-1][j-1][1]+prices[i],
是因為我們把k值跟買關聯,只要一買進,就代表着k值+1交易一次了,(即今天昨天買進一股,代表着新一輪交易開始啦)
也代表着上一輪的完整交易結束,如果在買的時候j就變化,賣的時候j也變化,那么,買和賣這么一次,j已經達到2了,停止
交易了,而題目給的是兩次完整的交易,如果偏要買賣都把j變化的話,初始化值j的時候必須設置為4(本題而言)*/
dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1]+prices[i]); //第i天手里沒有持有股
dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0]-prices[i]); //第i天手里持有股
}
}
}
//我們返回第n天的手里沒有出游股的最大交易次數時候的利潤值即可,如下:
return dp[n-1][k][0];
}
輸出:
至此,狀態的定義、初始值情況、狀態轉移方程都定義好了,即可以完成如下的多種情況的練習了,都套用上述模板。
二、[其他各種情況的題目練習]
題目1 股票買賣一次買入一次賣出
121. 買賣股票的最佳時機(力扣)
方法:一次遍歷記錄最低價格和最大利潤
輸入輸出
方法1:一次遍歷同時記錄最小值和最大利潤
class Solution {
//方法:一次遍歷記錄最低價格和最大利潤
public int maxProfit(int[] prices) {
int minPrice = Integer.MAX_VALUE;
int maxprofits = 0;
for(int i=0;i<prices.length;i++){
if(prices[i] < minPrice){
minPrice = prices[i];
}
maxprofits = (prices[i]-minPrice) > maxprofits?(prices[i]-minPrice):maxprofits;
}
return maxprofits;
}
}
輸出:
方法2:套用該類題的動態規划模板
//方法2:動態規划套模板
public static int maxProfit2(int[] prices) {
if(prices.length<=1){return 0;}
int n = prices.length;
int k = 1;
//new int[n][k][2]; 參數1:是第i天為止的利潤,參數2:表示最多可以完成幾筆交易,參數3:0表示未持有股票,1表示持有股票
int[][][] dp = new int[n][k+1][2];
for(int i=0;i<n;i++){
for(int j=1;j<=k;j++){
if(i==0){
dp[i][j][0] = 0;
dp[i][j][1] = -prices[0];
}else{
/*特別注意:這里在dp[i][j][0]中的參數2(dp[i-1][j][1]+prices[i] )為什么不是dp[i-1][j-1][1]+prices[i],是因為我們把k值跟買關聯,只要一買進,就代表着k值+1交易一次了,(即今天昨天買進一股,代表着新一輪交易開始啦)也代表着上一輪的完整交易結束,如果在買的時候j就變化,賣的時候j也變化,那么,買和賣這么一次,j已經達到2了,停止交易了,而題目給的是兩次完整的交易,如果偏要買賣都把j變化的話,初始化值j的時候必須設置為4(本題而言)*/
dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1]+prices[i]); //第i天手里沒有持有股
dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0]-prices[i]); //第i天手里持有股
}
}
}
//我們返回第n天的手里沒有出游股的最大交易次數時候的利潤值即可,如下:
return dp[n-1][k][0];
}
方法3:動態規划:因為是一次交易,把上述的數組縮減為2維
//方法3:動態規划:因為是一次交易,把上述的數組縮減為2維
public static int maxProfit(int[] arr){
if(arr.length<=1){return 0;}
//因為只能買賣一次,所以我們可以用二維表示
int n = arr.length;
int[][] dp = new int[n][2];
for(int i=0;i<n;i++){
if(i==0){
dp[i][0] = 0; //未持有
dp[i][1] = -arr[i]; //第一筆買入,利潤為負
}else{
//動態轉移方程
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+arr[i]); //參數2 是前一天賣出
//dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-arr[i]); //參數2:買入; 參數2這么寫不對
dp[i][1] = Math.max(dp[i-1][1],0-arr[i]); //因為只有一次交易,前一次沒買入,那么這次買入的話利潤就是-arr[i]
}
}
//返回
return dp[n-1][0];
}
題目2 股票買賣2次買入2次賣出
123. 買賣股票的最佳時機 III
題目
輸入輸出
方法1:動態規划
寫法1:j=買賣次數*2
//方法1:指定k = 買賣數*2
//交易兩次,指定k=2*2 (原因是在狀態方程中買一次加j變化1,賣一次也j變化1。故買賣兩次,即j變化4次)
//特點:白白的增加了2倍的時間復雜度,多了很多操作。 (time:10ms))
public int maxProfit1(int[] prices) {
if(prices.length<=1){return 0;}
int n = prices.length;
int k = 2*2;
//new int[n][k][2]; 參數1:是第i天為止的利潤,參數2:表示最多可以完成幾筆交易,參數3:0表示未持有股票,1表示持有股票
int[][][] dp = new int[n][k+1][2];
for(int i=0;i<n;i++){
for(int j=1;j<=k;j++){
if(i==0){
dp[i][j][0] = 0;
dp[i][j][1] = -prices[0];
}else{
//解釋:j設置為買賣交易的次數的2倍,即交易兩次,j設置為4;前一天買入 j就增加,前一天賣出,j也增加。總共兩次買賣4次操作,j變化4
dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j-1][1]+prices[i]); //第i天手里沒有持有股
dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0]-prices[i]); //第i天手里持有股
}
}
}
//我們返回第n天的手里沒有出游股的最大交易次數時候的利潤值即可,如下:
return dp[n-1][k][0];
}
動態規划
寫法2:j=買賣次數=2
//方法2:指定k = 買賣數
//交易兩次,把k和買入做關聯,只要是前一天買入了,那么后一天的買賣數就加1;而前一天賣出的話,還沒買,交易數不變。
//特點:時間復雜度小,即買賣2次,j也就只在買入的2次里發生了變化 (time:6ms)
public int maxProfit(int[] prices) {
if(prices.length<=1){return 0;}
int n = prices.length;
int k = 2;
//new int[n][k][2]; 參數1:是第i天為止的利潤,參數2:表示最多可以完成幾筆交易,參數3:0表示未持有股票,1表示持有股票
int[][][] dp = new int[n][k+1][2];
for(int i=0;i<n;i++){
for(int j=1;j<=k;j++){
if(i==0){
dp[i][j][0] = 0;
dp[i][j][1] = -prices[0];
}else{
/*特別注意:這里在dp[i][j][0]中的參數2(dp[i-1][j][1]+prices[i] )為什么不是dp[i-1][j-1][1]+prices[i],
是因為我們把k值跟買關聯,只要一買進,就代表着k值+1交易一次了,(即今天昨天買進一股,代表着新一輪交易開始啦)
也代表着上一輪的完整交易結束,如果在買的時候j就變化,賣的時候j也變化,那么,買和賣這么一次,j已經達到2了,停止
交易了,而題目給的是兩次完整的交易,如果偏要買賣都把j變化的話,初始化值j的時候必須設置為4(本題而言)*/
dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1]+prices[i]); //第i天手里沒有持有股
dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0]-prices[i]); //第i天手里持有股
}
}
}
//我們返回第n天的手里沒有出游股的最大交易次數時候的利潤值即可,如下:
return dp[n-1][k][0];
}
題目3 股票買賣多次買入多次賣出
122. 買賣股票的最佳時機 II
題目:
輸入輸出:
方法1:貪心算法解決
當我們在今天想買入的時候不仿先看看明天的時候能不能賣出(即明天比今天高,可獲利);每次考慮局部最優
class Solution {
//方法1:貪心:當我們在今天想買入的時候不仿先看看明天的時候能不能賣出(即明天比今天高,可獲利);每次考慮局部最優
// 貪心思想(每天都看后一天的情況,如果后一天價格高,就選擇今天買入)
public int maxProfit(int[] prices) {
int money = 0;
for(int i=0;i<prices.length-1;i++){ //為了保證數組不越界,i指向倒數第2個數就知道自己要不要最后買入了,不滿入就退出循環結束了
if(prices[i+1]>prices[i]){
money += prices[i+1]-prices[i]; //如果后一天比前一天的值大,前一天就買入,后一天賣出
}
}
return money;
}
}
輸出:
方法2:動態規划
思想參考:
代碼
//方法2:動態規划:因為是多次交易,k已經無需記錄了。把上述的數組縮減為2維
public static int maxProfit(int[] arr){
if(arr.length<=1){return 0;}
//因為只能買賣一次,所以我們可以用二維表示
int n = arr.length;
int[][] dp = new int[n][2];
for(int i=0;i<n;i++){
if(i==0){
dp[i][0] = 0; //未持有
dp[i][1] = -arr[i]; //第一筆買入,利潤為負
}else{
//動態轉移方程
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+arr[i]); //參數2 是前一天賣出
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-arr[i]); //因為多次交易,前一次沒買入,則這次,dp[i-1][0]-arr[i]
}
}
//返回
return dp[n-1][0];
}
題目3股票買賣K次買進賣出
188. 買賣股票的最佳時機 IV
題目
輸入輸出
直接套公式存在的問題:
代碼
class Solution {
//方法1:動態規划
public int maxProfit(int k, int[] prices) {
if(prices.length<=1){return 0;}
if(k>prices.length/2){
return maxProfit_k(prices);
}
int n = prices.length;
//int k; //直接使用形參k
//new int[n][k][2]; 參數1:是第i天為止的利潤,參數2:表示最多可以完成幾筆交易,參數3:0表示未持有股票,1表示持有股票
int[][][] dp = new int[n][k+1][2];
for(int i=0;i<n;i++){
for(int j=1;j<=k;j++){
if(i==0){
dp[i][j][0] = 0;
dp[i][j][1] = -prices[0];
}else{
/*特別注意:這里在dp[i][j][0]中的參數2(dp[i-1][j][1]+prices[i] )為什么不是dp[i-1][j-1][1]+prices[i],
是因為我們把k值跟買關聯,只要一買進,就代表着k值+1交易一次了,(即今天昨天買進一股,代表着新一輪交易開始啦)
也代表着上一輪的完整交易結束,如果在買的時候j就變化,賣的時候j也變化,那么,買和賣這么一次,j已經達到2了,停止
交易了,而題目給的是兩次完整的交易,如果偏要買賣都把j變化的話,初始化值j的時候必須設置為4(本題而言)*/
dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1]+prices[i]); //第i天手里沒有持有股
dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0]-prices[i]); //第i天手里持有股
}
}
}
//我們返回第n天的手里沒有出游股的最大交易次數時候的利潤值即可,如下:
return dp[n-1][k][0];
}
//方法:可以買賣多次的情況,調用貪心解決無線次買賣問題
/*思想:當我們在今天想買入的時候不仿先看看明天的時候能不能賣出(即明天比今天高,可獲利);每次考慮局部最優
貪心思想(每天都看后一天的情況,如果后一天價格高,就選擇今天買入)*/
public int maxProfit_k(int[] prices) {
if(prices.length==0) {return 0;}
int money = 0;
for(int i=0;i<prices.length-1;i++){ //為了保證數組不越界,i指向倒數第2個數就知道自己要不要最后買入了,不滿入就退出循環結束了
if(prices[i+1]>prices[i]){
money += prices[i+1]-prices[i]; //如果后一天比前一天的值大,前一天就買入,后一天賣出
}
}
return money;
}
}
輸出:
題目4 股票買賣K次買進賣出含義冷凍期
309. 最佳買賣股票時機含冷凍期
題目
思想:
主要點:
代碼思路
class Solution {
//動態規划:把狀態改為3: 0 表示不持股;1 表示持股; 2 表示處在冷凍
public int maxProfit(int[] prices) {
if(prices.length<=1){return 0;}
//因為只能買賣一次,所以我們可以用二維表示
int n = prices.length;
int[][] dp = new int[n][3];
for(int i=0;i<n;i++){
if(i==0){
dp[i][0] = 0; //未持有
dp[i][1] = -prices[i]; //第一筆買入,利潤為負
dp[i][2] = 0; //不可能事件,第0天就凍結
}else{
//動態轉移方程
//dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]); //參數2 是前一天賣出
//因為有冷凍期,所以在買入的時候要從其i-2天狀態看
//dp[i][1] = Math.max(dp[i-1][1],dp[i-2][0]-prices[i]); //買入
//0 表示不持股;1 表示持股; 2 表示處在冷凍
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][2] - prices[i]);
dp[i][2] = dp[i - 1][0]; //冷凍期必須從不持股來,因為剛剛賣了
}
}
//返回
return Math.max(dp[n-1][0],dp[n-1][2]);
}
}
輸出:
題目4 股票買賣K次買進賣出有手續費
714. 買賣股票的最佳時機含手續費
題目:
思路:
我們只要把可以買賣k次的情況在賣出的時候交個手續費就可以了,買入的時候不用交手續費,如下
//動態轉移方程
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]-fee);//參數2是前一天賣出,但是要收手續費
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i]); //前一天買入,買入是可以不收手續費的
Java代碼
class Solution {
//動態規划
public int maxProfit(int[] prices, int fee) {
if(prices.length<=1){return 0;}
//因為只能買賣一次,所以我們可以用二維表示
int n = prices.length;
int[][] dp = new int[n][2];
for(int i=0;i<n;i++){
if(i==0){
dp[i][0] = 0; //未持有
dp[i][1] = -prices[i]; //第一筆買入,利潤為負
}else{
//動態轉移方程
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]+prices[i]-fee); //參數2 是前一天賣出,但是要收手續費
dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i]); //前一天買入,買入是可以不收手續費的
}
}
//返回
return dp[n-1][0];
}
}