轉:DP問題總結

目錄  

 

一、動態規劃初探

      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”(即在一個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

     2、記憶化搜索

      遞推說白了就是在知道前i-1項的值的前提下,計算第i項的值,而記憶化搜索則是另外一種思路。它是直接計算第i項,需要用到第 j 項的值( j < i)時去查表,如果表裏已經有第 j 項的話,則直接取出來用,否則遞歸計算第 j 項,並且在計算完畢後把值記錄在表中。記憶化搜索在求解多維的情況下比遞推更加方便,【例題3】是我遇到的第一個記憶化搜索的問題,記憶猶新。

      【例題3】這個問題直接給出了一段求函數w(a, b, c)的僞代碼:

      function w(a, b, c):

       if a <=0 or b <=0 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、(

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的最大資金。

 

      

4、狀態壓縮模型

      

狀態壓縮的動態規劃,一般處理的是數據規模較小的問題,將狀態壓縮成k進制的整數,k取2時最爲常見。

      

【例題9】

對於一條n

(n <= 11)個點的哈密爾頓路徑C1C2...CN(經過每個點一次的路徑)的值由三部分組成:

      1、每個頂點的權值Vi的和

      2、對於路徑上相鄰的任意兩個頂點CiCi+1,累加權值乘積 Vi*Vi+1

      3、對於相鄰的三個頂點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 < 2n),爲了將問題簡化,我們虛擬出一個第0行,則有DP[0][j] = (j == 2n

-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個骨牌的條件有如下幾個:

       1、當前骨牌橫向長度記爲w,那麼w必須滿足 col + w <= N,否則就超出右邊界了。

       2、 nowstate & (blockMask[k][0]<<col)  == 0,即第i行,骨牌放入前對應的格子爲空(對應的格子表示骨牌佔據的格子,下同)

       3、nextstate & (blockMask[k][1]<<col)  == 0,即第i+1行,骨牌放入前對應的格子爲空

       4、最容易忽略的一點是,“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,其它量都不變即可,這表示了這個格子什麼都不放的情況。

 

      5、樹狀模型

      樹形動態規劃(樹形DP),是指狀態圖是一棵樹,狀態轉移也發生在樹上,父結點的值通過所有子結點計算完畢後得出。

 

 

 

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

   

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

      用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求解了。

      這題題目要求求出的樹爲非空樹,所以當所有權值都爲負數的情況下需要特殊處理,選擇所有權值中最大的那個作爲答案。
 

三、動態規劃的常用狀態轉移方程      

動態規劃算法三要素(摘自黑書,總結的很好,很有概括性):

              ①所有不同的子問題組成的表

              ②解決問題的依賴關係可以看成是一個圖

              ③填充子問題的順序(即對

②的圖進行

拓撲排序,填充的過程稱爲狀態轉移);

      則如果子問題的數目爲O(nt

),每個子問題需要用到

O(ne

)個子問題的結果,那麼我們稱它爲

tD/eD的問題,於是可以總結出四類常用的動態規劃方程:

      (下面會把opt作爲取最優值的函數(一般取min或max), w(j, i)爲一個實函數,其它變量都可以在常數時間計算出來)。)

       1、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(nt+e

)

,空間複雜度是O(nt

)

的算法,大多數情況下空間複雜度是很容易優化的,難點在於時間複雜度,下一章我們將詳細講解各種情況下的動態規劃優化算法。

 

四、動態規劃和數據結構結合的常用優化

       1、滾動數組

      【例題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(n2),利用滾動數組可以將空間降掉一維,變成O(n)。

       揹包問題的幾個狀態轉移方程同樣可以用滾動數組進行空間優化。

 

        

2、最長單調子序列

       

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(n2),利用單調性優化後可以將複雜度將至O(nlogn)。

 

      【例題14】

給定n個元素(n <= 100000)的序列,將序列的所有數分成x堆,每堆都是單調不增的,求x的最小值。

       結論:可以轉化成求原序列的最長遞增子序列。

       證明:因爲這x堆中每堆元素都是單調不增的,所以原序列的最長遞增子序列的每個元素在分出來的每堆元素中一定只出現最多一個,那麼最長遞增子序列的長度L的最大值爲x,所以x >= L。

而我們要求的是x的最小值,於是這個最小值就是 L 了。


       

3、矩陣優化

      

【例題15】三個小島,編號1、2、3,老王第0天在1號島上。這些島有一些奇怪的規則,每過1天,1號島上的人必須進入2、3號島;2號島上的人必須進入1號島;3號島上的人可以前往1、2或留在3號島

。問第n(n <=

109

)天老王在到達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島的方案數,利用矩陣的傳遞性,

A2的第i行的第j列則表示經過2天,從i島到j島的方案數,同樣的,

An 則表示了經過n天,從i島到j島的方案數,那麼問題就轉化成了求An 

MOD 100000007的值了。

       An

當n爲偶數的時候等於(

An/2

)*(A

n/2

);當n爲奇數的時候,等於(

An/2

)*

(

An/2)*A,這樣求解矩陣

An就可以在O(logn)的時間內完成了,加法和乘法對MOD操作都是可交換的(即 “先加再模” 和 “先模再加再模” 等價),所以可以在矩陣乘法求解的時候,一旦超出模數,就執行取模操作。

       最後求得的矩陣T = An MOD 

100000007,那麼T[1][1]就是我們要求的解了。

       

4、斜率優化

       【例題16】

n(n <= 500000)個單詞,每個單詞輸出的花費爲Ci(1 <= i <= n),將k個連續的單詞輸出在一行的花費爲:

       其中M爲常數,求一個最佳方案,使得輸出所有單詞總的花費最小。

       令d[i]爲前i個單詞組織出的最小花費,s[i] = sum{Ck | 0 <= k <= i },其中s[0] = 0

       狀態轉移方程 d[i] = min{ d[j] + (s[i] - s[j])

+ M | 0 <= j < i }   (1 <= i <= n)       這是個1D/1D問題,空間複雜度O(n), 時間複雜度爲O(n2

),對於500000的數據來說是不可能在給定時間出解的。

       令g[j] = d[j] + (s[i] - s[j])

2+ M,表示由 j 到 i 的決策。

       對於兩個決策點 j < k,如果k的決策值小於j (即g[k] < g[j]),則有:

       d[k] + (s[i] - s[k]) 2 + M < d[j] + (s[i] - s[j])

2

+ M

,然後將左邊轉化成關於j和k的表達式,右邊轉化成只有i的表達式。

       (中間省略t行推導過程...,t >= 5)

       (d[j] - d[k] + s[j]

2

- s[k]

2

) / (s[j] - s[k]) < 2*s[i]       令xj = d[j] + s[j]

2

,   yj = s[j],則原式轉化成: (xj - xk) / (yj - yk) < 2*s[i]

       不等式左邊是個斜率的形式,我們用斜率函數來表示 slope(j, k) = (xj - xk) / (yj - yk)

       那麼這裏我們可以得出兩個結論:

       1、當兩個決策j、k (j < k)滿足 slope(j, k) < 2*s[i]時,j決策是個無用決策,並且因爲s[i]是個單調不降的,所以對於i < i',則有slope(j, k) < 2*s[i] < 2*s[i'],即j決策在隨着i增大的過程中也是一直都用不到的。

       2、對於當前需要計算的值f[i],存在三個決策j、k、l,並且有 j < k < l,如果slope(j, k) > slope(k, l),則k這個決策是個無用決策,證明需要分情況討論:

             i. slope(k, l) < 2*s[i],則l的決策比k更優;

             ii. slope(k, l) >= 2*s[i],則 slope(j, k) > slope(k, l) >= 2*s[i],j的決策比k更優;

             綜上所述,當slope(j, k) > slope(k, l)時,k是無用決策;

       那麼可以用單調隊列來維護一個決策隊列的單調性,單調隊列存的是決策序列。

       一開始隊列裏只有一個決策,就是0這個點(虛擬出的初始決策),根據第一個結論,如果隊列裏面決策數目大於1,則判斷slope( Q[front], Q[front+1] ) < 2*s[i]是否成立,如果成立,Q[front]是個無用決策,front ++,如果不成立那麼Q[front]必定是當前i的最優決策,通過狀態轉移方程計算f[i]的值,然後再根據第二個結論,判斷slope(Q[rear-2], Q[rear-1]) > slope(Q[rear-1], i)是否成立,如果成立,那麼Q[rear-1]必定是個無用決策,rear --,如果不成立,則將 i 作爲當前決策 插入到隊列尾, 即 Q[rear++] = i。

       這題需要注意,斜率計算的時候,分母有可能爲0的情況。

       

