算法 | 《買賣股票的最佳時機》系列問題

買賣股票的最佳時機系列題目是leetcode的經(jīng)典系列題目杨名。對于這系列題目可以用一種思想解決之渐北,也就是DP狀態(tài)轉(zhuǎn)移思想。
本筆記收錄了Leetcode上的一個題解:一個方法團滅 6 道股票問題.
但其實通解有時不是很直觀,所以同時也記錄對于這系列問題的特解和有趣的思想艾猜。

一衬鱼、通用解法思想

(1)狀態(tài):

這個系列問題的「狀態(tài)」有三個业筏,第一個是天數(shù),第二個是允許交易的最大次數(shù)鸟赫,第三個是當(dāng)前的持有狀態(tài)(即之前說的 rest 的狀態(tài)蒜胖,我們不妨用 1 表示持有,0 表示沒有持有)抛蚤。然后我們用一個三維數(shù)組就可以裝下這幾種狀態(tài)的全部組合:

dp[i][k][0 or 1]
0 <= i <= n-1, 1 <= k <= K
n 為天數(shù)台谢,大 K 為最多交易數(shù)
此問題共 n × K × 2 種狀態(tài),全部窮舉就能搞定岁经。
for 0 <= i < n:
    for 1 <= k <= K:
        for s in {0, 1}:
            dp[i][k][s] = max(buy, sell, rest)
(2)狀態(tài)轉(zhuǎn)移:
狀態(tài)轉(zhuǎn)移
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
              max(   選擇 rest  ,           選擇 sell      )

解釋:今天我沒有持有股票对碌,有兩種可能:
要么是我昨天就沒有持有,然后今天選擇 rest蒿偎,所以我今天還是沒有持有朽们;
要么是我昨天持有股票怀读,但是今天我 sell 了,所以我今天沒有持有股票了骑脱。

dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
              max(   選擇 rest  ,           選擇 buy         )

解釋:今天我持有著股票菜枷,有兩種可能:
要么我昨天就持有著股票,然后今天選擇 rest叁丧,所以我今天還持有著股票啤誊;
要么我昨天本沒有持有,但今天我選擇 buy拥娄,所以今天我就持有股票了蚊锹。
(3)基態(tài):

有了狀態(tài)轉(zhuǎn)移方程后,我們還需要一個基態(tài)稚瘾,從這個基態(tài)依靠這些方程推出每個狀態(tài)的結(jié)果牡昆。

base case:
dp[-1][k][0] = dp[i][0][0] = 0
dp[-1][k][1] = dp[i][0][1] = -infinity

狀態(tài)轉(zhuǎn)移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])

二、題目解法:

1. 買賣股票的最佳時機

K=1 通用解法:

dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0] - prices[i]) 
            = max(dp[i-1][1][1], -prices[i])
解釋:k = 0 的 base case摊欠,所以 dp[i-1][0][0] = 0丢烘。

現(xiàn)在發(fā)現(xiàn) k 都是 1,不會改變些椒,即 k 對狀態(tài)轉(zhuǎn)移已經(jīng)沒有影響了播瞳。
可以進行進一步化簡去掉所有 k:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], -prices[i])
for (int i = 0; i < n; i++) {
    if (i - 1 == -1) {
        dp[i][0] = 0;
        // 解釋:
        //   dp[i][0] 
        // = max(dp[-1][0], dp[-1][1] + prices[i])
        // = max(0, -infinity + prices[i]) = 0
        dp[i][1] = -prices[i];
        //解釋:
        //   dp[i][1] 
        // = max(dp[-1][1], dp[-1][0] - prices[i])
        // = max(-infinity, 0 - prices[i]) 
        // = -prices[i]
        continue;
    }
    dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
    dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
}
return dp[n - 1][0];

通解簡化:

