夜深人靜寫算法(二)- 動態規劃

目錄  

一、動態規劃初探
      1、遞推
      2、記憶化搜索
      3、狀態和狀態轉移
      4、最優化原理和最優子結構
      5、決策和無後效性
二、動態規劃的經典模型
       1、線性模型
       2、區間模型
       3、揹包模型
       4、狀態壓縮模型
       5、樹狀模型
三、動態規劃的常用狀態轉移方程
       1、1D/1D
       2、2D/0D
       3、2D/1D
       4、2D/2D
四、動態規劃和數據結構結合的常用優化
       1、滾動數組
       2、最長單調子序列的二分優化
       3、矩陣優化
       4、斜率優化
       5、樹狀數組優化
       6、線段樹優化
       7、其他優化
五、動態規劃題集整理



一、動態規劃初探
      1、遞推
      暫且先不說動態規劃是怎麼樣一個算法,由最簡單的遞推問題說起應該是最恰當不過得了。因爲一來,遞推的思想非常淺顯,從初中開始就已經有涉及,等差數列 f[i] = f[i-1] + d( i > 0, d爲公差,f[0]爲初項)就是最簡單的遞推公式之一;二來,遞推作爲動態規劃的基本方法,對理解動態規劃起着至關重要的作用。理論的開始總是枯燥的,所以讓讀者提前進入思考是最能引起讀者興趣的利器,於是【例題1】應運而生。
     【例題1】在一個3 X N的長方形方格中,鋪滿1X2的骨牌(骨牌個數不限制),給定N,求方案數(圖一 -1-1爲N=2的所有方案),所以N=2時方案數爲3。
圖一 -1-1
       這是一個經典的遞推問題,如果覺得無從下手,我們可以來看一個更加簡單的問題,把問題中的“3”變成“2”(即在一個2 X N的長方形方格中鋪滿1 X 2的骨牌的方案)。這樣問題就簡單很多了,我們用f[i]表示2 X i的方格鋪滿骨牌的方案數,那麼考慮第i列,要麼豎着放置一個骨牌;要麼連同i-1列,橫着放置兩個骨牌,如圖一-1-2所示。由於骨牌的長度爲1 X 2,所以在第i列放置的骨牌無法影響到第i-2列。很顯然,圖一 -1-2中兩塊黑色的部分分別表示f[i-1]和f[i-2],所以可以得到遞推式f[i] = f[i-1] + f[i-2] (i >= 2),並且邊界條件f[0] = f[1] = 1。
圖一 -1-2
      再回頭來看3 X N的情況,首先可以明確當N等於奇數的時候,方案數一定爲0。所以如果用f[i] (i 爲偶數) 表示3Xi的方格鋪滿骨牌的方案數,f[i]的方案數不可能由f[i-1]遞推而來。那麼我們猜想f[i]和f[i-2]一定是有關係的,如圖一 -1-3所示,我們把第i列和第i-1列用1X2的骨牌填滿後,輕易轉化成了f[i-2]的問題,那是不是代表f[i] = 3*f[i-2]呢?
圖一 -1-3
      仔細想想才發現不對,原因是我們少考慮了圖一 -1-4的情況,這些情況用圖一 -1-3的情況無法表示,再填充完黑色區域後,發現和f[i-4]也有關係,但是還是漏掉了一些情況。
圖一 -1-4
      上面的問題說明我們在設計狀態(狀態在動態規劃中是個很重要的概念,在本章的第4小節會進行介紹總結)的時候的思維定式,當一維的狀態已經無法滿足我們的需求時,我們可以試着增加一維,用二維來表示狀態,用f[i][j]表示(3 X i) + j個多餘塊的擺放方案數,如圖一 -1-5所示:
圖一 -1-5
      轉化成二維後,我們可以輕易寫出三種情況的遞推式,具體推導方法見圖一 -1-6。
      f[i][0] = f[i-2][0] + f[i-1][1] + f[i-2][2]  
      f[i][1] = f[i-1][2]
      f[i][2] = f[i][0] + f[i-1][1]
      邊界條件     f[0][0] = f[1][1] = f[0][2] = 1