5、樹狀數組優化

       

樹狀數組是一種數據結構,它支持兩種操作:

       

1、對點更新,即在給你(x, v)的情況下,在下標x的位置累加一個和v(耗時  O(log(n)) )。

            函數表示   void add(x, v);

       

2、成端求和,給定一段區間[x, y],求區間內數據的和(這些數據就是第一個操作累加的數據,耗時

O(log(n))

)。

            函數表示   int sum(x, y);

       

用其它數據結構也是可以實現上述操作的,例如線段樹(可以認爲它是一種輕量級的線段樹,但是線段樹能解決的問題更加普遍,而樹狀數組只能處理求和問題),但是樹狀數組的實現方便、

常數

複雜度低,使得它在解決對點更新成端求和問題上成爲首選。這裏並不會講它的具體實現,有興趣請參見

樹狀數組

       【例題17】給定一個n

(n <= 100000)個元素的序列a[i] (1 <= a[i] <= INF),定義"和諧序列"爲序列的任何兩個相鄰元素相差不超過K,求a[i]的子序列中,"和諧序列"的個數 MOD 10000007。

 

       用d[i]表示以第i個元素結尾的"和諧序列"的個數, 令d[0] = 1, 那麼sum {d[i] | 1 <= i <= n}就是我們要求的解。列出狀態轉移方程:

       d[i] = sum{ d[j] | j==0 || j < i && abs(a[i] - a[j]) <= K }

       這是一個1D/1D問題,如果不進行任何優化,時間複雜度是O(n^2

)。

       我們首先假設K是個無限大的數,也就是不考慮abs(a[i] - a[j]) <= K這個限制,那這個問題要怎麼做?很顯然d[1] = 1,d[2] = 1 + d[1],d[3] = d[1] + d[2] + 1(這裏的1其實是狀態轉移方程中j == 0的情況,也就是隻有一個數a[i]的情況),更加一般地:

       d[i] = sum{d[j] | j < i} + 1

       對於這一步狀態轉移還是O(n)的,但是我們可以將它稍加變形,如下:

       d[i] = sum(1, INF) + 1;               (這裏的sum是樹狀數組的區間求和函數) 

       得到d[i]的值後,再將d[i]插入到樹狀數組中,利用樹狀數組第1條操作 add(a[i], d[i]);

       基於這樣的一種思路,我們發現即使有了限制K,同樣也是可以求解的,只是在求和的時候進行如下操作:

       d[i] = sum(a[i] - K, a[i] + K) + 1;

      這樣就把原本O(n)的狀態轉移變成了 O(logn),整個算法時間複雜度O(logn)。
        

