1. 程式人生 > >BFS(三):雙向廣度優先搜尋

BFS(三):雙向廣度優先搜尋

      所謂雙向廣度搜索指的是搜尋沿兩個方向同時進行:(1)正向搜尋:從初始結點向目標結點方向搜尋;(2)逆向搜尋:從目標結點向初始結點方向搜尋;當兩個方向的搜尋生成同一子結點時終止此搜尋過程。

      廣度雙向搜尋通常有兩種方法:(1)兩個方向交替擴充套件;(2)選擇結點個數較少的那個方向先擴充套件。方法(2)克服了兩方向結點的生成速度不平衡的狀態,可明顯提高效率。

【例1】Knight Moves (POJ 1915)

Description

Background
Mr Somurolov, fabulous chess-gamer indeed, asserts that no one else but him can move knights from one position to another so fast. Can you beat him?

The Problem
Your task is to write a program to calculate the minimum number of moves needed for a knight to reach one point from another, so that you have the chance to be faster than Somurolov.
For people not familiar with chess, the possible knight moves are shown in Figure 1.

Input

The input begins with the number n of scenarios on a single line by itself.

Next follow n scenarios. Each scenario consists of three lines containing integer numbers. The first line specifies the length l of a side of the chess board (4 <= l <= 300). The entire board has size l * l. The second and third line contain pair of integers {0, ..., l-1}*{0, ..., l-1} specifying the starting and ending position of the knight on the board. The integers are separated by a single blank. You can assume that the positions are valid positions on the chess board of that scenario.
Output

For each scenario of the input you have to calculate the minimal amount of knight moves which are necessary to move from the starting point to the ending point. If starting point and ending point are equal,distance is zero. The distance must be written on a single line.
Sample Input

3
8
0 0
7 0
100
0 0
30 50
10
1 1
1 1
Sample Output

5
28
0

       (1)程式設計思路1。

       先採用一般的單向廣度優先搜尋方法。設定陣列step[305][305]記錄走到某個位置需要的步數,陣列vis[305][305]來記錄某個位置是否已訪問過(值0代表未訪問,1代表已訪問過)。

       用陣列q[]來模擬佇列,front和rear分別為隊頭和隊尾指標。單向廣度優先搜尋的框架可寫為:

           佇列初始化,即front=rear=0;

           起點座標入佇列;

           while (front<rear)      // 佇列不為空

           {

                  隊頭元素出隊,送cur結點;

                  若cur結點的座標等於終點座標,則搜尋完成,返回;

                  將cur結點按規則展出新結點next(即用迴圈進行8個方向的新結點生成);

                     若next結點未訪問過,則置相應的值,並將next結點入隊;

            }

      (2)採用單向廣度優先搜尋的源程式。

#include <stdio.h>

int vis[305][305], step[305][305];

int dx[] = {-2, -2, -1, 1, 2, 2, 1, -1};

int dy[] = {-1, 1, 2, 2, 1, -1, -2, -2};

struct point

{

     int x, y;

};

int BFS(int start_x,int start_y,int end_x,int end_y,int n)

// 在n*n的棋盤中搜索從起點(start_x,strat_y)到終點(end_x,end_y)所需的最少步數

{

     int front,rear,i;

     point cur,next,q[90005];

     front=rear=0;

     cur.x = start_x;

     cur.y = start_y;

     vis[start_x][start_y] = 1;   // 設定探索標記為1

     q[rear++] = cur;              // 起始座標入隊

     while (front < rear)

     {

         cur = q[front++];       // 隊頭結點座標出隊

         for (i=0; i<8; ++i)

         {

             next.x = cur.x + dx[i];

             next.y = cur.y + dy[i];

             if (next.x<0 || next.x>=n || next.y<0 || next.y>=n)

                 continue;

             if (next.x==end_x && next.y==end_y)   // 到達目標位置

                  return step[cur.x][cur.y]+1;

             if (!vis[next.x][next.y])

             {

                 vis[next.x][next.y] = 1;    

                 step[next.x][next.y] = step[cur.x][cur.y] + 1; // 記錄步數

                 q[rear++] = next; // 當前合法座標位置入隊

             }

        }

    }

    return -1;  // 若搜尋不成功,表示不可達

}

