動態規劃算法

3.1 算法思想

和貪婪算法一樣,在動態規劃中,可將一個問題的解決方案視爲一系列決策的結果。不同的是,在貪婪算法中,每採用一次貪婪準則便做出一個不可撤回的決策,而在動態規劃中,還要考察每個最優決策序列中是否包含一個最優子序列。

例3-1 [最短路經] 考察圖1 2 - 2中的有向圖。假設要尋找一條從源節點s= 1到目的節點d= 5的最短路徑,即選擇此路徑所經過的各個節點。第一步可選擇節點2,3或4。假設選擇了節點3,則此時所要求解的問題變成:選擇一條從3到5的最短路徑。如果3到5的路徑不是最短的,則從1開始經過3和5的路徑也不會是最短的。例如,若選擇的子路徑(非最短路徑)是3,2,5 (耗費爲9 ),則1到5的路徑爲1,3,2,5 (耗費爲11 ),這比選擇最短子路徑3,4,5而得到的1到5的路徑1,3,4,5 (耗費爲9) 耗費更大。

所以在最短路徑問題中,假如在的第一次決策時到達了某個節點v,那麼不管v 是怎樣確定的,此後選擇從v 到d 的路徑時,都必須採用最優策略。

例3-2 [0/1揹包問題] 考察1 3 . 4節的0 / 1揹包問題。如前所述,在該問題中需要決定x1 .. xn的值。假設按i = 1,2,.,n 的次序來確定xi 的值。如果置x1 = 0,則問題轉變爲相對於其餘物品(即物品2,3,.,n),揹包容量仍爲c 的揹包問題。若置x1 = 1,問題就變爲關於最大揹包容量爲c-w1 的問題。現設r?{c,c-w1 } 爲剩餘的揹包容量。

在第一次決策之後,剩下的問題便是考慮揹包容量爲r 時的決策。不管x1 是0或是1,[x2 ,.,xn ] 必須是第一次決策之後的一個最優方案,如果不是,則會有一個更好的方案[y2,.,yn ],因而[x1,y2,.,yn ]是一個更好的方案。

假設n=3, w=[100,14,10], p=[20,18,15], c= 11 6。若設x1 = 1,則在本次決策之後,可用的揹包容量爲r= 116-100=16 。[x2,x3 ]=[0,1] 符合容量限制的條件,所得值爲1 5,但因爲[x2,x3 ]= [1,0] 同樣符合容量條件且所得值爲1 8,因此[x2,x3 ] = [ 0,1] 並非最優策略。即x= [ 1,0,1] 可改進爲x= [ 1,1,0 ]。若設x1 = 0,則對於剩下的兩種物品而言,容量限制條件爲11 6。總之,如果子問題的結果[x2,x3 ]不是剩餘情況下的一個最優解,則[x1,x2,x3 ]也不會是總體的最優解。

例3-3 [航費] 某航線價格表爲:從亞特蘭大到紐約或芝加哥,或從洛杉磯到亞特蘭大的費用爲$ 1 0 0;從芝加哥到紐約票價$ 2 0;而對於路經亞特蘭大的旅客,從亞特蘭大到芝加哥的費用僅爲$ 2 0。從洛杉磯到紐約的航線涉及到對中轉機場的選擇。如果問題狀態的形式爲(起點,終點),那麼在選擇從洛杉磯到亞特蘭大後,問題的狀態變爲(亞特蘭大,紐約)。從亞特蘭大到紐約的最便宜航線是從亞特蘭大直飛紐約,票價$ 1 0 0。而使用直飛方式時,從洛杉磯到紐約的花費爲$ 2 0 0。不過,從洛杉磯到紐約的最便宜航線爲洛杉磯-亞特蘭大-芝加哥-紐約,其總花費爲$ 1 4 0(在處理局部最優路徑亞特蘭大到紐約過程中選擇了最低花費的路徑:亞特蘭大-芝加哥-紐約)。

如果用三維數組(t a g,起點,終點)表示問題狀態,其中t a g爲0表示轉飛, t a g爲1表示其他情形,那麼在到達亞特蘭大後,狀態的三維數組將變爲( 0,亞特蘭大,紐約),它對應的最優路徑是經由芝加哥的那條路徑。

當最優決策序列中包含最優決策子序列時,可建立動態規劃遞歸方程( d y n a m i c -programming recurrence equation),它可以幫助我們高效地解決問題。

例3-4 [0/1揹包] 在例3 - 2的0 / 1揹包問題中,最優決策序列由最優決策子序列組成。假設f (i,y) 表示例1 5 - 2中剩餘容量爲y,剩餘物品爲i,i + 1,.,n 時的最優解的值,即:和利用最優序列由最優子序列構成的結論,可得到f 的遞歸式。f ( 1 ,c) 是初始時揹包問題的最優解。可使用( 1 5 - 2)式通過遞歸或迭代來求解f ( 1 ,c)。從f (n, * )開始迭式, f (n, * )由(1 5 - 1)式得出,然後由( 1 5 - 2)式遞歸計算f (i,*) ( i=n- 1,n- 2,., 2 ),最後由( 1 5 - 2)式得出f ( 1 ,c)。

對於例1 5 - 2,若0≤y<1 0,則f ( 3 ,y) = 0;若y≥1 0,f ( 3 ,y) = 1 5。利用遞歸式(1 5 - 2),可得f (2, y) = 0 ( 0≤y<10 );f(2,y)= 1 5(1 0≤y<1 4);f(2,y)= 1 8(1 4≤y<2 4)和f(2,y)= 3 3(y≥2 4)。因此最優解f ( 1 , 11 6 ) = m a x {f(2,11 6),f(2,11 6 - w1)+ p1} = m a x {f(2,11 6),f(2,1 6)+ 2 0 } = m a x { 3 3,3 8 } = 3 8。

現在計算xi 值,步驟如下:若f ( 1 ,c) =f ( 2 ,c),則x1 = 0,否則x1 = 1。接下來需從剩餘容量c-w1中尋求最優解,用f (2, c-w1) 表示最優解。依此類推,可得到所有的xi (i= 1.n) 值。

在該例中,可得出f ( 2 , 11 6 ) = 3 3≠f ( 1 , 11 6 ),所以x1 = 1。接着利用返回值3 8 -p1=18 計算x2 及x3,此時r = 11 6 -w1 = 1 6,又由f ( 2 , 1 6 ) = 1 8,得f ( 3 , 1 6 ) = 1 4≠f ( 2 , 1 6 ),因此x2 = 1,此時r= 1 6 -w2 = 2,所以f (3,2) =0,即得x3 = 0。

動態規劃方法採用最優原則( principle of optimality)來建立用於計算最優解的遞歸式。所謂最優原則即不管前面的策略如何,此後的決策必須是基於當前狀態(由上一次決策產生)的最優決策。由於對於有些問題的某些遞歸式來說並不一定能保證最優原則,因此在求解問題時有必要對它進行驗證。若不能保持最優原則,則不可應用動態規劃方法。在得到最優解的遞歸式之後,需要執行回溯(t r a c e b a c k)以構造最優解。

編寫一個簡單的遞歸程序來求解動態規劃遞歸方程是一件很誘人的事。然而,正如我們將在下文看到的,如果不努力地去避免重複計算,遞歸程序的複雜性將非常可觀。如果在遞歸程序設計中解決了重複計算問題時,複雜性將急劇下降。動態規劃遞歸方程也可用迭代方式來求解,這時很自然地避免了重複計算。儘管迭代程序與避免重複計算的遞歸程序有相同的複雜性,但迭代程序不需要附加的遞歸棧空間,因此將比避免重複計算的遞歸程序更快。


3.2 應用

3.2.1 0/1揹包問題

1. 遞歸策略

在例3 - 4中已建立了揹包問題的動態規劃遞歸方程,求解遞歸式( 1 5 - 2)的一個很自然的方法便是使用程序1 5 - 1中的遞歸算法。該模塊假設p、w 和n 爲輸入,且p 爲整型,F(1,c) 返回f ( 1 ,c) 值。

程序15-1 揹包問題的遞歸函數

int F(int i, int y)

{// 返回f ( i , y ) .

if (i == n) return (y < w[n]) ? 0 : p[n];

if (y < w[i]) return F(i+1,y);

return max(F(i+1,y), F(i+1,y-w[i]) + p[i]);

}

程序1 5 - 1的時間複雜性t (n)滿足:t ( 1 ) =a;t(n)≤2t(n- 1)+b(n>1),其中a、b 爲常數。通過求解可得t (n) =O( 2n)。

