動態規劃

目錄  

1、動態規劃初探
      一、遞推
      二、記憶化搜索
      三、狀態和狀態轉移
      四、最優化原理和最優子結構
      五、決策和無後效性

2、動態規劃的經典模型
       一、線性模型
       二、區間模型
       三、揹包模型
       四、狀態壓縮模型
       五、樹狀模型

3、動態規劃的經常使用狀態轉移方程
      一、1D/1D
       二、2D/0D
       三、2D/1D
       四、2D/2D

4、動態規劃和數據結構結合的經常使用優化
       一、滾動數組
       二、最長單調子序列的二分優化
       三、矩陣優化
       四、斜率優化
       五、樹狀數組優化
       六、線段樹優化
       七、其餘優化

5、動態規劃題集整理


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」(即在一個2XN的長方形方格中鋪滿1X2的骨牌的方案)。這樣問題就簡單不少了,咱們用f[i]表示2Xi的方格鋪滿骨牌的方案數,那麼考慮第i列,要麼豎着放置一個骨牌;要麼連同i-1列,橫着放置兩個骨牌,如圖2所示。因爲骨牌的長度爲1X2,因此在第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
     二、記憶化搜索
      遞推說白了就是在知道前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。
     三、狀態和狀態轉移           
      在介紹遞推和記憶化搜索的時候,都會涉及到一個詞---狀態,它表示瞭解決某一問題的中間結果,這是一個比較抽象的概念,例如【例題1】中的f[i][j],【例題2】中的FA[i]、FB[i],【例題3】中的f[a][b][c],不管是遞推仍是記憶化搜索,首先要設計出合適的狀態,而後經過狀態的特徵創建狀態轉移方程(f[i] = f[i-1] + f[i-2] 就是一個簡單的狀態轉移方程)。

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

      咱們假設如今沒有任何動態規劃的基礎,那麼看到這個問題首先想到的是什麼?
      我想到的是萬金油算法---枚舉(DFS),即枚舉a[i]這個元素取或不取,全部取的元素組成一個合法的子序列,枚舉的時候須要知足單調遞增這個限制,那麼對於一個n個元素的序列,最壞時間複雜度天然就是O(2 n),n等於30就已經很變態了更別說是1000。可是方向是對的,動態規劃求解以前先試想一下搜索的正確性,這裏搜索的正確性是很顯然的,由於已經枚舉了全部狀況,總有一種狀況是咱們要求的解。咱們嘗試將搜索的算法進行一些改進,假設第i個數取的狀況下已經搜索出的最大長度記錄在數組d中,即用d[i]表示當前搜索到的以a[i]結尾的最長單調子序列的長度,那麼若是下次搜索獲得的序列長度小於等於d[i],就沒必要往下搜索了(由於即使繼續日後枚舉,可以獲得的解一定不會比以前更長);反之,則須要更新d[i]的值。如圖一-4-1,紅色路徑表示第一次搜索獲得的一個最長子序列一、二、三、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】老王想在將來的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]就是咱們須要求的最小費用了。

2、動態規劃的經典模型
      一、線性模型
       線性模型的是動態規劃中最經常使用的模型,上文講到的最長單調子序列就是經典的線性模型,這裏的線性指的是狀態的排布是呈線性的。【例題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] }

       二、區間模型
      區間模型的狀態表示通常爲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]時,咱們將它變成更小的子問題求解,咱們有兩種決策:
      一、在A[j]後面添加一個字符A[i];
      二、在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)的優化算法。

      
三、揹包模型
      
揹包問題是動態規劃中一個最典型的問題之一。因爲網上有很是詳盡的揹包講解
這裏只將經常使用部分抽出來,具體推導過程詳見

       
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個物品拆分紅容量爲一、二、四、八、... 2^k、Mi-( 2^(k+1) - 1 ) 個對應價值爲Wi、2Wi、4Wi、8Wi、...、2^kWi、(
Mi-( 2^(k+1) - 1 )
)Wi的物品,而後採用01揹包求解。
               這樣作的時間複雜度降爲O(Vsum(logMi) )。
            
      
【例題8】一羣強盜想要搶劫銀行,總共N(N <= 100)個銀行,第i個銀行的資金爲Bi億,搶劫該銀行被抓機率Pi,問在被抓機率小於p的狀況下可以搶劫的最大資金是多少?
      p表示的是強盜在搶銀行時至少有一次被抓機率的上限,那麼選擇一些銀行,而且計算搶劫這些銀行都不被抓的的機率pc,則須要知足1 - pc < p。這裏的pc是全部選出來的銀行的搶劫時不被抓機率(即1 - Pi)的乘積,因而咱們用資金做爲揹包物品的容量,機率做爲揹包物品的價值,求01揹包。狀態轉移方程爲:
      f[j] = max{ f[j], f[j - pack[i].B] * (1-pack[i].p) }
      最後獲得的f[i]表示的是搶劫到 i 億資金的最大不被抓機率。令全部銀行資金總和爲V,那麼從V-0進行枚舉,第一個知足1 - f[i] < p的i就是咱們所要求的被抓機率小於p的最大資金。

      