// k == 1
int maxProfit_k_1(int[] prices) {
    int n = prices.length;
    // base case: dp[-1][0] = 0, dp[-1][1] = -infinity
    int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
    for (int i = 0; i < n; i++) {
        // dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
        // dp[i][1] = max(dp[i-1][1], -prices[i])
        dp_i_1 = Math.max(dp_i_1, -prices[i]);
    }
    return dp_i_0;
}

特解:

var maxProfit = function(prices) {
    // 維護一個最小值
    // 維護一個maxProfit
    let min = prices[0], maxProfit = 0
    for(const price of prices) {
        if(price < min) {
            min = price
        } else {
            if(price - min > maxProfit) {
                maxProfit = price - min
            }
        }
    }
    return maxProfit
};

2. 買賣股票的最佳時機 II

K=+Inf 通用解法:

dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
            = max(dp[i-1][k][1], dp[i-1][k][0] - prices[i])

我們發(fā)現(xiàn)數(shù)組中的 k 已經(jīng)不會改變了(k是無窮大 k-1近似于k),也就是說不需要記錄 k 這個狀態(tài)了:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
int maxProfit_k_inf(int[] prices) {
    int n = prices.length;
    int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
    for (int i = 0; i < n; i++) {
        int temp = dp_i_0; // 因為dp_i_1需要的是上個狀態(tài)的dp_i_0
        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
        dp_i_1 = Math.max(dp_i_1, temp - prices[i]);
    }
    return dp_i_0;
}

特解:每次只要是上升都買和賣免糕,這樣就可以無限獲利

var maxProfit = function(prices) {
    let profit = 0
    for(let i=0;i+1<prices.length;i++) {
        if(prices[i]<prices[i+1]) {
            profit += (prices[i+1]-prices[i])
        }
    }
    return profit
};

3. 最佳買賣股票時機含冷凍期

K=+Inf with cooldown 通解:
每次 sell 之后要等一天才能繼續(xù)交易赢乓。只要把這個特點融入上一題的狀態(tài)轉(zhuǎn)移方程即可:

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])
解釋:第 i 天選擇 buy 的時候,要從 i-2 的狀態(tài)轉(zhuǎn)移石窑,而不是 i-1 牌芋。
int maxProfit_with_cool(int[] prices) {
    int n = prices.length;
    int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
    int dp_pre_0 = 0; // 代表 dp[i-2][0]
    for (int i = 0; i < n; i++) {
        int temp = dp_i_0;
        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
        dp_i_1 = Math.max(dp_i_1, dp_pre_0 - prices[i]);
        dp_pre_0 = temp;
    }
    return dp_i_0;
}

4. 買賣股票的最佳時機含手續(xù)費

K=+Inf with fee:
每次交易要支付手續(xù)費,只要把手續(xù)費從利潤中減去即可尼斧。改寫方程:

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i] - fee)
解釋:相當(dāng)于買入股票的價格升高了。
在第一個式子里減也是一樣的试吁,相當(dāng)于賣出股票的價格減小了棺棵。
int maxProfit_with_fee(int[] prices, int fee) {
    int n = prices.length;
    int dp_i_0 = 0, dp_i_1 = Integer.MIN_VALUE;
    for (int i = 0; i < n; i++) {
        int temp = dp_i_0;
        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
        dp_i_1 = Math.max(dp_i_1, temp - prices[i] - fee);
    }
    return dp_i_0;
}

5. 買賣股票的最佳時機 III

K=2:
這道題 k = 2 和后面要講的 k 是任意正整數(shù)的情況中,對 k 的處理就凸顯出來了熄捍。我們直接寫代碼烛恤,邊寫邊分析原因。

原始的動態(tài)轉(zhuǎn)移方程余耽,沒有可化簡的地方
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
int max_k = 2;
int[][][] dp = new int[n][max_k + 1][2];
for (int i = 0; i < n; i++) {
    for (int k = max_k; k >= 1; k--) {
        if (i - 1 == -1) { /*處理 base case */ }
        dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
        dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
    }
}
// 窮舉了 n × max_k × 2 個狀態(tài)缚柏,正確。
return dp[n - 1][max_k][0];碟贾。

