优先搜索算法

来源:互联网 发布:sublime js代码整理 编辑:程序博客网 时间:2024/05/16 16:09

使用计算机求解的问题中,有许多问题是无法用数学公式进行计算推导采用模拟方法来找出答案的。这样的问题往往需要我们根据问题所给定的一些条件,在问题的所有可能解中用某种方式找出问题的解来,这就是所谓的搜索法或搜索技术。
通常用搜索技术解决的问题可以分成两类:一类问题是给定初始结点,要求找出符合约束条件的目标结点;另一类问题是给出初始结点和目标结点,找出一条从初始结点到达目标结点的路径
常见的搜索算法有枚举法、广度优先搜索法、深度优先搜索法、双向广度优先搜索法,A*算法、回溯法、分支定界法等。这里来讨论一下广度优先搜索法。
一.广度优先搜索算法
一般来说,可以采用搜索算法解决的这类问题的特点是:
1.有一组具体的状态,状态是问题可能出现的每一种情况。全体状态所构成的状态空间是有限的,问题规模较小。
2.在问题的解答过程中,可以从一个状态按照问题给定的条件,转变为另外的一个或几个状态。
3.可以判断一个状态的合法性,并且有明确的一个或多个目标状态。
4.所要解决的问题是:根据给定的初始状态找出目标状态,或根据给定的初始状态和结束状态,找出一条从初始状态到结束状态的路径。
采用广度优先搜索算法解答问题时,需要构造一个表明状态特征和不同状态之间关系的数据结构,这种数据结构称为结点。根据问题所给定的条件,从一个结点出发,可以生成一个或多个新的结点,这个过程通常称为扩展。结点之间的关系一般可以表示成一棵树,它被称为解答树。搜索算法的搜索过程实际上就是根据初始条件和扩展规则构造一棵解答树并寻找符合目标状态的结点的过程。
广度优先搜索算法中,解答树上结点的扩展是沿结点深度的“断层”进行,也就是说,结点的扩展是按它们接近起始结点的程度依次进行的。首先生成第一层结点,同时检查目标结点是否在所生成的结点中,如果不在,则将所有的第一层结点逐一扩展,得到第二层结点,并检查第二层结点是否包含目标结点,...对长度为n+1的任一结点进行扩展之前,必须先考虑长度为n的结点的每种可能的状态。因此,对于同一层结点来说,求解问题的价值是相同的,我们可以按任意顺序来扩展它们。这里采用的原则是先生成的结点先扩展。
广度优先搜索算法中,为了便于进行搜索,要设置一个表存储所有的结点,为了满足先生成的结点先扩展的原则,存储结点的表一般设计成队列的数据结构。搜索过程中不断地从队列头取出结点进行扩展。对生成的新结点,要检查它是否已在队列中存在,还要检查它是否目标结点。如果新结点是目标结点,则搜索成功,程序结束;若新结点不是目标结点,并且未曾在队列中出现过,则将它加入到队列尾,否则将它丢弃,再从队列头取出结点进行扩展......。最终可能产生两种结果:找到目标结点,或扩展完所有结点而没有找到目标结点。
如果目标结点存在于解答树的有限层上,广度优先搜索算法一定能保证找到一条通向它的最佳路径,因此广度优先搜索算法特别适用于只需求出最优解的问题。当问题需要给出解的路径,则要保存每个结点的来源,也就是它是从哪一个节点扩展来的。
对于不同的问题,用广度优先搜索法的算法基本上都是一样的。但表示问题状态的结点数据结构、新结点是否目标结点和是否重复结点的判断等方面则有所不同,对具体的问题需要进行具体分析。
二.广度优先搜索算法的算法框架
Private Type TNode                           '定义一个结点数据类型
             ....                                           '根据具体问题确定所需的数据类型
             End Type