圖一 -1-6
       如果N不是很大的情況,到這一步,我們的問題已經完美解決了,其實並不需要求它的通項公式,因爲我們是程序猿,一個for循環就能搞定了 <*_*>,接下來的求解就全仰仗於計算機來完成了。
      【例題2】對一個“01”串進行一次μ變換被定義爲:將其中的"0"變成"10","1"變成"01",初始串爲"1",求經過N(N <= 1000)次μ變換後的串中有多少對"00"(有沒有人會糾結會不會出現"000"的情況?這個請放心,由於問題的特殊性,不會出現"000"的情況)。圖一 -1-7表示經過小於4次變換時串的情況。
圖一 -1-7
       如果純模擬的話,每次μ變換串的長度都會加倍,所以時間和空間複雜度都是O(2^n),對於n爲1000的情況,完全不可能計算出來。仔細觀察這個樹形結構,可以發現要出現"00",一定是"10"和"01"相鄰產生的。爲了將問題簡化,我們不妨設A = "10", B = "01",構造出的樹形遞推圖如圖一 -1-8所示,如果要出現"00",一定是AB("1001")。
       令FA[i]爲A經過i次μ變換後"00"的數量,FA[0] = 0;FB[i]爲B經過i次μ變換後"00"的數量,FB[0] = 0。
       從圖中觀察得出,以A爲根的樹,它的左子樹的最右端點一定是B,也就是說無論經過多少次變換,兩棵子樹的交界處都不可能產生AB,所以FA[i] = FB[i-1] + FA[i-1](直接累加兩棵子樹的"00"的數量);而以B爲根的樹,它的左子樹的右端點一定是A,而右子樹的左端點呈BABABA...交替排布,所以隔代產生一次AB,於是FB[i] = FA[i-1] + FB[i-1] + (i mod 2) 。最後要求的答案就是FB[N-1],遞推求解。
圖一 -1-8
     2、記憶化搜索
      遞推說白了就是在知道前i-1項的值的前提下,計算第i項的值,而記憶化搜索則是另外一種思路。它是直接計算第i項,需要用到第 j 項的值( j < i)時去查表,如果表裏已經有第 j 項的話,則直接取出來用,否則遞歸計算第 j 項,並且在計算完畢後把值記錄在表中。記憶化搜索在求解多維的情況下比遞推更加方便,【例題3】是我遇到的第一個記憶化搜索的問題,記憶猶新。
      【例題3】這個問題直接給出了一段求函數w(a, b, c)的僞代碼:
      function w(a, b, c):
       if a <=or b <=or c <=0, then returns:1
       if a >20or b >20or c >20, then returns: w(20,20,20)
       if a < b and b < c, then returns: w(a, b, c-1)+ w(a, b-1, c-1)- w(a, b-1, c)    
       otherwise it returns: w(a-1, b, c)+ w(a-1, b-1, c)+ w(a-1, b, c-1)
      要求給定a, b, c,求w(a, b, c)的值。
            
      乍看下只要將僞代碼翻譯成實際代碼,然後直接對於給定的a, b, c,調用函數w(a, b, c)就能得到值了。但是隻要稍加分析就能看出這個函數的時間複雜度是指數級的(儘管這個三元組的最大元素只有20,這是個陷阱)。對於任意一個三元組(a, b, c),w(a, b, c)可能被計算多次,而對於固定的(a, b, c),w(a, b, c)其實是個固定的值,沒必要多次計算,所以只要將計算過的值保存在f[a][b][c]中,整個計算就只有一次了,總的時間複雜度就是O(n^3),這個問題的n只有20。
     3、狀態和狀態轉移           
      在介紹遞推和記憶化搜索的時候,都會涉及到一個詞---狀態,它表示瞭解決某一問題的中間結果,這是一個比較抽象的概念,例如【例題1】中的f[i][j],【例題2】中的FA[i]、FB[i],【例題3】中的f[a][b][c],無論是遞推還是記憶化搜索,首先要設計出合適的狀態,然後通過狀態的特徵建立狀態轉移方程(f[i] = f[i-1] + f[i-2] 就是一個簡單的狀態轉移方程)。

     4、最優化原理和最優子結構
      在介如果問題的最優解包含的子問題的解也是最優的,就稱該問題具有最有子結構,即滿足最優化原理。這裏我盡力減少理論化的概念,而改用一個簡單的例題來加深對這句話的理解。
     【例題4】給定一個長度爲n(1 <= n <= 1000)的整數序列a[i],求它的一個子序列(子序列即在原序列任意位置刪除0或多個元素後的序列),滿足如下條件:
      1、該序列單調遞增;
      2、在所有滿足條件1的序列中長度是最長的;
      這個問題是經典的動態規劃問題,被稱爲最長單調子序列。

      我們假設現在沒有任何動態規劃的基礎,那麼看到這個問題首先想到的是什麼?
      我想到的是萬金油算法---枚舉(DFS),即枚舉a[i]這個元素取或不取,所有取的元素組成一個合法的子序列,枚舉的時候需要滿足單調遞增這個限制,那麼對於一個n個元素的序列,最壞時間複雜度自然就是O(2n),n等於30就已經很變態了更別說是1000。但是方向是對的,動態規劃求解之前先試想一下搜索的正確性,這裏搜索的正確性是很顯然的,因爲已經枚舉了所有情況,總有一種情況是我們要求的解。我們嘗試將搜索的算法進行一些改進,假設第i個數取的情況下已經搜索出的最大長度記錄在數組d中,即用d[i]表示當前搜索到的以a[i]結尾的最長單調子序列的長度,那麼如果下次搜索得到的序列長度小於等於d[i],就不必往下搜索了(因爲即便繼續往後枚舉,能夠得到的解必定不會比之前更長);反之,則需要更新d[i]的值。如圖一-4-1,紅色路徑表示第一次搜索得到的一個最長子序列1、2、3、5,藍色路徑表示第二次搜索,當枚舉第3個元素取的情況時,發現以第3個數結尾的最長長度d[3] = 3,比本次枚舉的長度要大(本次枚舉的長度爲2),所以放棄往下枚舉,大大減少了搜索的狀態空間。
