1. 程式人生 > >【動態規劃】令你戰慄的神奇演算法:動態規劃基礎

【動態規劃】令你戰慄的神奇演算法:動態規劃基礎

  動態規劃,一種奇妙卻苦澀難懂的演算法,使若干小白頭疼,這次小編會系統的梳理動態規劃的基礎。

▎什麼是動態規劃?

一、概念引入

  1)動態規劃的歷史:動態規劃最早是在數學領域中使用的,最常見的是在運籌學中的運用,在20世紀50年代初,美國數學家R.E.Bellman等人在研究多階段決策過程的優化問題時,提出了著名的最優化原理。

  2)引入     現在思考一個問題:

  有面值為1元、2元和5元的硬幣若干枚,如何用最少的硬幣湊夠n元?

  首先,我們一定會想到5元硬幣面值大(利用貪心的思想),那麼一定會優先使用5元硬幣,再使用2元的,最後再用1元的,這很符合我們市場買菜時的思路,這麼做也沒毛病,但是如果硬幣面值變了呢?再思考接下來的問題:

  有面值為1元、5元和7元的硬幣若干枚,如何用最少的硬幣湊夠n元?

  那麼這該怎麼辦?還用剛才的思路嗎?顯然不行,比如我們要湊出11元,如果用剛才的方法就需要7元*1+1元*4,一共5枚硬幣,而實際上只需要3枚硬幣(5元*2+1元*1),看來這並不是一種最優的方法,只保證了每次選硬幣時當前是最少的,卻不能保證整個問題是最優的。這時我們會自然的想到暴力的搜尋,但是搜尋的時間複雜度往往是指數級的,當n很大時,這種演算法就滿足不了我們的需求了,這時就要請出動態規劃。

二、動態規劃的本質

  1)繼續分析

  動態規劃沒有想象中的複雜,我們不需要仔細思考這選擇的整個過程,先來思考最後一枚硬幣會是哪一種:顯然最後一枚可能是1元的,可能是5元的,也可能是7元的,那麼湊n元所需要的硬幣數就可能是湊n-1元的硬幣數+1(加1枚1元硬幣),可能是湊n-5元的硬幣數+1(1枚5元硬幣),也可能是湊n-7元的硬幣數+1(1枚7元硬幣)。看到這裡,似乎問題還是那麼複雜,原來是湊n元,現在卻只要知道湊n-1元的硬幣數、湊n-5元的硬幣數、湊n-7元的硬幣數,就知道了湊n元的硬幣數。如果原來的湊n枚硬幣的規模為n,那麼現在要處理的問題規模就是n-1、n-5、n-7,動態規劃的本質正是降低問題規模。那麼你可能就要問:這誰都能看出來,那麼湊n-1元的硬幣數、湊n-5元的硬幣數、湊n-7元的硬幣數又怎麼算?這是一個遞迴(遞推也能實現,遞迴好理解)的過程,比如說我們已經將湊n枚硬幣的問題處理成湊n-1、n-5、n-7的問題,我們可以再把n-1,n-5,n-7當做n繼續降低規模,直到降低到可以一眼看出來的規模大小,就可以得到這個子問題的答案,在遞歸回去。文字說不清楚,來個圖說明吧:

  

  原諒小編粗糙的畫技,其中湊1,2,3,4,5,6,7枚硬幣我們都可以用肉眼看出來,可以作為邊界條件,然後逐個迴帶就可以了,只要對比每個子問題的值,選出最小的(因為題中說要用最少的硬幣),就可以得到原來問題的解。其實動態規劃可以理解為經過優化的暴力演算法,因為很多時候暴力演算法的時間複雜度都會很高,滿足不了我們的需求,但暴力演算法會有很多冗餘的重複計算,所以動態規劃的本質也在於去除冗餘計算,像上面的硬幣問題還可以這用到了降低規模,還可以繼續優化,稍後會講到。

  2)總結一下:動態規劃就是把大問題分成幾個小問題,再繼續分解小問題,降低問題規模,去除一些冗餘的計算。

  3)術語總結:子問題——說白了就是每個問題分解成的小問題,這些小問題對於原來的問題來說就是子問題與問題的關係。

三、三大性質

   1)重疊子問題

  重疊子問題指的就是原來的問題每次分解的子問題都是相同或相似的,那樣原問題就可以儘可能少的呼叫子問題的結果,那樣就可以降低時間複雜度。如果子問題間並不相同或相似,那麼動態規劃就失去了它的意義,時間複雜度可想而知,還不如用普通的打暴力實在。

  2)最優子結構

  最優子結構的意思就是說子問題中的最優解就是原問題的最優解,比如說我現在要湊11元,那麼湊6元的硬幣數+1枚5元硬幣就是湊11元的最優的子問題,其中最優子問題:湊6元的方案(5元*1+1元*1)+1枚5元硬幣就是原問題:湊11元的方案(5元*2+1元*1)的解.

  3)無後效性

  無後效性就是說已經決定了的子問題的解將不會再受其他因素改變,比如說我已經知道了湊6元需要2枚硬幣,那麼將不會再改動,不會再因為湊7元、湊8元、湊9元等發生改變,依舊是兩枚硬幣。

  同時也不會再關心這6元硬幣是怎樣湊來的,不管我們用了怎樣惡毒的手段,怎樣卑劣的方法,總之我們知道6元硬幣是由2枚硬幣湊成的,具體是那兩種面值的硬幣,我們不會過問。


  瞭解了這些以後,我們在遇到題時,判斷一道題能不能動態規劃,可以判斷這道題是否滿足這三大性質,如果滿足,就可以放心的動態規劃了。

 

