源自《演算法筆記》動態規劃是一種用來解決一類最優化問題的演算法思想。動態規劃在乙個複雜的問題分解成若干個子問題。通過綜合此問題的最優解。來得到原問題的最優解。
需要注意的是,會將每個求解過的子問題的解記錄下來。這樣當下一次再碰到同樣的子問題時,不可以直接使用之前記錄的結果而不是重複計算。
一般可以使用遞迴或者遞推的寫法來實現動態規劃。其中遞迴寫法在此處又稱為記憶化搜尋。
以菲波那切數列為例。
#includeint dp[100];
/* 斐波那契的動態規劃實現 */
int fbi(int n)
if(dp[n] != -1)
else
}int main()
fbi(41);
for(int i = 0; i< 40; i++)
return 0;
}
這樣就把已經計算過的內容記錄了下來,於是當下次再碰到需要計算相同的內容時,就能直接使用上次計算的結果,這可以省去大半無效計算,而這也是記憶化搜尋這個名字的由來。
通過上面的例子可以引申出乙個概念:如果乙個問題可以被分解為若干個子問題,且這些子問題會重複出現,那麼就稱這個問題擁有重疊子問題。動態規劃通過記錄重疊子問題的解,來使下次碰到相同的子問題時直接使用之前記錄的結果,以此避免大量重複計算。因此,乙個問題必須擁有重疊子問題,才能使用動態規劃去解決。
以經典的數塔問題為例,如圖11-3所示,將一些數字排成數塔的形狀,其中第一層有乙個數字,第二層有兩個數字……第n層有n個數字。現在要從第一層走到第n層,每次只能走向下一層連線的兩個數字中的乙個,問:最後將路徑上所有數字相加後得到的和最大是多少?
一開始,從第一層的5出發,按5→8→7的路線來到7,並列舉從7出發的到達最底層的所有路徑。但是,之後當按5→3→7的路線再次來到7時,又會去列舉從7出發的到達最底層的所有路徑,這就導致了從7出發的到達最底層的所有路徑都被反覆地訪問,做了許多多餘的計算。事實上,可以在第一次列舉從7出發的到達最底層的所有路徑時就把路徑上能產生的最大和記錄下來,這樣當再次訪問到7這個數字時就可以直接獲取這個最大值,避免重複計算。
由上面的考慮不妨令dp[i][j]
表示從第 i 行第 j 個數字出發的到達最底層的所有路徑中能得到的最大和,例如dp[3][2]
就是圖中的 7 到最底層的路徑最大和。在定義這個陣列之後,dp[1][1]
就是最終想要的答案,現在想辦法求出它。
注意到乙個細節:如果要求出「從位置(1,1)到達最底層的最大和」dp[1][1]
,那麼一定要先求出它的兩個子問題「從位置(2,1)到達最底層的最大和dp[2][1]
」和「從位置(2,2)到達最底層的最大和dp[2][2]
」,即進行了一次決策:走數字5的左下還是右下。於是dp[l][1]
就是dp[2][1]
和dp[2][2]
的較大值加上5。寫成式子就是:
dp[1][1]=max(dp[2][1],dp[2][2])+f[1][1]
由此可以歸納得到這麼乙個資訊:如果要求出dp[i][i]
,那麼一定要先求出它的兩個子問題「從位置(i+1,j)到達最底層的最大和dp[i+1][j]
」和「從位置(i+1,j+1)到達最底層的最大和dp[i+1][j+1]
」,即進行了一次決策:走位置(i,j)的左下還是右下。於是dp[i][i]
就是dp[i+1][i]
和dp[i+1][i+1]
的較大值加上f[i][j]
。寫成式子就是:
dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+f[i][j]
把dp[i][j]
稱為問題的狀態,而把上面的式子稱作狀態轉移方程,它把狀態dp[i][j]
轉移為dp[i+1][j]
和dp[i+1][i+1]
。可以發現,狀態dp[i][j]
只與第 i+1 層的狀態有關,而與其他層的狀態無關,這樣層號為i的狀態就總是可以由層號為 i+1 的兩個子狀態得到。那麼,如果總是將層號增大,什麼時候會到頭呢?可以發現,數塔的最後一層的dp值總是等於元素本身,即dp[n][i]
=f[n][j]
(1≤j≤n),把這種可以直接確定其結果的部分稱為邊界,而動態規劃的遞推寫法總是從這些邊界出發,通過狀態轉移方程擴散到整個dp陣列。這樣就可以從最底層各位置的dp值開始,不斷往上求出每一層各位置的dp值,最後就會得到dp[1][1]
,即為想要的答案。
這種思想的動態規劃**如下:
int main()
printf("%d\n",dp[1][1]);//dp[1][1]即為需要的答案『』
return 0;
}
通過上面的例子再引申出乙個概念:如果乙個問題的最優解可以由其子問題的最優解有效地構造出來,那麼稱這個問題擁有最優子結構(optimal substructure)。最優子結構保證了動態規劃中原問題的最優解可以由子問題的最優解推導而來。因此,乙個問題必須擁有最優子結構,才能使用動態規劃去解決。例如數塔問題中,每乙個位置的dp值都可以由它的兩個子問題推導得到。
至此,重疊子問題和最優子結構的內容已介紹完畢。需要指出,乙個問題必須擁有重疊子問題和最優子結構,才能使用動態規劃去解決。下面指出這兩個概念的區別:
分治與動態規劃。分治和動態規劃都是將問題分解為子問題,然後合併子問題的解得到原問題的解。但是不同的是,分治法分解出的子問題是不重疊的,因此分治法解決的問題不擁有重疊子問題,而動態規劃解決的問題擁有重疊子問題。例如,歸併排序和快速排序都是分別處理左序列和右序列,然後將左右序列的結果合併,過程中不出現重疊子問題,因此它們使用的都是分治法。另外,分治法解決的問題不一定是最優化問題,而動態規劃解決的問題一定是最優化問題。
貪心與動態規劃。貪心和動態規劃都要求原問題必須擁有最優子結構。二者的區別在於,貪心法採用的計算方式類似於上面介紹的「自頂向下」,但是並不等待子問題求解完畢後再選擇使用哪乙個,而是通過一種策略直接選擇乙個子問題去求解,沒被選擇的子問題就不去求解了,直接拋棄。也就是說,它總是只在上一步選擇的基礎上繼續選擇,因此整個過程以一種單鏈的流水方式進行,顯然這種所謂「最優選擇」的正確性需要用歸納法證明。例如對數塔問題而言,貪心法從最上層開始,每次選擇左下和右下兩個數字中較大的乙個,一直到最底層得到最後結果,顯然這不一定可以得到最優解。而動態規劃不管是採用自底向上還是自頂向下的計算方式,都是從邊界開始向上得到目標問題的解。也就是說,它總是會考慮所有子問題,並選擇繼承能得到最優結果的那個,對暫時沒被繼承的子問題,由於重疊子問題的存在,後期可能會再次考慮它們,因此還有機會成為全域性最優的一部分,不需要放棄。所以貪心是一種壯士斷腕的決策,只要進行了選擇,就不後悔;動態規劃則要看哪個選擇笑到了最後,暫時的領先說明不了什麼。
C語言 遞迴入門
遞迴是什麼,遞迴就是一種解決問題的方法 程式自身呼叫自身叫做遞迴。它的核心在於 大事化小!先舉幾個例子 1.接受乙個整型值,按順序列印它的每一位。只考慮正數 如 1234 應輸出1 2 3 4 1 先討論如果不遞迴該怎樣處理,一般步驟是這樣的,先判斷這個數是幾位數,然後在記錄下這個數的每一位,最後輸...
C語言演算法描述基礎
1.2 選擇排序演算法 1.3 插入排序演算法 1.4 快速排序演算法 2.查詢演算法 定義 演算法是一些常見問題的通用解決方法 排序演算法可以按照某種順序把一組數字排列好 排序演算法每次只把乙個數字放在合適的位置上,通過大量重複以上過過程最終把所有數字都放到合適的位置上 為了把合適的數字放到合適的...
C語言 遞迴入門 漢諾塔問題
在定義乙個過程或函式時,出現呼叫本過程或本函式的成分稱為遞迴 如果乙個遞迴過程或函式中的遞迴呼叫語句是最後一條執行語句,則稱這種遞迴呼叫為尾遞迴 例如 計算階乘函式 intf int n else return f n 1 n 遞迴解決問題應滿足三個條件 需要解決的問題可以轉化為乙個或多個子問題求解...