例3-5 設n= 5,p= [ 6 , 3 , 5 , 4 , 6 ],w=[2,2,6,5,4] 且c= 1 0 ,求f ( 1 , 1 0 )。爲了確定f ( 1 , 1 0 ),調用函數F ( 1 , 1 0 )。遞歸調用的關係如圖1 5 - 1的樹型結構所示。每個節點用y值來標記。對於第j層的節點有i=j,因此根節點表示F ( 1 , 1 0 ),而它有左孩子和右孩子,分別對應F ( 2 , 1 0 )和F ( 2 , 8 )。總共執行了2 8次遞歸調用。但我們注意到,其中可能含有重複前面工作的節點,如f ( 3 , 8 )計算過兩次,相同情況的還有f ( 4 , 8 )、f ( 4 , 6 )、f ( 4 , 2 )、f ( 5 , 8 )、f ( 5 , 6 )、f ( 5 , 3 )、f (5,2) 和f ( 5 , 1 )。如果保留以前的計算結果,則可將節點數減至1 9,因爲可以丟棄圖中的陰影節點。

正如在例3 - 5中所看到的,程序1 5 - 1做了一些不必要的工作。爲了避免f (i,y)的重複計算,必須定義一個用於保留已被計算出的f (i,y)值的表格L,該表格的元素是三元組(i,y,f (i,y) )。在計算每一個f (i,y)之前,應檢查表L中是否已包含一個三元組(i,y, * ),其中*表示任意值。如果已包含,則從該表中取出f (i,y)的值,否則,對f (i,y)進行計算並將計算所得的三元組(i,y,f (i,y) )加入表L。L既可以用散列(見7 . 4節)的形式存儲,也可用二叉搜索樹(見11章)的形式存儲。

2. 權爲整數的迭代方法

當權爲整數時,可設計一個相當簡單的算法(見程序1 5 - 2)來求解f ( 1 ,c)。該算法基於例3 - 4所給出的策略,因此每個f (i,y) 只計算一次。程序1 5 - 2用二維數組f [ ][ ]來保存各f 的值。而回溯函數Tr a c e b a c k用於確定由程序1 5 - 2所產生的xi 值。函數K n a p s a c k的複雜性爲( n c),而Tr a c e b a c k的複雜性爲( n )。

程序15-2 f 和x 的迭代計算

template<class T>

void Knapsack(T p[], int w[], int c, int n, T** f)

{// 對於所有i和y計算f [ i ] [ y ]

// 初始化f [ n ] [ ]

for (int y = 0; y <= yMax; y++)

f[n][y] = 0;

for (int y = w[n]; y <= c; y++)

f[n][y] = p[n];

// 計算剩下的f

for (int i = n - 1; i > 1; i--) {

for (int y = 0; y <= yMax; y++)

f[i][y] = f[i+1][y];

for (int y = w[i]; y <= c; y++)

f[i][y] = max(f[i+1][y], f[i+1][y-w[i]] + p[i]);

}

f[1][c] = f[2][c];

if (c >= w[1])

f[1][c] = max(f[1][c], f[2][c-w[1]] + p[1]);

}

template<class T>

void Traceback(T **f, int w[], int c, int n, int x[])

{// 計算x

for (int i = 1; i < n; i++)

if (f[i][c] == f[i+1][c]) x[i] = 0;

else {x[i] = 1;

c -= w[i];}

x[n] = (f[n][c]) ? 1 : 0;

}

3. 元組方法( 選讀)

程序1 5 - 2有兩個缺點:1) 要求權爲整數;2) 當揹包容量c 很大時,程序1 5 - 2的速度慢於程序1 5 - 1。一般情況下,若c>2n,程序1 5 - 2的複雜性爲W (n2n )。可利用元組的方法來克服上述兩個缺點。在元組方法中,對於每個i,f (i, y) 都以數對(y, f (i, y)) 的形式按y的遞增次序存儲於表P(i)中。同時,由於f (i, y) 是y 的非遞減函數,因此P(i) 中各數對(y, f (i, y)) 也是按f (i, y) 的遞增次序排列的。

例3-6 條件同例3 - 5。對f 的計算如圖1 5 - 2所示。當i= 5時,f 由數對集合P( 5 ) = [ ( 0 , 0 ) , ( 4 , 6 ) ]表示。而P( 4 )、P( 3 )和P( 2 )分別爲[ ( 0 , 0 ) , ( 4 , 6 ) , ( 9 , 1 0 ) ]、[ ( 0 , 0 ) ( 4 , 6 ) , ( 9 , 1 0 ) , ( 1 0 , 11)] 和[ ( 0 , 0 ) ( 2 , 3 ) ( 4 , 6 ) ( 6 , 9 ) ( 9 , 1 0 ) ( 1 0 , 11 ) ]。

爲求f ( 1 , 1 0 ),利用式(1 5 - 2)得f ( 1 , 1 0 ) = m a x{f ( 2 , 1 0 ),f ( 2 , 8 ) + p 1}。由P( 2 )得f ( 2 , 1 0 ) = 11、f (2,8)=9 (f ( 2 , 8 ) = 9來自數對( 6,9 ) ),因此f ( 1 , 1 0 ) = m a x{11 , 1 5}= 1 5。現在來求xi 的值,因爲f ( 1 , 1 0 ) =f ( 2 , 6 ) +p1,所以x1 = 1;由f ( 2 , 6 ) =f ( 3 , 6 - w 2 ) +p2 =f ( 3 , 4 ) +p2,得x2 = 1;由f ( 3 , 4 ) =f ( 4 , 4 ) =f ( 5 , 4 )得x3=x4 = 0;最後,因f ( 5 , 4 )≠0得x5= 1。

檢查每個P(i) 中的數對,可以發現每對(y,f (i,y)) 對應於變量xi , ., xn 的0/1 賦值的不同組合。設(a,b)和(c,d)是對應於兩組不同xi , ., xn 的0 / 1賦值,若a≥c且b<d,則(a, b) 受(b, c) 支配。被支配者不必加入P(i)中。若在相同的數對中有兩個或更多的賦值,則只有一個放入P(i)。假設wn≤C,P(n)=[(0,0), (wn , pn ) ],P(n)中對應於xn 的兩個數對分別等於0和1。對於每個i,P(i)可由P(i+ 1 )得出。首先,要計算數對的有序集合Q,使得當且僅當wi≤s≤c且(s-wi , t-pi )爲P(i+1) 中的一個數對時,(s,t)爲Q中的一個數對。現在Q中包含xi = 1時的數對集,而P(i+ 1 )對應於xi = 0的數對集。接下來,合併Q和P(i+ 1 )並刪除受支配者和重複值即可得到P(i)。

例3-7 各數據同例1 5 - 6。P(5)=[(0,0),(4,6)], 因此Q= [ ( 5 , 4 ) , ( 9 , 1 0 ) ]。現在要將P( 5 )和Q合併得到P( 4 )。因( 5 , 4 )受( 4 , 6 )支配,可刪除( 5 , 4 ),所以P(4)=[(0,0), (4,6), (9,10)]。接着計算P( 3 ),首先由P( 4 )得Q=[(6,5), (10,11 ) ],然後又由合併方法得P(3)=[(0,0), (4,6), (9,10), (10,11 ) ]。最後計算P( 2 ):由P( 3 )得Q= [ ( 2 , 3 ),( 6 , 9 ) ],P( 3 )與Q合併得P(2)=[(0,0), (2,3), (4,6), (6,9), (9,10). (10,11 ) ]。因爲每個P(i) 中的數對對應於xi , ., xn 的不同0 / 1賦值,因此P(i) 中的數對不會超過2n-i+ 1個。計算P(i) 時,計算Q需消耗( |P(i+ 1 ) |)的時間,合併P(i+1) 和Q同樣需要( |P(i+ 1 ) | )的時間。計算所有P(i) 時所需要的總時間爲: (n ?i=2|P(i + 1)|= O ( 2n )。當權爲整數時,|P(i) |≤c+1, 此時複雜性爲O ( m i n {n c, 2n } )。

如6 . 4 . 3節定義的,數字化圖像是m×m的像素陣列。假定每個像素有一個0 ~ 2 5 5的灰度值。因此存儲一個像素至多需8位。若每個像素存儲都用最大位8位,則總的存儲空間爲8m2 位。爲了減少存儲空間,我們將採用變長模式( variable bit scheme),即不同像素用不同位數來存儲。像素值爲0和1時只需1位存儲空間;值2、3各需2位;值4,5,6和7各需3位;以此類推,使用變長模式的步驟如下:

1) 圖像線性化根據圖15-3a 中的折線將m×m維圖像轉換爲1×m2 維矩陣。