▎狀態與轉移

  1)動態規劃的實現形式:動態規劃只是一種優化思想,並不是一類程式碼,所以動態規劃的一般實現形式有:遞推和遞迴。(本次先用遞迴實現)

  2)狀態&設計狀態:狀態是一個難點,很多小白(包括小編)都經常犯迷糊,正確的設計狀態才是解決一道題的關鍵。狀態可以理解為定義動態規劃用到的陣列,那麼數組裡存的是什麼呢?這就需要設計,這一過程就可以理解為設計狀態。這個陣列可以有一維也可以設計更多維度,這些維度所在的下表表示了一些資訊。比如剛才的硬幣問題,我們就可以定義一個f [ ]陣列,那麼f [ i ]代表什麼意思呢?表示要湊i枚硬幣需要用f [ i ]枚硬幣,沒錯,狀態就已經設計好了,夠簡單吧,但是很多題的狀態可不是這麼容易的。

  3)轉移&狀態轉移方程:設計好狀態後,就要思考每個狀態間的關係,這時有兩個方向:1.我從哪裡來(遞迴,這個常用,似乎遞推也可以),2.我到哪裡去(遞推)。比如說剛才的硬幣問題,f [11]顯然可以用f [4]、f [6]、f [10]中較小的一個重新整理它的值(我從哪裡來),當然,f [11]也可以用來重新整理f [12]、f [16]、f [18]的值(我到哪裡去),這個重新整理的過程就叫做轉移,當然,我們絕對是用我從哪裡來居多,後文也會一直用這種思路。有剛才的硬幣問題可以得出一個式子:f [n]=min
{f [n-1]+1,f [n-5]+1,f [n-7]+1},像這樣的式子叫做狀態轉移方程,可以理解為狀態與狀態間的橋樑。正確的設計狀態才會使狀態轉移方程更容易的得到。

 

▎記憶化搜尋VS陣列遞推

  1)記憶化搜尋

  剛才硬幣問題的策略並不是最優的,甚至都算不上是動態規劃,只能算是個普通的遞迴。那麼就要優化,採用記憶化的手段,那麼記憶化是什麼呢?記憶化就是說把已經算好的值儲存下來。

  為什麼要記憶化呢?舉個栗子:還是硬幣問題,比如說我要呼叫f [18]時就已經計算過一次f [13]了,而呼叫f [20]時也要計算f [13],那麼f [13]就被重複計算了,如果重複計算的子問題規模很小,那麼很快就得到結果了,但是規模一旦大起來了,速度可想而知,如果我們把這些重複計算好的結果都儲存起來,那麼是不是再呼叫的時候就可以直接出結果了?

  2)陣列遞推

  這種實現方法不會出現重複計算的情況,也避免了函式的呼叫,但是不好想,之後會具體講的。

  3)對比

  記憶化搜尋好想好實現,但是時間上除了必要的運算還會有函式呼叫的時間,所以時間上較慢,卻容易實現。

 

▎硬幣問題:記憶化搜尋實現

 1 #include<iostream>
 2 using namespace std;
 3 int n,f[1000],a[1000];//設計狀態 
 4 int coin(int i)
 5 {
 6     if(a[i]!=0) return a[i];//如果算過直接返回 
 7     if(i==1) return 1;
 8     else if(i==2) return 2;
 9     else if(i==3) return 3;
10     else if(i==4) return 4;
11     else if(i==5) return 1;
12     else if(i==6) return 2;
13     else if(i==7) return 3;
14     f[i]=min(coin(i-1)+1,min(coin(i-5)+1,coin(i-7)+1));//狀態轉移方程 
15     return a[i]=f[i];//存起來記憶算好的值 
16 }
17 int main()
18 {
19     cin>>n;
20     cout<<coin(n);
21     return 0;
22 }

 

▎難題詳解

  看完上面簡單的硬幣問題,接下來就要碾壓一下你的心態了,廢話不多說,看一道超麻煩的題。


 

山區建小學

【題目描述】

政府在某山區修建了一條道路,恰好穿越總共m個村莊的每個村莊一次,沒有迴路或交叉,任意兩個村莊只能通過這條路來往。已知任意兩個相鄰的村莊之間的距離為di(為正整數),其中,0<i<m。為了提高山區的文化素質,政府又決定從m個村中選擇n個村建小學(設0<n≤m<500)。請根據給定的m、n以及所有相鄰村莊的距離,選擇在哪些村莊建小學,才使得所有村到最近小學的距離總和最小,計算最小值。

【輸入】

第1行為m和n,其間用空格間隔

第2行為m−1 個整數,依次表示從一端到另一端的相鄰村莊的距離,整數之間以空格間隔。

例如:

 

10 3
2 4 6 5 2 4 3 1 3

 

表示在10個村莊建3所學校。第1個村莊與第2個村莊距離為2,第2個村莊與第3個村莊距離為4,第3個村莊與第4個村莊距離為6,...,第9個村莊到第10個村莊的距離為3。

 

【輸出】

各村莊到最近學校的距離之和的最小值。

【輸入樣例】

10 2
3 1 3 1 1 1 1 1 3

【輸出樣例】

18

【來源】


No

這道題太麻煩,感覺寫到一篇部落格裡慌得不行。

發一下之前小編寫過的題解:山區建小