搜索与回溯算法(一)

简单深度优先搜索问题

搜索与回溯是计算机解题中常用的算法,很多问题无法根据某种确定的计算法则来求解,可以利用搜索与回溯的技术求解。回溯是搜索算法中的一种控制策略。它的基本思想是:为了求得问题的解,先选择某一种可能情况向前探索,在探索过程中,一旦发现原来的选择是错误的,就退回一步重新选择,继续向前探索,如此反复进行,直至得到解或证明无解。
       如迷宫问题:进入迷宫后,先随意选择一个前进方向,一步步向前试探前进,如果碰到死胡同,说明前进方向已无路可走,这时,首先看其它方向是否还有路可走,如果有路可走,则沿该方向再向前试探;如果已无路可走,则返回一步,再看其它方向是否还有路可走;如果有路可走,则沿该方向再向前试探。按此原则不断搜索回溯再搜索,直到找到新的出路或从原路返回入口处无解为止。


递归回溯法算法框架

 1 procedure Search(k:integer);
 2 begin
 3    for i:=1 to 算符种数 Do
 4      if  满足条件 then 
 5        begin
 6           保存结果
 7           if  到目的地 then 输出解
 8             else Search(k+1);
 9           恢复:保存结果之前的状态{回溯一步}
10        end;
11 end;
递归回溯法算法框架[一]
 1 procedure Search(k:integer);
 2 begin
 3   if  到目的地 then 输出解
 4     else
 5        for i:=1 to 算符种数 Do
 6          if  满足条件 then 
 7            begin
 8               保存结果
 9               Search(k+1,参数表);
10               恢复:保存结果之前的状态
11           end;
12 end;
递归回溯法算法框架[二]
 1 procedure Search(k:integer);
 2 begin
 3   if  到目的地 then
 4      begin
 5         输出解;
 6         exit;
 7       end;
 8   for i:=1 to 算符种数 Do
 9        if  满足条件 then 
10            begin
11               保存结果
12               Search(k+1,参数表);
13               恢复:保存结果之前的状态    
14           end;
15 end;
递归回溯法算法框架[三]

 

【例1】求从1~4中任意挑出3个数的所有组合方案。

1 For i:=1 to 4 do
2   For j:=i+1 to 4 do
3      For k:=j+1 to 4 do writeln(i,j,k);
直接枚举
1 Procedure DFS(k:integer);
2   Var i:integer;
3   Begin
4       If k>3 then begin print(Ans);exit;end;
5       For i:=1 to 4 do
6         if i>Ans[k-1] then 
7           begin Ans[k]:=I; DFS(k+1);end;
8   End;
深搜

【例2】求从1~N中任意挑出m个数的所有组合方案。

1 Procedure DFS(k:integer);
2   Var i:integer;
3   Begin
4      If k>M then begin print(Ans);exit;end;
5    For i:=1 to N do
6       if i>Ans[k-1] then 
7           begin Ans[k]:=I; DFS(k+1);end;
8 End;
深搜
 1 type arr=array[0..50] of integer;
 2 var  n,m,i:integer;
 3        ans:arr;
 4 procedure print(a:arr);
 5   begin
 6     for i:=1 to m do write(ans[i]); writeln;
 7   end;
 8 procedure dfs(k:integer);
 9   var i:integer;
10   begin
11     if k>m then begin print(ans);exit;end;
12     for i:=ans[k-1]+1 to n do 
13        begin  ans[k]:=I; dfs(k+1);  end;
14   end;
15 begin
16   readln(n,m);
17   dfs(1);
18 end.
完整程序

【例3】求1,2,3三个数的全排列并输出。

