第四章 搜索算法策略
4.1枚举算法
枚举法(通常也称穷举法)是指在一个有穷的可能的解的集合中,枚举出集合中的每一个元素,用题目给定的约束条件去判断其是否符合条件,若满足条件,则该元素即为整个问题的解;否则就不是问题的解。
【枚举算法解题必须满足下列条件】
⑴ 可预先确定解元素的个数n,且问题的规模不是很大;
⑵ 对于每个解变量A1,A2,…An的可能值必须为一个连续的值域。
【枚举算法实现】
通常使用嵌套的FOR结构循环语句枚举每个变量的取值,在最里层的循环中判断是否满足给定的条件,若满足则输出一个解。
例:求满足表达式A+B=C的所有整数解,其中A,B,C为1~3之间的整数。
分析:本题非常简单,即枚举所有情况,符合表达式即可。算法如下:
for A := 1 to 3 do
for B := 1 to 3 do
for C := 1 to 3 do
if A + B = C then
Writeln(A,‘+’,B,‘=’,C);
上例中的解变量有3个:A,B,C。其中
A解变量值的可能取值范围A∈{1,2,3}
B解变量值的可能取值范围B∈{1,2,3}
C解变量值的可能取值范围C∈{1,2,3}
则问题的可能解有27个
(A,B,C)∈{(1,1,1),(1,1,2),(1,1,3),
(1,2,1),(1,2,2),(1,2,3),
………………………………
(3,3,1),(3,3,2),(3,3,3)}
在上述可能解集合中,满足题目给定的检验条件的解元素,即为问题的解。
【枚举算法优化】
⑴ 减少枚举的变量。
在使用枚举法之前,先考虑一下解元素之间的关联,将那些能由已枚举解元素推算出来的变量直接用计算表达式代替。
⑵ 减少枚举变量的值域。
通过各枚举变量间的数学关系定义解元素的值域,在保证不遗漏的情况下越小越好。
⑶ 分解约束条件。
将拆分的约束条件嵌套在相应的循环体间,即尽可能根据约束条件减少变量个数和缩小值域。
例1:一根29cm长的尺子,只允许在上面刻七个刻度,要能用它量出1~29cm的各种长度。试问这刻度应该怎么选择?
const n=29; m=1;
var a:array [1..7] of byte;
b:array [1..n] of 0..1; i,j:integer;a2,a3,a4,a5,a6,a7:integer;
begin
fillchar(a,sizeof(a),0);
a[1]:=m;
for a2:=2 to n-7 do
for a3:=a2+1 to n-6 do
for a4:=a3+1 to n-5 do
for a5:=a4+1 to n-4 do
for a6:=a5+1 to n-3 do
for a7:=a6+1 to n-2 do
begin
a[2]:=a2;a[3]:=a3;a[4]:=a4;a[5]:=a5;a[6]:=a6;a[7]:=a7;
for i:=1 to 29 do b[i]:=0;
for i:=1 to 7 do
begin
b[a[i]]:=1; b[n-a[i]]:=1; b[n]:=1;
for j:=i+1 to 7 do b[abs(a[j]-a[i])]:=1;
end;
j:=0;
for i:=1 to n do j:=j+b[i];
if j=n then
for i:=1 to 7 do if i<>7 then write(a[i]:4) else writeln(a[i]:4);
end;
end.
例2: 巧妙填数
1 | 9 | 2 |
3 | 8 | 4 |
5 | 7 | 6 |
将1~9这九个数字填入九个空格中。每一横行的三个数字组成一个三位数。如果要使第二行的三位数是第一行的两倍, 第三行的三位数是第一行的三倍, 应怎样填数。如图
分析:本题目有9个格子,要求填数,如果不考虑问题给出的条件,共有9!=362880种方案,在这些方案中符合问题条件的即为解。因此可以采用枚举法。
但仔细分析问题,显然第一行的数不会超过400,实际上只要确定第一行的数就可以根据条件算出其他两行的数了。这样仅需枚举400次。因此设计参考程序:
program exam9;
var
i,j,k,s:integer;
function sum(s:integer):integer;
begin
sum:=s div 100 + s div 10 mod 10 + s mod 10
end;
function mul(s:integer):longint;
begin
mul:=(s div 100) * (s div 10 mod 10) * (s mod 10)
end;
begin
for i:=1 to 3 do
for j:=1 to 9 do
if j<>i then for k:=1 to 9 do
if (k<>j) and (k<>i) then begin
s := i*100 + j*10 +k; {求第一行数}
if 3*s<1000 then
if (sum(s)+sum(2*s)+sum(3*s)=45) and
(mul(s)*mul(2*s)*mul(3*s)=362880) then {满足条件,并数字都由1~9组成}
begin
writeln(s);
writeln(2*s);
writeln(3*s);
writeln;
end;
end;
end.
例3:有4个学生,上地理课时提出我国四大淡水湖的排序如下。
甲:洞庭湖最大,洪泽湖最小,鄱阳湖第三;
乙:洪泽湖最大,洞庭湖最小,鄱阳湖第二,太湖第三;
丙:红泽湖最小,洞庭湖第三;
丁:鄱阳湖最大,太湖最小,洪泽湖第二,洞庭湖第三;
对于各个湖泊应处的地位,每个人只说对了一个。
根据以上情况,编一个程序,让计算机判断各个湖泊应处在第几位。
分析:
var k,d,h,b,t:integer;
begin
k:=0;
for d:=1 to 4 do
for h:=1 to 4 do
begin
if h<> d then for b:=1 to 4 do
begin
if (b<>h) and (b<>d) then
begin
t:=10-d-h-b;
if ord(d=1)+ord(h=4)+ord(b=2)=1 then
begin
if ord(h=1)+ord(d=4)+ord(b=2)+ord(t=3)=1 then
if ord(h=4)+ord(d=3)=1 then
if ord(b=1)+ord(t=4)+ord(h=2)+ord(d=3)=1 then
begin
writeln('dth=',d);
writeln('hzh=',h);
writeln('byh=',b);
writeln('th=',t);
end;
end
else writeln('false');
end;
end;
end;
end.
例4:最佳游览线路 (NOI94)
某旅游区的街道成网格状(如图)。其中东西向的街道都是旅游街,南北向的街道都是林荫道。由于游客众多,旅游街被规定为单行道,游客在旅游街上只能从西向东走,在林阴道上则既可从南向北走,也可以从北向南走。
阿龙想到这个旅游区游玩。他的好友阿福给了他一些建议,用分值表示所有旅游街相邻两个路口之间的街道值得游览的程度,分值时从-100到100的整数,所有林阴道不打分。所有分值不可能全是负分。
例如图是被打过分的某旅游区的街道图:
阿龙可以从任一个路口开始游览,在任一个路口结束游览。请你写一个程序,帮助阿龙找一条最佳的游览线路,使得这条线路的所有分值总和最大。
输入数据:
输入文件是INPUT.TXT。文件的第一行是两个整数M和N,之间用一个空格符隔开,M表示有多少条旅游街(1≦M≦100),N表示有多少条林阴道(1≦M≦20001)。接下来的M行依次给出了由北向南每条旅游街的分值信息。每行有N-1个整数,依次表示了自西向东旅游街每一小段的分值。同一行相邻两个数之间用一个空格隔开。
输出数据:
输出文件是OUTPUT.TXT。文件只有一行,是一个整数,表示你的程序找到的最佳游览线路的总分值。
输入输出示例:
INPUT.TXT | OUTPUT.TXT |
3 6 -50 –47 36 –30 –23 17 –19 –34 –13 –8 -42 –3 –43 34 –45 | 84 |
分析:
设Lij为第I条旅游街上自西向东第J段的分值(1 ≦ I ≦ M,1 ≦ J ≦ N – 1)。例如样例中L12=-47,L23=-34,L34=34。
我们将网格状的旅游区街道以林荫道为界分为N – 1个段,每一段由M条旅游街的对应成段,即第J段成为{L1J,L2J,……,LMJ}(1≦ J ≦ N – 1)。由于游览方向规定横向自西向东,纵向即可沿林阴道由南向北,亦可由北向南,而横行的街段有分值,纵行无分值,因此最佳游览路现必须具备如下三个特征:
来自若干个连续的段,每一个段中取一个分值;
每一个分值是所在段中最大的;
起点段和终点段任意,但途经段的分值和最大。
设Li为第I个段中的分值最大的段。即Li=Max{L1I,L2I,……,LMI}(1≦I≦N – 1)。例如对于样例数据:
L1=Max(-50,17,-42)=17;
L2=Max(-47,-19,-3)=-3;
L3=Max(36,-34,-43)=36;
L4=Max(-30,-13,34)=34;
L5=Max(-23,-8,-45)=-8;
有了以上的基础,我们便可以通过图示描述解题过程,见下图。
我们把段设为顶点,所在段的最大分值设为顶点的权,各顶点按自西向东的顺序相连,组成一条游览路线。显然,如果确定西端为起点、东段为终点,则这条游览路线的总分值最大。
问题是某些段的最大分值可能是负值,而最优游览路线的起点和终点任意,在这种情况下,上述游览路线就不一定最佳了。因此,我们只能枚举这条游览路线的所有可能的子路线,从中找出一条子路线IàI+1à……àJ(1 ≦ I<J ≦ N – 1),使得经过顶点的权和LI+LI+1+……+LJ最大。
设Best为最佳游览路线的总分值,初始时为0;
Sum为当前游览路线的总分值。
我们可以得到如下算法:
Best := 0; Sum := 0;
for I := 1 to N – 2 do
for J := I + 1 to N – 1 do begin
Sum := LI + …… + LJ;
if Sum > Best then
Best := Sum;
end
显然,这个算法的时间复杂度为O(N2)。而N在1~20001之间,时间复杂度比较高。于是,我们必须对这个算法进行优化。
仍然从顶点1开始枚举最优路线。若当前子路线延伸至顶点I时发现总分值Sum≦0,则应放弃当前子路线。因为无论LI+1为何值,当前子路线延伸至顶点I+1后的总分值不会大于Li+1。因此应该从顶点I+1开始重新考虑新的一条子路线。通过这种优化,可以使得算法的时间复杂度降到了O(N)。
优化后的算法描述如下:
Best := 0; Sum := 0;
for I := 1 to N – 1 do
begin
Sum := Sum + LI;
if Sum > Best then
Best := Sum;
if Sum < 0 then
Sum := 0;
end
程序描述如下:
{$R-,S-,Q-}
{$M 65520,0,655360}
program Noi94;
const
MaxN = 20001; {林阴道数的上限}
Inp = 'input.txt';
Outp = 'output.txt';
var
M, N: Word; {旅游街数和林阴道数}
Best: Longint; {最佳游览路线的总分值}
Score: array [1..MaxN] of ShortInt; {描述每个段的最大分值}
procedure Init;
var
I, J, K: Integer;
Buffer: array [1 .. 4096] of Char; {文件缓冲区}
begin
Assign(Input, Inp);
Reset(Input);
SetTextBuf(Input, Buffer); {开辟文件缓冲区}
Readln(M, N); {读入旅游街数和林阴道数}
FillChar(Score,Sizeof(Score),-128); {初始化各段的最大分值}1000 0000
for I := 1 to M do {计算1~N –1段的最大分值 }
for J := 1 to N - 1 do begin
Read(K);
if K > Score[J] then Score[J] := K;
end;
Close(Input);
end;
procedure Out;
begin
Assign(Output, Outp);
Rewrite(Output);
Writeln(Best);
Close(Output);
end;
procedure Main;
var
I: Integer;
Sum: Longint; {当前游览路线的总分值}
begin
{最佳游览路线的总分值和当前游览路线的总分值初始化}
Best := 0;
Sum := 0;
for I := 1 to N - 1 do begin {顺序枚举游览路线的总分值}
Inc(Sum, Score[I]); {统计当前游览路线的总分值}
if Sum > Best then Best := Sum; {若当前最佳,则记下}
if Sum < 0 then Sum := 0; {若总分值<0,则考虑一条新路线}
end;
end;
begin
Init; {输入数据}
Main; {主过程}
Out; {输出}
end.