【演算法】動態規劃
阿新 • • 發佈:2020-03-12
# 動態規劃
> **動態規劃**(dynamic programming):它是把研究的問題分成若干個階段,且在**每一個階段**都要“動態地”做出決策,從而使整個階段都要取得最優效果。
>> **理解:**其實,無非就是利用**歷史記錄**,來避免我們的重複計算。
而這些歷史記錄,我們得需要一些變數來儲存,一般是用**一維陣列**或者**多維陣列**來儲存。
其實我挺好奇為什麼用動態規劃這個名字的,所以我花時間找了一下,如果大家想要知道這個名字的由來,可以去看看:[動態規劃](https://en.wikipedia.org/wiki/Dynamic_programming#History)
## 動態規劃四步走
**動態規劃四步走:**
- **明確陣列的含義:**建立陣列,明確陣列元素的含義;
- **製作歷史記錄表:**根據陣列建表,填入初始值,利用遞推關係式寫程式推出其他空表項。
> **注意:**這個是為我們通過初始值和遞推關係式寫出程式提供視覺化條件以及思路,把抽象的東西可視化了,時時刻刻都知道自己要幹嘛。
當然,如果腦子裡有思路可以忽略。。
- **尋找陣列初始值:**尋找陣列元素初始值;
> **注意:**這個**初始值**要特別的給出一個**出口**,因為它們不是被遞推出來的。
- **找出遞推關係式:**找出陣列元素遞推關係式。
> **注意:**可以從 **dp[i] = ?** 這一數學公式開始推想。
### 明確陣列的含義
> **第一步:**定義陣列元素的含義。
上面說了,我們會用一個數組,來儲存歷史記錄,假設用一維陣列 dp[]吧。這個時候有一個非常非常重要的點,就是規定你這個陣列元素的含義,即 你的 dp[i] 是代表什麼意思?
那麼下面我來舉個**例子**吧!
**問題描述:**一隻青蛙一次可以跳上1級臺階,也可以跳上2級。求該青蛙跳上一個n級的臺階總共有多少種跳法。
首先,拿到這個題,我們需要判斷用什麼方法,要跳上n級的臺階,我們可能需要用到前幾級臺階的資料,即 **歷史記錄**,所以我們可以用**動態規劃**。
然後依據上面我說的第一步,建立陣列 dp[] ,那麼順理成章我們的 dp[i] 應該規定含義為:**跳上一個i級的臺階總共有dp[i]種解法**。
那麼,求解dp[n]就是我們的任務。
### 製作歷史記錄表
1. 根據陣列,**製表**,確定一維表、二維表;
2. **填入初始值**;
3. 根據遞推關係式,寫程式**推出剩餘的空表項**。
> **注意:**這裡一維表比較簡單可能體現不出它的作用,到二維表它就能很方便的將資料可視化了。
此題,由於明確了陣列的含義,我們可以確定是一張一維表。
**歷史記錄表:**
|陣列dp|1|2|3|…|n|
|---|---|---|---|---|---|
|值| | | | | |
### 尋找陣列初始值
> **第二步:**找出初始值。
利用我們學過數學歸納法,我們可以知道如果要進行遞推,我們需要一個初始值來推出結果值,也就是我們常說的第一張多米諾骨牌。
本題的初始值很容易我們就找出來了,
- **當 n = 1 時**,即 只有一級臺階,那麼我們的青蛙只用跳一級就可以了,只有一種跳法,dp[1] = 1;
- **當 n = 2 時**,即 有兩級臺階,我們的青蛙有兩種選擇,一級一級的跳 和 一次跳兩級,dp[2] = 2;
- **當 n = 3 時**,即 有三級臺階,我們的青蛙跳一級 + dp[2],或 跳兩級 + dp[1],這時候我們就反應過來了,需要進行下一步找出 n 的遞推關係式。
---
**歷史記錄表:**
|陣列dp|1|2|3|…|n|
|---|---|---|---|---|---|
|值|1|2| | | |
### 找出遞推關係式
> **第三步:**找出陣列元素之間的關係式。
動態規劃有一點類似於**數學歸納法**的,當我們要計算 dp[n] 時,是可以利用 dp[1]……dp[n-2]、dp[n-1] ,來推出 dp[n] 的,也就是可以利用**歷史資料**來推出新的元素值,所以我們要找出陣列元素之間的關係式,例如, dp[i] = dp[i-1] + dp[i-2] ,這個就是它們的遞推關係式了。而這一步,也是最難的一步,後面我會講幾種型別的題來說。
當 **n = i** 時,即 有 i 級臺階,我們的青蛙最後究竟怎麼樣到達的這第 i 級臺階呢?
因為青蛙的彈跳力有限,只能一次跳一級或者兩級,所以我們有兩種方式可以到達最後的這第 i 級:
- 從 i-1 處跳一級
- 從 i-2 處跳兩級
所以,我們只需要把青蛙跳上 i-1 級臺階 和 i-2 級臺階的跳法加起來,我們就可以得到到達第 i 級的跳法(i≥3),即
$$dp[i] = dp[i-1] + dp[i-2], (i≥3)$$
這樣我們知道了初始值dp[1]、dp[2],可以**從dp[3]開始**遞推出4、5、6、...、n。
---
**歷史記錄表:**
|陣列dp|1|2|3|…|n|
|---|---|---|---|---|---|
|值|1|2|**3**|… | |
用程式迴圈得出後面的空表項。
---
你看有了初始值,以及陣列元素之間的關係式,那麼我們就可以像數學歸納法那樣遞推得到dp[n]的值了,而dp[n]的含義是由你來定義的,你想求什麼,就定義它是什麼,這樣,這道題也就解出來了。
**答案:**
```
// 青蛙跳臺階
int f(int n) {
// 特別給初始值的出口
if(n <= 2)
return n;
// 建立陣列儲存歷史資料
int[] dp = new int[n+1];
// 給出初始值
dp[1] = 1;
dp[2] = 2;
// 通過遞推關係式來計算出 dp[n]
for(int i = 3; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2];
}
// 把最終結果返回
return dp[n];
}
```
# 例項
## 超級青蛙跳臺階
一個臺階總共有 n 級,超級青蛙有能力一次跳到 n 階臺階,也可以一次跳 n-1 階臺階,也可以跳 n-2 階臺階……也可以跳 1 階臺階。
問超級青蛙跳到 n 層臺階有多少種跳法?(n<=50)
例如:
輸入臺階數:3
輸出種類數:4
解釋:4 種跳法分別是(1,1,1),(1,2),(2,1),(3)
---
**答案:**
> 這裡我是運用了**“數學”**來得出式子的,為了告訴大家不要拘泥於程式,數學也是一個很有用的工具。
用 **Fib(n)** 表示超級青蛙