天天看點

“動态規劃”這詞太吓人,其實可以叫“狀态緩存”

摘要:平時練習算法題學習算法知識時,經常會發現題解裡寫着“動态規劃”,裡面一上來就是一個複雜的dp公式,對于新人來說除了說聲“妙啊”,剩下就是疑惑,他是怎麼想到這個公式的?我能想到嗎?這玩意工作中有用嗎?

作者:breakDraw。

平時練習算法題學習算法知識時,經常會發現題解裡寫着“動态規劃”,裡面一上來就是一個複雜的dp公式,對于新人來說除了說聲

“動态規劃”這詞太吓人,其實可以叫“狀态緩存”

剩下就是疑惑,他是怎麼想到這個公式的?我能想到嗎?這玩意工作中有用嗎?

加上“動态規劃”這高端的名字,然後就勸退了不少試圖去了解他的人。

“動态規劃”這詞太吓人,其實可以叫“狀态緩存”

動态規劃聽起來太吓人,可以換個說法

我在内心更喜歡叫他“狀态緩存”

如果是服務開發,相信很熟悉這個詞語, 利用緩存來加快一些重複的請求的響應速度。

而這個緩存的特點是 和其他緩存有所關聯。

比如我們的服務要計算7天内的某金錢總和,計算後要緩存一下。

後來又收到一個請求,要計算8天内的金錢總和

那我們隻需要取之前算過的7天内的金錢綜合,加上第8天的金錢就行了。

1+4的思考套路

自己針對動态規劃總結了一個自己的思考套路,我叫他1組例子4個問題,就叫1+4好了,通過這5個過程,可以站在普通人的角度(就是非acm大佬那種的角度),去了解動态規劃是如何被思考出來的

  • 在逾時的思路上寫出一組計算過程的例子
  • 在逾時例子的基礎上,有哪些重複、浪費的地方?
  • 如何定義dp數組
  • 狀态的變化方向是什麼,是怎麼變化的
  • 邊界狀态是什麼

簡單例子

以一道簡單題為例:

爬樓梯:

https://leetcode-cn.com/problems/climbing-stairs/

“動态規劃”這詞太吓人,其實可以叫“狀态緩存”

這時候就要靜下心,觀察這個解法的例子中是否有重複經曆的場景,而這個重複經曆的場景就叫狀态。

我處理動态規劃的題目時, 都會問自己3個問題,一般就能順利地解決。

①在逾時的思路上寫出一組計算過程的例子

如果我們考慮最簡單的解法, 就是從起點開始,每次選擇走1步或者走2步,看下能否走到終點,能走到則方法數+1。

但這種方法注定逾時(O(n^2))

但我還是照着這個過程模拟了一下,随便列了幾個

1 ->2-> 3-> 4-> 5

1 ->2 ->3-> 5

1->3->4->5

1->3->5

②在逾時例子的基礎上,有哪些重複、浪費的地方?

在上面,我發現了重複的地方

“動态規劃”這詞太吓人,其實可以叫“狀态緩存”

也就是說

從3到5總共就2種路線,已經在1->2之後計算過了,我後面從1走到3再往後走時,沒必要再去算了。

換言之,當我走到3的時候,其實早就可以知道後面還剩下多少種走法。

發現重複的地方後,就可以開始建立dp公式了。

③如何定義dp數組?

定義dp數組,也就是定義上面提到的重複的地方。重新看下之前的那句話

當我走到3的時候,其實早就可以知道後面還剩下多少種走法。

是以dp[3]代表的就是從3往後,有多少種可走的方法。

④狀态的變化方向是什麼,是怎麼變化的

  • 首先思考狀态的變化方向

    重新看這句話:

當我走到3的時候,其實早就可以知道後面還剩下多少種走法

說明結果取決于往 後面 的狀态

是以我們要先計算後面的狀态, 即從後往前算

  • 接着思考這個後面的狀态和目前的狀态有什麼聯系,是怎麼變化的

這個一般都包含在題目條件中

根據題意,要麼走2步,要麼走1步,是以每當我走到一層時,下一次就2種狀态可以變化。

那麼對于第3層而言,他後續有2種走法,走1步或者走2步

