算法:買賣股票系列

Leetcode上有一個買賣股票系列的算法問題,主要區別在因而否有交易次數限制、是否交易有冷卻期、是否有交易手續費等條件。本文探究的就是這個系列的通用思路和解法、不一樣條件時的修改以及最優解。閱讀本文須要事先對這個系列各個問題的題目有必定的瞭解,瞭解動態規劃。本文會從最複雜的條件開始,得出最通用的解法,因此一開始反而是最難的,推薦有興趣、有耐心的讀者先從頭至尾閱讀一遍,若是難以理解的話,再從最簡單的條件開始閱讀,這樣就能夠深入瞭解這個系列的解題思路,掌握解題模板。本文代碼使用的語言是Javajava

核心思路

定義一個二維數組int[][] profit = new int[n][2],其中profit[i][0]表示第i天賣出股票(沒有持有股票)時的最大收益,profit[i][1]表示表示第i天買入股票(持有股票)時的最大收益算法

那麼狀態轉移方程是:數組

// 第i天的賣出狀態 = Max(前一天賣出狀態,前一天買入狀態 + 賣出股票得到的收益)
profit[i][0] = Math.max(profit[i - 1][0], profit[i - 1][1] + prices[i]);
// 第i天的買入狀態 = Max(前一天買入狀態,前一天前一次已賣出狀態 - 買入股票扣除的收益)
profit[i][1] = Math.max(profit[i - 1][1], profit[i - 1][0] - prices[i]);
複製代碼

這個系列全部問題的解答都是基於這個狀態轉移方程函數

最通用的解法

如下代碼是包含了Leetcode上買賣股票系列全部不一樣條件下的通用解優化

// k表示交易次數,fee表示交易手續費,m表示交易冷卻期
public int maxProfit(int k, int[] prices, int fee, int m) {
    if (k == 0 || prices == null || prices.length < 2) {
      	return 0;
    }
    int n = prices.length;
	
    // 進行一次徹底的交易須要兩天,因此當 k > n/2 的時候,就能夠天天都進行一次買入(賣出)操做,也就是能夠交易無數次
    if (k > (n >> 1)) {
      	int[][] profit = new int[n][2];
        for (int i = 0; i < n; i++) {
            // 處理初始狀態
            if (i == 0) {
                profit[i][0] = 0;
                profit[i][1] = -prices[0];
                continue;
            }
            // 處理有交易冷卻期時,前 m + 1 天的狀況
            if (i < m + 1) {
                profit[i][0] = Math.max(profit[i - 1][0], profit[i - 1][1] + prices[i] - fee);
                profit[i][1] = Math.max(profit[i - 1][1], 0 - prices[i]);
                continue;
            }
            // 核心,狀態轉移方程
            profit[i][0] = Math.max(profit[i - 1][0], profit[i - 1][1] + prices[i] - fee);
            profit[i][1] = Math.max(profit[i - 1][1], profit[i - (m + 1)][0] - prices[i]);
            
        }
        return profit[n - 1][0];
    }
    
    int[][][] profit = new int[n][k + 1][2];

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < k + 1; j++) {
            // 處理初始狀態
            if (i == 0) {
              	profit[i][j][0] = 0;
                profit[i][j][1] = -prices[0];
                continue;
            }
            if (j == 0) {
                profit[i][j][0] = 0;
                profit[i][j][1] = -prices[0];
              	continue;
            }
            // 處理有交易冷卻期時,前 m + 1 天的狀況
            if (i < m + 1) {
              	profit[i][j][0] = Math.max(profit[i - 1][j][0], profit[i - 1][j][1] + prices[i] - fee);
              	profit[i][j][1] = Math.max(profit[i - 1][j][1], 0 - prices[i]);
              	continue;
                
            }
            // 核心,狀態轉移方程
            profit[i][j][0] = Math.max(profit[i - 1][j][0], profit[i - 1][j][1] + prices[i] - fee);
            profit[i][j][1] = Math.max(profit[i - 1][j][1], profit[i - (m + 1)][j - 1][0] - prices[i]);
        }
    }
    return profit[n - 1][k][0];
}
複製代碼

從上面函數能夠看出,i表示的天數這一維度能夠省略,但若是有交易冷卻期這個條件的話,須要額外添加一個數組來保存[i - (m + 1), i - 1]天前的值,優化後的代碼以下spa