6、線段樹優化

       線段樹

是一種完全二叉樹,它支持區間求和、區間最值等一系列區間問題,這裏爲了將問題簡化,直接給出求值函數而暫時不去討論它的具體實現,有興趣的可以自行尋找資料。線段樹可以擴展到二維,二維線段樹是一棵四叉樹,一般用於解決平面統計問題,參見

二維線段樹。       

這裏給出線段樹的求區間最值需要用到的函數,即:

       

1、insert(x, v)            在下標爲x的位置插入一個值v;  耗時 O( log(n) )

       

2、query(l, r)             求下標在[l, r]上的最值;             耗時 O( log(n) )

       【例題18】詳見 例題13

       

還是以最長單調子序列爲例,狀態轉移方程爲:

       

d[i] = max{ d[j] | j < i && a[j] < a[i] } + 1

       這裏我們首先要保證 1 <= a[i] <= n,如果a[i]是實數域的話,首先要對a[i]進行離散化,排序後從小到大重新編號,最小的a[i]編號爲1,最大的a[i]編號爲n。

       對於狀態轉移執行線段樹的詢問操作: d[i] = query( 1, a[i] - 1 ) + 1

       然後再執行插入操作: insert( a[i], d[i] )

       狀態轉移同樣耗時O(logn)。

       這個思想和樹狀數組的思想很像,大體思路都是將數本身映射到某個數據結構的下標。

 

       7、其他優化

       a.散列HASH狀態表示

       b.結合數論優化

       c.結合計算幾何優化

       d.四邊形不等式優化

       e.等等


轉自:http://cppblog.com/menjitianya/archive/2015/10/23/212084.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章