1. 程式人生 > >動態規劃:從入門到放棄

動態規劃:從入門到放棄

Dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems,solving each of those subproblems just once,and storing their solutions.

斐波拉契數列

由一個眾所周知的例子入手,在學習C語言的時候,講到遞迴的時候的經典例子,Fibonacci數列的求解。題目如下:

下面就是當時簡單粗暴的解法,利用遞迴的方式。

下面看看演算法的執行流程,假如輸入6,那麼執行的遞迴樹如下所示。

 

 

上面的每個節點都會被執行一次,導致同樣的節點被重複的執行,比如fib(2)被執行了5次。這樣導致時間上的浪費,如果遞迴呼叫也會導致空間的浪費,導致棧溢位的問題。

下面說一個比較無聊的問題,什麼是動態規劃?動態規劃和分治法看起來是非常像的思想,但是兩者的區別也是非常明顯的。分治法是將問題劃分為互不相交的子問題,遞迴的求解子問題,再將它們的解組合起來,求出原問題的解。而動態規劃是應用於子問題重疊的情況,即不同的子問題具有公共的子子問題。也就是上面Fibonacci的例子,上面的遞迴的求解方式就是分治演算法,由於它的子問題是相互相關的,此時利用分治法就做了很多重複的工作,它會反覆求解那些公共子子問題。而動態規劃演算法對每一個子子問題只求解一次,將其儲存在一個表格中,從而避免重複計算。

下面我們介紹兩種方法來說明動態規劃的演算法:自頂向下的備忘錄法和自底向上的方法。

1、自頂而下的備忘錄法

上述演算法中,利用meno陣列來存放斐波拉契數列中的每一個值,由於是自頂往下遞迴,它還是會最先遞迴到meno[3],從此刻開始在往上計算,然後依次儲存計算結果在meno陣列中,避免了重複運算。

下面是枯燥的概念,可以直接跳過。在動態規劃當中包含三個重要的概念:最優子結構、邊界、狀態轉移公式。對於上面這個演算法來說,meno[10]的最優子結構就是fib(9,meno)和fib(8,meno)了;邊界就是meno[2]與meno[1]了;狀態轉移方程就是meno[n] = fib(n - 1, meno) + fib(n - 2, meno)。注意最優子結構和狀態轉移方程的區別,個人理解是最優子結構是針對具體某個值來說的,而狀態方程就是它的那個整體的推算方程。

2、自底而上的方法

自頂而下的方式來計算最終的結果,還是有一個遞迴的過程。既然最終是從fib(1)開始算,那麼直接從fib(1)計算不就得了,先運算元問題,再算父問題。

我們從接觸斐波拉契數列開始,就是遞迴的方式,弄到最後,發現還是直接來方便很多啊。但是該方法對於空間還是有一定的浪費,下面,我們對其空間再壓縮一點。

從上面的例子可以看到自頂向下的方式的動態規劃其實包含了遞迴,而遞迴就會有額外的開銷的;而使用自底向上的方式可以避免。看來斐波拉契真是個好東西,遞迴的時候用它來入門,現在動態規劃也是用它來入門。

拓展例題:有一座n級臺階的樓梯,從下往上走,每跨一步只能向上1級或者2級臺階。要求用程式來求有多少種走法。(分析一下,其實就是斐波拉契數列!)

最長上升子序列

下面繼續探討動態規劃的問題,繼續一個栗子:求一個數列中最長上升子序列的長度(LIS,Longest Increasing Subsequence)的問題。

例如如下的一個數列:

它的最長上升子數列就是這樣的了:

[1,2,3,4],長度為4,所以這個數列的最長上升子數列長度就是4。

對於這個問題,最簡單的求解方式就是暴力求解了,直接窮舉。

 

 

直接這樣找出所有的上升子序列,然後用肉眼觀察哪個是最長的。顯然,1,2,3,4是最長的,所以最長上升子序列的長度是4。

我們來看看這個方法的時間複雜度:

這就太消耗時間了。我們現在用動態規劃試一下,看看有什麼驚喜。

根據動態規劃的定義,首先我們需要把原來的問題分解成了幾個相似的子問題。但是,不同於斐波拉契數列的例子,這個如何分解原問題並不是那麼一目瞭然。

原來的問題是求LIS(n),現在我們需要找的就是LIS(n)和LIS(k)之間的關係1<=k<=n。如下所示:

 

 

這裡我們可以看到,LIS(K+1)要麼等於LIS(K),要麼加了一。其實也很好理解,基本上就是,在前面所有的LIS種找到一個最長的LIS(i),如果A(K)比這個找到LIS(i)的尾項A(i)要大,則LIS(K)=LIS(i)+1,否則LIS(K)=LIS(i)。

這樣的話,我們就分解了原問題,並且找到了原問題和子問題之間的關係:

i是對應的最大LIS(i)。也就是說,計算LIS(n)需要計算之前所有的LIS(K):

 

 

同理,我們可以儲存子問題的結果,讓每個子問題只被計算一次。需要計算的子問題就只剩下藍色標出的部分了:

 

 