public int maxProfit(int k, int[] prices, int fee, int m) {
    if (k == 0 || prices == null || prices.length < 2) {
        return 0;
    }
    
    int n = prices.length;

    if (k > (n >> 1)) {
        int sell = 0;
        int buy = Integer.MIN_VALUE + fee;
      	// 保存 [i - (m + 1), i - 1] 天前的值
        int[] preSells = new int[m + 1];
        for (int i = 0; i < prices.length; i++) {
            sell = Math.max(sell, buy + prices[i] - fee);
            buy = Math.max(buy, preSells[i % (m + 1)] - prices[i]);
            preSells[i % (m + 1)] = sell;
        }
        return sell;
    }
    
    int[] sells = new int[k];
    int[] buys = new int[k];
    // 保存 [i - (m + 1), i - 1] 天前的值
    int[][] preSells = new int[k][m + 1];
    
    // 處理初始狀態
    for (int i = 0; i < k; i++) {
        sells[i] = 0;
        buys[i]=  Integer.MIN_VALUE + fee;
    }
	
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < k; j++) {
            if (j == 0) {
                sells[j] = Math.max(sells[j], buys[j] + prices[i] - fee);
                buys[j] = Math.max(buys[j], -prices[i]);
                preSells[j][i % (m + 1)] = sells[j];
                continue;
            }
            sells[j] = Math.max(sells[j], buys[j] + prices[i] - fee);
            buys[j] = Math.max(buys[j], preSells[j - 1][i % (m + 1)] - prices[i]);
            preSells[j][i % (m + 1)] = sells[j];
        }
    }
    return sells[k - 1];
}
複製代碼

這個系列全部問題均可以在上面的代碼基礎上進行修改優化,去除沒必要要的代碼便可得出解code

只能交易k次

Leetcode的188題it

因爲只有一個交易次數的條件,因此不須要m,也不須要fee,直接簡化代碼便可io

public int maxProfit(int k, int[] prices) {
    if (k == 0 || prices == null || prices.length < 2) {
     	 return 0;
    }
    
    int n = prices.length;

    // 進行一次徹底的交易須要兩天,因此當 k > n/2 的時候,就能夠天天都進行一次買入(賣出)操做,也就是能夠交易無數次
    if (k > (n >> 1)) {
        int[][] profit = new int[n][2];

        for (int i = 0; i < n; i++) {
            if (i == 0) {
                profit[i][0] = 0;
                profit[i][1] = -prices[0];
                continue;
            }
            profit[i][0] = Math.max(profit[i - 1][0], profit[i - 1][1] + prices[i]);
            profit[i][1] = Math.max(profit[i - 1][1], profit[i - 1][0] - prices[i]);
        }
        return profit[n - 1][0];
    }

    int[][][] profit = new int[n][k + 1][2];

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < k + 1; j++) {
            if (i == 0) {
              	profit[i][j][0] = 0;
                profit[i][j][1] = -prices[0];
                continue;
            }
            if (j == 0) {
              	profit[i][j][0] = 0;
                profit[i][j][1] = -prices[0];
                continue;
            }
            profit[i][j][0] = Math.max(profit[i - 1][j][0], profit[i - 1][j][1] + prices[i]);
            profit[i][j][1] = Math.max(profit[i - 1][j][1], profit[i - 1][j - 1][0] - prices[i]);
        }
    }
    return profit[n - 1][k][0];
}
複製代碼

優化function

public int maxProfit(int k, int[] prices) {
    if (k == 0 || prices == null || prices.length < 2) {
        return 0;
    }

    int n = prices.length;

    if (k > (n >> 1)) {
        int sell = 0;
        int buy = Integer.MIN_VALUE;
        for (int price : prices) {
            sell = Math.max(sell, buy + price);
            buy = Math.max(buy, sell - price);
        }
        return sell;
    }

    int[] sells = new int[k];
    int[] buys = new int[k];

    for (int i = 0; i < k; i++) {
        sells[i] = 0;
        buys[i]=  Integer.MIN_VALUE;
    }

    for (int price : prices) {
        for (int i = 0; i < k; i++) {
            if (i == 0) {
                sells[i] = Math.max(sells[i], buys[i] + price);
                buys[i] = Math.max(buys[i], -price);
                continue;
            }
            sells[i] = Math.max(sells[i], buys[i] + price);
            buys[i] = Math.max(buys[i], sells[i - 1] - price);
        }
    }
    return sells[k - 1];
}
複製代碼