圖一-4-1
      這時候,我們其實已經不經意間設計好了狀態,就是上文中提到的那個d[i]數組,它表示的是以a[i]結尾的最長單調子序列的長度,那麼對於任意的i,d[i] 一定等於 d[j] + 1 ( j < i ),而且還得滿足 a[j] < a[i]。因爲這裏的d[i]表示的是最長長度,所以d[i]的表達式可以更加明確,即:
      d[i] = max{ d[j] | j < i && a[j] < a[i] } + 1
      這個表達式很好的闡釋了最優化原理,其中d[j]作爲d[i]的子問題,d[i]最長(優)當且僅當d[j]最長(優)。當然,這個方程就是這個問題的狀態轉移方程。狀態總數量O(n), 每次轉移需要用到前i項的結果,平攤下來也是O(n)的,所以該問題的時間複雜度是O(n^2),然而它並不是求解這類問題的最優解,下文會提到最長單調子序列的O(nlogn)的優化算法。

      5、決策和無後效性
      一個狀態演變到另一個狀態,往往是通過“決策”來進行的。有了“決策”,就會有狀態轉移。而無後效性,就是一旦某個狀態確定後,它之前的狀態無法對它之後的狀態產生“效應”(影響)。
      【例題5】老王想在未來的n年內每年都持有電腦,m(y, z)表示第y年到第z年的電腦維護費用,其中y的範圍爲[1, n],z的範圍爲[y, n],c表示買一臺新的電腦的固定費用。 給定矩陣m,固定費用c,求在未來n年都有電腦的最少花費。
