終於來到了算法設計思想中最有趣的這部分,在去年的google筆試中,7道算法設計題有2道動態規劃(Dynamic Programming)。
看了這麼久的算法,這部分也是唯一感覺到了比較難的地方,
從這篇文章開始,將花連續的篇幅來討論一些對動態規劃的認識和其中的問題。這包括一些例子:計算二項式係數,Warshall算法求傳遞閉包,Floyd算法求完全最短路徑,構造最
有二叉查找樹,揹包問題和記憶功能。也包括一些其他問題的解題報告(動態規劃確實很難,對這一章的內容,我將搜索一些其他類型的問題來寫解題報告,以真正的
理解動態規劃),例如矩陣連乘,最長公共子列,等等。
--------------------------------------------------------------------------------------------------------------------------------------------------
1,什麼是動態規劃(DP)?
非常重要!,不要認爲概念不重要,理解的深刻,你才知道對於什麼樣的問題去考慮有沒有動態規劃的方法,以及如何去使用動態規劃。
1)動態規劃是運籌學中用於求解決策過程中的最優化數學方法。 當然,我們在這裏關注的是作爲一種算法設計技術,作爲一種使用多階段決策過程最優的通用方法。
它是應用數學中用於解決某類最優化問題的重要工具。
2)如果問題是由交疊的子問題所構成,我們就可以用動態規劃技術來解決它,一般來說,這樣的子問題出現在對給定問題求解的遞推關係中,這個遞推關係包含了相
同問題的更小子問題的解。動態規劃法建議,與其對交疊子問題一次又一次的求解,不如把每個較小子問題只求解一次並把結果記錄在表中(動態規劃也是空間換時間
的),這樣就可以從表中得到原始問題的解。
關鍵詞:
它往往是解決最優化問題滴
問題可以表現爲多階段決策(去網上查查什麼是多階段決策!)
交疊子問題:什麼是交疊子問題,最有子結構性質。
動態規劃的思想是什麼:記憶,空間換時間,不重複求解,由交疊子問題從較小問題解逐步決策,構造較大問題的解。
-------------------------------------------------------------------------------------------------------------------------------------------------
關於斐波拉切數列可以作爲最簡單的一個例子來解釋動態規劃的思想,在前面講斐波拉切數列時說過了,不再敘述。
一般來說,一個經典的動態規劃算法時自底向上的(從較小問題的解,由交疊性質,逐步決策處較大問題的解),它需要解出給定問題的所有較小子問題。動態規劃的
一個變種是試圖避免對不必要的子問題求解。如果採用自頂向下的遞歸來解,那麼就避免了不必要子問題的求解(相對於動態規劃表現出優勢),然而遞歸又會導致對
同一個子問題多次求解(相對於動態規劃表現出劣勢),所以將遞歸和動態規劃結合起來,就可以設計一種基於記憶功能的從頂向下的動態規劃算法,在後面會講。
------------------------------------------------------------------------------------------------------------------------------------------------
計算二項式係數:
在排列組合裏面,我們有下面的式子(很容易用組合的定義來證明):
這個式子將C(n , k)的計算問題表述爲了(問題描述)C(n-1 , k -1)和C(n -1, k)兩個較小的交疊子問題。
初始條件:C(n , n) = C(n , 0) = 1
我們可以用下列填矩陣的方式求出C(n , k):
該算法的時間複雜度是多少呢?可以大概的估計下,只填了下三角矩陣,爲n*k/2 = n*k,具體的次數爲:
矩陣怎麼填(填矩陣的順序)?
按行來填矩陣:算法僞代碼:
第1個for是控制行的,要填到第n行。第2個for來控制每行填到哪的,到i和k的較小值。從這2個for也可以看出複雜度是n*k。
實現:
/*第八章 動態規劃 計算二項式係數*/
publicclass BinoCoeff {
/**
* @param args
*/
publicstaticvoid main(String[] args) {
// TODO Auto-generated method stub
int result = Binomial(8,3);
System.out.println("輸出8的二項式係數:");
for(int i =0;i <=8;i++)
System.out.println("C"+"("+8+","+ i +")"+" ———— "+ Binomial(8,i));
}
publicstaticint Binomial(int n,int k){
//計算二項式係數C(n,k)
int[][] result =newint[n+1][n+1];
for(int i =0;i <= n;i++) //按行來填矩陣
{
for(int j =0;j <= min(i,k);j++) // min(i,k)是這一行需要填的列數
{
//if(j == 0 || j == k)//書上寫錯了,他媽的
if(j ==0|| j == i)
result[i][j] =1;
else
result[i][j] = result[i -1][j -1] + result[i -1][j];
}
}
return result[n][k];
}
privatestaticint min(int i,int k){
if(i < k)
return i;
return k;
}
}
結果:
輸出8的二項式係數:
C(8,0) ———— 1
C(8,1) ———— 8
C(8,2) ———— 28
C(8,3) ———— 56
C(8,4) ———— 70
C(8,5) ———— 56
C(8,6) ———— 28
C(8,7) ———— 8
C(8,8) ———— 1
其實可以返回整個矩陣,這樣就可以一次把所有的C(n , k)都計算出來。
--------------------------------------------------------------------------------------------------------------------------------------------------
再看動態規劃:
上面棕色字體標出的就是一個動態規劃算法的幾個關鍵點:
1)怎麼描述問題,要把問題描述爲交疊的子問題
2)交疊子問題的初始條件(邊界條件)
3)動態規劃在形式上往往表現爲填矩陣的形式(在後面會看到,有的可以優化空間複雜度,開一個數組即可,優化也是根據遞推式的依賴形式的,後面有篇文章詳細說明)
4)填矩陣的方式(或者說順序)表明了什麼?--它表明了這個動態規劃從小到大產生的過程,專業點的說就是遞推式的依賴形式決定了填矩陣的順序。
---------------------------------------------------------------------------------------------------------------------------------------------------
習題8.1 決定這一章的習題都認真的做一遍
1
a,相同點是動態規劃和分治法都劃分爲了較小規模問題的解
b,不同點是動態規劃的較小子問題是交疊的,而且要存儲較小子問題的解
2
a,參見代碼,已實現
b,也可以按列來填矩陣(想想爲什麼?)---實際上這個問題就表明了再看動態規劃第四點(填矩陣的方式表明了什麼)
3
easy,在講解中已多處指出
4
a, 空間效率也是nk,參見代碼,或者從矩陣上也可看出
b,可以,這個問題的表明了再看動態規劃第三點(優化空間複雜度)
---爲什麼可以優化,上面說過,可不可以優化,以及如何優化空間複雜度依賴於它的遞推形式:
---從填矩陣的那張圖可以看出,這個動態規劃產生各項的過程(如果按行填的話)是上一行的第 i-1 項和第 i 項加起來產生下一行的第 i 項,傳統上,我們從左往右填。
---事實上,根據它的產生過程(這個產生過程依賴於遞推式自身的數學特徵),可以從右往左填,這樣開一個數組就行,在原數組上本地不動的填數,從右往左填可
以保證一個位置在覆蓋以後不會再被用到(這是由遞推式的屬性決定的,需要畫一畫纔看的比較清楚)。
這樣開一個K大的數組就行了,具體的實現就不寫了,已經分析的很清楚了,實現也不難
---------------------------------------------------------------------------------------------------------------------------------------------------
由以上分析,加習題,相信對於動態規劃到底是什麼,核心思想,具體的操作細節,以及對於動態規劃的理解都加深了吧,
有2點我覺得非常重要,一是填矩陣的順序,二是動態規劃空間複雜度的優化:這2點都跟遞推式的依賴關係有關(這是本質),在形式上就表現爲填矩陣的時候你的
順序要確保每填一個新位置時你所用到的那些位置(即它依賴的)要已經填好了,在空間優化上表現爲當一個位置在以後還有用的時候你不能覆蓋它。
這2條結論非常重要,是深刻理解動態規劃的一個重要階梯,我也是好久慢慢悟出來的,當然,它們有更professional的表述,在下一篇文章裏我會貼出。
---------------------------------------------------------------------------------------------------------------------------------------------------
再來看2道實踐的習題吧,也比較簡單:
9,靠,電子版的竟然跟紙質版的不太一樣,電子版書上沒有這個題,截不了圖,抄一下吧:
問題:
國際象棋中的車可以水平的或豎直的移動,一個車要從一個棋盤的一角移到對角線的另一角,有多少種最短路徑?
a,用動態規劃算法求解
b,用初等排列組合知識求解
b)先說b吧,這是個很簡單的高中排列組合題目了,假設棋盤大小是n*n的(囧,象棋棋盤多大這個得想想才知道,就說n吧),答案是C(2n , n)
a)用a方法做下吧(主要是培養下怎麼去建立動態規劃的遞推式)
問題是從(0,0)移動到(n,n)有多少種方法?(最短路,即橫n豎n,不能回退)
設C[i , j]表示從(0,0)移動到(i ,j)的方法數(描述問題,怎麼去刻畫C[i , j]的含義,是動態規劃的一個關鍵點):
那麼怎麼才能走到(i ,j)呢,它的上一步必定是(i-1 ,j)或者(i ,j-1)-------(分析動態規劃問題的逆向思維,很重要,後面要講)
這樣就將問題描述爲了交疊子問題:
C[i , j] = C[i -1, j] + C[i , j-1] ( C[i , j]的含義 )
我們要求的是C[n , n]
初始條件:
C[0 , j] = j j從0到n
C[i , 0] = i i從0到n
即第一行第一列確定。
填矩陣的形式:可以按行也可以按列。
以上分析畫個圖很容易看出來。剩下的實現就很簡單了。
10
第一問就是個概率題,聽起來比較拗口,其實不難,屬於高中概率水平:
有了遞推式後,發現其實跟上一題完全一樣,就是遞推式裏多乘了個概率值,難怪電子版省略了一個題,剩下的略
--------------------------------------------------------------------------------------------------------------------------------------------------
非常重要:
解這兩道題後,應該知道一個動態規劃的設計過程是怎樣的:
個人體會是動態規劃的難點在於前期的設計:
a)怎麼描述問題,使它能表述爲一個動態規劃問題(具備什麼特徵?最有子結構,多階段決策,思考)
b)遞推式的寫出(逆向思維去分析或正向思維去遞歸),確定你要求的是哪個值
c)有了遞推式可以畫個矩陣的圖(一般只從式子上不太容易看出來,當然,對於牛人來說可以藐視),在圖中關注以下兩點:
初始條件
填矩陣的順序(即怎麼去寫代碼控制語句)
有了這些之後,其實動態規劃的代碼都很簡單,它的難點在於問題的描述和解決階段,而不在於寫代碼的階段,剩下的寫代碼基本上就是照着公式填矩陣。
--------------------------------------------------------------------------------------------------------------------------------------------------
差點把一個重要的問題忘了:
我們來看看象棋問題的動態規劃描述,它爲什麼可以描述爲動態規劃的?
關於可以描述爲交疊子問題,上面分析過了,
我們再說下最有子結構和多階段決策:
最優子結構:有準確的定義,可以參見一些資料,我自己描述下就是:在動態規劃求解過程中的,子問題產生的解對於子問題來說也是一個最優解
多階段決策:一步步的決策,無後效性,決策只依賴於當前狀態,不依賴於之前的狀態。
看看象棋問題的最優子結構性質:在到達終點之前的任意(i , j)點所走過的方法數都是最少的。
多階段決策:每次決定往哪走只跟當前在哪有關,跟以前怎麼走的無關。
--------------------------------------------------------------------------------------------------------------------------------------------------
總結:
動態規劃的思想,理解深度,以上亮色字體標出部分!!!!
這篇信息量還是很大的,要仔細理解,多看幾遍。