只能交易兩次

Leetcode的123題

因爲只有一個交易次數的條件,因此不須要m,也不須要fee,直接簡化代碼得

public int maxProfit(int[] prices) {
    if (prices == null || prices.length < 2) {
      	return 0;
    }
    int k = 2;
    int n = prices.length;

    int[][][] profit = new int[n][k + 1][2];

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < k + 1; j++) {
            if (i == 0) {
                profit[i][j][0] = 0;
                profit[i][j][1] = -prices[0];
                continue;
            }
            if (j == 0) {
                profit[i][j][0] = 0;
                profit[i][j][1] = -prices[0];
                continue;
            }

            profit[i][j][0] = Math.max(profit[i - 1][j][0], profit[i - 1][j][1] + prices[i]);
            profit[i][j][1] = Math.max(profit[i - 1][j][1], profit[i - 1][j - 1][0] - prices[i]);
        }
    }

    return profit[n - 1][k][0];
}
複製代碼

優化,因爲只能交易兩次,因此只須要兩組變量來保存結果便可

public int maxProfit(int[] prices) {
    int sell1 = 0;
    int buy1 = Integer.MIN_VALUE;
    int sell2 = 0;
    int buy2 = Integer.MIN_VALUE;

    for (int price : prices) {
        sell1 = Math.max(sell1, buy1 + price);
        buy1 = Math.max(buy1, -price);
        sell2 = Math.max(sell2, buy2 + price);
        buy2 = Math.max(buy2, sell1 - price);
    }
    return sell2;
}
複製代碼

只能交易一次

Leetcode的121題

因爲只有一個交易次數的條件,因此不須要m,也不須要fee;同時由於只能交易一次,因此k = 1,也就是能夠省略

public int maxProfit(int[] prices) {
    if (prices == null || prices.length < 2) {
      	return 0;
    }
    int n = prices.length;
    int[][] profit = new int[n][2];

    for (int i = 0; i < n; i++) {
      	if (i == 0) {
      	    profit[i][0] = 0;
      	    profit[i][1] = -prices[0];
      	    continue;
      	}
      	profit[i][0] = Math.max(profit[i - 1][0], profit[i - 1][1] + prices[i]);
      	// 由於只能交易一次,因此這裏的原來的 profit[i - 1][0] 恆等於 0
      	profit[i][1] = Math.max(profit[i - 1][1], -prices[i]);
    }
    return profit[n - 1][0];
}
複製代碼

優化

public int maxProfit(int[] prices) {
    int sell = 0;
    int buy = Integer.MIN_VALUE;
    for (int price : prices) {
        sell = Math.max(sell, buy + price);
        buy = Math.max(buy, -price);
    }

    return sell;
}
複製代碼

這個題目有更通俗直接的解法

// 遍歷的同時,保存數組中的最小值,更新最大差值
public int maxProfit(int[] prices) {
    int profit = 0;
    int min = Integer.MAX_VALUE;
    for (int price : prices) {
        if (price < min) {
            min = price;
        } else if (price - min > profit) {
            profit = price - min;
        }
    }
    return profit;
}
複製代碼

能夠交易無數次

Leetcode的122題

因爲只一個交易次數的條件,因此不須要m,也不須要fee;至於k這一維度,也能夠刪除,有兩種理解方式

  1. 能夠交易無數次,也就是k = +∞,這時候k ≈ k - 1,因此k能夠認爲是沒有做用的,能夠刪除k這一維度
  2. 以前須要k這一維度是由於有交易次數限制,天天都要進行遍歷,從k次交易內選擇最優方案;但若是沒有交易次數限制,則能夠認爲天天都進行交易,收益能夠一直累加,下一天直接取以前的最優方案便可,因此能夠刪除k這一維度
public int maxProfit(int[] prices) {
    if (prices == null || prices.length < 2) {
      	return 0;
    }
    int n = prices.length;
    int[][] profit = new int[n][2];


    for (int i = 0; i < n; i++) {
        if (i == 0) {
            profit[i][0] = 0;
            profit[i][1] = -prices[0];
            continue;
        }
        profit[i][0] = Math.max(profit[i - 1][0], profit[i - 1][1] + prices[i]);
      	// 這裏的 profit[i - 1][0] 表示前一天的最優方案,由於沒有交易次數限制,也就是收益能夠一直累加
        profit[i][1] = Math.max(profit[i - 1][1], profit[i - 1][0] - prices[i]);
    }
    return profit[n - 1][0];
}
複製代碼