int main()

{

    int nCase,sx,sy,tx,ty,size,i,j;

    scanf("%d", &nCase);

    while (nCase--)

    {

         scanf("%d", &size);

         for (i=0;i<size;i++)

                for (j=0;j<size;j++)

                      vis[i][j]=step[i][j]=0;

         scanf("%d %d", &sx, &sy);

         scanf("%d %d", &tx, &ty);

         if (sx==tx && sy==ty)

         {

             printf("0\n");

         }

         else

         {

             printf("%d\n",BFS(sx,sy,tx,ty,size));

         }   

     }

     return 0;

 }

       (3)程式設計思路2。

      用同一個佇列來儲存正向和逆向擴充套件的結點。開始時,將起點座標和終點座標同時入佇列。這樣,第1個出隊的座標是起點,正向搜尋擴充套件佇列;第2個出隊的座標是終點,逆向搜尋擴充套件佇列。…,兩個方向的擴充套件依次交替進行。

       由於採用雙向搜尋,如何知道某個結點是正向還是逆向擴充套件來的呢?

       簡單修改vis[][]陣列元素的置值方法即可。初始時,vis陣列的全部元素值為0,由正向擴充套件來的結點的vis對應元素值置為1,由逆向擴充套件來的結點的vis對應元素值置為2。

      設當前結點為cur,由cur可以擴展出新結點next。若vis[next.x][next.y]==0,則next結點未訪問過,將next結點入隊並進行相應設定;若vis[next.x][next.y]!=0,則next結點已訪問過。由於vis[cur.x][cur.y]記錄的是當前擴充套件方向(1代表正向,2代表逆向),若vis[next.x][next.y] != vis[cur.x][cur.y],則表示next結點以前按相反的方向訪問過,正向和反向遇到了同一個結點,搜尋成功。

      (4)採用雙向廣度優先搜尋的源程式。

#include <stdio.h>

int vis[305][305], step[305][305];

int dx[] = {-2, -2, -1, 1, 2, 2, 1, -1};

int dy[] = {-1, 1, 2, 2, 1, -1, -2, -2};

struct point

{

     int x, y;

};

int BFS(int start_x,int start_y,int end_x,int end_y,int n)

// 在n*n的棋盤中搜索從起點(start_x,strat_y)到終點(end_x,end_y)所需的最少步數

{

     int front,rear,i;

      point cur,next,q[90005];

      front=rear=0;

     cur.x = start_x;

     cur.y = start_y;

     vis[start_x][start_y] = 1;  // 從起始位置開始的探索標記為1

     q[rear++] = cur;         // 起始座標入隊

     next.x = end_x;

     next.y = end_y;

     vis[end_x][end_y] = 2;   // 從終點位置開始的探索標記為 2

     q[rear++] = next;    // 終點座標入隊

     while (front < rear)

     {

         cur = q[front++];     /* 隊首結點座標出隊 */

         for (i=0; i<8; ++i)

         {

             next.x = cur.x + dx[i];

             next.y = cur.y + dy[i];

             if (next.x<0 || next.x>=n || next.y<0 || next.y>=n)

                 continue;

             if (!vis[next.x][next.y])

             {

                 vis[next.x][next.y] = vis[cur.x][cur.y];     // 設為與當前探索路徑相同的標記

                 step[next.x][next.y] = step[cur.x][cur.y] + 1; // 記錄步數

                 q[rear++] = next; // 當前合法座標位置入隊

             }

             else if (vis[cur.x][cur.y] != vis[next.x][next.y])

             {   // 說明從起點出發的探索與從終點出發的探索重合

                 return step[cur.x][cur.y]+step[next.x][next.y]+1;

             }

        }

    }

    return -1;  // 若搜尋不成功,表示不可達

}

int main()

