深度优先搜索

深度优先搜索所遵循的搜索策略是尽可能“深”地搜索图。在深度优先搜索中,对于最新发现的结点,如果它还有以此为起点而未搜索的边,就沿此边继续搜索下去。当结点v的所有边都已被探寻过,搜索将回溯到发现结点v有那条边的始结点。这一过程一直进行到已发现从源结点可达的所有结点为止。如果还存在未被发现的结点,则选择其中一个作为源结点并重复以上过程,整个过程反复进行直到所有结点都被发现为止。

深度优先搜索的基本算法如下:

procedure dfs_try(i)                             {递归算法的过程}

  for i:=1 to maxr do

    begin

      if 子结点mr符合条件 then

        begin

          产生的子结点mr入栈;

          if 子结点mr是目标结点 then 输出;

            else dfs_try(i+1);

          栈顶元素出栈;

        end;

   end;

procedure dfs(dep)                                  {非递归算法的过程}

  dep:=0;

  repeat

    dep:=dep+1;

    j:=0;p:=false;

    repeat

      j:=j+1;

      if mr 符合条件 then

        begin

          产生子结点mr并将其记录;

          if 子结点是目标结点 then 输出并出栈;

            else p:=true;

         end

         else

           if j>=maxj then

             begin

               dep:=dep-1;                                 {回溯}

               if dep>0 then 取回栈顶元素;

                     else p:=true;

             end

            else p:=false;

        until p=true;

      until p=0;

全排列-基本算法

采用深度优先搜索算法,即对每一位上的数进行枚举而求得所有排列。一般利用递归完成。全排列是将一组数按一定顺序进行排列,如果这组数有n个,那么全排列数为n!个。

深度优先搜索法有两大基本特点:

1. 对已产生的结点按深度排序,深度大的先得到扩展,即先产生它的子结点;

2. 深度大的结点是后产生的,但先得到扩展,即“后产生先扩展”。因此用堆栈作为该算法的主要数据结构,存储产生的结点,先把产生的数入栈,产生栈顶(即深度最大的元素)子结点,子结点产生以后,出栈,再产生栈顶子结点。

例5 输入n,输出1,2,…,n的全排列(n≤8)。

输入输出示例

sample input:

3

sample output:

1 2 3

1 3 2

2 1 3

2 3 1

3 1 2

3 2 1

【算法分析】

输出全排列是一经典搜索问题。关键在于:数字不能重复出现。所以用一个数组vis记录每个数字是否出现过。每确定一个数字,就把这个数字的vis值设为true,需要注意的是:回溯的时候需要把vis重新设为false。

【参考程序】

var

  n:integer;

  a:array[1..8] of integer;

  vis:array[1..8] of boolean;

procedure search(depth:integer);

  var i:integer;

  begin

    if (depth>n) then

      begin

        for i:=1 to n-1 do

          write(a[i],′′);

        writeln(a[n]);                             

        exit;

       end;

     for i:=1 to n do                       {枚举下一个出现的数字}

     if not vis[i] then                      {判断是否出现过}

        begin

          a[depth]:=i;                          {没有出现过,把第depth个数设为i}

          vis[i]:=true;                          {i出现过}

          search(depth+1);                        {递归}

          vis[i]:=false;                           {恢复i}

        end;

    end;{search}

  begin

    read(n);

    fillchar(vis,sizeof(vis),false);                {全部没有出现}

    search(1);

  end.

附: exit 是退出当前程序块。即在任何子程序中执行 exit , 那么将退出这个子程序。如果是在主程序中执行 exit,那么将退出整个程序。

相当于 goto 这个程序块的末尾的 end 。

例如:试除法判断素数时,一旦整除,就把函数值赋为false ,然后exit。

注意:类似上面,exit 也是只对当前这一个子程序产生作用,如果多重嵌套子程序,那么其中某个子程序执行了exit 以后,将返回到调用它的那个语句的下一个语句。

例如:

function f(x:longint):longint;

var

begin

 if ... then exit(f(x+1));  <--这里指f:=f(x+1);把f(x+1)当作答案退出.

 ...

end;

例6 输入n,输出集合{1,2,…,n}的所有子集(n≤8)。

输入输出示例

sample input:

3

sample output:

{}

{3}

{2}

{2,3}

{1}

{1,3}

{1,2}

{1,2,3}

【算法分析】

输出所有子集也是一经典搜索问题。

设S是一个{1,2,…,n}的子集。可以这样认为:一个元素要么属于S,要么不属于S,只有这两种情况。

【参考程序】

var

  n:integer;

  size:integer;

  s:array[1..8] of integer;                         {子集元素个数}

procedure search(depth:integer);                        {子集元素}

  var i:integer;

  begin

    if (depth>n) then

      begin

        write(′{′);

        for i:=1 to size-1 do

          write(s[i],′,′);

        if (size>0) then

          write(s[size]);

         writeln(′}′)

         exit;

       end;

     search(depth+1);                           {depth不属于子集}

     size:=size+1;

     s[size]:=depth;

     search(depth+1);                             {depth属于子集}

     size:=size-1;

    end;{search}

  begin

    read(n);

    size:=0;

    search(1);

  end.

 以上两种搜索都是非常常用的方式,很多复杂的题目都可以转化为这种模式。

例7 resume问题

小修是个几何迷。她有一天画了一个正n边形,并且将n个顶点用1,2,…,n这n个连续自然数随手编了一下号。然后她又画了一些不相交的对角线,如图4-3所示。顶点旁的数字是小修随手编的号。她把所有的边和对角线都写在一张纸上。对图4-3,她写了:(1,3),(3,2),(2,4),(4,5),(5,1),(1,4),(3,4)。