Dim State() As TNode                       '定义TNode类型的数组,作为存储结点的队列
Private Sub BFS()                              'BFS算法主程序
  Dim Temp As TNode                       'TNode型临时结点
  Dim Head As Integer,Tail As Integer '队列头指针和尾指针
  ReDim State(0)
  InputData                                             '从文件中读入数据进行初始化  
  Head=0
  Tail=0                                                  ' 队列头指针和尾指针都指向队列头
  Do  While  Head<=Tail                       '队列非空时循环
          '根据具体问题确定一个结点怎样扩展
   Temp= State(0)                                   '取队列头的结点
   If  Extend Then                                  '如果该结点可以扩展则产生一个新结点
    If  Not Repeat Then                          '如果新结点未曾在队列中出现过
     Tail=Tail+1                                      ' 将新结点加入队列尾   
     ReDim Preserve State(Tail)
     State(Tail) =Temp
     State(Tail) .Sire=Head                      '记录父结点标识
     If Found  Then                                  ' 如果新结点是目标结点
       Tail=Tail+1                                     ' 将队列尾结点的父结点指针指向队列尾   
       ReDim Preserve State(Tail)
       State(Tail) =Tail-1
       PrintPath                                        '输出路径
       Exit Sub                                         '退出程序  
     End If
    End If
  End If
  Head=Head+1                                     '队列头的结点扩展完后出队,取下一结点扩展
Loop         
End Sub
其中的
InputData是从文件中读入初始化的数据,对问题的初始状态等进行设置的子过程
Extend是判断结点是否能扩展的子过程,如果能则产生新结点
Repeat是检查新结点是否在队列中已经出现的函数,返回一个布尔值
Find则是检查新结点是否目标结点的函数,也返回一个布尔值
这些过程和函数要根据具体问题进行编写。另外,PrintPath用递归方式输出路径的子过程:
Private Sub PrintPath(State() As Node,ByVal  k As Integer)
  If k>0 Then
   k=State(k).Sire
   PrintPath State,k
   OutState State(k)
  End If
End Sub
因为当搜索到目标结点时,该结点记录的是其父结点的标识,所以需要再将队列尾指针前移,将其父结点指向当前的队尾,也即搜索到的目标结点。另外得到的路径是从队列尾回到队列头的逆向路径,输出时要逆转,所以采用递归方式,可以自动输出正向路径。子过程OutState与具体的问题有关,要视需要输出的结点信息而定。
这里将整个求解问题的过程分成了多个子过程,对不同的问题,程序的框架是不变的,只需根据实际情况编写子过程的代码即可。  
下面的例子中,虽然某些子过程只有一条语句,为与算法框架一致,仍使用单独的子过程表示。
三.广度优先搜索算法的例子
下面来看一些可以采用广度优先搜索算法求解的例子。
1.分油问题
一个一斤的瓶子装满油,另有一个七两和一个三两的空瓶,再没有其它工具。只用这三个瓶子怎样精确地把一斤油分成两个半斤油。
选择广度优先算法来求解分油问题可以得到通过最少步骤完成分油的最优解。
分油过程中需要表示的状态是各个油瓶所装的油,这里用一个数组来存放当前油瓶中的油,油瓶用数组下标区分,而油瓶中的油则是数组元素的值。表示分油状态的特征的结点应当包括各个油瓶的状态和结点的来源,也就是扩展出它的父结点。因此用一个自定义数据类型来表示。
分油过程中,要将油从一个油瓶倒入另一个油瓶,可能的情形只有6种,每种情形的序号与油瓶编号的关系如下表所示,这也是一个节点可能扩展的6种情形。


Private Type TNode                           '结点数据结构
            Bottle(2) As Integer               '当前油瓶中的油
            Sire As Integer                       '结点的父结点
            Souc As Integer                     '倒出有的油瓶
            Dest As Integer                      '倒入油的油瓶
            End Type
Dim State() As TNode                      '存储结点的队列
Dim Capacity(2) As Integer              '油瓶的容量
Dim Halves As Integer                      '平分的油

Private Sub InputData()                    '输入子过程,从文件中读入数据初始化初始结点
Dim Fname As String                      '文件名变量
With CommonDialog1                  
  .Filter = "(*.txt)|*.txt"
  .ShowOpen
  Fname = .FileName
End With
Open Fname For Input As #1
With State(0)
  For i = 0 To 2
   Input #1, .Bottle(i)
  Next
End With
For i = 0 To 2
  Input #1, Capacity(i)
Next
Input #1, Halves
Close
End Sub

Private Function Carry(Temp As TNode, ByVal i As Integer, _
                       ByVal j As Integer) As Boolean               '分油的子过程
With Temp
  If .Bottle(i) > 0 And Capacity(j) > .Bottle(j) Then        '若油瓶i非空,油瓶j不满      
   If .Bottle(i) > Capacity(j) - .Bottle(j) Then           ' 若油瓶i的油多于油瓶j剩余容量
    .Bottle(i) = .Bottle(i) - Capacity(j) + .Bottle(j)    '若油瓶i剩余的油
    .Bottle(j) = Capacity(j)                                         '油瓶j装满
   Else                                                                        '否则
    .Bottle(j) = .Bottle(i) + .Bottle(j)                          '当前油瓶j的油
    .Bottle(i) = 0                                                         '油瓶i空
   End If
   .Souc = i                                                                 '从油瓶i倒出油
   .Dest = j                                                                 '油瓶j倒入油
   Carry = True                                                          '可将油从油瓶i倒入油瓶j
  End If
End With
End Function

Private Function Rept(State() As TNode, Temp As TNode, _
                   ByVal k As Integer) As Boolean'子过程,判断新结点是否已出现在队列中
Dim i As Integer, j As Integer
For i = 0 To k
  Rept = True
  For j = 0 To 2
   If Temp.Bottle(j) <> State(i).Bottle(j) Then
    Rept = False
    Exit For
   End If
  Next
  If Rept = True Then Exit Function
Next
End Function

Private Function Find(Temp As TNode) As Boolean
                                                       '子过程,判断新结点是否目标结点
If Temp.Bottle(0) = Halves Or Temp.Bottle(1) = Halves Then Find = True
End Function

Private Sub OutS(Temp As TNode)   
With Temp
  Print .Souc; "-->"; .Dest; "  ";
  For i = 0 To 2
   Print .Bottle(i);
  Next
  Print
End With
End Sub

Private Sub PrintPath(State() As TNode, ByVal k As Integer)
If k > 0 Then
  k = State(k).Sire
  PrintPath State, k
  OutS State(k)
End If
End Sub

Private Sub BFS()                                                     'BFS主程序
Dim Temp As TNode
Dim Head As Integer, Tail As Integer
Dim i As Integer, j As Integer
Static m As Integer
Head = 0
Tail = 0
Do While Head <= Tail
  For i = 0 To 5
   Temp = State(Head)
    If Carry( Temp, i / 2, (i + 3) / 2 Mod 3) Then
     If Not Rept(State, Temp, Tail) Then
       Tail = Tail + 1
       ReDim Preserve State(Tail)
       State(Tail) = Temp
       State(Tail).Sire = Head
       If Find(Temp) Then
         Tail = Tail + 1
         ReDim Preserve State(Tail)
         State(Tail).Sire = Tail - 1
         PrintPath State, Tail
         Exit Sub
       End If
     End If
   End If
  Next
  Head = Head + 1
Loop
End Sub

 

二.移动球的问题
10个盒子排成一列,前面两个是空的,后面盒子相间放着4个红球和4个白球,若每次可移动任意两个相邻的球进入空盒,移动时两球不得更动其原来次序。目标是将4个红球连在一起,空盒位置不限。试编程,求出一种方案并输出每移动一次后的放球的状态。下图是一种球放置的最初状态,其中O表示空盒子,A表示红球,B表示白球。
O O A B A B A B A B
状态结点包含的信息有:用一个数组存放球的放置状态,父结点标识和空盒位置。
Private Type TNode
        Boxs(9) As String
        Sire As Integer
        Spac As Integer
        End Type
Dim State() As TNode
Dim n As Integer

Private Sub InputData()
Dim Fname As String
Dim i As Integer
On Error GoTo ExitSub
With CommonDialog1
  .Filter = "(*.txt)|*.txt"
  .ShowOpen
  Fname = .FileName
End With
Open Fname For Input As #1
With State(0)
  Input #1, n, .Spac
  For i = 0 To 2 * n - 1
   Input #1, .Boxs(i)
  Next
End With
Close
ExitSub:
End Sub

Private Function SpacMove(Temp As TNode, ByVal i As Integer) As Boolean
With Temp
  If Not (i - 1 = .Spac Or i = .Spac Or i + 1 = .Spac) Then
   .Boxs(.Spac) = .Boxs(i)
   .Boxs(.Spac + 1) = .Boxs(i + 1)
   .Boxs(i) = "O"
   .Boxs(i + 1) = "O"
   .Spac = i
   SpacMove = True
  End If