四、狀態壓縮模型
      
狀態壓縮的動態規劃,通常處理的是數據規模較小的問題,將狀態壓縮成k進制的整數,k取2時最爲常見。
      
【例題9】
對於一條n
(n <= 11)個點的哈密爾頓路徑C1C2...CN(通過每一個點一次的路徑)的值由三部分組成:
      一、每一個頂點的權值Vi的和
      二、對於路徑上相鄰的任意兩個頂點CiCi+1,累加權值乘積 Vi*Vi+1
      三、對於相鄰的三個頂點CiCi+1Ci+2,若是Ci和Ci+2之間有邊,那麼累加權值三乘積 Vi*Vi+1*Vi+2
      求值最大的哈密爾頓路徑的權值 和 這樣的路徑的個數。

      枚舉全部路徑,判斷找出值最大的,複雜度爲O(n!),取締!
      因爲點數較少,採用二進制表示狀態,用d[i][j][k]表示某條哈密爾頓路徑的最大權值,其中i是一個二進制整數,它的第t位爲1表示t這個頂點在這條哈密爾頓路徑上,爲0表示不在路徑上。j和k分別爲路徑的最後兩個頂點。那麼圖二-4-1表示的狀態就是:
      d[ (11101111)2][7][1]
圖二-4-1
 
      明確了狀態表示,那麼咱們假設02356這5個點中和7直接相連的是i,因而就轉化成了子問題...->j -> i -> 7,咱們能夠枚舉i = 0, 2, 3, 5, 6。
 
      直接給出狀態轉移方程:
      d[i][j][k] = max{ d[i ^ (1<<k)][t][j] + w(t, j, k)   |   (i & (1<<t)) != 0 } 
      這裏用到了幾個位運算:i ^ (1<<k)表示將i的二進制的第k位從1變成0,i & (1<<t)則爲判斷i的二進制表示的第t位是否爲1,即該路徑中是否存在t這個點。這個狀態轉移的實質就是將本來的 ...->j -> k 轉化成更加小規模的去掉k點後的子問題 ... -> t -> j 求解。而w(t, j, k)則表示 t->j->k這條子路徑上產生的權值和,這個能夠由定義在O(1)的時間計算出來。
      d[ (1<<j) | (1<<k) ][j][k] 爲全部的兩個點的路徑的最大值,即最小的子問題。這個問題的狀態並不是線性的,因此用記憶化搜索來求解狀態的值會事半功倍。
     
【例題10】
方塊A 方塊B

      
利用以上兩種積木(任意數量,可進行旋轉和翻轉),拼出一個m*n( 1<= m <= 9, 1 <= n <= 9 )的矩形,問這樣的方式有多少種。如m = 2, n = 3的狀況 ,有如下5種拼接方式:

圖二-4-2
       
經典問題,2進制狀態壓縮。有固定套路,就不糾結是怎麼想出來的了, 反正第一次看到這種問題我是想不出來,你呢?可是照例仍是得引導一下。
      若是問題不是求放滿的方案數,而是求前M-1行放滿,而且第M行的奇數格放上骨牌而偶數格不放 或者 第M行有一個格子留空 或者 第M行的首尾兩個格子留空,求方案數(這是三個問題,分別對應圖二-4-3的三個圖)。這樣的問題能夠出一籮筐了,由於第M行的狀況總共有 2^n,按照這個思路下去,咱們發現第i (1 <= i <= m)行的狀態頂多也就 2^n
種,這裏的狀態能夠用一個二進制整數來表示, 對於第i行,若是這一行的第j個格子被骨牌填充則這個二進制整數的第j位爲1,不然爲0
       圖二-4-3中的三個圖的第M行狀態能夠分別表示爲(101010)  2、(110111)  2、(011110)  2,那麼若是咱們已知第i行的狀態k對應的方案數,而且狀態k放置幾個骨牌後可以將i+1行的狀態變成k',那麼第i+1行的k'這個狀態的方案數必然包含了第i行的狀態k的方案數,這個放置骨牌的過程就是狀態轉移。
