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); 
{终止条件:任一方队空(无解)或搜索深度超过1010步内无解) 
或双方均溢出(可能有解也可能无解,应尽量避免,要尽量把节 
点数组开大一点,采用双向搜索,采取剪枝措施等)
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.

 

posted @ 2010-10-11 21:19  lj_cherish  阅读(761)  评论(0编辑  收藏  举报