4.4 深度优先搜索中的优化策略
4.4 深度优先搜索中的优化策略
例12:字串变换。
[问题描述]:
已知有两个字串 A$, B$ 及一组字串变换的规则(至多6个规则):
A1$ -> B1$
A2$ -> B2$
规则的含义为:在 A$中的子串 A1$ 可以变换为 B1$、A2$ 可以变换为 B2$ …。
例如:A$='abcd' B$='xyz'
变换规则为:
‘abc’->‘xu’ ‘ud’->‘y’ ‘y’->‘yz’
则此时,A$ 可以经过一系列的变换变为 B$,其变换的过程为:
‘abcd’->‘xud’->‘xy’->‘xyz’
共进行了三次变换,使得 A$ 变换为B$。
[输入] :文件格式如下:
A$ B$
A1$ B1$
A2$ B2$ 变换规则
... ...
所有字符串长度的上限为 20。
[输出] :输出至屏幕。格式如下:
若在10步(包含10步)以内能将A$变换为B$ ,则输出最少的变换步数;否则输出"NO ANSWER!"
[输入样例]
abcd xyz
abc xu
ud y
y yz
[输出样例]
3
procedure search(depth:integer;s:string);
var i,j,L:integer;
begin
if depth>=answer then exit;
if s=b then begin answer:=depth; exit; end;
L=length(s);
for i:=1 to n do
for j:=1 to L-len[i]+1 do
if copy(s,j,len[i])=s1[i] then
search(depth+1,copy(s,1,j-1)+s2[i]+copy(s,j+len[i],L-j-len[i]+1));
end;
{main}
search(0,A);
参考程序
优化前:
program aa;
var a,b:string;
n,ans:integer;
len:array[1..6] of integer;
s1,s2:array[1..6] of string;
procedure init(var s1,s2:string);
var s:string;
begin
readln(s);
s1:=copy(s,1,pos(' ',s)-1);
s2:=copy(s,pos(' ',s)+1,length(s)-pos(' ',s));
end;
procedure dfs(g:integer; s:string);
var i,j,l:integer;
begin
if (g>n) then exit;
if s=b then
begin
ans:=g;
exit;
end;
L:=length(s);
for i:=1 to n do
for j:=1 to L-len[i]+1 do
//枚举替换的开始位置
if (copy(s,j,len[i])=s1[i]) then
dfs(g+1,copy(s,1,j-1)+s2[i]+copy(s,j+len[i],L-j-len[i]+1));
end;
begin
assign(input,'a1.in');
reset(input);
assign(output,'a1.out');
rewrite(output);
init(a,b);
n:=0;
while (not seekeof) do
begin
inc(n);
init(s1[n],s2[n]); //区分s1[i], s2[i]
len[n]:=length(s1[n]); //记录s1[i]的长度为 len[i]
end;
ans:=11;
dfs(0,a);
if (ans>=11) then writeln('NO answer')
else write(ans);
close(input);
close(output);
end.
优化方法1:
1、分支定界法。即在不知道最优解的时候,先用一重循环枚举最优解,然后用搜索判断是否可行。
2、剪枝。其实就跟走迷宫避开死胡同差不多。若我们把搜索的过程看成是对一棵树的遍历,那么剪枝顾名思义,就是将树中的一些“死胡同”,不能到达我们需要的解的枝条“剪”掉,以减少搜索的时间。
搜索算法,绝大部分需要用到剪枝。然而,不是所有的枝条都可以剪掉,这就需要通过设计出合理的判断方法,以决定某一分支的取舍。在设计判断方法的时候,需要遵循一定的原则。
剪枝的原则
(1)正确性
枝条不是爱剪就能剪的。如果随便剪枝,把带有最优解的那一分支也剪掉了的话,剪枝也就失去了意义。所以,剪枝的前提是一定要保证不丢失正确的结果。
(2)准确性
在保证了正确性的基础上,我们应该根据具体问题具体分析,采用合适的判断手段,使不包含最优解的枝条尽可能多的被剪去,以达到程序“最优化”的目的。可以说,剪枝的准确性,是衡量一个优化算法好坏的标准。
(3)高效性 设计优化程序的根本目的,是要减少搜索的次数,使程序运行的时间减少。但为了使搜索次数尽可能的减少,我们又必须花工夫设计出一个准确性较高的优化算法,而当算法的准确性升高,其判断的次数必定增多,从而又导致耗时的增多,这便引出了矛盾。
因此,如何在优化与效率之间寻找一个平衡点,使得程序的时间复杂度尽可能降低,同样是非常重要的。倘若一个剪枝的判断效果非常好,但是它却需要耗费大量的时间来判断、比较,结果整个程序运行起来也跟没有优化过的没什么区别,这样就太得不偿失了。
综上所述,我们可以把剪枝优化的主要原则归结为六个字:正确、准确、高效。
procedure search(depth:integer;s:string;last:integer);
var i,j,L:integer;
begin
if depth=0 then
begin
if s=b then begin writeln(answer);halt; end;
exit;
end;
L=length(s);
for i:=1 to n do
for j:=max(1,last-len[i]+1) to L-len[i]+1 do {减少重复搜索}
if copy(s,j,len[i])=s1[i] then
search(depth-1,copy(s,1,j-1)+s2[i]+copy(s,j+len[i],L-j-len[i]+1));
end;
{main}
for answer:=0 to 10 do {分支定界}
search(answer,A,1);
参考程序:
优化后:
program asdf;
var a,b:string;
n,ans:longint;
s1,s2:array[1..6] of string;
len:array[1..6] of integer;
procedure init(var s3,s4:string);
var s:string;
begin
readln(s);
s3:=copy(s,1,pos(' ',s)-1);
s4:=copy(s,pos(' ',s)+1,length(s)-pos(' ',s));
end;
function max(x,y:integer):integer;
begin
if x>y then exit(x) else exit(y);
end;
procedure shen(g:integer; s:string; last:integer);
var i,j,l:integer;
begin
if g=0 then // exit;
begin
if s=b then
begin
writeln(ans); close(output);halt;
end;
exit;
end;
l:=length(s);
for i:=1 to n do
for j:=max(1,last-len[i]+1) to l-len[i]+1 do
if copy(s,j,len[i])=s1[i] then
shen(g-1,copy(s,1,j-1)+s2[i]+copy(s,j+len[i],l-j-len[i]+1),j);
end;
begin
assign(input,'a1.in'); reset(input);
assign(output,'kk.out'); rewrite(output);
init(a,b);
n:=0;
while not seekeof do
begin
inc(n);
init(s1[n],s2[n]);
len[n]:=length(s1[n]);
end;
for ans:=0 to 10 do shen(ans,a,1);
writeln('No');
close(input); close(output);
end.
双向广搜
1.1 广度双向搜索的概念
所谓双向搜索指的是搜索沿两个方向同时进行:
正向搜索:从初始结点向目标结点方向搜索;
逆向搜索:从目标结点向初始结点方向搜索;
当两个方向的搜索生成同一子结点时终止此搜索过程。
1. 3 广度双向搜索算法
广度双向搜索通常有两中方法:
1. 两个方向交替扩展
2. 选择结点个数较少的那个方向先扩展.
方法2克服了两方向结点的生成速度不平衡的状态,明显提高了效率。
算法说明:
设置两个队列c:array[0..1,1..maxn] of jid,分别表示正向和逆向的扩展队列。
设置两个头指针head:array[0..1] of integer 分别表示正向和逆向当前将扩展结点的头指针。
设置两个尾指针tail:array[0..1] of integer 分别表示正向和逆向的尾指针。
maxn表示队列最大长度。
算法描述如下:
1.主程序代码
repeat
{选择节点数较少且队列未空、未满的方向先扩展}
if (tail[0]<=tail[1]) and not((head[0]>=tail[0])or(tail[0]>=maxn)) then expand(0);
if (tail[1]<=tail[0]) and not((head[1]>=tail[1])or(tail[1]>=maxn)) then expand(1);
{如果一方搜索终止,继续另一方的搜索,直到两个方向都终止}
if not((head[0]>=tail[0])or(tail[0]>=maxn)) then expand(0);
if not((head[1]>=tail[1])or(tail[1]>=maxn)) then expand(1);
Until ((head[0]>=tail[0])or(tail[0]>=maxn)) And ((head[1]>=tail[1])or(tail[1]>=maxn))
2.expand(st:0..1)程序代码如下:
inc(head[st];
取出队列当前待扩展的结点c[st,head[st]]
for i:=1 to maxk do
begin
if tail[st]>=maxn then exit;
inc(tail[st]);
产生新结点;
check(st);{检查新节点是否重复}
end;
3.check(st:0..1)程序代码:
for i:=1 to tail[st]-1 do
if c[st,tail[st]]^.*=c[st,i]^.* then begin dec(tail[st]);exit end;
bool(st);{如果节点不重复,再判断是否到达目标状态}
4.bool(st:0..1)程序代码:
for i:=1 to tail[1-st] do
if c[st,tail[st]]^.*=c[1-st,i]^.* then print(st,tail[st],i);
{如果双向搜索相遇(即得到同一节点),则输出结果}
5.print(st,tail,k)程序代码:
if st=0 then begin print0(tail);print1(k) end;
else begin print0(k);print1(tail) end;
6.print0(m)程序代码:
if m<>0 then begin print(c[0,m]^.f);输出c[0,m]^.* end;
{输出正方向上产生的结点}
7.print1(m)程序代码;
n:=c[1,m]^.f
while m<>0
begin
输出c[1,n]^.*;
n:=c[1,n]^.f;
end
{输出反方向上产生的结点}
{$A-,B-,D-,E-,F-,G-,I-,L-,N-,O-,P-,Q-,R-,S-,T-,V-,X-,Y-}
{$M 8192,0,655360}
program NOIPG2;
const maxn=2300;
type
node=record{定义节点数据类型}
str:string[115];dep:byte;
end; {str表示字串,其长度不会超过115(长度超过115的字串
不可能通过变换成为目标字串,因为题目限定变换10次之内,且串长
不超过20,即起始串最多可经过5次变换时增长,中间串的最大长度
为20+5*19=115,否则经过余下的步数不可能变为长度不超过20的
目标串),dep表示深度}
ctype=array[1..maxn]of ^node;
bin=0..1;
var
maxk:byte;c:array [0..1]of ctype;
x0:array[0..6,0..1]of string[20];
filename:string;
open,closed:array [0..1] of integer;
procedure Init;{读取数据,初始化}
var f:text;temp:string;i,j:integer;
begin
for i:=0 to 1 do
for j:=1 to maxn do new(c[i,j]);
write(\'Input filename:\');readln(filename);
assign(f,filename);reset(f);i:=0;
while not eof(f) and (i<=6) do begin
readln(f,temp);
x0[i,0]:=copy(temp,1,pos(\' \',temp)-1);
x0[i,1]:=copy(temp,pos(\' \',temp)+1,length(temp));
inc(i);
end;
maxk:=i-1;close(f);
end;
procedure calc;
var i,j,k:integer;st:bin;
d:string;f:text;
procedure bool(st:bin);{判断是否到达目标状态或双向搜索相遇}
var i:integer;
begin
if x0[0,1-st]=c[st,closed[st]]^.str then begin
{如果到达目标状态,则输出结果,退出}
writeln(c[st,closed[st]]^.dep);
halt;
end;
for i:=1 to closed[1-st] do
if c[st,closed[st]]^.str=c[1-st,i]^.str then begin
{如果双向搜索相遇(即得到同一节点),
则输出结果(2个方向搜索的步数之和),退出}
writeln(c[st,closed[st]]^.dep+c[1-st,i]^.dep);
halt;
end;
end;
procedure checkup(st:bin);{判断节点是否与前面重复}
var i:integer;
begin
for i:=1 to closed[st]-1 do
if c[st,i]^.str=c[st,closed[st]]^.str then begin
dec(closed[st]);exit;{如果节点重复,则删除本节点}
end;
bool(st);{如果节点不重复,再判断是否到达目标状态}
end;
procedure expand(st:bin);{扩展产生新节点}
var i,j,k,lx,ld:integer;
begin
inc(open[st]);d:=c[st,open[st]]^.str;{队首节点出队}
k:=c[st,open[st]]^.dep;ld:=length(d);
for i:=1 to maxk do begin
{从队首节点(父节点)出发产生新节点(子节点)}
lx:=length(x0[i,st]);
for j:=1 to ld do begin
if (copy(d,j,lx)=x0[i,st]) and (length(copy(d,1,j-1)+x0[i,1-st]
+copy(d,j+lx,ld))<=115) then begin
{如果新节点的串长超过115,则不扩展!即剪掉此枝}
if closed[st]>=maxn then exit;{如果队列已满,只好退出}
inc(closed[st]);{新节点入队}
c[st,closed[st]]^.str:=copy(d,1,j-1)+x0[i,1-st]+copy(d,j+lx,ld);
c[st,closed[st]]^.dep:=k+1;{子节点深度=父节点深度+1}
checkup(st);{检查新节点是否重复}
end;
end;
end;
end;
Begin
for st:=0 to 1 do begin{正向(st=0)逆向(st=1)搜索节点队列初始化}
open[st]:=0;closed[st]:=1;
c[st,closed[st]]^.str:=x0[0,st];c[st,closed[st]]^.dep:=0;
bool(st);
end;
repeat
{选择节点数较少且队列未空、未满、深度未达到10的方向先扩展}
if (open[0]<=open[1]) and not ((open[0]>=closed[0]) or
(closed[0]>=maxn) or (c[0,closed[0]]^.dep>10)) then expand(0);
if (open[1]<=open[0]) and not ((open[1]>=closed[1]) or
(closed[1]>=maxn) or (c[1,closed[1]]^.dep>10)) then expand(1);
{如果一方搜索终止,继续另一方的搜索,直到两个方向都终止}
if not ((open[0]>=closed[0]) or (closed[0]>=maxn) or
(c[0,closed[0]]^.dep>10)) then expand(0);
if not ((open[1]>=closed[1]) or (closed[1]>=maxn) or
(c[1,closed[1]]^.dep>10)) then expand(1);
until (open[0]>=closed[0]) or (c[0,closed[0]]^.dep>10) or (closed[0]>=maxn)
and (closed[1]>=maxn) or (open[1]>=closed[1]) or (c[1,closed[1]]^.dep>10);
{终止条件:任一方队空(无解)或搜索深度超过10(10步内无解)
或双方均溢出(可能有解也可能无解,应尽量避免,要尽量把节
点数组开大一点,采用双向搜索,采取剪枝措施等)}
End;
BEGIN
init; calc; writeln(\'NO ANSWER!\')
END.
例13:数的划分
[问题描述]
将整数n分成k份,且每份不能为空,任意两分不能相同(不考虑顺序)。
例如:n=7,k=3,下面三种分法被认为是相同的。
1,1,5; 1,5,1; 5,1,1;
问有多少种不同的分法。
输入:n,k (6<n≤200,2≤k≤6)
输出:一个整数,即不同的分法。
[样例]:
输入:7 3
输出:4
[说明]:(此部分不用输出)
样例中的4种分法为:1,1,5; 1,2,4; 1,3,3; 2,2,3;
算法分析:
这是一道整数划分的问题。这种分解,较为直接的思路是递归。可以采用深度优先搜索的方法。我们定义一个过程,使其反复递归穷举第1份、第2份……第i份,然后寻找出可行的路径,时间复杂度O(nk)。这种方法思路便捷,但也会超时。
题目要求搜索时要有一定的顺序。比如我们可以规定后一个数总不小于前一个数。
【算法分析】
本题就是求把n无序划分为k份的方案数。也就是求方程x1+x2+…+xk=n,1=x0≤x1≤x2≤…≤xn的解数。
搜索的方法是依次枚举x1, x2, …, xk的值,然后判断。如果这样直接搜索,程序的速度是非常慢的。
下面用数学方法对搜索进行优化:
①如果我们已经知道了x1, x2, …,,正在枚举,当然不能小于,但是的上界呢?
≤n吗?≤n虽然是正确的,但是不需要这么大,可以证明≤(n- x1- x2-…-)/(k+1-i),具体证明过程请大家自行完成。
②提前回溯。
如果x1, x2, …, 都已经确定了,我们需要搜索吗?不需要。=n- x1- x2-…-,而且≥(证明需要利用前面一个优化),所以只要把计数器加一就可以了。
其实可以精益求精,其实都不用搜索的,只要把计数器加[left/2] -+1就可以了。
加了两个优化之后,搜索速度已经非常快了。
本题实现的时候,可以巧妙地通过递归传递参数的方法简化程序。
【参考程序】
var n,k,total:integer;
procedure search(u,last,deep:integer);
var i:integer;
begin
if deep=k then begin inc(total); exit; end;
for i:=last to u div 2 do
search(u-i,i,deep+1); {寻找从i开始的与u-i相符的k-(deep+1)个数}
end;
Begin
readln(n,k);
search(n,1,1);
writeln(total);
end.
优化后
program aa;
var a:array[1..100]of integer;
n,k:integer;
total:longint;
procedure mm(d,l,last:integer);
var i,j:integer;
begin
if d=2 then
begin
total:=total+l div 2-last+1;
for i:=1to l div 2-last+1 do
begin
a[2]:=last+i-1;
for j:=k downto 2 do
write(a[j],' ');
writeln(l-a[2]);
end;
end
else
for i:=last to l div d do
begin
a[d]:=i;
mm(d-1,l-i,i);
end;
end;
begin
readln(n,k);
mm(k,n,1);
writeln(total);
end.
例14:矩形覆盖
Description
[问题描述]:
在平面上有n个点(n≤100),每个点用一对整数坐标来表示。例如:当n=4时,4个点的坐标分别为:P1(1,1),P2(2,2),P3(6,3),P4(7,0)
这些点可以用k个矩形(k<=4)全部覆盖,矩形的边平行于坐标轴。如图一,当k=2是,可用如图二的两个矩形s1,s2覆盖,s1,s2面积和为4。问题是当n个点坐标和k给出后,怎样才能使得覆盖所有点的k个矩形的面积之和为最小呢。约定:
◇ 覆盖一个点的矩形面积为0;
◇ 覆盖平行于坐标轴直线上点的矩形面积也为0;
◇ 各个矩形间必须完全分开(边线、顶点也不能重合);
Input
n k
x1 y1
x2 y2
......
xn yn (0≤xi,yi≤500)
Output
仅含一个整数,即满足条件的最小的矩形面积之和。
Sample Input
4 2
1 1
2 2
3 6
0 7
Sample Output
4
type integer=longint;
tdata=record
sx,sy,tx,ty:integer;
end;
var n,k,i,answer: integer;
x,y:array [1..50] of integer;
data:array[1..4] of tdata;
procedure changemin(var a:integer;b:integer);
begin if b<a then a:=b; end;
procedure changemax(var a:integer;b:integer);
begin if b>a then a:=b; end;
procedure search(depth:integer);
var s,i,j:integer;temp:tdata;
begin
s:=0;
for i:=1 to k do
if data[i].sx<=500 then
begin
s:=s+(data[i].tx-data[i].sx)*(data[i].ty-data[i].sy);
for j:=i+1 to k do
if (data[j].sx<=500) and (data[j].sx<=data[i].tx) and
(data[i].sx<=data[j].tx) and (data[j].sy<=data[i].ty) and
(data[i].sy<=data[j].ty) then exit; {两个矩形相交就跳出}
end;
if s>=answer then exit;
if depth>n then begin answer:=s; exit; end;
for i:=1 to k do
begin
temp:=data[i];
changemin(data[i].sx,x[depth]);
changemin(data[i].sy,y[depth]);
changemax(data[i].tx,x[depth]);
changemax(data[i].ty,y[depth]);
search(depth+1);
data[i]:=temp;
if data[i].sx>500 then break; {避免重复搜索}
end;
end;
Begin
assign(input,'input.txt'); reset(input);
read(n,k);
for i:=1 to n do read(x[i],y[i]);
close(input);
answer:=maxlongint;
for i:=1 to k do
begin
data[i].sx:=501;
data[i].sy:=501;
data[i].tx:=-1;
data[i].ty:=-1;
end;
search(1);
writeln(answer);
end.