1 For i1:=1 to 3 do
2   For i2:=1 to 3 do
3     For i3:=1 to 3 do
4      If (i1<>i2) and (i2<>i3)and(i3<>i1) 
5        then writeln(i1,i2,i3);
直接枚举
1 For i1:=1 to 3 do
2   For i2:=1 to 3 do
3     If i2<>i1 then
4       For i3:=1 to 3 do
5        If (i2<>i3) and (i1<>i3) then
6           writeln(i1,i2,i3);
剪枝枚举
 1 S:=[];
 2     For i1:=1 to 3 do begin
 3       S:= S + [i1];
 4       For i2:=1 to 3 do
 5         If not (i2 in S) then begin
 6          S:= S + [i2];
 7          For i3:=1 to 3 do 
 8            If not(i3 in S) then writeln(i1,i2,i3);
 9          S:=S-[i2];
10         End;
11       S:=S-[i1];
12      End;
集合
 1 Procedure DFS(k:integer);
 2   Var i:integer;
 3   Begin
 4     If k>3 then begin print(Ans);exit;end;
 5     For i:=1 to 3 do 
 6       If not(i in S) then begin
 7         S:=S+[i];
 8         Ans[k]:=i;
 9         DFS(k+1);
10         S:=S-[i];
11       End; 
12    End;
深搜

【例4】求从1~N中任意挑出m个数的所有排列方案。

 1 Procedure DFS(k:integer);
 2   Var i:integer;
 3   Begin
 4      If k>M then begin print(Ans);exit;end;
 5      For i:=1 to N do 
 6        If not(i in S) then begin
 7          S:=S+[i];
 8          Ans[k]:=i;
 9          DFS(k+1);
10          S:=S-[i];
11        End; 
12   End;
解法一
 1 Procedure DFS(s:se;k:integer);
 2   Var i:integer;
 3   Begin
 4      If k>M then begin print(Ans);exit;end;
 5      For i:=1 to N do 
 6        If not(i in S) then begin
 7          Ans[k]:=i;
 8          DFS(s+[i],k+1);
 9         End; 
10   End;
解法二

由上边的程序代码,我们可以看出深搜算法其实也是枚举,只不过采用了递归结构使枚举的层数可以更多,代码结构更清晰罢了。

注意:由于递归结构在pascal里使用的是系统栈,空间有限,所以尽量不要在子程序里定义太多变量甚至数组,否则递归层数多了容易造成系统栈溢出,建议能定义成全局变量的尽量用全局变量,如果使用了全局变量,注意递归后要恢复保存前的状态

【例5】八皇后问题:要在国际象棋棋盘中放八个皇后,使任意两个皇后都不能互相吃。(提示:皇后能吃同一行、同一列、同一对角线的任意棋子)

放置第i个(行)皇后的算法为:

 1 procedure Search(i);
 2 begin
 3   for 第i个皇后的位置=1 to 8 do;       //在本行的8列中去试
 4    if 本行本列允许放置皇后 then
 5     begin
 6      放置第i个皇后;
 7                   对放置皇后的位置进行标记;
 8      if i=8 then 输出                 //已经放完个皇后
 9         else Search(i+1);          //放置第i+1个皇后
10      对放置皇后的位置释放标记,尝试下一个位置是否可行;
11     end12 end
View Code 

【算法分析】
        显然问题的关键在于如何判定某个皇后所在的行、列、斜线上是否有别的皇后;

        可以从矩阵的特点上找到规律,如果在同一行,则行号相同;

                                               如果在同一列上,则列号相同;

                                               如果同在/ 斜线上的行列值之和相同;

                                               如果同在\ 斜线上的行列值之差相同;

从下图可验证:
搜索与回溯算法(一)_第1张图片