這里 k 取值范圍比較小币喧,所以可以不用 for 循環(huán)轨域,直接把 k = 1 和 2 的情況手動列舉出來也可以:

dp[i][2][0] = max(dp[i-1][2][0], dp[i-1][2][1] + prices[i])
dp[i][2][1] = max(dp[i-1][2][1], dp[i-1][1][0] - prices[i])
dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
dp[i][1][1] = max(dp[i-1][1][1], -prices[i])

int maxProfit_k_2(int[] prices) {
    int dp_i10 = 0, dp_i11 = Integer.MIN_VALUE;
    int dp_i20 = 0, dp_i21 = Integer.MIN_VALUE;
    for (int price : prices) {
        dp_i20 = Math.max(dp_i20, dp_i21 + price);
        dp_i21 = Math.max(dp_i21, dp_i10 - price);
        dp_i10 = Math.max(dp_i10, dp_i11 + price);
        dp_i11 = Math.max(dp_i11, -price);
    }
    return dp_i20;
}

6. 買賣股票的最佳時機 IV

K = Any integer
有了上一題 k = 2 的鋪墊,這題應(yīng)該和上一題的第一個解法沒啥區(qū)別杀餐。但是出現(xiàn)了一個超內(nèi)存的錯誤干发,原來是傳入的 k 值會非常大,dp 數(shù)組太大了∈非蹋現(xiàn)在想想枉长,交易次數(shù) k 最多有多大呢?
一次交易由買入和賣出構(gòu)成琼讽,至少需要兩天必峰。所以說有效的限制 k 應(yīng)該不超過 n/2,如果超過钻蹬,就沒有約束作用了吼蚁,相當(dāng)于 k = +infinity。這種情況是之前解決過的脉让。
直接把之前的代碼重用:

int maxProfit_k_any(int max_k, int[] prices) {
    int n = prices.length;
    if (max_k > n / 2) 
        return maxProfit_k_inf(prices);

    int[][][] dp = new int[n][max_k + 1][2];
    for (int i = 0; i < n; i++) 
        for (int k = max_k; k >= 1; k--) {
            if (i - 1 == -1) { /* 處理 base case */ }
            dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
            dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);     
        }
    return dp[n - 1][max_k][0];
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末桂敛,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子溅潜,更是在濱河造成了極大的恐慌术唬,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件滚澜,死亡現(xiàn)場離奇詭異粗仓,居然都是意外死亡,警方通過查閱死者的電腦和手機设捐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門借浊,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人萝招,你說我怎么就攤上這事蚂斤。” “怎么了槐沼?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵曙蒸,是天一觀的道長。 經(jīng)常有香客問我岗钩,道長纽窟,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任兼吓,我火速辦了婚禮臂港,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己审孽,他們只是感情好县袱,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著瓷胧,像睡著了一般显拳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上搓萧,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天杂数,我揣著相機與錄音,去河邊找鬼瘸洛。 笑死揍移,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的反肋。 我是一名探鬼主播那伐,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼石蔗!你這毒婦竟也來了罕邀?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤养距,失蹤者是張志新(化名)和其女友劉穎诉探,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體棍厌,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡肾胯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了耘纱。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片敬肚。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖束析,靈堂內(nèi)的尸體忽然破棺而出艳馒,到底是詐尸還是另有隱情,我是刑警寧澤员寇,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布弄慰,位于F島的核電站,受9級特大地震影響丁恭,放射性物質(zhì)發(fā)生泄漏曹动。R本人自食惡果不足惜斋日,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一牲览、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦第献、人聲如沸贡必。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽仔拟。三九已至,卻和暖如春飒赃,著一層夾襖步出監(jiān)牢的瞬間利花,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工载佳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留炒事,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓蔫慧,卻偏偏與公主長得像挠乳,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子姑躲,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容