圖二-4-3
       用一個二維數組DP[i][j] 表示第i行的狀態爲j的骨牌放置方案數(其中 1<=i<=m, 0 <= j < 2 n),爲了將問題簡化,咱們虛擬出一個第0行,則有DP[0][j] = (j == 
2 n
-1) ? 1 : 0;這個就是咱們的 初始狀態,它的含義是這樣的,由於第0行是咱們虛擬出來的,因此第0行只有徹底放滿的時候纔有意義,也就是第0行所有放滿(狀態的二進制表示全爲1,即十進制表示的
2n
-1
 )的方案數爲1,其它狀況均爲0。
       那麼如何進行狀態轉移呢?假設第3行的某個狀態 (101000) 2的方案數DP[3][ (101000) 2 ] = 5,如圖二-4-4所示:
圖二-4-4
       咱們須要作的就是經過各類方法將第3行填滿,從而獲得一系列第4行可能的狀態集合S,而且對於每個在S中的狀態s,執行DP[4][s] += DP[3][ (101000) 2 ](兩個狀態可達,因此方案數是可傳遞的,又由於多個不一樣的狀態可能到達同一個狀態,因此採用累加的方式)。
       根據給定的骨牌,咱們能夠枚舉它的擺放方式,圖二-4-5展現了三種骨牌的擺放方式以及可以轉移到的狀態,可是這裏的狀態轉移還沒結束,由於第3行還沒有放滿,問題求的是將整個棋盤鋪滿的方案數,因此只有當第i行所有放滿後,才能將狀態轉移給i+1行。
圖二-4-5
       枚舉擺放的這一步能夠採用dfs遞歸枚舉列,遞歸出口爲列數col == N時。dfs函數的原型能夠寫成以下的形式:
       void dfs (  int col ,  int nextrow ,  int nowstate ,  int nextstate , LL cnt ) ;
       // col       表示當前枚舉到的列編號
        // nextrow   表示下一行的行編號
        // nowstate  表示當前枚舉骨牌擺放時第i  行的狀態(隨着放置骨後會更新)
        // nextstate 表示當前枚舉骨牌擺放時第i+1行的狀態(隨着放置骨後會更新)
        // cnt       狀態轉移前的方案數,即第i行在放置骨牌前的方案數
       而後再來看如何將骨牌擺上去,這裏對骨牌進行歸類,旋轉以後獲得以下六種狀況:
圖二-4-6
圖二-4-7
       爲了方便敘述,分別給每一個類型的骨牌強加了一個奇怪的名字,都是按照它自身的形狀來命名的,o(╯□╰)o。而後咱們發現它們都被圈定在一個2X2的格子裏,因此每一個骨牌均可以用2個2位的2進制整數來表示,編碼方式相似上面的狀態表示法(參照圖6,若是骨牌對應格子爲藍色則累加格子上的權值),定義以下:

int blockMask [ 6 ] [ 2 ]  =  {
      { 1 ,  1 } ,     // 豎向2X1
      { 3 ,  0 } ,     // 橫向1X2
      { 3 ,  1 } ,     // 槍
      { 3 ,  2 } ,     // 7
      { 1 ,  3 } ,     // L
      { 2 ,  3 } ,     // J
} ;
       blockMask [k ] [0]表示骨牌第一行的狀態,blockMask [k ] [1 ]表示骨牌第二行的狀態。這樣一來就能夠經過簡單的位運算來判斷第k塊骨牌是否能夠放在(i, col)這個格子上,這裏的i表示行編號,col則表示列編號。接下來須要用到位運算進行狀態轉移,因此先介紹幾種經常使用的位運算:
       a. x & (1<<i)   值若是非0,表示x這個數字的二進制表示的第i(i >= 0)位爲1,不然爲0;
       b. x & (y<<i)   值若是非0,表示存在一個p(i <= p < i+k),使得x這個數字的二進制表示的第p位和y的p-i位均爲1(k爲y的二進制表示的位數);
       c. x | (1<<i)   將x的第i位變成1(固然,有可能本來就爲1,這個不影響);
       d. x | (y<<i)   將x的第i~i+k-1位和y進行位或運算(k爲y的二進制表示的位數),這一步就是模擬了 骨牌擺放
       那麼這個格子能夠放置第k個骨牌的條件有以下幾個:
       一、當前骨牌橫向長度記爲w,那麼w必須知足 col + w <= N,不然就超出右邊界了。
       二、 nowstate  (blockMask [k ] [ 0 ]<<col)  == 0,即第i行,骨牌放入前 對應的格子爲空( 對應的格子表示骨牌佔據的格子,下同)
       三、nextstate  (blockMask [k ] [ 1 ]<<col)  == 0,即第i+1行,骨牌放入前對應的格子爲空
       四、最容易忽略的一點是,「J」骨牌放置時,它的缺口部分以前必需要被骨牌鋪滿,不然就沒法知足第i行全鋪滿這個條件了,如圖二-4-8所示的狀況。