考虑每行有且仅有一个皇后,设一维数组A[1..8]表示皇后的放置:第i行皇后放在第j列,用A[i]=j来表示,即下标是行数内容是列数。例如:A[3]=5就表示第3个皇后在第3行第5列上。

      判断皇后是否安全,即检查同一列、同一对角线是否已有皇后,建立标志数组b[1..8]控制同一列只能有一个皇后,若两皇后在同一对角线上,则其行列坐标之和行列坐标之差相等,故亦可建立标志数组c[1..16]、d[-7..7]控制同一对角线上只能有一个皇后。
      如果斜线不分方向,则同一斜线上两皇后的行号之差的绝对值与列号之差的绝对值相同。在这种方式下,要表示两个皇后I和J不在同一列或斜线上的条件可以描述为:A[I]<>A[J] AND ABS(I-J)<>ABS(A[I]-A[J]){I和J分别表示两个皇后的行号}

 1 program ex5_4;
 2 var  a:array[1..8] of integer;
 3     b:array[1..8] of boolean;
 4     c:array[1..16] of boolean;
 5     d:array[-7..7] of boolean;
 6     sum:integer;
 7 procedure print;
 8 var   i:integer;
 9 begin
10    inc(sum); writeln(' sum=',sum);    //方案数累加1
11    for i:=1 to 8 do write(a[i]:4);    //输出一种方案
12 end;
13 procedure Search(t:integer);
14 var   j:integer;
15 begin
16     for j:=1 to 8 do                 //每个皇后都有8位置(列)可以试放
17        if b[j] and c[t+j] and d[t-j] then      //寻找放置皇后的位置
18         begin                            //放置皇后,建立相应标志值
19            a[t]:=j;                        //摆放皇后
20         b[j]:=false;                    //宣布占领第j列
21         c[t+j]:=false; d[t-j]:=false;   //占领两个对角线
22       if t=8 then print                 //个皇后都放置好,输出
23           else Search(t+1);                //继续递归放置下一个皇后
24      b[j]:=true;              //递归返回即为回溯一步,当前皇后退出
25      c[t+j]:=true;  d[t-j]:=true;
26        end;
27 end;
28 BEGIN           
29  fillchar(b,sizeof(b),#1);        //数组b、c、d初始化,赋初值True
30  fillchar(c,sizeof(c),#1);
31  fillchar(d,sizeof(d),#1);
32  sum:=0;                     //用于统计方案数
33  Search(1);             //从第1个皇后开始放置
34 END.
参考程序

【例】任何一个大于1的自然数n,总可以拆分成若干个小于n的自然数之和。

当n=7共14种拆分方法:

7=1+1+1+1+1+1+1

7=1+1+1+1+1+2

7=1+1+1+1+3

7=1+1+1+2+2

7=1+1+1+4

7=1+1+2+3

7=1+1+5

7=1+2+2+2

7=1+2+4

7=1+3+3

7=1+6

7=2+2+3

7=2+5

7=3+4

total=14

 1 【参考程序】
 2 program ex5_3;
 3 var a:array[0..100]of integer;
 4       n,t,total:integer;
 5 procedure print(t:integer);
 6 var i:integer;
 7 begin
 8   write(n,'=');
 9   for i:=1 to t-1 do  write(a[i],'+');   
10       //输出一种拆分方案
11   writeln(a[t]);
12   total:=total+1;                    
13       //方案数累加1
14 end;
15 procedure Search(s,t:integer);
16 var i:integer;
17 begin
18    for i:=1 to s do
19      if (a[t-1]<=i)and(i<n) then       
20    //当前数i要大于等于前1位数,
21       且不过n
22 begin
23         a[t]:=i;                     
24         //保存当前拆分的数i
25         s:=s-a[t];                   
26         //s减去数i, s的值将继续拆分
27         if s=0 then print(t)          
28         //当s=0时,拆分结束输出结果
29           else Search(s,t+1);    
30         //当s>0时,继续递归
31         s:=s+a[t];                
32         //回溯:加上拆分的数,
33             以便产分所有可能的拆分
34       end;
35 end;
36 BEGIN
37   readln(n);
38   Search(n,1);                     
39       //将要拆分的数n传递给s
40   writeln('total=',total);             
41      //输出拆分的方案数
42   readln;
43 END.

 

你可能感兴趣的:(搜索与回溯算法(一))