優化

public int maxProfit(int[] prices) {
    int sell = 0;
    int buy = Integer.MIN_VALUE;
    for (int price : prices) {
        sell = Math.max(sell, buy + price);
        buy = Math.max(buy, sell - price);
    }
    return sell;
}
複製代碼

能夠看出只能交易一次與能夠交易無數次的區別:

  1. 只能交易一次,前一天的收益恆爲0
  2. 能夠交易無數次,收益能夠一直累加

更通俗直接的解法,貪心

// 因爲不限交易次數,因此只要後面的價格比較大,都能得到收益
public int maxProfit(int[] prices) {
    int maxProfit = 0;
    for (int i = 0; i < prices.length - 1; i++) {
        int today = prices[i];
        int tomorrow = prices[i + 1];
        if (today < tomorrow) {
            maxProfit += tomorrow - today; 
        }
    }
    return maxProfit;
}
複製代碼

能夠交易無數次,有一天的冷卻期

Leetcode的309題

在能夠交易無數次的條件上,加上m這個條件便可

public int maxProfit(int[] prices) {
    if (prices == null || prices.length < 2) {
        return 0;
    }
    // 若是冷卻期是n天的話,讓 m = n 便可
    int m = 1;
    int n = prices.length;
    int[][] profit = new int[n][2];


    for (int i = 0; i < n; i++) {
        if (i == 0) {
            profit[i][0] = 0;
            profit[i][1] = -prices[0];
            continue;
        }
        if (i < m + 1) {
            profit[i][0] = Math.max(profit[i - 1][0], profit[i - 1][1] + prices[i]);
            profit[i][1] = Math.max(profit[i - 1][1], 0 - prices[i]);
            continue;
        }
        profit[i][0] = Math.max(profit[i - 1][0], profit[i - 1][1] + prices[i]);
        profit[i][1] = Math.max(profit[i - 1][1], profit[i - (m + 1)][0] - prices[i]);
    }
    return profit[n - 1][0];
}
複製代碼

優化

public int maxProfit(int[] prices) {
    // 若是冷卻期是n天的話,讓 m = n 便可
    int m = 1;
    int sell = 0;
    int buy = Integer.MIN_VALUE;
    int[] preSells = new int[m + 1];
    for (int i = 0; i < prices.length; i++) {
        sell = Math.max(sell, buy + prices[i]);
        buy = Math.max(buy, preSells[i % (m + 1)] - prices[i]);
        preSells[i % (m + 1)] = sell;
    }
    return sell;
}
複製代碼

能夠交易無數次,無冷卻期,有手續費

Leetcode的714題

在能夠交易無數次的條件上,加上fee這個條件便可

public int maxProfit(int[] prices, int fee) {
    if (prices == null || prices.length < 2) {
      	return 0;
    }
    int n = prices.length;
    int[][] profit = new int [n][2];
    for (int i = 0; i < n; i++) {
        if (i == 0) {
            profit[i][0] = 0;
            profit[i][1] = -prices[0];
            continue;
        }
        profit[i][0] = Math.max(profit[i - 1][0], profit[i - 1][1] + prices[i] - fee);
        profit[i][1] = Math.max(profit[i - 1][1], profit[i - 1][0] - prices[i]);
    }
    return profit[n - 1][0];
}
複製代碼

優化

public int maxProfit(int[] prices) {
    int sell = 0;
    // 以防溢出
    int buy = Integer.MIN_VALUE + fee;
    for (int price : prices) {
        sell = Math.max(sell, buy + price - fee);
        buy = Math.max(buy, sell - price);
    }
    return sell;
}
複製代碼

總結

從最複雜的條件開始思考這個系列解法或許有點反人類,可是我我的以爲這樣纔是最能全局深入理解掌握這個系列的辦法,最複雜的狀況都解決了,剩下的就很簡單了。相信各位讀者看完以上這麼多不一樣條件下的解法以及優化後的代碼,必定會對動態規劃有更深的理解,若是有更優的解法,歡迎一塊兒探討。

相關文章
相關標籤/搜索