End With
End Function

Private Function Rept(State() As TNode, Temp As TNode, ByVal k As Integer) As Boolean
For i = 0 To k
  Rept = True
  For j = 0 To 2 * n - 1
   If State(i).Boxs(j) <> Temp.Boxs(j) Then
    Rept = False
    Exit For
   End If
  Next
  If Rept Then Exit Function
Next
End Function

Private Function Find(Temp As TNode) As Boolean
  i = 0
j = 0
Do While i < 7
  If Temp.Boxs(i) = "A" Then
   j = j + 1
   If j = 4 Then
    Find = True
    Exit Function
   End If
  Else
   If j > 0 Then Exit Do
  End If
  i = i + 1
Loop
End Function
        
Private Sub OutS(Temp As TNode)
For i = 0 To 2 * n - 1
  Print Temp.Boxs(i);
Next
Print
End Sub

Private Sub PrintPath(State() As TNode, ByVal k As Integer)
If k > 0 Then
  k = State(k).Sire
  PrintPath State, k
  OutS State(k)
End If
End Sub

Private Sub BFS()
Dim Temp As TNode
Dim Head As Integer, Tail As Integer
ReDim State(0)
InputData
Head = 0
Tail = 0
Do While Head <= Tail
  For i = 0 To 2 * (n - 1)
   Temp = State(Head)
   If SpacMove(Temp, i) Then
    If Not Rept(State, Temp, Tail) Then
     Tail = Tail + 1
     ReDim Preserve State(Tail)
     State(Tail) = Temp
     State(Tail).Sire = Head
     'OutS Temp
     If Find(Temp) Then
      Tail = Tail + 1
      ReDim Preserve State(Tail)
      State(Tail).Sire = Tail - 1
      PrintPath State, Tail
      Exit Sub
     End If
    End If
   End If
  Next
  Head = Head + 1
Loop
End Sub

三.农夫,狼,羊,菜过河,船只可运农夫和一件物品,农夫划船。要求全部安全过河,避免出现农夫不在时,羊吃菜,狼吃羊。
状态结点用一个数组Farmer存储农夫,狼,羊,菜的位置,数组元素为0时表示在河的原来的一边,为1时表示在河的另一边。状态的变化是农夫携带某个物品过河引起的。
Private Type TNode
            Farmer(3) As Integer
            Father As Integer
            End Type
Dim State() As TNode

Private Sub Init()                                         '子过程,初始化
ReDim State(0)
For i = 0 To 3
  State(0).Farmer(i) = 0
Next
End Sub

Private Function Security(Temp As TNode, ByVal i As Integer,  _
                                           ByVal j As Integer) As Boolean     '子过程,判断状态安全性
With Temp
  If j <> 0 And .Farmer(j) = .Farmer(0) Then
   .Farmer(j) = (.Farmer(j) + 1) Mod 2
  End If
  i = (i + 1) Mod 2
  For k = 1 To 3
   n = n + .Farmer(k) * 2 ^ (3 - k)
  Next
  n = n + i * 8
  If n < 5 Or n > 10 Then
   Temp.Farmer(0) = i
   Security = True
  End If   
End With
End Function

Private Function Rept(State() As TNode, Temp As TNode,  _
                                     ByVal Tail As Integer) As Boolean  '子过程,判断状态是否重复
For i = 0 To Tail
  Rept = True
  For j = 0 To 3
   If Temp.Farmer(j) <> State(i).Farmer(j) Then
    Rept = False
    Exit For
   End If
  Next
  If Rept Then Exit Function
Next
End Function

Private Sub OutS(Temp As TNode)
For i = 0 To 3
   Print Temp.Farmer(i);
Next
Print
End Sub

Private Sub PrintPath(State() As TNode, ByVal Tail As Integer)
If Tail > 0 Then
  Tail = State(Tail).father
  PrintPath State, Tail
  OutS State(Tail)
End If
End Sub

Private Function Find(Temp As TNode) As Boolean
For i = 0 To 3
  Find = True
  If Temp.Farmer(i) <> 1 Then
   Find = False
   Exit Function
  End If
Next
End Function