圖二-4-8
       當四個條件都知足就能夠遞歸進入下一層了,遞歸的時候也是採用位運算,實現以下:
       dfs ( col + 1 , nextrow , nowstate | (blockMask [k ] [ 0 ] < <col ) , nextstate | (blockMask [k ] [ 1 ] < <col ) , cnt  ) ;
       這裏的位或運算(|)就是模擬將一個骨牌擺放到指定位置的操做(參見位運算d)。
       固然,在枚舉到第col列的時候,有可能(i, col)這個格子已經被上一行的骨牌給「佔據」了(是否被佔據能夠經過 (1<<col) & nowstate 獲得),這時候咱們只須要繼續遞歸下一層,只遞增col,其它量都不變便可,這表示了這個格子什麼都不放的狀況。

      五、樹狀模型
      樹形動態規劃(樹形DP),是指狀態圖是一棵樹,狀態轉移也發生在樹上,父結點的值經過全部子結點計算完畢後得出。

    【例題11】給定一顆樹,和樹上每一個結點的權值,求一顆非空子樹,使得權和最大。php

   

      用d[1][i] 表示i這個結點選中的狀況下,以i爲根的子樹的權和最大值;html

      用d[0][i]表示i這個結點不選中的狀況下,以i爲根的子樹的權和最大值;面試

      d[1][i] = v[i] + sum{ d[1][v] | v是i的直接子結點 && d[1][v] > 0 }算法

      d[0][i] = max( 0, max{ max( d[0][v], d[1][v] ) | v是i的直接子結點 } )數組

      這樣,構造一個以1爲根結點的樹,而後就能夠經過dfs求解了。數據結構

      這題題目要求求出的樹爲非空樹,因此當全部權值都爲負數的狀況下須要特殊處理,選擇全部權值中最大的那個做爲答案。

app

3、動態規劃的經常使用狀態轉移方程
      
動態規劃算法三要素(摘自黑書,總結的很好,頗有歸納性):
              ①全部不一樣的子問題組成的表
              ②解決問題的依賴關係能夠當作是一個圖
              ③填充子問題的順序(即對
②的圖進行
拓撲排序,填充的過程稱爲狀態轉移);
      則若是子問題的數目爲O(n t
),每一個子問題須要用到
O(n e
)個子問題的結果,那麼咱們稱它爲
tD/eD的問題,因而能夠總結出四類經常使用的動態規劃方程:
      (下面會把opt做爲取最優值的函數(通常取min或max), w(j, i)爲一個實函數,其它變量均可以在常數時間計算出來)。)
       一、1D/1D
             d[i] = opt{ d[j] + w(j, i) | 0 <= i < j } (1 <= i <= n)
             【例題4】和【例題5】都是這類方程。
       2
、2D/0D
             d[i][j] = opt{ d[i-1][j] + xi, d[i][j-1] + yj, d[i-1][j-1] + zij }     (1<= i, j <= n)
             【例題7】是這類方程的變形,最典型的見最長公共子序列問題。
       3
、2D/1D

              d[i][j] = w(i, j) + opt{ d[i][k-1] + d[k][j] }, (1 <= i < j <= n)
               區間模型經常使用方程。
               另一種經常使用的2D/1D的方程爲:
              d[i][j] = opt{ d[i-1][k] + w(i, j, k) | k < j }    (1<= i <= n, 1 <= j <= m)
       4
、2D/2D
              
d[i][j] = opt{ d[i'][j'] + w(i', j', i, j) |  0 <= i' < i, 0 <= j' < j}
             常見於二維的迷宮問題,因爲複雜度比較大,因此通常配合數據結構優化,如線段樹、樹狀數組等。
       對於一個
tD/eD 的動態規劃問題,在不通過任何優化的狀況下,能夠粗略獲得一個時間複雜度是
O(n t+e
)
,空間複雜度是O(n t
)
的算法,大多數狀況下空間複雜度是很容易優化的,難點在於時間複雜度,下一章咱們將詳細講解各類狀況下的動態規劃優化算法。