那麼他的情況就是dp[3] = dp[3+1] + dp{3+2}

如果層數設為i,那麼這個變化情況就是

dp[i] = dp[i+1] + dp[i+2]

⑤邊界狀态是什麼?

邊界狀态就是不需要依賴後面的狀态了,直接可以得到結果的狀态。

在這裡肯定就是最後一層dp[n], 最後一層預設是一種走法。 dp[n]=1

實作

根據上面的過程,自己便定義了這個狀态和變化

  • 定義:dp[i] : 代表從第i層往後,有多少種走法
  • 方向和變化:dp[i] = dp[i+1] + dp[i+2];
  • 邊界: dp[n] = 1

    根據這個寫代碼就很容易了

    代碼:

public int climbStairs(int n) {
        int[] dp = new int[n + 1];
        dp[n] = 1;
        dp[n-1] = 1;
        for(int i = n-2; i >=0;i--) {
            dp[i] = dp[i+1] + dp[i+2];
        }
        return dp[0];
    }      

進階版,二維的動态規劃

https://leetcode-cn.com/problems/number-of-ways-to-stay-in-the-same-place-after-some-steps/

“動态規劃”這詞太吓人,其實可以叫“狀态緩存”

逾時的思路肯定是像搜尋一樣模拟所有的行走過程。

先假設1個steps=5, arrlen=3的情況

随便先列幾個。模拟一下不斷走的位置。數字指的是目前位置。

0->1->2->1->0->0

0->1->2->1->1->0

0->1->1->1->1->0

0->1->1->1->0->0

0->0->1->1->1->0

……

0->0->1->1->0->0

我發現這部分标粗的部分重複了,

換句話說

當我還剩2步且目前位置為1的時候,後面還有多少種走法,其實早就知道了。

涉及了2個關鍵因素: 剩餘步數和目前值,是以得用二維數組

是以

dp[realstep][index]

就代表了 剩餘步數為step且位置為index時, 後續還剩多少種走法。

  • 先思考變化方向

“當我還剩2步且目前位置為1的時候,後面 還有多少種走法,其實早就知道了。”

這個後面是指啥, 後面會怎麼變?

後面肯定是步數越來越少的情況, 并且位置會根據規律變化。 是以變化方向是步數變少,位置則按照規定去變。

那麼這個固定越來越少的這個“剩餘步數”,就是核心的變化方向。

我們計算時,可以先計算小的剩餘步數的狀态, 再去算大的剩餘步數。

  • 如何變化

根據題意和方向,剩餘步數肯定-1, 然後位置有3種選擇(減1,不變,加1), 那麼方法就是3種選擇的相加。

dp[step][index] = dp[step-1][index-1] + dp[step-1][index] + dp[step-1][index+1]

剩餘步數為0時,隻有目前位置為0才是我們最終想要的方案,把值設為1并提供給後面用,其他位置且步數為0時都認為是0。

dp[0][0] = 1;

dp[0][index] = 0;(index>0)

那麼最終出來了

  • 定義:dp{realstep][index]: 剩餘步數為step且位置為index時, 後續還剩多少種走法。
  • 方向和變化:dp[step][index] = dp[step-1][index-1] + dp[step-1][index] + dp[step-1][index+1]
  • 邊界: dp[0][0] = 1;

記憶體溢出處理

不過這題因為是困難題,是以給上面這個公式設立了一個小難度:

“動态規劃”這詞太吓人,其實可以叫“狀态緩存”

數組長度非常大,導緻如果index的範圍我們選擇為0~arrLen-1, 那麼最大情況dp[500][10^6]注定逾時記憶體範圍。

這時候就要去思考index設那麼大是不是沒必要

一般我們可以自己列這種情況的小例子,例如

step=2, arr=10

然後看下index有沒有必要設成0~9,随便走幾步

0->1->0

0->0->0

嗯?我發現就3種情況,arr後面那麼長不用啦?

于是發現規律:

剩餘的步數,必須支撐他傳回原點!

也就是說,其實index的最大範圍最多就是step/2, 不能再多了,再多肯定回不去了。

于是問題解決。

其他類似題目練習

https://leetcode-cn.com/problems/minimum-cost-for-tickets/

​​​​