一.试题 在一个园形操场的四周摆放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 |