4、動態規劃和數據結構結合的經常使用優化
       一、滾動數組
      【例題12】例題7(迴文串那題)的N變成5000,其他不變。

       回憶一下那個問題的狀態轉移方程以下: 
       d[i][j] =   {
                                 d[i+1][j-1]                           | A[i] == A[j] 
                                 
min{ d[i+1][j], d[i][j-1] } + 1  
|  
A[i] 
!= A[j]
                      }
       咱們發現將d[i][j]理解成一個二維的矩陣,i表示行,j表示列,那麼第i行的結果只取決於第i+1和第i行的狀況,對於第i+2行它表示並不關心,那麼咱們只要用一個d[2][N]的數組就能保存狀態了,其中d[0][N]爲奇數行的狀態值,d[1][N]爲偶數行的狀態值,當前須要計算的狀態行數爲奇數時,會利用到
d[1][N]的部分狀態,奇數行計算完畢,
d[1][N]整行狀態都沒用了,能夠用於下一行狀態的保存,相似「傳送帶」的滾動來循環利用空間資源,美其名曰 - 滾動數組。
       這是個2D/0D問題,理論的空間複雜度是O(n 2),利用滾動數組能夠將空間降掉一維,變成O(n)。
       揹包問題的幾個狀態轉移方程一樣能夠用滾動數組進行空間優化。

        
二、最長單調子序列
       
d[i] = max{ d[j] | j < i && a[j] < a[i] } + 1;
       
那個問題的狀態轉移方程以下:
      
【例題13】例題4(最長遞增子序列那題)的N變成100000,其他不變。

       首先明確決策的概念,咱們認爲 j 和 k (j < i, k < i)都是在計算d[i]時的兩個決策。那麼假設他們知足a[j] < a[k
](它們的狀態對應就是d[j] 和 d[k]),若是a[i] > a[k],則必然有a[i] > a[j],可以選k作決策的也必然可以選 j 作決策,那麼如若此時d[j] >= d[k],顯然k不多是最優決策(j的決策始終比它優,以j作決策,a[ j ]的值小但狀態值卻更大),因此d[k]是不須要保存的。
       
基於以上理論,咱們能夠採用二分枚舉,維護一個值 (這裏的值指的是a[i]) 遞增的決策序列,不斷擴大決策序列,最後決策的數目就是最長遞增子序列的長度。具體作法是:
       
枚舉i,若是a[i]比決策序列中最大的元素的值還大,則將i插入到決策序列的尾部;不然二分枚舉決策序列,找出其中值最小的一個決策k,而且知足a[k] > a[i],而後用決策i替換決策k。
       
這是個1
D/1D問題,理論的時間複雜度是O(n 2),利用單調性優化後能夠將複雜度將至O(nlogn)。

      【例題14】
給定n個元素(n <= 100000)的序列,將序列的全部數分紅x堆,每堆都是單調不增的,求x的最小值。
       結論:能夠轉化成求原序列的最長遞增子序列。
       證實:由於這x堆中每堆元素都是單調不增的,因此原序列的最長遞增子序列的每一個元素在分出來的每堆元素中必定只出現最多一個,那麼最長遞增子序列的長度L的最大值爲x,因此x >= L。
而咱們要求的是x的最小值,因而這個最小值就是 L 了。

       
三、矩陣優化
      
【例題15】三個小島,編號一、二、3,老王第0天在1號島上。這些島有一些奇怪的規則,每過1天,1號島上的人必須進入二、3號島;2號島上的人必須進入1號島;3號島上的人能夠前往一、2或留在3號島
。問第n(n <=
10 9
)天老王在到達1號島的行走方案,因爲數據比較大,只須要輸出 ans MOD 100000007的值便可。
圖四-3-1
       
臨時想的一個問題,首先看問題有幾個維度,島和天數,並且狀態轉移是常數級的,因此這是個2D/0D問題,咱們用f[i][j]表示第i天在j號島上的方案數,那麼初始狀態f[0][1] = 1, f[0][2] = f[0][3] = 0。
       
狀態轉移能夠參考圖四-3-1,有:
       
f[i][1] = f[i-1][2] + f[i-1][3]
       
f[i][2] = f[i-1][1] + f[i-1][3]
       
f[i][3] = f[i-1][1] + f[i-1][3] 
       
圖四-3-2
       
令這個矩陣爲A,Aij表示從i號島到j號島是否連通,連通標1,不連通標0,它還有另一個含義,就是通過1天,從i島到j島的方案數,利用矩陣的傳遞性,
A 2的第i行的第j列則表示通過2天,從i島到j島的方案數,一樣的,
A n 
則表示了通過n天,從i島到j島的方案數,那麼問題就轉化成了求A n 
MOD 100000007的值了。
       
An
當n爲偶數的時候等於(
An/2
)*(A
n/2
);當n爲奇數的時候,等於(
An/2
)*
相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息