也就是(紅色箭頭表示呼叫了儲存的資料,並未進行計算):

 

 

我們可以看到,採用了動態規劃之後,時間複雜度大大降低了:縱軸方向的遞迴計算返回時間複雜度是O(n),橫軸方向每行求Max的時間複雜度是O(logn),所以總共的時間複雜度就是O(nlogn),遠遠小於暴力窮舉法的O(n!)。

下面是動態規劃的程式碼:

但是上面的這個方法的時間複雜度是O(n^2),並沒有達到O(nlogn)。其中的dp[j](0 <= j <= i)來表示在i之前的LIS的長度,而dp[i]表示以i結尾的子序列中LIS的長度。在判斷中加入 dp[i] < (dp[j] + 1)這個判斷,來減少重複計算。

但是上面的講解不是說有時間複雜度為O(nlogn)的演算法的嗎?有!利用二分法+動態規劃就成了。程式碼如下:

下面以一個數組舉例來說明這種演算法是怎麼實現的?對於arr[9]={2,1,5,3,6,4,8,9,7}陣列,我們來一步一步推理。

第一步:把arr[0]=2放入ans陣列中,注意,這個ans陣列用於存放最大上升子序列的元素,令ans[0]=2,此時len=1;

第二步:把arr[1]=1放入ans陣列中,令ans[0]=1,也就是說長度為1的LIS的最小末尾是1,而ans[0]=2沒有作用了,此時len=1;

第三步:arr[2]=5,arr[2]>ans[0],所以令ans[1]=arr[2]=5,此時len=2,也就是ans[2]={1,5},len=2;

第四步,arr[3]=3,它正好在1,5之間,放在1處肯定是不行的,因為1<3,長度為1的LIS最小末尾應該是1,長度為2的LIS最小末尾是3,於是可以把5淘汰掉,此時ans[2]={1,3},len=2;

第五步:arr[4]=6,它在3的後面,則可以將6放在3後面,此時ans[3]={1,3,6},len=3;

第六步:arr[5]=4,它在3與6,於是將6去掉,此時ans[3]={1,3,4},len=3;

第七步:arr[6]=8,它比4大,直接放在ans陣列末尾,此時ans[4]={1,3,4,8},len=4;

第八步:arr[7]=9,它比8大,直接放在ans陣列末尾,此時ans[4]={1,3,4,8,9},len=5;

第九步:arr[8]=7,它4、8之間,此時ans[4]={1,3,4,7,9},但是len不會更新,仍然是5。

特別提醒:這個1,3,4,7,9不是LIS字串,本題中的LIS字串應該是1,3,4,8,9。7代表的意思是儲存5位長度LIS的最小末尾是7,所以在我們的ans陣列,是儲存對應長度LIS的最小末尾。有了這個末尾,我們就可以一個一個插入資料。例如這道題,如果這個arr陣列7後面還有8、9,那麼就可以繼續的更新資料了,得到LIS的長度為6。

在插入資料的過程中,我們是替換而沒有挪動資料,那麼插入演算法的話,就是二分查詢來插入了,時間複雜度為O(nlogn)。

鋼條切割

 

 

 

 

 

 

 

 

這是《演算法導論》上面動態規劃章節的例題,上面已經描述得非常清楚了,下面我們也是用三種方法來解這個問題。

遞迴版本

這種自頂向下遞迴實現的效率會非常低,因為它會對相同的引數值進行遞迴呼叫,反覆求解子問題。時間複雜度為O(2^n)。

備忘錄版本

自底向上的動態規劃

自底向上的動態規劃問題中最重要的是理解第二個for迴圈,這裡外面的迴圈是求r[1],r[2]……,裡面的迴圈是求出r[1],r[2]……的最優解,也就是說r[i]中儲存的是鋼條長度為i時劃分的最優解,這裡面涉及到了最優子結構問題,也就是一個問題取最優解的時候,它的子問題也一定要取得最優解。下面是長度為4的鋼條劃分的結構圖。

國王與金礦

有一個國家發現了金礦,每座金礦的黃金儲量不同,需要參與的人不同。參與挖礦的人為10人,每座金礦要麼挖,要麼不挖。每座金礦的黃金數與需要的人如下圖所示,怎麼樣分配才能挖到最多的黃金呢?

 

 

對於每個金礦都有挖和不挖兩種選擇,所以問題的最優子結構有兩個,例如現在有4個金礦挖,則剩餘的人要麼是10個,要麼就是10個人減第5個需要的人。

我們令金礦數為n,工人數為w,金礦的黃金量為g[],金礦的用工量為p[]。有如下關係式。

 

 

該方法的時間複雜度為O(n*w),空間複雜度為O(w)。對於動態規劃方法解法來說,當輸入的礦山數多的時候,它的效率會非常高,但是當工人數多的時候,它的效率會低,而且低於簡單的遞迴。

最後結尾,補充知乎關於動態規劃問題的一個問答總結!

參考文件

https://blog.csdn.net/u013309870/article/details/75193592

https://www.zhihu.com/question/23995189

https://juejin.im/post/5a29d52cf265da43333e4da7