考慮第 i 年是否要換電腦,換和不換是不一樣的決策,那麼我們定義一個二元組(a, b),其中 a < b,它表示了第a年和第b年都要換電腦(第a年和第b年之間不再換電腦),如果假設我們到第a年爲止換電腦的最優方案已經確定,那麼第a年以前如何換電腦的一些列步驟變得不再重要,因爲它並不會影響第b年的情況,這就是無後效性。     
更加具體得,令d[i]表示在第i年買了一臺電腦的最小花費(由於這臺電腦能用多久不確定,所以第i年的維護費用暫時不計在這裏面),如果上一次更換電腦的時間在第j年,那麼第j年更換電腦到第i年之前的總開銷就是c + m(j, i-1),於是有狀態轉移方程:
d[i] = min{ d[j] + m(j, i-1) |  1 <=  j < i  } + c
        這裏的d[i]並不是最後問題的解,因爲它漏算了第i年到第n年的維護費用,所以最後問題的答案:
        ans  = min{ d[i] + m(i, n)  | 1 <= i < n }
        我們發現兩個方程看起來很類似,其實是可以合併的,我們可以假設第n+1年必須換電腦,並且第n+1年換電腦的費用爲0,那麼整個階段的狀態轉移方程就是:
        d[i] = min{ d[j] + m(j, i-1) | 1 <= j < i } + w(i)    其中w(i) = (i==n+1)?0:c;
        d[n+1]就是我們需要求的最小費用了。

二、動態規劃的經典模型
      1、線性模型
       線性模型的是動態規劃中最常用的模型,上文講到的最長單調子序列就是經典的線性模型,這裏的線性指的是狀態的排布是呈線性的。【例題6】是一個經典的面試題,我們將它作爲線性模型的敲門磚。
      
【例題6】在一個夜黑風高的晚上,有n(n <= 50)個小朋友在橋的這邊,現在他們需要過橋,但是由於橋很窄,每次只允許不大於兩人通過,他們只有一個手電筒,所以每次過橋的兩個人需要把手電筒帶回來,i號小朋友過橋的時間爲T[i],兩個人過橋的總時間爲二者中時間長者。問所有小朋友過橋的總時間最短是多少。
圖二-1-1
      
每次過橋的時候最多兩個人,如果橋這邊還有人,那麼還得回來一個人(送手電筒),也就是說N個人過橋的次數爲2*N-3(倒推,當橋這邊只剩兩個人時只需要一次,三個人的情況爲來回一次後加上兩個人的情況...)。有一個人需要來回跑,將手電筒送回來(也許不是同一個人,realy?!)這個回來的時間是沒辦法省去的,並且回來的次數也是確定的,爲N-2,如果是我,我會選擇讓跑的最快的人來幹這件事情,但是我錯了...如果總是跑得最快的人跑回來的話,那麼他在每次別人過橋的時候一定得跟過去,於是就變成就是很簡單的問題了,花費的總時間: 
T = minPTime * (N-2) + (totalSum-minPTime)
來看一組數據 四個人過橋花費的時間分別爲 1 2 5 10,按照上面的公式答案是19,但是實際答案應該是17。
      
具體步驟是這樣的:
第一步:1和2過去,花費時間2,然後1回來(花費時間1);
第二歩:3和4過去,花費時間10,然後2回來(花費時間2);
第三部:1和2過去,花費時間2,總耗時17。
所以之前的貪心想法是不對的。
我們先將所有人按花費時間遞增進行排序,假設前i個人過河花費的最少時間爲opt[i],那麼考慮前i-1個人過河的情況,即河這邊還有1個人,河那邊有i-1個人,並且這時候手電筒肯定在對岸,所以
      opt[i] = opt[i-1] + a[1] + a[i]        (讓花費時間最少的人把手電筒送過來,然後和第i個人一起過河)
      如果河這邊還有兩個人,一個是第i號,另外一個無所謂,河那邊有i-2個人,並且手電筒肯定在對岸,所以
      opt[i] = opt[i-2] + a[1] + a[i] + 2*a[2]    (讓花費時間最少的人把電筒送過來,然後第i個人和另外一個人一起過河,由於花費時間最少的人在這邊,所以下一次送手電筒過來的一定是花費次少的,送過來後花費最少的和花費次少的一起過河,解決問題)
      所以 opt[i] = min{opt[i-1] + a[1] + a[i] , opt[i-2] + a[1] + a[i] + 2*a[2] }

       2、區間模型
