1、前言
動態規劃和分治算法非常類似,都是通過組合子問題的解來求解原問題。分治算法將問題劃分為互不相交的子問題,而動態適應于子問題重疊的情況
動態規劃常用于求解“最優化問題”,這類問題有很多個解,我們希望尋找最優解(最大值或最小值)
通常按如下四個步驟來設計一個動態規劃算法:
- 刻畫一個最優解的結構特征
- 遞歸地定義最優解的值
- 計算最優解的值,通常采用自底向上方法
- 利用計算出的信息構造一個最優解
或者說,動態規劃有3個關鍵要素。
- 1、最佳子問題(將復雜問題轉化成簡單問題)
- 2、邊界條件(一般是當數據量極少情況下的結果,比如n==0或n==1時的情況)
- 3、狀態轉移方程(考慮 n-1 和 n這兩種規模時,結果之間的推導)
2、鋼條切割問題
給定一段長為n的鋼條和一個價格表,求解收益最大的鋼條切割方案
鋼條價格如上,怎么切割才能有最大收益呢?通過鋼條價格,我們能手動計算出鋼條最佳切割方案。
定義長為n的鋼條切割最大收益為Rn,假設在k位置將鋼條切成兩段能獲得最大收益,那么這兩段k和n-k肯定也是最大收益,如果我們遍歷循環,可得到如下公式:
最大收益為,不切割以及從1到n-1位置切割的收益最大值。
換一個角度重新考慮,如果從1到n-1位置切割,但左邊的鋼條不再切割,右邊的鋼條使用最佳方案切割。這樣得到的會不是也是最佳切割方案呢?
很明顯,這種做法也是正常的,想象最終切割結果,肯定也是符合這種模式的。這樣簡單的遞歸更利于編碼。
動態規劃的精髓在于保存子問題的解,以提高效率。在編碼中我們新定義Rn,保存n長度的最大收益,并采用自底向上方法,即先求R0,R1,再求其它,如果單純使用暴力遞歸的話,效率非常低。
public static int[] steel(int[] p){
int[] s = new int[p.length];
s[0] = 0;
int max = 0;
//因為長度為0的鋼條,切割最大收益為0,S[0]已知,所以i從1開始
for (int i = 1; i < p.length; i++) {
max = 0;
for (int j = 1; j <= i; j++) {
//j也必須從1開始,如果從0開始,i等于1時,s[i-j]就等于s[1]了,而s[1]還是未知值
if (max < (p[j] + s[i-j])) {
max = p[j] + s[i-j];
}
}
s[i] = max;
}
return s;
}
2、最大子數組和
給出一個數組,求最大子數組和。例如給出如下數組,最大子數組和為20
-2, 11, -4, 13, -5, -2
此問題解題思路和1不一樣。
仔細考慮,如果設長度為n的最大子數組和為Sn,那么Sn和Sn-1有什么關系呢?動態規劃最重要的思想就是將大問題分解為子問題,并由子問題求解。
直接考慮Sn和Sn-1,好像二者沒有任何關系,如果我們換個角度考慮,長度為n的數組,且以n結尾的最大子數組和為Sn,這么定義的話,那么根據Sn的值,如果Sn大于0,那么Sn+1等于Sn加上a[n],如果小于0,那么Sn+1等于a[n]。
公式為:
dp[i+1] = max(dp[i]+a[i+1] , a[i+1])
public static int getMaxSubArray(int[] array){
int[] b = new int[array.length];
int max = 0;
b[0] = array[0];
max = b[0];
for (int i = 1; i < array.length; i++) {
if (b[i-1] > 0) {
b[i] = b[i-1] + array[i];
}else {
b[i] = array[i];
}
if (max < b[i]) {
max = b[i];
}
}
return max;
}
3、結語
動態規劃還有其它經典問題,比如矩陣最少相乘次數,最長公共子序列,最優二叉搜索樹等等。本文只講了兩個簡單例子,簡述動態規劃的原理,可以更深入思考,二維的動態規劃問題解決,以加深理解。
以上代碼均已上傳至本人的github,歡迎訪問