分享一個簡單但挺有意思的演算法題2-貪心-單調棧-動態規劃

tfzh 發表於 2022-04-25
演算法

1. 題目描述

LeetCode 122.買賣股票的最佳時機 II
在每一天,你可以決定是否購買和/或出售股票。你在任何時候最多隻能持有一股股票。你也可以先購買,然後在同一天出售。返回你能獲得的最大利潤 。

示例 :

*輸入:prices = [7,1,5,3,6,4]

輸出:7

解釋:在第 2 天(股票價格 = 1)的時候買入,在第 3 天(股票價格 = 5)的時候賣出, 這筆交易所能獲得利潤 = 5 - 1 = 4 。

隨後,在第 4 天(股票價格 = 3)的時候買入,在第 5 天(股票價格 = 6)的時候賣出, 這筆交易所能獲得利潤 = 6 - 3 = 3 。
總利潤為 4 + 3 = 7 。*

這道題常見且並不難,有意思的是解法也非常多,尤其適合入門級別的單調棧和動態規劃

2. 貪心演算法

這是最容易想到的解法,因為交易次數不受限,只需要在股價的每個上坡底部買入,坡頂賣出,則一定是利益最大化的

function maxProfit(prices){
        let ans = 0;
    for (let i = 0; i < prices.length - 1; i++) {
            if (prices[i + 1] > prices[i]) {  // 在上坡過程中,【每天都交易】和【底部買入,坡頂賣出】是完全等效的(忽略手續費)
                ans += (prices[i + 1] - prices[i]);
            }
    }
    return ans
}

3. 單調棧

單調棧顧名思義是單調遞增/減的一種棧結構,股價的上坡在資料結構上的表示,其實就是一個遞增的單調棧,我們只要依次找到所有的上坡,此時棧頂減去棧底,則是單次上坡的最大利潤

function maxProfit(prices){
        //這裡只末尾+0就夠了
        prices.push(0) //前後+0,是單調棧很常見的處理措施,確保起止元素一定能形成首個坡,終止末個的坡
        let ans = 0
        let stack = []
        
    for (let i = 0; i < prices.length; i++) {
            //stack[stack.length - 1] 單調棧棧頂,即本次上坡最大值
            if(stack.length > 0 && prices[i] < stack[stack.length - 1]){
                //棧頂
                let top    = stack[stack.length - 1]
                //棧底
                let bottom = stack[0]
                ans += top - bottom
                stack = []//清棧
            }
            stack.push(prices[i])
    }
    return ans
}
這裡主要講單調棧,所以保留了棧結構,實際本題比較簡單,只需記錄棧頂棧底即可
簡化後:
function maxProfit(prices){
        prices.push(0)
        let ans    = 0
        let top    = prices[0]
        let bottom = prices[0]
        
    for (let i = 0; i < prices.length; i++) {
            if(prices[i] >= top){
                top = prices[i]
            }else{
                ans += top - bottom
                top = bottom = prices[i]
            }
    }
    return ans
}
本題是單調棧最基礎的應用,複雜點的比如接雨水柱狀圖中最大的矩形,都是單調棧的應用場景,總之,單調棧是一個強大有趣的資料結構。

4. 動態規劃

不難得出每日只有持倉空倉兩種狀態:

對於今日持倉狀態,今天賬號的最大餘額為【昨日持倉】和【昨日空倉 - 今日股價】(買入所以扣錢)中的較大值

對於今日空倉狀態,今天賬號的最大餘額為【昨日空倉】和【昨日持倉 + 今日股價】(賣出所以加錢)中的較大值

最後一天平倉,即空倉狀態下賬號餘額就是最大收益
得到狀態轉移方程:

$$對於持倉狀態 f(i)_持 = max( f(i-1)_持, + f(i-1)_空 - price[i] )$$

$$對於空倉狀態 f(i)_空 = max( f(i-1)_空, + f(i-1)_持 + price[i] )$$
function maxProfit(prices){
        //最初始的狀態
        let dp = []
        dp.push(
            {
                'positon':      -prices[0],//持倉
                'short_positon':0//空倉
            }
        )        
    for (let i = 1; i < prices.length; i++) {
            let status = {
                //本次選擇持倉,則賬戶最大金額為max(昨天持倉,昨天空倉-今日股價)
                'positon':       Math.max(dp[i-1].positon, dp[i-1].short_positon - prices[i]),
                //本次選擇空倉,則賬戶最大金額為max(昨天空倉,昨天持倉+今日股價)
                'short_positon': Math.max(dp[i-1].short_positon, dp[i-1].positon + prices[i])
            }
    }
    return dp[prices.length-1].short_positon
}

由於只用得到昨日的資料,故而不用儲存每日的持倉狀態,只需要記錄昨天即可,
簡化後:

function maxProfit(prices){
        //最初始的狀態
        let positon       = -prices[0] //持倉
        let short_positon = 0 //空倉
             
    for (let i = 1; i < prices.length; i++) {
            let new_positon       = Math.max(positon, short_positon - prices[i])
            let new_short_positon = Math.max(short_positon, positon + prices[i])
            positon               = new_positon
            short_positon         = new_short_positon
    }
    return short_positon
}
動態規劃是一個強大有趣的演算法。