一.試題 在一個園形操場的四周擺放N堆石子(N≤100),現要將石子有次序地合併成一堆。規定 每次只能選相鄰的兩堆合併成新的一堆,並將新的一堆的石子數,記爲該次合併的得分。 編一程序,由文件讀入堆數N及每堆的石子數(≤20), ①選擇一種合併石子的方案,使得做N-1次合併,得分的總和最小; ②選擇一種合併石子的方案,使得做N-1次合併,得分的總和最大。 例如,所示的4堆石子,每堆石子數(從最上面的一堆數起,順時針數)依 次爲4594。則3次合併得分總和最小的方案:8+13+22=43 得分最大的方案爲:14+18+22=54 輸入數據: 文件名由鍵盤輸入,該文件內容爲: 第一行爲石子堆數N; 第二行爲每堆的石子數,每兩個數之間用一個空格符分隔。 輸出數據: 輸出文件名爲output.txt 從第1至第N行爲得分最小的合併方案。第N+1行是空行。從第N+2行到第2N+1行是得 分最大合併方案。 每種合併方案用N行表示,其中第i行(1≤i≤N)表示第i 次合併前各堆的石子數(依 順時針次序輸出,哪一堆先輸出均可)。 要求將待合併的兩堆石子數以相應的負數表示,以便標識。 輸入輸出範例: 輸入文件內容: 4 4 59 4 輸出文件內容: -4 5 9 -4 -8-5 9 -13 -9 22 4 -5 -9 4 4 -14 -4 -4-18 22 二.算法分析 競賽中多數選手都不約而同地採用了儘可能逼近目標的貪心法來逐次合併:從最上面 的一堆開始,沿順時針方向排成一個序列。 第一次選得分最小(最大)的相鄰兩堆合併, 形成新的一堆;接下來,在N-1堆中選得分最小(最大)的相鄰兩堆合併……,依次類推, 直至所有石子經N-1次合併後形成一堆。 例如有6堆石子,每堆石子數(從最上面一堆數起,順時針數)依次爲3 46 5 4 2 要求選擇一種合併石子的方案,使得做5次合併,得分的總和最小。 按照貪心法,合併的過程如下: 每次合併得分 第一次合併 3 4 6 5 4 2 ->5 第二次合併 5 4 6 5 4 ->9 第三次合併 9 6 5 4 ->9 第四次合併 9 6 9 ->15 第五次合併 15 9 ->24 24 總得分=5+9+9+15+24=62 但是當我們仔細琢磨後,可得出另一個合併石子的方案: 每次合併得分 第一次合併 3 4 6 5 4 2 ->7 第二次合併 7 6 5 4 2 ->13 第三次合併 13 5 4 2 ->6 第四次合併 13 5 6 ->11 第五次合併 13 11 ->24 24 總得分=7+6+11+13+24=61 顯然,後者比貪心法得出的合併方案更優。 題目中的示例故意造成一個貪心法解題的 假像,誘使讀者進入“陷阱”。爲了幫助讀者從這個“陷阱”裏走出來, 我們先來明確一個問題: 1.最佳合併過程符合最佳原理 使用貪心法之所以可能出錯, 是因爲每一次選擇得分最小(最大)的相鄰兩堆合併,不一定保證餘下的合併過程能導致最優解。聰明的讀者馬上會想到一種理想的假設:如果N-1次合併的全局最優解包含了每一次合併的子問題的最優解,那麼經這樣的N-1次合併後的得分總和必然是最優的。 例如上例中第五次合併石子數分別爲13和11的相鄰兩堆。 這兩堆石頭分別由最初的第1,2,3堆(石頭數分別爲3,4,6)和第4,5,6堆(石頭數分別爲5,4,2)經4次合併後形成的。於是問題又歸結爲如何使得這兩個子序列的N-2 次合併的得分總和最優。爲了實現這一目標,我們將第1個序列又一分爲二:第1、2堆構成子序列1, 第3堆爲子序列2。第一次合併子序列1中的兩堆,得分7; 第二次再將之與子序列2的一堆合併,得分13。顯然對於第1個子序列來說,這樣的合併方案是最優的。同樣,我們將第2個子序列也一分爲二;第4堆爲子序列1,第5,6堆構成子序列2。第三次合併子序列2中的2堆,得分6;第四次再將之與子序列1中的一堆合併,得分13。顯然對於第二個子序列來說,這樣的合併方案也是最優的。 由此得出一個結論──6堆石子經 過這樣的5次合併後,得分的總和最小。我們把每一次合併劃分爲階段,當前階段中計算出的得分和作爲狀態, 如何在前一次合併的基礎上定義一個能使目前得分總和最大的合併方案作爲一次決策。很顯然,某階段的狀態給定後,則以後各階段的決策不受這階段以前各段狀態的影響。 這種無後效性的性質符最佳原理,因此可以用動態規劃的算法求解。 2.動態規劃的方向和初值的設定 採用動態規劃求解的關鍵是確定所有石子堆子序列的最佳合併方案。 這些石子堆子序列包括: {第1堆、第2堆}、{第2堆、第3堆}、……、{第N堆、第1堆}; {第1堆、第2堆、第3堆}、{第2堆、第3堆、第4堆}、……、{第N堆、第1堆、第2堆};…… {第1堆、……、第N堆}{第1堆、……、第N堆、第1堆}……{第N堆、第1堆、……、第N-1堆} 爲了便於運算,我們用〔i,j〕表示一個從第i堆數起,順時針數j堆時的子序列{第i堆、第i+1堆、……、第(i+j-1)mod n堆} 它的最佳合併方案包括兩個信息: ①在該子序列的各堆石子合併成一堆的過程中,各次合併得分的總和; ②形成最佳得分和的子序列1和子序列2。由於兩個子序列是相鄰的, 因此只需記住子序列1的堆數; 設 f〔i,j〕──將子序列〔i,j〕中的j堆石子合併成一堆的最佳得分和; c〔i,j〕──將〔i,j〕一分爲二,其中子序列1的堆數; (1≤i≤N,1≤j≤N) 顯然,對每一堆石子來說,它的 f〔i,1〕=0 c〔i,1〕=0 (1≤i≤N) 對於子序列〔i,j〕來說,若求最小得分總和,f〔i,j〕的初始值爲∞; 若求最大得分總和,f〔i,j〕的初始值爲0。(1≤i≤N,2≤j≤N)。 動態規劃的方向是順推(即從上而下)。先考慮含二堆石子的N個子序列(各子序列分別從第1堆、第2堆、……、第N堆數起,順時針數2堆)的合併方案 f〔1,2〕,f〔2,2〕,……,f〔N,2〕 c〔1,2〕,c〔2,2〕,……,c〔N,2〕 然後考慮含三堆石子的N個子序列(各子序列分別從第1堆、第2堆、……、第N堆數起,順時針數3堆)的合併方案 f〔1,3〕,f〔2,3〕,……,f〔N,3〕 c〔1,3〕,c〔2,3〕,……,c〔N,3〕 …… 依次類推,直至考慮了含N堆石子的N個子序列(各子序列分別從第1堆、第2堆、 ……、第N堆數起,順時針數N堆)的合併方案 f〔1,N〕,f〔2,N〕,……,f〔N,N〕 c〔1,N〕,c〔2,N〕,……,c〔N,N〕 最後,在子序列〔1,N〕,〔2,N〕,……,〔N,N〕中,選擇得分總和(f值)最小(或最大)的一個子序列〔i,N〕(1≤i≤N),由此出發倒推合併過程。 3.動態規劃方程和倒推合併過程 對子序列〔i,j〕最後一次合併,其得分爲第i堆數起,順時針數j堆的石子總數t。被合併的兩堆石子是由子序列〔i,k〕和〔(i+k-1)mod n+1,j-k〕(1≤k≤j-1)經有限次合併形成的。爲了求出最佳合併方案中的k值,我們定義一個動態規劃方程: 當求最大得分總和時 f〔i,j〕=max{f〔i,k〕+f〔x,j-k〕+t} 1≤k≤j-1 c〔i,j〕=k│ f〔i,j〕=f〔i,k〕+f〔x,j-k〕+t (2≤j≤n,1≤i≤n) 當求最小得分總和時 f〔i,j〕=min{f〔i,k〕+f〔x,j-k〕+t} 1≤k≤j-1 c〔i,j〕=k│ f〔i,j〕=f〔i,k〕+f〔x,j-k〕+t (2≤j≤n,1≤i≤n) 其中x=(i+k-1)modn+1,即第i堆數起,順時針數k+1堆的堆序號。 例如對上面例子中的6(3 4 6 5 4 2 )堆石子,按動態規劃方程順推最小得分和。 依次得出含二堆石子的6個子序列的合併方案 f〔1,2〕=7 f〔2,2〕=10 f〔3 ,2〕=11 c〔1,2〕=1 c〔2,2〕=1 c〔3,2〕=1 f〔4,2〕=9 f〔5,2〕=6 f〔6,2〕=5 c〔4,2〕=1 c〔5, 2〕=1 c〔6,2〕=1 含三堆石子的6(3 4 6 5 4 2 )個子序列的合併方案 f〔1,3〕=20 f〔2,3〕=25 f〔3,3〕=24 c〔1,3〕=2 c〔2,3〕=2 c〔3,3〕=1 f〔4,3〕=17 f〔5,3〕=14 f〔6,3〕=14 c〔4,3〕=1 c〔5,3〕=1 c〔6,3〕=2 含四堆石子的6(3 4 6 5 4 2 )個子序列的合併方案 f〔1,4〕=36 f〔2,4〕=38 f〔3,4〕=34 c〔1,4〕=2 c〔2,4〕=2 c〔3,4〕=1 f〔4,4〕=28 f〔5,4〕=26 f〔6,4〕=29 c〔4,4〕=1 c〔5,4〕=2 c〔6,4〕=3 含五堆石子的6(3 4 6 5 4 2 )個子序列的合併方案 f〔1,5〕=51 f〔2,5〕=48 f〔3,5〕=45 c〔1,5〕=3 c〔2,5〕=2 c〔3,5〕=2 f〔4,5〕=41 f〔5,5〕=43 f〔6,5〕=45 c〔4,5〕=2 c〔5,5〕=3 c〔6,5〕=3 含六堆石子的6(3 4 6 5 4 2 )個子序列的合併方案 f〔1,6〕=61 f〔2,6〕=62 f〔3,6〕=61 c〔1,6〕=3 c〔2,6〕=2 c〔3,6〕=2 f〔4,6〕=61 f〔5,6〕=61 f〔6,6〕=62 c〔4,6〕=3 c〔5,6〕=4 c〔6,6〕=3 f〔1,6〕是f〔1,6〕,f〔2,6〕,……f〔6,6〕中的最小值,表明最小得分和是由序列〔1,6〕經5次合併得出的。我們從這個序列出發, 按下述方法倒推合併過程: 由c〔1,6〕=3可知,第5次合併的兩堆石子分別由子序列〔1,3〕和子序列〔4,3〕經4次合併後得出。其中c〔1,3〕=2可知由子序列〔1,3〕合併成的一堆石子是由子序列〔1,2〕和第三堆合併而來的。而c〔1,2〕=1,以表明了子序列〔1,2〕的合併方案是第1堆合併第2堆。 由此倒推回去,得出第1,第2次合併的方案,每次合併得分 第一次合併 3 4 6…… ->7 第二次合併 7 6…… ->13 13…… 子序列〔1,3〕經2次合併後合併成1堆, 2次合併的得分和=7+13=20。 c〔4,3〕=1,可知由子序列〔4,3〕合併成的一堆石子是由第4堆和子序列〔5, 2〕合併而來的。而c〔5,2〕=1,又表明了子序列〔5,2〕的合併方案是第5堆合併第6堆。由此倒推回去,得出第3、第4次合併的方案 每次合併得分: 第三次合併 ……54 2 ->6 第四次合併 ……5 6 ->11 ……11 子序列〔4,3〕經2次合併後合併成1堆,2次合併的得分和=6+11=17。 第五次合併是將最後兩堆合併成1堆,該次合併的得分爲24。 顯然,上述5次合併的得分總和爲最小 20+17+24=61 上述倒推過程,可由一個print(〔子序列〕)的遞歸算法描述 procedure print (〔i,j〕) begin if j〈〉1 then {繼續倒推合併過程 begin print(〔i,c〔i,j〕〕;{倒推子序列1的合併過程} print(〔i+c〔i,j〕-1〕mod n+1,j-c〔i,j〕) {倒推子序列2的合併過程} for K:=1 to N do{輸出當前被合併的兩堆石子} if (第K堆石子未從圈內去除) then begin if(K=i)or(K=X)then置第K堆石子待合併標誌 else第K堆石子未被合併; end;{then} 第i堆石子數←第i堆石子數+第X堆石子數; 將第X堆石子從圈內去除; end;{then} end;{print} 例如,調用print(〔1,6〕)後的結果如下: print(〔1,6〕)⑤ ┌──────┴──────┐ print(〔1,3〕)② print(〔4,3〕)④ ┌─────┴─────┐ ┌─────┴─────┐ print(〔1,2〕)① print(〔3,1〕) print(〔4,1〕) print(〔5,2〕)③ ┌──────┴──────┐ ┌──────┴──────┐ print(〔1,1〕) print(〔2,1〕) print(〔5,1〕) print(〔6,1〕) (圖6.2-5) 其中回溯至 ① 顯示 3 46 5 4 ② 顯示 7 65 4 2 ③ 顯示 13 54 2 ④ 顯示 135 6 ⑤ 顯示 13 11 注:調用print過程後,應顯示6堆石子的總數作爲第5次合併的得分。 Program Stones; Type Node = Record{當前序列的合併方案} c : Longint;{得分和} d : Byte{子序列1的堆數} End; SumType = Array [1..100,1..100] of Longint; {sumtype[i,j]-子序列[i,j]的石子總數} Var List : Array [1..100,1..100] of Node; {list[i,j]-子序列[i,j]的合併方案} Date, Dt : Array [1..100] of Integer; {Date[i]-第i堆石子數,Dt-暫存Date} Sum : ^SumType;{sum^[i,j]-指向子序列[i,j]的石子總數的指針} F : Text;{文件變量} Fn : String;{文件名串} N, i, j : Integer;{N-石子堆數,i,j-循環變量} Procedure Print(i, j : Byte);{遞歸打印子序列[i,j]的合併過程} Var k, x : Shortint;{k-循環變量;x-子序列2中首堆石子的序號} Begin If j <> 1 Then Begin{繼續倒推合併過程} Print(i, List[i,j].d);{倒推子序列1的合併過程} x := (i + List[i, j].d - 1) Mod N + 1;{求子序列2中首堆石子的序號} Print(x, j - List[i, j].d);{倒推子序列2的合併過程} For k := 1 to N Do{輸出當前合併第i堆,第x堆石子的方案} If Date[k] > 0 Then Begin If (i= k)or(x=k)Then Write(F, - Date[k], ' ') Else Write(F, Date[k], ' ') End; { Then } Writeln(F);{輸出換行符} Date[i] := Date[i] + Date[x];{原第i堆和第x堆合併成第i堆} Date[x] := - Date[x]{將原第x堆從圈內去除} End { Then } End; { Print } Procedure Main(s : Shortint); Var i, j, k : Integer; t, x : Longint; Begin For i := 1 to N Do Begin{僅含一堆石子的序列不存在合併} List[i, 1].c := 0; List[i, 1].d := 0 End; {For} For j := 2 to N Do{順推含2堆,含3堆……含N堆石子的各子序列的合併方案} For i := 1 to N Do Begin{當前考慮從第i堆數起,順時針數j堆的子序列} If s = 1 Then List[i, j].c := Maxlongint{合併[i,j]子序列的得分和初始化} Else List[i, j].c := 0; t := Sum^[i, j];{最後一次合併的得分爲[i,j]子序列的石子總數} For k := 1 to j - 1 Do Begin{子序列1的石子堆數依次考慮1堆……j-1堆} x := (i + k - 1) Mod N + 1;{求子序列2首堆序號} If (s=1) And (List[i,k].c + List[x,j-k].c+t < List[i, j].c) Or (s=2) And (List[i,k].c + List[x,j-k].c+t > List[i, j].c) { 若該合併方案爲目前最佳,則記下} Then Begin List[i, j].c := List[i, k].c + List[x, j - k].c + t; List[i, j].d := k End { Then } End { For } End; { For } {在子序列[1,N],[2,N],……,[N, N]中選擇得分總和最小(或最大)的一個子序列} k := 1; x := List[1, N].c; For i := 2 to N Do If (s = 1) And (List[i, N].c < x) Or (s = 2) And (List[i, N].c > x) Then Begin k := i; x := List[i, N].c End; { Then } Print(k, N);{由此出發,倒推合併過程} Writeln(F, Sum^[1, N]);{輸出最後一次將石子合併成一堆的石子總數} Writeln(F); Writeln(list[k, N].c) End; { Main } Begin Write('File name = ');{輸入文件名串} Readln(Fn); Assign(F, Fn);{該文件名串與文件變量連接} Reset(F);{文件讀準備} Readln(F, N);{讀入石子堆數} For i := 1 to N Do Read(F, Date[i]);{讀入每堆石子數} New(Sum);{求每一個子序列的石子數sum} For i := 1 to N Do Sum^[i, 1] := Date[i]; For j := 2 to N Do For i := 1 to N Do Sum^[i, j] := Date[i] + Sum^[i Mod N + 1, j - 1]; Dt := Date;{暫存合併前的各堆石子,結構相同的變量可相互賦值} Close(F);{關閉輸入文件} Assign(F, 'OUTPUT.TXT');{文件變量與輸出文件名串連接} Rewrite(F);{文件寫準備} Main(1);{求得分和最小的合併方案} Date := Dt;{恢復合併前的各堆石子} Main(2);{求得分和最大的合併方案} Close(F){關閉輸出文件} End.
轉載自:http://www.sdfz.com.cn/article/showarticle.asp?articleid=947 |