过了几个星期,她无意中发现了这张写着字的纸,可是怎么也找不着那个几何图形了。她很想把n边形的编号复原,可是试了一天也没弄出来。你能帮助她吗?

输入数据

第一行n(n≤50)。

下面的若干行每行两个数a,b,表示纸上写着(a,b)。

输出数据

仅一行,按顺序依次输出顶点的编号,每两个之间用一个空格

隔开,行末不要有多余空格。对于上面的例子,你的输出应该是1 3 2 4 5。

1 5 4 2 3也是符合题目要求的。两者区别只是逆时针和顺时针而已。

但是你的输出只能是1 3 2 4 5!也就是说你必须把两个符合要求的输出比较大小

(先比较第一位;第一位相等就比较第二位;第二位相等……以此类推),你的输出

应该是较小者。

输入输出示例

sample input:

5

1 3

3 2

2 4

4 5

5 1

1 4

3 4

sample output:

1 3 2 4 5

【算法分析】

首先我们不要被题目所迷惑,我们可以抛开点之间的几何位置,其实这个n多边形的顶点顺序其实就是一个1到n的排列,,…,但是,要求满足-,-,…-,- 之间有边(其实就是图论中的哈密尔顿圈)。

由于题目中n≤50,所以我们只要一一枚举每一个排列,判断一下就可以了,本题已经和第一题非常相似了。

另外还有一点,输出要求最小。处理这一点的方法就是从小到大枚举下一个点的编号。起点的选择也应该从小到大(其实以1为始点肯定可以找到一个满足条件的排列)。                                                  

【参考程序】

const maxn=50;

var

  n:integer;

  a:array[1..maxn] of integer;

  g:array[1..maxn,1..maxn] of Boolean;            {g[i,j]表示i,j之间是否有边}

  vis:array[1..maxn] of Boolean;

procedure init;

  var

    u,v:integer;

  begin

    read(n);

    fillchar(g,sizeof(g),false);

    while not seekeof do                        {seekeof用于判断文件是否结束}

      begin

        read(u,v);

        g[u,v]:=true;

        g[v,u]:=true;

      end;

  end;{init}

procedure search(depth:integer);

  var i:integer;

  begin

    if (depth>n) then

      begin                                          {输出结果}

        if (g[a[1],a[n]]) then

          begin

            for i:=1 to n-1 do

              write(a[i],′′);

            writeln(a[n]);

            halt;

          end;

        exit;

      end;

    for i:=1 to n  do                                {枚举下一个点的编号}

      if (not vis[i]) and (g[a[depth-1],i]) then

        begin

          a[depth]:=i;

          vis[i]:=true;

          search(depth+1);

          vis[i]:=false;

        end;

 end;{search}

begin

  assign(input,'input.txt');reset(input);

  assign(output,'output.txt');rewrite(output);

  init;

  fillchar(vis,sizeof(vis),false);

  a[1]:=1;

  vis[1]:=true;

  search(2);

end.

附:SeekEoln和Eoln用于判断文件行结束,SeekEof和Eof用于判断文件结束,但它们之间还是有区别的。Eoln和Eof只判断当前的所在位置是否位于行结束符上或者文件尾部,而SeekEoln和SeekEof会自动跳过所有连续的空格和制表符再进行判断,因此,在执行函数SeekEoln或SeekEof后,当前的所在位置不会是空格或者制表符。

halt 直接中断该程序

exit 跳出过程或函数. 如果在主程序,则效果和halt一样。在函数中,exit(i);表示把i的值赋给函数并退出函数。

break 跳出本层循环

continue:跳过执行的这步for循环。

例8 染色问题

给定一张无向图,要求对图进行染色,有边相连的点不能染同一种颜色。求最少需要几种颜色可以染完。

输入数据:

n,m表示图的点数和边数。(n≤10)

m行每行两个整数,表示一条边。

输出数据:

最少需要的颜色数。

输入输出示例:

sample input:

5 5

1 2

2 3

3 4

4 5

5 1

sample output:

3

【算法分析】

题目要求求最小需要的颜色数目。如果图可以用k种颜色染,它一定可以用k+1种颜色染,同样如果图不可以用k种颜色染,它一定不可以用k-1种颜色染。所以可以从小到大依次枚举k,然后判断用k中颜色是否可以染。如何判断用k种颜色是否可以染呢?可以枚举每一个结点的颜色,判断是否有相连的点同色。

【参考程序】

const maxn=10;

var n,m,k:integer;

    colour:array[1..maxn] of integer;

    g:array[1..maxn,1..maxn] of boolean;         {g[i,j]表示i,j之间是否有边}

procedure init;

  var i,u,v:integer;

  begin

    read(n,m);

    fillchar(g,sizeof(g),false);

    for i:=1 to m do

      begin

        read(u,v);

        g[u,v]:=true;

        g[v,u]:=true;

      end;

  end;{init}

procedure search(depth:integer);

  var i:integer;

    r:array[1..maxn] of boolean;

  begin

    if (depth>n) then

      begin

        writeln(k);

        close(input);close(output);

        halt;

      end;

    fillchar(r,sizeof(r),false);    {r[i]表示与depth相连的点中是否有颜色为i的}

    for i:=1 to depth-1 do

      if (g[depth,i]) then

        r[colour[i]]:=true;

    for i:=1 to k do

      if not r[i] then

        begin

          colour[depth]:=i;

          search(depth+1);

        end;

   end;{search}

begin

  assign(input,'input.txt');reset(input);

  assign(output,'output.txt');rewrite(output);

  init;

  for k:=1 to n do search(1);

end.

 

 

         

  

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