2) 分段將像素組分成若干個段,分段原則是:每段中的像素位數相同。每個段是相鄰像素的集合且每段最多含2 5 6個像素,因此,若相同位數的像素超過2 5 6個的話,則用兩個以上的段表示。

3) 創建文件創建三個文件:S e g m e n t L e n g t h, BitsPerPixel 和P i x e l s。第一個文件包含在2 )中所建的段的長度(減1 ),文件中各項均爲8位長。文件BitsPerPixel 給出了各段中每個像素的存儲位數(減1),文件中各項均爲3位。文件Pixels 則是以變長格式存儲的像素的二進制串。

4) 壓縮文件壓縮在3) 中所建立的文件,以減少空間需求。

上述壓縮方法的效率(用所得壓縮率表示)很大程度上取決於長段的出現頻率。

例3-8 考察圖15-3b 的4×4圖像。按照蛇形的行主次序,灰度值依次爲1 0,9,1 2,4 0,5 0,3 5,1 5,1 2,8,1 0,9,1 5,11,1 3 0,1 6 0和2 4 0。各像素所需的位數分別爲4,4,4,6,6,6,4,4,4,4,4,4,4,8,8和8,按等長的條件將像素分段,可以得到4個段[ 1 0,9,1 2 ]、[ 4 0,5 0,3 5 ]、[15, 12, 8, 10, 9, 15, 11] 和[130, 160, 240]。因此,文件SegmentLength 爲2,2,6,2;文件BitsPerSegment 的內容爲3,5,3,7;文件P i x e l s包含了按蛇形行主次序排列的1 6個灰度值,其中頭三個各用4位存儲,接下來三個各用6位,再接下來的七個各用4位,最後三個各用8位存儲。因此存儲單元中前3 0位存儲了前六個像素:

1010 1001 1100 111000 110010 100011

這三個文件需要的存儲空間分別爲:文件SegmentLength 需3 2位;BitsPerSegment 需1 2位;Pixels 需8 2位,共需1 2 6位。而如果每個像素都用8位存儲,則存儲空間需8×1 6 = 1 2 8位,因而在本例圖像中,節省了2位的空間。

假設在2) 之後,產生了n 個段。段標題(segment header)用於存儲段的長度以及該段中每個像素所佔用的位數。每個段標題需11位。現假設li 和bi 分別表示第i 段的段長和該段每個像素的長度,則存儲第i 段像素所需要的空間爲li *bi 。在2) 中所得的三個文件的總存儲空間爲11 n+n ?i = 1li bi。可通過將某些相鄰段合併的方式來減少空間消耗。如當段i 和i+ 1被合併時,合併後的段長應爲li +li + 1。此時每個像素的存儲位數爲m a x {bi,bi +1 } 位。儘管這種技術增加了文件P i x e l s的空間消耗,但同時也減少了一個段標題的空間。

例3-9 如果將例1 5 - 8中的第1段和第2段合併,合併後,文件S e g m e n t L e n g t h變爲5,6,2,BitsPerSegment 變爲5,3,7。而文件Pixels 的前3 6位存儲的是合併後的第一段:001010 001001 001100 111000 110010 100011其餘的像素(例1 5 - 8第3段)沒有改變。因爲減少了1個段標題,文件S e g m e n t L e n g t h和BitsPerPixel 的空間消耗共減少了11位,而文件Pixels 的空間增加6位,因此總共節約的空間爲5位,空間總消耗爲1 2 1位。

我們希望能設計一種算法,使得在產生n 個段之後,能對相鄰段進行合併,以便產生一個具有最小空間需求的新的段集合。在合併相鄰段之後,可利用諸如L Z W法(見7 . 5節)和霍夫曼編碼(見9 . 5 . 3節)等其他技術來進一步壓縮這三個文件。

令sq 爲前q 個段的最優合併所需要的空間。定義s0 = 0。考慮第i 段(i>0 ),假如在最優合併C中,第i 段與第i- 1,i- 2,.,i-r+1 段相合並,而不包括第i-r 段。合併C所需要的空間消耗等於:第1段到第i-r 段所需空間+ l s u m (i-r+ 1 ,i) * b m a x (i-r+ 1 ,i) + 11

其中l s u m(a, b)=b ?j =a

lj,bmax (a, b)= m a x {ba , ..., bb }。假如在C中第1段到第i-r 段的合並不是最優合併,那麼需要對合並進行修改,以使其具有更小的空間需求。因此還必須對段1到段i-r 進行最優合併,也即保證最優原則得以維持。故C的空間消耗爲:

si = si-r +l s u m(i-r+1, i)*b m a x(i-r+1, i)+ 11

r 的值介於1到i 之間,其中要求l s u m不超過2 5 6 (因爲段長限制在2 5 6之內)。儘管我們不知道如何選擇r,但我們知道,由於C具有最小的空間需求,因此在所有選擇中, r 必須產生最小的空間需求。

假定k a yi 表示取得最小值時k 的值,sn 爲n 段的最優合併所需要的空間,因而一個最優合併可用kay 的值構造出來。

例3-10 假定在2) 中得到五個段,它們的長度爲[ 6,3,1 0,2,3 ],像素位數爲[ 1,2,3,2,1 ],要用公式(1 5 - 3)計算sn,必須先求出sn-1,.,s0 的值。s0 爲0,現計算s1:s1 =s0 +l1 *b1+ 11 = 1 7k a y1 = 1s2 由下式得出:

s2 = m i n {s1 +l2 b2 , s0 + (l1 +l2 ) * m a x {b1 , b2} } + 11 = m i n { 1 7 + 6 , 0 + 9 * 2 } + 11 = 2 9

k a y2 = 2

以此類推,可得s1.s5 = [ 1 7,2 9,6 7,7 3,82] ,k a y1.k a y5 = [ 1,2,2,3,4 ]。因爲s5 = 8 2,所以最優空間合併需8 2位的空間。可由k a y5 導出本合併的方式,過程如下:因爲k a y5 = 4,所以s5 是由公式(1 5 - 3)在k=4 時取得的,因而最優合併包括:段1到段( 5 - 4 ) = 1的最優合併以及段2,3,4和5的合併。最後僅剩下兩個段:段1以及段2到段5的合併段。

1. 遞歸方法

用遞歸式(1 5 - 3)可以遞歸地算出si 和k a yi。程序1 5 - 3爲遞歸式的計算代碼。l,b,和k a y是一維的全局整型數組,L是段長限制( 2 5 6),h e a d e r爲段標題所需的空間( 11 )。調用S ( n )返回sn 的值且同時得出k a y值。調用Tr a c e b a c k ( k a y, n )可得到最優合併。

現討論程序1 5 - 3的複雜性。t( 0 ) =c(c 爲一個常數): (n>0),因此利用遞歸的方法可得t (n) = O ( 2n )。Tr a c e b a c k的複雜性爲(n)。

程序15-3 遞歸計算s , k a y及最優合併

int S(int i)

{ / /返回S ( i )並計算k a y [ i ]

if (i == 0 ) return 0;

//k = 1時, 根據公式( 1 5 - 3)計算最小值

int lsum = l[i],bmax = b[i];

int s = S(i-1) + lsum * bmax;

kay[i] = 1;

/ /對其餘的k計算最小值並求取最小值

for (int k = 2; k <= i && lsum+l[i-k+1] <= L; k++) {

lsum += l[i-k+1];

if (bmax < b[i-k+1]) bmax = b[i-k+1];

int t = S(i-k);

if (s > t + lsum * bmax) {

s = t + lsum * bmax;

kay[i] = k;}

}

return s + header;

}

void Traceback(int kay[], int n)

{// 合併段

if (n == 0) return;

Tr a c e b a c k ( k a y, n-kay[n]);

cout << "New segment begins at " << (n - kay[n] + 1) << endl;

}

2. 無重複計算的遞歸方法

通過避免重複計算si,可將函數S的複雜性減少到(n)。注意這裏只有n個不同的si。

例3 - 11 再考察例1 5 - 1 0中五個段的例子。當計算s5 時,先通過遞歸調用來計算s4,.,s0。計算s4 時,通過遞歸調用計算s3,.,s0,因此s4 只計算了一次,而s3 計算了兩次,每一次計算s3要計算一次s2,因此s2 共計算了四次,而s1 重複計算了1 6次!可利用一個數組s 來保存先前計算過的si 以避免重複計算。改進後的代碼見程序1 5 - 4,其中s爲初值爲0的全局整型數組。

程序15-4 避免重複計算的遞歸算法

int S(int i)

{ / /計算S ( i )和k a y [ i ]

/ /避免重複計算

if (i == 0) return 0;

if (s[i] > 0) return s[i]; //已計算完

/ /計算s [ i ]

/ /首先根據公式(1 5 - 3)計算k = 1時最小值

int lsum = l[i], bmax = b[i];

s[i] =S(i-1) + lsum * bmax;

kay[i] = 1;

/ /對其餘的k計算最小值並更新

for (int k = 2; k <= i && lsum+l[i-k+1] <= L; k++) {

lsum += l[i-k+1];

if (bmax < b[i-k+1]) bmax = b[i-k+1];

int t = S(i-k);

if (s[i] > t + lsum * bmax) {

s[i] = t + lsum * bmax;

kay[i] = k;}

}

s[i] += header;

return s[i];

}

爲了確定程序1 5 - 4的時間複雜性,我們將使用分期計算模式( amortization scheme)。在該模式中,總時間被分解爲若干個不同項,通過計算各項的時間然後求和來獲得總時間。當計算si 時,若sj 還未算出,則把調用S(j) 的消耗計入sj ;若sj 已算出,則把S(j) 的消耗計入si (這裏sj依次把計算新sq 的消耗轉移至每個sq )。程序1 5 - 4的其他消耗也被計入si。因爲L是2 5 6之內的常數且每個li 至少爲1,所以程序1 5 - 4的其他消耗爲( 1 ),即計入每個si 的量是一個常數,且si 數目爲n,因而總工作量爲(n)。

3. 迭代方法

倘若用式(1 5 - 3)依序計算s1 , ., sn,便可得到一個複雜性爲(n)的迭代方法。在該方法中,在si 計算之前, sj 必須已計算好。該方法的代碼見程序1 5 - 5,其中仍利用函數Tr a c e b a c k(見程序1 5 - 3)來獲得最優合併。

程序15-5 迭代計算s和k a y

void Vbits (int l[], int b[], int n, int s[], int kay[])

{ / /計算s [ i ]和k a y [ i ]

int L = 256, header = 11 ;

s[0] = 0;

/ /根據式(1 5 - 3)計算s [ i ]

for (int i = 1; i <= n; i++) {

// k = 1時,計算最小值

int lsum = l,

bmax = b[i];

s[i] = s[i-1] + lsum * bmax;

kay[i] = 1;

/ /對其餘的k計算最小值並更新

for (int k=2; k<= i && lsum+l[i-k+1]<= L; k++) {

lsum += l[i-k+1];

if (bmax < b[i-k+1]) bmax = b[i-k+1];

if (s[i] > s[i-k] + lsum * bmax){

s[i] = s[i-k] + lsum * bmax;

kay[i] = k; }

}

s[i] += header;

}

}


3.2.3 矩陣乘法鏈

m×n矩陣A與n×p矩陣B相乘需耗費(m n p)的時間(見第2章練習1 6)。我們把m n p作爲兩個矩陣相乘所需時間的測量值。現假定要計算三個矩陣A、B和C的乘積,有兩種方式計算此乘積。在第一種方式中,先用A乘以B得到矩陣D,然後D乘以C得到最終結果,這種乘法的順序可寫爲(A*B) *C。第二種方式寫爲A* (B*C) ,道理同上。儘管這兩種不同的計算順序所得的結果相同,但時間消耗會有很大的差距。

例3-12 假定A爲1 0 0×1矩陣,B爲1×1 0 0矩陣,C爲1 0 0×1矩陣,則A*B的時間耗費爲10 0 0 0,得到的結果D爲1 0 0×1 0 0矩陣,再與C相乘所需的時間耗費爲1 000 000,因此計算(A*B) *C的總時間爲1 010 000。B*C的時間耗費爲10 000,得到的中間矩陣爲1×1矩陣,再與A相乘的時間消耗爲1 0 0,因而計算A*(B*C)的時間耗費竟只有10 100!而且,計算( A*B)*C時,還需10 000個單元來存儲A*B,而A*(B*C)計算過程中,只需用1個單元來存儲B*C。

下面舉一個得益於選擇合適秩序計算A*B*C矩陣的實例:考慮兩個3維圖像的匹配。圖像匹配問題的要求是,確定一個圖像需旋轉、平移和縮放多少次才能逼近另一個圖像。實現匹配的方法之一便是執行約1 0 0次迭代計算,每次迭代需計算1 2×1個向量T:

T=?A(x, y, z) *B(x, y, z)*C(x, y, z )

其中A,B和C分別爲1 2×3,3×3和3×1矩陣。(x , y, z) 爲矩陣中向量的座標。設t 表示計算A(x , y, z) *B(x , y, z) *C(x , y, z)的計算量。假定此圖像含2 5 6×2 5 6×2 5 6個向量,在此條件中,這1 0 0個迭代所需的總計算量近似爲1 0 0 * 2 5 63 * t≈1 . 7 * 1 09 t。若三個矩陣是按由左向右的順序相乘的,則t = 1 2 * 3 * 3 + 1 2 * 3 *1= 1 4 4;但如果從右向左相乘, t = 3 * 3 * 1 + 1 2 * 3 * 1 = 4 5。由左至右計算約需2 . 4 * 1 011個操作,而由右至左計算大概只需7 . 5 * 1 01 0個操作。假如使用一個每秒可執行1億次操作的計算機,由左至右需4 0分鐘,而由右至左只需1 2 . 5分鐘。

在計算矩陣運算A*B*C時,僅有兩種乘法順序(由左至右或由右至左),所以可以很容易算出每種順序所需要的操作數,並選擇操作數比較少的那種乘法順序。但對於更多矩陣相乘來說,情況要複雜得多。如計算矩陣乘積M1×M2×.×Mq,其中Mi 是一個ri×ri + 1 矩陣( 1≤i≤q)。不妨考慮q=4 的情況,此時矩陣運算A*B*C*D可按以下方式(順序)計算:

A* ( (B*C) *D) A* (B* (C*D)) (A*B) * (C*D) (A* (B*C) ) *D

不難看出計算的方法數會隨q 以指數級增加。因此,對於很大的q 來說,考慮每一種計算順序並選擇最優者已是不切實際的。

現在要介紹一種採用動態規劃方法獲得矩陣乘法次序的最優策略。這種方法可將算法的時間消耗降爲(q3 )。用Mi j 表示鏈Mi×.×Mj (i≤j)的乘積。設c(i,j) 爲用最優法計算Mi j 的消耗,k a y(i, j) 爲用最優法計算Mi j 的最後一步Mi k×Mk+1, j 的消耗。因此Mij 的最優算法包括如何用最優算法計算Mik 和Mkj 以及計算Mik×Mkj 。根據最優原理,可得到如下的動態規劃遞歸式:k a y(i,i+s)= 獲得上述最小值的k. 以上求c 的遞歸式可用遞歸或迭代的方法來求解。c( 1,q) 爲用最優法計算矩陣鏈的消耗,k a y( 1 ,q) 爲最後一步的消耗。其餘的乘積可由k a y值來確定。

1. 遞歸方法

與求解0 / 1揹包及圖像壓縮問題一樣,本遞歸方法也須避免重複計算c (i, j) 和k a y(i, j),否則算法的複雜性將會非常高。

例3-13 設q= 5和r =(1 0 , 5 , 1 , 1 0 , 2 , 1 0),式中待求的c 中有四個c的s= 0或1,因此用動態規劃方法可立即求得它們的值: c( 1 , 1 ) =c( 5 , 5 ) = 0 ;c(1,2)=50; c( 4 , 5 ) = 2 0 0。現計算C( 2,5 ):c( 2 , 5 ) = m i n {c( 2 , 2 ) +c(3,5)+50, c( 2 , 3 ) +c(4,5)+500, c( 2 , 4 ) +c( 5 , 5 ) + 1 0 0 } (1 5 - 5)其中c( 2 , 2 ) =c( 5 , 5 ) = 0;c( 2 , 3 ) = 5 0;c(4,5)=200 。再用遞歸式計算c( 3 , 5 )及c( 2 , 4 ) :c( 3 , 5 ) = m i n {c( 3 , 3 ) +c(4,5)+100, c( 3 , 4 ) +c( 5 , 5 ) + 2 0 } = m i n { 0 + 2 0 0 + 1 0 0 , 2 0 + 0 + 2 0 } = 4 0c( 2 , 4 ) = m i n {c( 2 , 2 ) +c( 3 , 4 ) + 1 0 ,c( 2 , 3 ) +c( 4 , 4 ) + 1 0 0 } = m i n { 0 + 2 0 + 1 0 , 5 0 + 1 0 + 2 0 } = 3 0由以上計算還可得k a y( 3 , 5 ) = 4,k ay( 2 , 4 ) = 2。現在,計算c(2,5) 所需的所有中間值都已求得,將它們代入式(1 5 - 5)得:

c(2,5)=min{0+40+50, 50+200+500, 30+0+100}=90且k a y( 2 , 5 ) = 2

再用式(1 5 - 4)計算c( 1 , 5 ),在此之前必須算出c( 3 , 5 )、c(1,3) 和c( 1 , 4 )。同上述過程,亦可計算出它們的值分別爲4 0、1 5 0和9 0,相應的k a y 值分別爲4、2和2。代入式(1 5 - 4)得:

c(1,5)=min{0+90+500, 50+40+100, 150+200+1000, 90+0+200}=190且k a y( 1 , 5 ) = 2

此最優乘法算法的消耗爲1 9 0,由k a y(1,5) 值可推出該算法的最後一步, k a y(1,5) 等於2,因此最後一步爲M1 2×M3 5,而M12 和M35 都是用最優法計算而來。由k a y( 1 , 2 ) = 1知M12 等於M11×M2 2,同理由k a y( 3 , 5) = 4得知M35 由M3 4×M55 算出。依此類推,M34 由M3 3×M44 得出。因而此最優乘法算法的步驟爲:

M11×M2 2 = M1 2

M3 3×M4 4 = M3 4

M3 4×M5 5 = M3 5

M1 2×M3 5 = M1 5

計算c(i, j) 和k a y (i, j) 的遞歸代碼見程序1 5 - 6。在函數C中,r 爲全局一維數組變量, k a y是全局二維數組變量,函數C返回c(i j) 之值且置k a y [a] [b] =k ay (a , b) (對於任何a , b),其中c(a , b)在計算c(i,j) 時皆已算出。函數Traceback 利用函數C中已算出的k a y值來推導出最優乘法算法的步驟。

設t(q)爲函數C的複雜性,其中q=j-i+ 1(即Mij 是q個矩陣運算的結果)。當q爲1或2時,t(q) =d,其中d 爲一常數;而q> 2時,t (q)=2q-1?k = 1t (k ) +e q,其中e 是一個常量。因此當q>2時,t(q)>2t (q- 1 ) +e,所以t (q)= W ( 2q)。函數Traceback 的複雜性爲(q)。

程序15-6 遞歸計算c (i, j) 和kay (i, j)

int C(int i, int j)

{ / /返回c(i,j) 且計算k(i,j) = kay[i][j]

if (i==j) return 0; //一個矩陣的情形

if (i == j-1) { //兩個矩陣的情形

kay[i][i+1] = i;

return r[i]*r[i+1]*r[r+2];}

/ /多於兩個矩陣的情形

/ /設u爲k = i 時的最小值

int u = C(i,i) + C(i+1,j) + r[i]*r[i+1]*r[j+1];

kay[i][j] = i;

/ /計算其餘的最小值並更新u

for (int k = i+1; k < j; k++) {

int t = C(i,k) + C(k+1,j) + r[i]*r[k+1]*r[j+1];

if (r < u) {//小於最小值的情形

u = t;

kay[i][j] = k;

}

return u;

}

void Traceback (int i, int j ,int **kay)

{ / /輸出計算Mi j 的最優方法

if ( i == j) return;

Traceback(i, kay[i][j], kay);

Traceback(kay[i][j]+1, j, kay);

cout << "Multiply M" << i << ", "<< kay[i][j];

cout << " and M " << (kay[i][j]+1) << ", " << j << end1;

}

2. 無重複計算的遞歸方法

若避免再次計算前面已經計算過的c(及相應的k a y),可將複雜性降低到(q3)。而爲了避免重複計算,需用一個全局數組c[ ][ ]存儲c(i, j) 值,該數組初始值爲0。函數C的新代碼見程序1 5 - 7:

程序15-7 無重複計算的c (i, j) 計算方法

int C(int i,int j)

{ / /返回c(i,j) 並計算k a y ( i , j ) = k a y [ I ] [ j ]

/ /避免重複計算

/ /檢查是否已計算過

if (c[i][j] >) return c[i][j];

/ /若未計算,則進行計算

if(i==j) return 0; //一個矩陣的情形

i f ( i = = j - 1 ) { / /兩個矩陣的情形

kay[i][i+1]=i;

c [ i ] [ j ] = r [ i ] * r [ i + 1 ] * r [ i + 2 ] ;

return c[i][j];}

/ /多於兩個矩陣的情形

/ /設u爲k = i 時的最小值

int u=C(i,i)+C(i+1,j)+r[i]*r[i+1]*r[j+1];

k a y [ i ] [ j ] = i ;

/ /計算其餘的最小值並更新u

for (int k==i+1; k<j;k++){

int t=C(i,k)+C(k+1,j)+r[i]*r[k+1]*r[j+1];

if (t<u) {// 比最小值還小

u = t ;

k a y [ i ] [ j ] = k ; }

}

c [ i ] [ j ] = u ;

return u;

}

爲分析改進後函數C 的複雜性,再次使用分期計算方法。注意到調用C(1, q) 時每個c (i, j)(1≤i≤j≤q)僅被計算一次。要計算尚未計算過的c(a,b),需附加的工作量s =j-i>1。將s 計入第一次計算c (a, b) 時的工作量中。在依次計算c(a, b) 時,這個s 會轉計到每個c (a, b) 的第一次計算時間c 中,因此每個c (i, i) 均被計入s。對於每個s,有q-s+ 1個c(i, j) 需要計算,因此總的工作消耗爲q-1 ?s=1(q-s+ 1) = (q3 )。

3. 迭代方法

c 的動態規劃遞歸式可用迭代的方法來求解。若按s = 2,3,.,q-1 的順序計算c (i, i+s),每個c 和kay 僅需計算一次。

例3-14 考察例3 - 1 3中五個矩陣的情況。先初始化c (i, i) (0≤i≤5) 爲0,然後對於i=1, ., 4分別計算c (i, i+ 1 )。c (1, 2)= r1 r2 r3 = 5 0,c (2, 3)= 5 0,c ( 3,4)=20 和c (4, 5) = 2 0 0。相應的k ay 值分別爲1,2,3和4。

當s= 2時,可得:

c( 1 , 3 ) = m i n {c( 1 , 1 ) +c(2,3)+ r1 r2 r4 , c( 1 , 2 ) +c( 3 ,3 )+r1r3r4 }=min

=150

且k a y( 1 , 3 ) = 2。用相同方法可求得c( 2 , 4 )和c( 3 , 5 )分別爲3 0和4 0,相應k a y值分別爲2和3。

當s= 3時,需計算c(1,4) 和c( 2 , 5 )。計算c(2,5) 所需要的所有中間值均已知(見( 1 5 - 5 )式),代入計算公式後可得c( 2 , 5 ) = 9 0,k a y( 2 , 5 ) = 2。c( 1 , 4 )可用同樣的公式計算。最後,當s= 4時,可直接用(1 5 - 4)式來計算c( 1 , 5 ),因爲該式右邊所有項都已知。

計算c 和kay 的迭代程序見函數M a t r i x C h a i n(見程序1 5 - 8),該函數的複雜性爲(q3 )。計算出kay 後同樣可用程序1 5 - 6中的Traceback 函數推算出相應的最優乘法計算過程。

程序15-8 c 和kay 的迭代計算

void MatrixChain(int r[], int q, int **c, int **kay)

{// 爲所有的Mij 計算耗費和k a y

// 初始化c[i][i], c[i][i+1]和k a y [ i ] [ i + 1 ]

for (int i = 1; i < q; i++) {

c[i][i] = 0;

c[i][i+1] = r[i]*r[i+1]*r[i+2];

kay[i][i+1] = i;

}

c[q][q] = 0;

/ /計算餘下的c和k a y

for (int s = 2; s < q; s++)

for (int i = 1; i <= q - s; i++) {

// k = i時的最小項

c[i][i+s] = c[i][i] + c[i+1][i+s] + r[i]*r[i+1]*r[i+s+1];

kay[i][i+s] = i;

// 餘下的最小項

for (int k = i+1; k < i + s; k++) {

int t = c[i][k] + c[k+1][i+s] + r[i]*r[k+1]*r[i+s+1];

if (t < c[i][i+s]) {// 更小的最小項

c[i][i+s] = t;

kay[i][i+s] = k;}

}

}

}

3.2.4 最短路徑

假設G爲有向圖,其中每條邊都有一個長度(或耗費),圖中每條有向路徑的長度等於該路徑中各邊的長度之和。對於每對頂點(i, j),在頂點i 與j 之間可能有多條路徑,各路徑的長度可能各不相同。我們定義從i 到j 的所有路徑中,具有最小長度的路徑爲從i 到j 的最短路徑。

例3-15 如圖1 5 - 4所示。從頂點1到頂點3的路徑有

1) 1,2,5,3

2) 1,4,3

3) 1,2,5,8,6,3

4) 1,4,6,3

由該圖可知,各路徑相應的長度爲1 0、2 8、9、2 7,因而路徑3) 是該圖中頂點1到頂點3的最短路徑。

在所有點對最短路徑問題( a l l - p a i r sshorest-paths problem)中,要尋找有向圖G中每對頂點之間的最短路徑。也就是說,對於每對頂點(i, j),需要尋找從i到j 的最短路徑及從j 到i 的最短路徑。因此對於一個n 個頂點的圖來說,需尋找p =n(n-1) 條最短路徑。假定圖G中不含有長度爲負數的環路,只有在這種假設下才可保證G中每對頂點(i, j) 之間總有一條不含環路的最短路徑。當有向圖中存在長度小於0的環路時,可能得到長度爲-∞的更短路徑,因爲包含該環路的最短路徑往往可無限多次地加上此負長度的環路。

設圖G中n 個頂點的編號爲1到n。令c (i, j, k)表示從i 到j 的最短路徑的長度,其中k 表示該路徑中的最大頂點。因此,如果G中包含邊<i, j>,則c(i, j, 0) =邊<i, j> 的長度;若i= j ,則c(i,j, 0)=0;如果G中不包含邊<i, j>,則c (i, j, 0)= +∞。c(i, j, n) 則是從i 到j 的最短路徑的長度。

例3-16 考察圖1 5 - 4。若k=0, 1, 2, 3,則c (1, 3, k)= ∞;c (1, 3, 4)= 2 8;若k = 5, 6, 7,則c (1, 3,k) = 1 0;若k=8, 9, 10,則c (1, 3, k) = 9。因此1到3的最短路徑長度爲9。對於任意k>0,如何確定c (i, j, k) 呢?中間頂點不超過k 的i 到j 的最短路徑有兩種可能:該路徑含或不含中間頂點k。若不含,則該路徑長度應爲c(i, j, k- 1 ),否則長度爲c(i, k, k- 1) +c (k, j, k- 1 )。c(i, j, k) 可取兩者中的最小值。因此可得到如下遞歸式:

c( i, j, k)= m i n {c(i, j, k-1), c (i, k, k- 1) +c (k, j, k- 1 ) },k>0

以上的遞歸公式將一個k 級運算轉化爲多個k-1 級運算,而多個k-1 級運算應比一個k 級運算簡單。如果用遞歸方法求解上式,則計算最終結果的複雜性將無法估量。令t (k) 爲遞歸求解c (i, j, k) 的時間。根據遞歸式可以看出t(k) = 2t(k- 1 ) +c。利用替代方法可得t(n) = ( 2n )。因此得到所有c (i, j, n) 的時間爲(n2 2n )。

當注意到某些c (i, j, k-1) 值可能被使用多次時,可以更高效地求解c (i, j, n)。利用避免重複計算c(i, j, k) 的方法,可將計算c 值的時間減少到(n3 )。這可通過遞歸方式(見程序1 5 - 7矩陣鏈問題)或迭代方式來實現。出迭代算法的僞代碼如圖1 5 - 5所示。

 

/ /尋找最短路徑的長度

/ /初始化c(i,j,1)

for (int i=1; i < = n ; i + +)

for (int j=1; j<=n; j+ + )

c ( i ,j, 0 ) = a ( i ,j); // a 是長度鄰接矩陣

/ /計算c ( i ,j, k ) ( 0 < k < = n )

for(int k=1;k<=n;k++)

for (int i=1;i<=n;i++)

for (int j= 1 ;j< = n ;j+ + )

if (c(i,k,k-1)+c(k,j, k - 1 ) < c ( i ,j, k - 1 ) )

c ( i ,j, k ) = c ( i , k , k - 1 ) + c ( k ,j, k - 1 ) ;

else c(i,j, k ) = c ( i ,j, k - 1 ) ;

圖15-5 最短路徑算法的僞代碼

 

注意到對於任意i,c(i,k,k) =c(i,k,k- 1 )且c(k,i,k) =c(k,i,k- 1 ),因而,若用c(i,j)代替圖1 5 - 5的c(i,j,k),最後所得的c(i,j) 之值將等於c(i,j,n) 值。此時圖1 5 - 5可改寫成程序1 5 - 9的C + +代碼。程序1 5 - 9中還利用了程序1 2 - 1中定義的AdjacencyWDigraph 類。函數AllPairs 在c 中返回最短路徑的長度。若i 到j 無通路,則c [i] [j]被賦值爲N o E d g e。函數AllPairs 同時計算了k a y [ i ] [ j ],其中kay[i][j] 表示從i 到j 的最短路徑中最大的k 值。在後面將看到如何根據kay 值來推斷出從一個頂點到另一頂點的最短路徑(見程序1 5 - 1 0中的函數O u t p u t P a t h)。

程序1 5 - 9的時間複雜性爲(n3 ),其中輸出一條最短路徑的實際時間爲O (n)。

程序15-9 c 和kay 的計算

template<class T>

void AdjacencyWDigraph<T>::Allpairs(T **c, int **kay)

{ / /所有點對的最短路徑

/ /對於所有i和j,計算c [ i ] [ j ]和k a y [ i ] [ j ]

/ /初始化c [ i ] [ j ] = c(i,j,0)

for (int i = 1; i <= n; i++)

for (int j = 1; j <= n; j++) {

c[i][j] = a[i][j];

kay[i][j] = 0;

}

for (i = 1; i <= n; i++)

c[i][i] = 0;

// 計算c[i][j] = c(i,j,k)

for (int k = 1; k <= n; k++)

for (int i = 1; i <= n; i++)

for (int j = 1; j <= n; j++) {

T t1 = c[i][k];

T t2 = c[k][j];

T t3 = c[i][j];

if (t1 != NoEdge && t2 != NoEdge && (t3 == NoEdge || t1 + t2 < t3)) {

c[i][j] = t1 + t2;

kay[i][j] = k;}

}

}

程序15-10 輸出最短路徑

void outputPath(int **kay, int i, int j)

{// 輸出i 到j 的路徑的實際代碼

if (i == j) return;

if (kay[i][j] == 0) cout << j << ' ';

else {outputPath(kay, i, kay[i][j]);

o u t p u t P a t h ( k a y, kay[i][j], j);}

}

template<class T>

void OutputPath(T **c, int **kay, T NoEdge, int i, int j)

{// 輸出從i 到j的最短路徑

if (c[i][j] == NoEdge) {

cout << "There is no path from " << i << " to " << j << endl;

r e t u r n ; }

cout << "The path is" << endl;

cout << i << ' ';

o u t p u t P a t h ( k a y, i , j ) ;

cout << endl;

}

例3-17 圖15-6a 給出某圖的長度矩陣a,15-6b 給出由程序1 5 - 9所計算出的c 矩陣,15-6c 爲對應的k a y值。根據15-6c 中的kay 值,可知從1到5的最短路徑是從1到k a y [ 1 ] [ 5 ] = 4的最短路徑再加上從4到5的最短路徑,因爲k a y [ 4 ] [ 5 ] = 0,所以從4到5的最短路徑無中間頂點。從1到4的最短路徑經過k a y [ 1 ] [ 4 ] = 3。重複以上過程,最後可得1到5的最短路徑爲:1,2,3,4,5。

3.2.5 網絡的無交叉子集

