“動態規划”這詞太嚇人,其實可以叫“狀態緩存”


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

本文分享自華為雲社區《動態規划究竟是怎么想到的?【奔跑吧!JAVA】》,原文作者: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->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的時候,后面還有多少種走法,其實早就知道了。

③如何定義dp數組?

重新看這句話:

當我還剩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->1->0

0->0->0

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

於是發現規律:

剩余的步數,必須支撐他返回原點!

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

於是問題解決。

其他類似題目練習

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

 

點擊關注,第一時間了解華為雲新鮮技術~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM