1. 程式人生 > 實用技巧 >【演算法筆記】動態規劃

【演算法筆記】動態規劃

什麼是動態規劃

動態規劃(dynamic programming)是一種通過求解組合子問題的方法來遞迴的求解原問題的方法,與分治法極為相似,但是其特點在於,在遞迴的時候會大量的遇到相同的子問題。我們會記住該子問題的結果,避免重複求解。
動態規劃大量的用於解決最優化問題中,當然某些不是最優化的問題也可以用分治法解決,比如爬梯子問題、揹包問題。凡是能將問題刻畫為若干子問題組合的題目,都可以用動態規劃來求解。

如何來設計動態規劃演算法

我們以一個簡單的例子來說明:打家劫舍
你是一個專業的小偷,計劃偷竊沿街的房屋。每間房內都藏有一定的現金,影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統,如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。

給定一個代表每個房屋存放金額的非負整數陣列,計算你在不觸動警報裝置的情況下,能夠偷竊到的最高金額。

示例 1:

輸入: [1,2,3,1]
輸出: 4
解釋: 偷竊 1 號房屋 (金額 = 1) ,然後偷竊 3 號房屋 (金額 = 3)。
     偷竊到的最高金額 = 1 + 3 = 4 。

示例 2:

輸入: [2,7,9,3,1]
輸出: 12
解釋: 偷竊 1 號房屋 (金額 = 2), 偷竊 3 號房屋 (金額 = 9),接著偷竊 5 號房屋 (金額 = 1)。
     偷竊到的最高金額 = 2 + 9 + 1 = 12 。

1 找子問題

對於最優化問題,找子問題有一個關鍵的要素,就是一定要有選擇,通常如何選擇用max或者min。而選擇的專案就是一群與主問題結構相似且規模更小的子問題。且子問題通常要被描述成一個或多個輸入,單個數值輸出的形式:

\[y=dp(x_1,x_2,...),其中y\in\mathbb{R} \]

所以對於本問題,我們可以根據原問題直接寫出子問題的輸入輸出結構

	輸入:整數陣列s  
	輸出:能剽竊到的最高金額y    

然後就是處理dp函式的實現,dp函式內部一定要實現如何分割子問題,就是將s分割成一個或者多個比s小的序列,對於最優問題如何去找這個分割點,就在於如何去做選擇。小偷站在\(s[0]\)家門口,需要做出選擇偷還是不偷,如果選擇偷的話,那他就不能選擇偷下一家,如果不偷那他就可以偷下一家,小偷需要權衡這兩種情況獲得的利益y,才能做出選擇,所以可以分成兩個dp問題。

  1. 偷s[0]家的,拿到了錢,那麼不能偷s[1]家的,所以在s[2]家即以後組成新的陣列,又考慮同樣的選擇問題。
  2. 不偷s[0]家的,直接來到下一家,所以在s[1]家即以後組成新的陣列,又考慮同樣的選擇問題。

2 遞迴的定義最優解

找到子問題就好辦了,我們用公式寫出dp函式,這裡注意dp函式就代表小偷的盈利。\(s_i\)代表陣列s中第i個元素之後的子陣列。\(s_0\)代表整個陣列。s[k]代表第k個元素的數值。s最後一個元素為s[m]

\[dp(s_i)=max(\overbrace{dp(s_{i+1})}^{不偷},\overbrace{s[i]+dp(s_{i+2})}^偷) \]

養成好習慣,遞迴公式注意初始值。

\[dp(s_m)=s[m]\\ dp(s_{m+1})=0 \]

3 自底向上計算

有了遞迴公式,我們雖然可以直接用巢狀函式來寫,不過堆疊扛不住。像斐波那契數列那樣,我們從初始值開始往上算,就不用浪費堆疊來調函數了。上面我們看出,顯然這個初始值不是很好看,所以我們換個表述方法,小偷從最後那個家開始往前偷,效果是一樣:

\[ dp(s_i)=max(\overbrace{dp(s_{i-1})}^{不偷},\overbrace{s[i]+dp(s_{i-2})}^偷)\\ dp(s_0)=s[0]\\ dp(\varnothing)=0 或者說dp(s_{-1})=0 \]

dp輸入本來是個字串,但是給我的有用資訊只有一個i,所以直接構成一個一維陣列。(如果給我的資訊有n個維度,則需要申請n維陣列)

\[ dp(i)=max(\overbrace{dp(i-1)}^{不偷},\overbrace{s[i]+dp(i-2)}^偷)\\ dp(0)=s[0]\\ dp(1)=max(s[0],s[1]) \]

依次計算\(dp(2),dp(3),...,dp(m)\)
這裡值得注意的是由於dp[-1]不存在,所以我用dp[1]來充當一個初始值了,在某些時候,尤其是二維陣列的時候,為了優化計算,可以考慮dp[-1]存在的情況,就是陣列整體右移,將dp[-1]的情況用dp[0]代替。以空間換時間。

4 儲存最優解

如果題目有要求需要儲存最優解。在dp遞迴的時候,可以順便記錄下所有的選擇,最後可以一次串起來。

總結

這裡只舉例了一維的情況,當dp輸入包含兩個維度的資訊的時候,則需要二維dp陣列來計算。可以通過畫矩陣圖,搞清楚dp[i,j]的依賴關係,然後再設計計算順序。如果是多維空間的話,陣列量將特別大,這個時候會可以根據這種依賴關係來降低空間複雜度。有時候這個dp點只會和鄰近的dp點有關係,不會和歷史dp點有關係,這時候歷史dp點就可以捨棄不要。
dp難點在於找子問題,這也是需要訓練的地方,可以針對兩種題來做。

  1. 對於最優化問題,就是在於選擇不同的情況,在題目中找出需要做出選擇情況的時候,這裡”情況“一般就是子問題。
  2. 對於非優化問題,一般就是那種排列組合題,其實和上面思想一樣,只不過\(max(dp_1,dp_2,...)\)變成了\(dp_1+dp_2+...\)或者\(dp_1\times dp_2 \times...\)。也是要分“情況”,只不過這裡不需要做選擇了。

寫在最後

本人也處於學習階段,奈何dp問題怎麼找子問題確實難,所以必須給自己做個總結,不然稍微變通一下就不會了。大神直接忽略。