區間模型的狀態表示一般爲d[i][j],表示區間[i, j]上的最優解,然後通過狀態轉移計算出[i+1, j]或者[i, j+1]上的最優解,逐步擴大區間的範圍,最終求得[1, len]的最優解。
     
【例題7】給定一個長度爲n(n <= 1000)的字符串A,求插入最少多少個字符使得它變成一個迴文串。
典型的區間模型,迴文串擁有很明顯的子結構特徵,即當字符串X是一個迴文串時,在X兩邊各添加一個字符'a'後,aXa仍然是一個迴文串,我們用d[i][j]來表示A[i...j]這個子串變成迴文串所需要添加的最少的字符數,那麼對於A[i] == A[j]的情況,很明顯有 d[i][j] = d[i+1][j-1] (這裏需要明確一點,當i+1 > j-1時也是有意義的,它代表的是空串,空串也是一個迴文串,所以這種情況下d[i+1][j-1] = 0);當A[i] != A[j]時,我們將它變成更小的子問題求解,我們有兩種決策:
      1、在A[j]後面添加一個字符A[i];
      2、在A[i]前面添加一個字符A[j];
      根據兩種決策列出狀態轉移方程爲:
            d[i][j] = min{ d[i+1][j], d[i][j-1] } + 1;                (每次狀態轉移,區間長度增加1)
      空間複雜度O(n^2),時間複雜度O(n^2), 下文會提到將空間複雜度降爲O(n)的優化算法。
3、揹包模型
揹包問題是動態規劃中一個最典型的問題之一。由於網上有非常詳盡的揹包講解這裏只將常用部分抽出來,具體推導過程詳見《揹包九講》

a.0/1揹包
            
有N種物品(每種物品1件)和一個容量爲V的揹包。放入第 i 種物品耗費的空間是Ci,得到
的價值是Wi。求解將哪些物品裝入揹包可使價值總和最大。
               
f[i][v]表示前i種物品恰好放入一個容量爲v的揹包可以獲得的最大價值。
            
決策爲第i個物品在前i-1個物品放置完畢後,是選擇放還是不放,狀態轉移方程爲: 
               
f[i][v] = max{ f[i-1][v], f[i-1][v - Ci] +Wi }
               
時間複雜度O(VN),空間複雜度O(VN) (空間複雜度可利用滾動數組進行優化達到O(V),下文會介紹滾動數組優化)。

      b.完全揹包
               
有N種物品(每種物品無限件)和一個容量爲V的揹包。放入第 i 種物品耗費的空間是Ci,得到
的價值是Wi。求解將哪些物品裝入揹包可使價值總和最大。
               
f[i][v]表示前i種物品恰好放入一個容量爲v的揹包可以獲得的最大價值。
               f[i][v] = max{ f[i-1][v - kCi] + kWi  | 0 <= k <= v/Ci
 }        (當k的取值爲0,1時,這就是01揹包的狀態轉移方程)
               時間複雜度O( VNsum{V/Ci} ),空間複雜度在用滾動數組優化後可以達到
O( V )。
               進行優化後(此處省略500字),狀態轉移方程變成:
               f[i][v] = max{ f[i-1][v],  f[i][v - Ci] +Wi }   
               時間複雜度降爲
O(VN)。
      c.多重揹包
               有N種物品(每種物品Mi件)和一個容量爲V的揹包。放入第i種物品耗費的空間是Ci,得到
的價值是Wi。求解將哪些物品裝入揹包可使價值總和最大。
               f[i][v]表示前i種物品恰好放入一個容量爲v的揹包可以獲得的最大價值。
               f[i][v] = max{ f[i-1][v - kCi] + kWi  | 0 <= k <= Mi }
               時間複雜度O( Vsum(Mi) ),
空間複雜度仍然可以用滾動數組優化後可以達到
O( V )。
               優化:採用二進制拆分物品,將Mi個物品拆分成容量爲1、2、4、8、... 2^k、Mi-( 2^(k+1) - 1 ) 個對應價值爲Wi、2Wi、4Wi、8Wi、...、2^kWi、(
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章