在11 . 5 . 3節的交叉分佈問題中,給定一個每邊帶n 個針腳的佈線通道和一個排列C。頂部的針腳i 與底部的針腳Ci 相連,其中1≤i≤n,數對(i, Ci ) 稱爲網組。總共有n 個網組需連接或連通。假設有兩個或更多的佈線層,其中有一個爲優先層,在優先層中可以使用更細的連線,其電阻也可能比其他層要小得多。佈線時應儘可能在優先層中佈設更多的網組。而剩下的其他網組將佈設在其他層。當且僅當兩個網組之間不交叉時,它們可佈設在同一層。我們的任務是尋找一個最大無交叉子集(Maximum Noncrossing Su b s e t,M N S )。在該集中,任意兩個網組都不交叉。因(i, Ci ) 完全由i 決定,因此可用i 來指定(i, Ci )。

例3-18 考察圖1 5 - 7(對應於圖1 0 - 1 7)。( 1 , 8 )和( 2 , 7 )(也即1號網組和2號網組)交叉,因而不能佈設在同一層中。而( 1 , 8 ),(7,9) 和(9,10) 未交叉,因此可佈設在同一層。但這3個網組並不能構成一個M N S,因爲還有更大的不交叉子集。圖1 0 - 1 7中給出的例子中,集合{ ( 4 , 2 ) ,( 5 , 5 ) , ( 7 , 9 ) , ( 9 , 1 0 )}是一個含4個網組的M N S。

設M N S(i, j) 代表一個M N S,其中所有的(u, Cu ) 滿足u≤i,Cu≤j。令s i z e(i,j) 表示M N S(i,j)的大小(即網組的數目)。顯然M N S(n,n)是對應於給定輸入的M N S,而s i z e(n,n)是它的大小。

例3-19 對於圖1 0 - 1 7中的例子,M N S( 1 0 , 1 0 )是我們要找的最終結果。如例3 - 1 8中所指出的,s i z e( 1 0 , 1 0 ) = 4,因爲( 1 , 8 ),( 2 , 7 ),( 7 , 9 ),( 8 , 3 ),( 9 , 1 0 )和( 1 0 , 6 )中要麼頂部針腳編號比7大,要麼底部針腳編號比6大,因此它們都不屬於M N S( 7 , 6 )。因此只需考察剩下的4個網組是否屬於M N S( 7 , 6 ),如圖1 5 - 8所示。子集{( 3 , 4 ) , ( 5 , 5 )}是大小爲2的無交叉子集。沒有大小爲3的無交叉子集,因此s i z e( 7 , 6) = 2。

當i= 1時,( 1 ,C1) 是M N S( 1 ,j) 的唯一候選。僅當j≥C1 時,這個網組纔會是M N S( 1 ,j) 的一個成員.

下一步,考慮i>1時的情況。若j<Ci,則(i,Ci ) 不可能是M N S( i,j) 的成員,所有屬於M N S(i,j) 的(u, Cu ) 都需滿足u<i且Cu<j,因此:s i z e(i,j) =s i z e(i- 1 ,j), j<Ci (1 5 - 7)

若j≥Ci,則(i,Ci ) 可能在也可能不在M N S(i,j) 內。若(i,Ci ) 在M N S(i,j) 內,則在M N S(i,j)中不會有這樣的(u,Cu ):u<i且Cu>Ci,因爲這個網組必與(i, Ci ) 相交。因此M N S(i,j) 中的其他所有成員都必須滿足條件u<i且Cu<Ci。在M N S(i,j) 中這樣的網組共有Mi- 1 , Ci- 1 個。若(i,Ci ) 不在M N S(i,j)中,則M N S(i,j) 中的所有(u, Cu ) 必須滿足u<i;因此s i z e(i,j)=s i z e(i- 1 ,j)。雖然不能確定(i, Ci )是否在M N S(i,j) 中,但我們可以根據獲取更大M N S的原則來作出選擇。因此:s i z e(i,j) = m a x {s i z e(i-1 ,j), s i z e(i- 1 ,Ci-1)+1}, j≥Ci (1 5 - 8)

雖然從(1 5 - 6)式到( 1 5 - 8)式可用遞歸法求解,但從前面的例子可以看出,即使避免了重複計算,動態規劃遞歸算法的效率也不夠高,因此只考慮迭代方法。在迭代過程中先用式(1 5 - 6)計算出s i ze ( 1 ,j),然後再用式(1 5 - 7)和(1 5 - 8)按i=2, 3, ., n 的順序計算s i ze (i,j),最後再用Traceback 來得到M N S(n, n) 中的所有網組。

例3-20 圖1 5 - 9給出了圖1 5 - 7對應的s i z e(i,j) 值。因s i z e( 1 0 , 1 0) = 4,可知M N S含4個網組。爲求得這4個網組,先從s i ze ( 1 0 , 1 0 )入手。可用(1 5 - 8)式算出s i z e( 1 0 , 1 0 )。根據式(1 5 - 8)時的產生原因可知s i ze ( 1 0 , 1 0)=s i z e( 9 , 1 0 ),因此現在要求M NS ( 9 , 1 0 )。由於M NS ( 1 0 , 1 0 )≠s i z e( 8 , 1 0 ),因此M NS (9,10) 中必包含9號網組。M N S(9,10) 中剩下的網組組成M NS ( 8 , C9- 1)=M N S( 8 , 9 )。由M N S( 8 , 9 ) =M NS (7,9) 知,8號網組可以被排除。接下來要求M N S( 7 , 9 ),因爲s i z e( 7 , 9 )≠s i z e( 6 , 9 ),所以M N S中必含7號網組。M NS (7,9) 中餘下的網組組成M NS ( 6 , C7- 1 ) =M N S( 6 , 8 )。根據s i z e( 6 , 8 ) =s i z e( 5 , 8 )可排除6號網組。按同樣的方法, 5號網組,3號網組加入M N S中,而4號網組等其他網組被排除。因此回溯過程所得到的大小爲4的M N S爲{ 3 , 5 , 7 , 9 }。

注意到在回溯過程中未用到s i z e( 1 0 ,j) (j≠1 0 ),因此不必計算這些值。

程序1 5 - 11給出了計算s i z e ( i , j ) 的迭代代碼和輸出M N S的代碼。函數M N S用來計算s i ze (i,j) 的值,計算結果用一個二維數組M N來存儲。size[i][j] 表示s i z e(i,j),其中i=j= n 或1≤i<n,0≤j≤n,計算過程的時間複雜性爲(n2 )。函數Traceback 在N et[0 : m - 1]中輸出所得到的M N S,其時間複雜性爲(n)。因此求解M M S問題的動態規劃算法的總的時間複雜性

爲(n2 )。

程序1 5 - 11 尋找最大無交叉子集

void MNS(int C[], int n, int **size)

{ / /對於所有的i 和j,計算s i z e [ i ] [ j ]

/ /初始化s i z e [ 1 ] [ * ]

for (int j = 0; j < C[1]; j++)

size[1][j] = 0;

for (j = C[1]; j <= n; j++)

size[1][j] = 1;

// 計算size[i][*], 1 < i < n

for (int i = 2; i < n; i++) {

for (int j = 0; j < C[i]; j++)

size[i][j] = size[i-1][j];

for (j = C[i]; j <= n; j++)

size[i][j] = max(size[i-1][j], size[i-1][C[i]-1]+1);

}

size[n][n] = max(size[n-1][n], size[n-1][C[n]-1]+1);

}

void Traceback(int C[], int **size, int n, int Net[], int& m)

{// 在N e t [ 0 : m - 1 ]中返回M M S

int j = n; // 所允許的底部最大針腳編號

m = 0; // 網組的遊標

for (int i = n; i > 1; i--)

// i 號n e t在M N S中?

if (size[i][j] != size[i-1][j]){// 在M N S中

Net[m++] = i;

j = C[i] - 1;}

// 1號網組在M N S中?

if (j >= C[1])

Net[m++] = 1; // 在M N S中

}

3.2.6 元件摺疊