Private Sub BFS()
Dim Temp As TNode
Dim Head As Integer, Tail As Integer
Head = 0
Tail = 0
Do While Head <= Tail
  For j = 0 To 3
   Temp = State(Head)
   i = Temp.Farmer(0)                                         'i记录当前农夫在河的哪一边
   If Security(Temp, i, j) Then                             '如果新状态安全,则
    If Not Rept(State, Temp, Tail) Then              '新状态不是重复的,则
     Tail = Tail + 1                                                '将新状态加入队列
     ReDim Preserve State(Tail)
     State(Tail) = Temp
     State(Tail).father = Head
     If Found(Temp) Then
      Tail = Tail + 1
      ReDim Preserve State(Tail)
      State(Tail).father = Tail - 1
      PrintPath State, Tail
      Exit Sub
     End If
    End If
   End If
  Next
  Head = Head + 1
Loop
End Sub

四.跳马问题
找出国际象棋棋盘上的一个马从一个起始位置经过最少步数到达终止位置的路径。
问题分析:国际象棋的棋盘是64个格子,每个格子的位置从左上角开始计算,从左向右是1~8行、从上到下是1~8列,如(1,1)是棋盘左上角,而(8,8)是棋盘右下角。马是走L形路线:在一个方向走两格再在另一方向走一格,如果马在(i,j),则它可能走的位置有8个:i±2,j±1和i±1,j±2。用两个数组dx()和dy()存储坐标增量,便于编程。
表示马在某个状态的结点包含有马所在位置的行、和列,以及父结点标识。马走一步则状态变化一次。
用一个布尔数组标识马已经过的格子,简化了重复结点的判断。

Private Type TNode
        Rows As Integer
        Cols As Integer
        Sire As Integer
        End Type
Dim State() As TNode, Goal As TNode
Dim Rep(8, 8) As Boolean
Dim dx(7) As Integer, dy(7) As Integer

Private Sub InputData()
With CommonDialog1
  .Filter = "(*.txt)|*.txt"
  .ShowOpen
  fname = .FileName
End With
Open fname For Input As #1
Input #1, State(0).Rows, State(0).Cols
Input #1, Goal.Rows, Goal.Cols
For i = 0 To 7
  Input #1, dx(i), dy(i)
Next
Close
For i = 1 To 8
  For j = 0 To 8
   Rep(i, j) = False
  Next
Next
End Sub

Private Function HoresMove(Temp As TNode, ByVal k As Integer) As Boolean
With Temp
  If .Rows + dx(k) >= 1 And .Rows + dx(k) <= 8 And _
     .Cols + dy(k) >= 1 And .Cols + dy(k) <= 8 Then
   .Rows = .Rows + dx(k)
   .Cols = .Cols + dy(k)
   HoresMove = True
  End If
End With
End Function

Private Function Rept(Temp As TNode) As Boolean
With Temp
  If Not Rep(.Rows, .Cols) Then
   Rep(.Rows, .Cols) = True
  Else
   Rept = True
  End If
End With
End Function

Private Function Find(Temp As TNode) As Boolean
If Temp.Rows = Goal.Rows And Temp.Cols = Goal.Cols Then Find = True
End Function

Private Sub OutS(Temp As TNode)
Print Temp.Rows; Temp.Cols
End Sub

Private Sub PrintPath(State() As TNode, ByVal k As Integer)
If k > 0 Then
  k = State(k).Sire
  PrintPath State, k
  OutS State(k)
End If
End Sub

Private Sub BFS()
Dim Head As Integer, Tail As Integer
Dim Temp As TNode
ReDim State(0)
InputData
Head = 0
Tail = 0
Do While Head <= Tail
  For i = 0 To 7
   Temp = State(Head)
   If HoresMove(Temp, i) Then
    If Not Rept(Temp) Then
     Tail = Tail + 1
     ReDim Preserve State(Tail)
     State(Tail) = Temp
     State(Tail).Sire = Head
     If Find(Temp) Then
      Tail = Tail + 1
      ReDim Preserve State(Tail)
      State(Tail).Sire = Tail - 1
      PrintPath State, Tail
      Head = Head - 1
      Exit Sub
     End If
    End If
   End If
  Next
  Head = Head + 1
Loop

原创粉丝点击