動態規劃(DP)教程
大家好,我是Weekoder!
Week_team 的同學們都來聽課啦!
如果你還沒有加入 Week_team,點選這裡即可加入我們!
動態規劃的概念
動態規劃(DP)聽起來是個非常嚇人的東西,實際上……確實是個嚇人的東西。但是我想告訴大家,動態規劃並沒有那麼難,主要有兩個關鍵點:
-
多練。
-
多思考。
就是這麼簡單。
但實際做起來卻不簡單。
那麼,我們偉大旅程的第一步就是——搞懂動態規劃是什麼!
首先,動態規劃也稱 DP,他適合解決的問題型別有兩種:
-
最值問題。(最大最小值)
-
計數問題。(請問一共有多少種方案數?)
只要知道這些就夠了。
那 DP 又分為哪幾個步驟呢?
-
找到決策。
-
設計狀態。
-
考慮轉移。
沒有看懂沒關係,我們一個個來。
以B3636為例子,展開我們的講解。
1. 找到決策
這一步比較簡單,但也是最重要的一步。
題目描述裡有這樣一段話:
機器貓有兩種操作可以做。假設現在已經有 \(x\) 個字,機器貓可以選擇:
- 往文件最後加一個字。字數變成 \(x+1\)。
- 把文件複製貼上一遍。字數變成 \(2x\)。
這給了我們一個啟發。假設現在你是機器貓,你坐在電腦前開始打字。噠噠噠噠噠噠噠噠噠噠噠(超真實打字音效)。你的螢幕上有若干個字,這時候就到了最讓人犯難的時候了:到底該加一個字還是貼上一遍?答案是不需要知道。唯一需要知道的是當前有兩個選擇:加字和貼上一遍。這就是我們的決策。
恭喜你找到決策了!啪啪啪啪啪(鼓掌)!
簡單總結:找到決策就是模擬一遍題目的過程,找到需要進行抉擇的地方。
2. 設計狀態
設計狀態對初學者來說往往是很讓他們困擾的。那該怎麼辦呢?最好的方法還是多練。但這裡我也會提供一種設計狀態的思路。
狀態是什麼?我們在求解動態規劃問題時,往往需要陣列,如 \(dp[10010]\)。你可以把狀態理解為你對陣列元素的定義。如對於這道題,我們可以把 \(dp_i\) 定義為打了 \(i\) 個字所需要的最少步驟,那麼答案就是 \(dp_n\)(打 \(n\) 個字所需要的最少步驟)。這就像你對函式意義的定義。
那我們拿到一道題之後該怎麼去定義呢?其實對於我,一開始的時候可以定義的比較隨便。但一旦發現很難求解,就要及時考慮加入狀態。比如定義的狀態是 \(dp_i\),那麼就加入一個 \(j\),變為 \(dp_{i,j}\)。在各種各樣的題目中,甚至會出現四維陣列 \(dp_{i,j,k,l}\)。
那麼這道題的狀態就設計為 \(dp_i\) 表示打 \(i\) 個字所需要的最少步驟。
注意:最好的方法還是多練,多刷題!
3. 考慮轉移
這是很多人眼中最難的一步。其實我覺得這一步並沒有設計狀態難,因為他和第一步是緊密相關的。
我們第一步說到了決策,那我們就要想一想怎麼利用這個決策把狀態一一填滿。
這個時候其實是要設計一個狀態轉移方程。就像這樣:
現在我們要把這個 \(?\) 填上。我們想一想 \(dp_i\) 能從哪個地方做決策而來?就像已經打了 \(i\) 個字的機器貓,想知道他有可能是打了幾個字的機器貓做了一步決策而來的。顯然,我們之前說過,機器貓要麼只能打一個字(\(x+1\)),要麼只能貼上一遍文字(\(2x\))。現在反過來看,不就是打了 \(i-1\) 和 \(i\div2\) 個字的機器貓嗎?一個打一個字就變成了 \(i\),另一個貼上一遍文字也變成了 \(i\)。那我們既然要求最小步驟,肯定要在兩種情況裡取最小值再加上做出決策的步驟(\(1\) 步)啦。看看狀態轉移方程吧:
這樣就設計成功了!
其實我認為,狀態轉移方程就是決策的逆運算。
這樣就結束了。
還有一點比較重要的是,設定初始狀態。像剛才的例子,初始狀態就是 \(dp_1=0\),因為一開始就有一個字,不需要進行任何操作。當然,全域性陣列預設是 \(0\),所以不需要單獨加上這樣一條指令。
我們只需要迴圈從小到大擴充套件,套用狀態轉移方程就行了。
\(\text{Code:}\)
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
int n, dp[N];
int main() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cin >> n;
for (int i = 2; i <= n; i++) {
if (i % 2 == 0)
dp[i] = min(dp[i - 1], dp[i / 2]) + 1;
else
dp[i] = dp[i - 1] + 1;
}
cout << dp[n];
return 0;
}
這裡有一個細節,就是在迴圈裡並沒有直接套用公式,而是判斷了字數的奇偶。因為當字數為奇數時,是不可能從 \(i\div2\) 轉移過來的。而奇數的情況一定不能寫成這樣:
這裡不能取 min,因為只有 \(i-1\) 這一種情況,上面的是在兩種情況:\(i-1\) 和 \(i\div2\) 中取 min。在求最值問題時才會有上面這種寫法。
總結
我相信如果你是初學者,光看程式碼看定是不能完全看懂的。可以私信我 @Weekoder 或者在評論區發表意見和疑問。
作業:動態規劃初步題單。
再見!