{

    int nCase,sx,sy,tx,ty,size,i,j;

     scanf("%d", &nCase);

    while (nCase--)

    {

         scanf("%d", &size);

         for (i=0;i<size;i++)

               for (j=0;j<size;j++)

                    vis[i][j]=step[i][j]=0;

         scanf("%d %d", &sx, &sy);

         scanf("%d %d", &tx, &ty);

         if (sx==tx && sy==ty)

         {

             printf("0\n");

         }

         else

         {

             printf("%d\n",BFS(sx,sy,tx,ty,size));

         }   

     }

     return 0;

 }

(5)程式設計思路3。

      定義兩個佇列q1[]和q2[]分別用於兩個方向的擴充套件,兩個佇列的隊頭指標和隊尾指標分別為front1、front2和rear1、rear2。雙向廣度優先搜尋的框架還可寫成:
      void BFS()
      {

           將起始節點放入佇列q1,將目的節點放入佇列q2;

           當兩個佇列都未空時,作如下迴圈
           {
                 如果佇列q1裡的未處理節點比q2中的少(即rear1-front1 < rear2-front2),

                        則擴充套件佇列q1;

                 否則擴充套件佇列q2;
             }

      }

      (6)採用兩個佇列的雙向廣度優先搜尋方法的源程式。

#include <stdio.h>

int vis[305][305], step[305][305];

int dx[] = {-2, -2, -1, 1, 2, 2, 1, -1};

int dy[] = {-1, 1, 2, 2, 1, -1, -2, -2};

struct point

{

     int x, y;

};

int BFS(int start_x,int start_y,int end_x,int end_y,int n)

// 在n*n的棋盤中搜索從起點(start_x,strat_y)到終點(end_x,end_y)所需的最少步數

{

     int front1,rear1,front2,rear2,i,flag;

     point cur,next,q1[45001],q2[45001];

     front1=rear1=0;

     front2=rear2=0;

     cur.x = start_x;

     cur.y = start_y;

     vis[start_x][start_y] = 1;  // 設定正向探索標記為1

     q1[rear1++] = cur;           // 起始座標入正向佇列

     next.x = end_x;

     next.y = end_y;

     vis[end_x][end_y] = 2;      // 設定逆向探索標記為2

     q2[rear2++] = next;          // 終點座標入逆向佇列

     while (front1 < rear1 && front2<rear2)

     {

            if (rear1-front1 < rear2-front2)

                    {

                             cur = q1[front1++]; flag=1;    // 擴充套件正向佇列

                    }

                    else

                    {

                             cur = q2[front2++]; flag=2;    // 擴充套件逆向佇列

                    }

           for (i=0; i<8; ++i)

          {

             next.x = cur.x + dx[i];

             next.y = cur.y + dy[i];

             if (next.x<0 || next.x>=n || next.y<0 || next.y>=n)

                 continue;

             if (!vis[next.x][next.y])

             {

                 vis[next.x][next.y] = flag;    

                 step[next.x][next.y] = step[cur.x][cur.y] + 1;

                 if (flag==1)

                    q1[rear1++] = next;

                                      else

                    q2[rear2++] = next;

             }

             else if (vis[cur.x][cur.y] != vis[next.x][next.y])

             {

                    return step[cur.x][cur.y]+step[next.x][next.y]+1;

             }

        }

    }

    return -1;  // 若搜尋不成功,表示不可達

}

int main()

{

    int nCase,sx,sy,tx,ty,size,i,j;

    scanf("%d", &nCase);

    while (nCase--)

    {

         scanf("%d", &size);

         for (i=0;i<size;i++)

               for (j=0;j<size;j++)

                      vis[i][j]=step[i][j]=0;

         scanf("%d %d", &sx, &sy);

         scanf("%d %d", &tx, &ty);

         if (sx==tx && sy==ty)

         {

             printf("0\n");

         }

         else

         {

             printf("%d\n",BFS(sx,sy,tx,ty,size));

         }   

     }

     return 0;

 }

 

 

 

&n