在設計電路的過程中,工程師們會採取多種不同的設計風格。其中的兩種爲位-片設計(bit-slice design)和標準單元設計(standard-cell design)。在前一種方法中,電路首先被設計爲一個元件棧(如圖15-10a 所示)。每個元件Ci 寬爲wi ,高爲hi ,而元件寬度用片數來表示。圖15-10a 給出了一個四片的設計。線路是按片來連接各元件的,即連線可能連接元件Ci 的第j片到元件Ci+1 的第j 片。如果某些元件的寬度不足j 片,則這些元件之間不存在片j 的連線。當圖1 5 -10a 的位-片設計作爲某一大系統的一部分時,則在V L SI ( Very Large Scale Integrated) 芯片上爲它分配一定數量的空間單元。分配是按空間寬度或高度的限制來完成的。現在的問題便是如何將元件棧摺疊到分配空間中去,以便儘量減小未受限制的尺度(如,若高度限制爲H時,必須摺疊棧以儘量減小寬度W)。由於其他尺度不變,因此縮小一個尺度(如W)等價於縮小面積。可用折線方式來摺疊元件棧,在每一折疊點,元件旋轉1 8 0°。在圖15-10b 的例子中,一個1 2元件的棧摺疊成四個垂直棧,摺疊點爲C6 , C9 和C1 0。摺疊棧的寬度是寬度最大的元件所需的片數。在圖15-10b 中,棧寬各爲4,3,2和4。摺疊棧的高度等於各棧所有元件高度之和的最大值。在圖15-10b 中棧1的元件高度之和最大,該棧的高度決定了包圍所有棧的矩形高度。

實際上,在元件摺疊問題中,還需考慮連接兩個棧的線路所需的附加空間。如,在圖1 5 -10b 中C5 和C6 間的線路因C6 爲摺疊點而彎曲。這些線路要求在C5 和C6 之下留有垂直空間,以便能從棧1連到棧2。令ri 爲Ci 是摺疊點時所需的高度。棧1所需的高度爲5 ?i =1hi +r6,棧2所需高度爲8 ?i=6hi +r6+r9。

在標準單元設計中,電路首先被設計成爲具有相同高度的符合線性順序的元件排列。假設此線性順序中的元件爲C1,.,Cn,下一步元件被摺疊成如圖1 5 - 11所示的相同寬度的行。在此圖中, 1 2個標準單元摺疊成四個等寬行。摺疊點是C4,C6 和C11。在相鄰標準單元行之間,使用佈線通道來連接不同的行。摺疊點決定了所需佈線通道的高度。設li 表示當Ci 爲摺疊點時所需的通道高度。在圖1 5 - 11的例子中,佈線通道1的高度爲l4,通道2的高度爲l6,通道3的高度爲l11。位-片棧摺疊和標準單元摺疊都會引出一系列的問題,這些問題可用動態規劃方法來解決。

1. 等寬位-片元件摺疊

定義r1 = rn+1 =0。由元件Ci 至Cj 構成的棧的高度要求爲j ?k= ilk+ ri+ rj + 1。設一個位-片設計中所有元件有相同寬度W。首先考察在摺疊矩形的高度H給定的情況下,如何縮小其寬度。設Wi

爲將元件Ci 到Cn 摺疊到高爲H的矩形時的最小寬度。若摺疊不能實現(如當ri +hi>H時),取Wi =∞。注意到W1 可能是所有n 個元件的最佳摺疊寬度。

當摺疊Ci 到Cn 時,需要確定摺疊點。現假定摺疊點是按棧左到棧右的順序來取定的。若第一點定爲Ck+ 1,則Ci 到Ck 在第一個棧中。爲了得到最小寬度,從Ck+1 到Cn 的摺疊必須用最優化方法,因此又將用到最優原理,可用動態規劃方法來解決此問題。當第一個摺疊點k+ 1已知時,可得到以下公式:

Wi =w+ Wk + 1 (1 5 - 9)

由於不知道第一個摺疊點,因此需要嘗試所有可行的摺疊點,並選擇滿足( 1 5 - 9)式的摺疊點。令h s u m(i,k)=k ?j = ihj。因k+ 1是一個可行的摺疊點,因此h s u m(i, k) +ri +rk+1 一定不會超過H。

根據上述分析,可得到以下動態規劃遞歸式:

這裏Wn+1 =0,且在無最優摺疊點k+ 1時Wi 爲∞。利用遞歸式(1 5 - 1 0),可通過遞歸計算Wn , Wn- 1., W2 , W1 來計算Wi。Wi 的計算需要至多檢查n-i+ 1個Wk+ 1,耗時爲O (n-k)。因此計算所有Wi 的時間爲O (n2 )。通過保留式(1 5 - 1 0)每次所得的k 值,可回溯地計算出各個最優的摺疊點,其時間耗費爲O (n)。

現在來考察另外一個有關等寬元件的摺疊問題:摺疊後矩形的寬度W已知,需要儘量減小其高度。因每個摺疊矩形寬爲w,因此摺疊後棧的最大數量爲s=W / w。令Hi, j 爲Ci , ., Cn 摺疊成一寬度爲jw 的矩形後的最小高度, H1, s 則是所有元件摺疊後的最小高度。當j= 1時,不允許任何摺疊,因此:Hi,1 =h s u m(i,n) +ri , 1≤i≤n

另外,當i=n 時,僅有一個元件,也不可能摺疊,因此:Hn ,j=hn+rn , 1≤j≤s

在其他情況下,都可以進行元件摺疊。如果第一個摺疊點爲k+ 1,則第一個棧的高度爲

h s u m(i,k) +ri +rk+ 1。其他元件必須以至多(j- 1 ) *w 的寬度摺疊。爲保證該摺疊的最優性,其他元件也需以最小高度進行摺疊.

因爲第一個摺疊點未知,因此必須嘗試所有可能的摺疊點,然後從中找出一個使式(1 5 - 11)的右側取最小值的點,該點成爲第一個摺疊點。

可用迭代法來求解Hi, j ( 1≤i≤n, 1≤j≤s),求解的順序爲:先計算j=2 時的H i, j,再算j= 3,.,以此類推。對應每個j 的Hi, j 的計算時間爲O (n2 ),所以計算所有H i, j 的時間爲O(s n2 )。通過保存由( 1 5 - 1 2)式計算出的每個k 值,可以採用複雜性爲O (n) 的回溯過程來確定各個最優的摺疊點。

2. 變寬位-片元件的摺疊

首先考察摺疊矩形的高度H已定,欲求最小的摺疊寬度的情況。令Wi 如式(1 5 - 1 0)所示,按照與(1 5 - 1 0)式相同的推導過程,可得:

Wi = m i n {w m i n(i, k) +Wk+1 | h s u m(i,k)+ ri +rk+ 1≤H, i≤k≤n} (1 5 - 1 3)

其中Wn+1=0且w m i n(i,k)= m ini≤j≤k{wj }。可用與(1 5 - 1 0)式一樣的方法求解(1 5 - 1 3)式,所需時間爲O(n2 )。

當摺疊寬度W給定時,最小高度摺疊可用折半搜索方法對超過O(n2 )個可能值進行搜索來實現,可能的高度值爲h(i,j)+ri +rj + 1。在檢測每個高度時,也可用( 1 5 - 1 3)式來確定該摺疊的寬度是否小於等於W。這種情況下總的時間消耗爲O (n2 l o gn)。

3. 標準單元摺疊

用wi 定義單元Ci 的寬度。每個單元的高度爲h。當標準單元行的寬度W 固定不變時,通過減少摺疊高度,可以相應地減少摺疊面積。考察Ci 到Cn 的最小高度摺疊。設第一個摺疊點是Cs+ 1。從元件Cs+1 到Cn 的摺疊必須使用最小高度,否則,可使用更小的高度來摺疊Cs+1 到Cn,從而得到更小的摺疊高度。所以這裏仍可使用最優原理和動態規劃方法。

令Hi , s 爲Ci 到Cn 摺疊成寬爲W的矩形時的最小高度,其中第一個摺疊點爲Cs+ 1。令w s u m(i, s)=s ?j = iwj。可假定沒有寬度超過W的元件,否則不可能進行摺疊。對於Hn,n 因爲只有一個元件,不存在連線問題,因此Hn, n =h。對於H i, s(1≤i<s≤n)注意到如果w s u m(i, s )>W,不可能實現摺疊。若w s u m(i,s)≤W,元件Ci 和C j + 1 在相同的標準單元行中,該行下方佈線通道的高度爲ls+ 1(定義ln+1 = 0)。因而:Hi, s = Hi+1, k (1 5 - 1 4)

當i=s<n 時,第一個標準單元行只包含Ci 。該行的高度爲h 且該行下方佈線通道的高度爲li+ 1。因Ci+ 1 到Cn 單元的摺疊是最優的.

爲了尋找最小高度摺疊,首先使用式( 1 5 - 1 4)和(1 5 - 1 5)來確定Hi, s (1≤i≤s≤n)。最小高度摺疊的高度爲m in{H1 , s}。可以使用回溯過程來確定最小高度摺疊中的摺疊點。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章