1. 程式人生 > >A*,Dijkstra,BFS演算法效能比較及A*演算法的應用

A*,Dijkstra,BFS演算法效能比較及A*演算法的應用

                    一之續、A*,Dijkstra,雙向BFS演算法效能比較及A*演算法的應用


作者:July   二零一一年三月十日。
出處:http://blog.csdn.net/v_JULY_v
-------------------------------------------------- 

引言:
    最短路徑的各路演算法A*演算法Dijkstra 演算法BFS演算法,都已在本BLOG內有所闡述了。其中,Dijkstra 演算法,後又寫了一篇文章繼續闡述:二(續)、理解Dijkstra演算法。但,想必,還是有部分讀者對此類最短路徑演算法,並非已瞭然於胸,或者,無一個總體大概的印象。

    本文,即以演示圖的形式,比較它們各自的尋路過程,讓各位對它們有一個清晰而直觀的印象。
    我們比較,以下五種演算法:
        1. A* (使用曼哈頓距離)
        2. A* (採用歐氏距離)
        3. A* (利用切比雪夫距離)
        4. Dijkstra 
        5. Bi-Directional Breadth-First-Search(雙向廣度優先搜尋)

    咱們以下圖為例,圖上色方塊代表起始點,色方塊代表目標點,色的方塊代表障礙物,白色的方塊代表可以通行的路徑。
    下面,咱們隨意擺放起始點綠塊,目標點紅塊的位置,然後,在它們中間隨便畫一些障礙物,
    最後,執行程式,比較使用上述五種演算法,得到各自不同的路徑,各自找尋過程中所覆蓋的範圍,各自的工作流程

,並從中可以窺見它們的效率高低。


A*、Dijkstra、BFS演算法效能比較演示:
    ok,任意擺放綠塊與紅塊的三種狀態():
一、起始點綠塊,與目標點紅塊在同一條水平線上:


各自的搜尋路徑為:
        1. A* (使用曼哈頓距離)

        2. A* (採用歐氏距離)

        3. A* (利用切比雪夫距離)

        4. Dijkstra 演算法.//很明顯,Dijkstra 搜尋效率明顯差於上述A* 演算法。(看它最後找到目標點紅塊所走過的路徑,和覆蓋的範圍,即能輕易看出來,下面的比較,也是基於同一個道理。看路徑,看覆蓋的範圍,評價一個演算法的效率)。

 

       5. Bi-Directional Breadth-First-Search(雙向廣度優先搜尋) 

二、起始點綠塊,目標點紅塊在一斜線上:

各自的搜尋路徑為:
        1. A* (使用曼哈頓距離)

        2. A* (採用歐氏距離)

        3. A* (利用切比雪夫距離)

        4. Dijkstra 演算法。 //與上述A* 演算法比較,覆蓋範圍大,搜尋效率較低。

 

        5. Bi-Directional Breadth-First-Search(雙向廣度優先搜尋)

三、起始點綠塊,目標點紅塊被多重障礙物阻擋:

各自的搜尋路徑為(同樣,還是從綠塊到紅塊):
        1. A* (使用曼哈頓距離)

        2. A* (採用歐氏距離)..

        3. A* (利用切比雪夫距離)

        4. Dijkstra....

        5. Bi-Directional Breadth-First-Search(雙向廣度優先搜尋) //覆蓋範圍同上述Dijkstra 演算法一樣很大,效率低下。


A*搜尋演算法的高效之處
      如上,是不是對A*、Dijkstra、雙向BFS演算法各自的效能有了個總體大概的印象列?由上述演示,我們可以看出,在最短路徑搜尋效率上,一般有A*>Dijkstra、雙向BFS,其中Dijkstra、雙向BFS到底哪個演算法更優,還得看具體情況。
      由上,我們也可以看出,A*搜尋演算法的確是一種比較高效的尋路演算法。

      A*演算法最為核心的過程,就在每次選擇下一個當前搜尋點時,是從所有已探知的但未搜尋過點中(可能是不同層,亦可不在同一條支路上),選取f值最小的結點進行展開。
      而所有“已探知的但未搜尋過點”可以通過一個按f值升序的佇列(即優先佇列)進行排列。
      這樣,在整體的搜尋過程中,只要按照類似廣度優先的演算法框架,從優先佇列中彈出隊首元素(f值),對其可能子結點計算g、h和f值,直到優先佇列為空(無解)或找到終止點為止。

      A*演算法與廣度、深度優先和Dijkstra 演算法的聯絡就在於當g(n)=0時,該演算法類似於DFS,當h(n)=0時,該演算法類似於BFS。且同時,如果h(n)為0,只需求出g(n),即求出起點到任意頂點n的最短路徑,則轉化為單源最短路徑問題,即Dijkstra演算法。這一點,可以通過上面的A*搜尋樹的具體過程中將h(n)設為0或將g(n)設為0而得到。

BFS、DFS與A*搜尋演算法的比較
    參考了演算法驛站上的部分內容:
    不管以下論述哪一種搜尋,都統一用這樣的形式表示:搜尋的物件是一個圖,它面向一個問題,不一定有明確的儲存形式,但它裡面的一個結點都有可能是一個解(可行解),搜尋的目的有兩個方面,或者求可行解,或者從可行解集中求最優解。
    我們用兩張表來進行搜尋,一個叫OPEN表,表示那些已經展開但還沒有訪問的結點集,另一個叫CLOSE表,表示那些已經訪問的結點集。

蠻力搜尋(BFS,DFS)
BFS(Breadth-First-Search 寬度優先搜尋)
  首先將起始結點放入OPEN表,CLOSE表置空,演算法開始時:
    1、如果OPEN表不為空,從表中開始取一個結點S,如果為空演算法失敗
    2、S是目標解嗎?是,找到一個解(繼續尋找,或終止演算法);不是到3
    3、將S的所有後繼結點展開,就是從S可以直接關聯的結點(子結點),如果不在CLOSE表中,就將它們放入OPEN表末尾,而把S放入CLOSE表,重複演算法到1。

DFS(Depth-First-Search 深度優先搜尋)
  首先將起始結點放入OPEN表,CLOSE表置空,演算法開始時:
    1、如果OPEN表不為空,從表中開始取一個結點S,如果為空演算法失敗
    2、S是目標解嗎?是,找到一個解(繼續尋找,或終止演算法);不是到3
    3、將S的所有後繼結點展開,就是從S可以直接關聯的結點(子結點),如果不在CLOSE表中,就將它們放入OPEN表開始,而把S放入CLOSE表,重複演算法到1。

是否有看出:上述的BFS和DFS有什麼不同?
    仔細觀察OPEN表中待訪問的結點的組織形式,BFS是從表頭取結點,從表尾新增結點,也就是說OPEN表是一個佇列,是的,BFS首先讓你想到‘佇列’;而DFS,它是從OPEN表頭取結點,也從表頭新增結點,也就是說OPEN表是一個棧!

    DFS用到了棧,所以有一個很好的實現方法,那就是遞迴,系統棧是計算機程式中極重要的部分之一。用遞迴也有個好處就是,在系統棧中只需要存結點最大深度那麼大的空間,也就是在展開一個結點的後續結點時可以不用一次全部展開,用一些環境變數記錄當前的狀態,在遞迴呼叫結束後繼續展開。

利用系統棧實現的DFS
函式 dfs(結點 s)
{
      s超過最大深度了嗎?是:相應處理,返回;
      s是目標結點嗎?是:相應處理;否則:
      {
            s放入CLOSE表;
            for(c=s.第一個子結點 ;c不為空 ;c=c.下一個子結點() )
                  if(c不在CLOSE表中)
                        dfs(c);遞迴
      }
}

    如果指定最大搜索深度為n,那系統棧最多使用n個單位,它相當於有狀態指示的OPEN表,狀態就是c,在棧裡存了前面搜尋時的中間變數c,在後面的遞迴結束後,c繼續後移。在象棋等棋類程式中,就是用這樣的DFS的基本模式搜尋棋局局面樹的,因為如果用OPEN表,有可能還沒完成搜尋OPEN表就暴滿了,這是難於控制的情況。

    我們說DFS和BFS都是蠻力搜尋,因為它們在搜尋到一個結點時,在展開它的後續結點時,是對它們沒有任何‘認識’的,它認為它的孩子們都是一樣的‘優秀’,但事實並非如此,後續結點是有好有壞的。好,就是說它離目標結點‘近’,如果優先處理它,就會更快的找到目標結點,從而整體上提高搜尋效能。

啟發式搜尋
    為了改善上面的演算法,我們需要對展開後續結點時對子結點有所瞭解,這裡需要一個估值函式,估值函式就是評價函式,它用來評價子結點的好壞,因為準確評價是不可能的,所以稱為估值。打個比方,估值函式就像一臺顯微鏡,一雙‘慧眼’,它能分辨出看上去一樣的孩子們的手,哪個很髒,有細菌,哪個沒有,很乾淨,然後對那些乾淨的孩子進行獎勵。這裡相當於是需要‘排序’,排序是要有代價的,而花時間做這樣的工作會不會對整體搜尋效率有所幫助呢,這完全取決於估值函式。

    排序,怎麼排?用哪一個?快排吧,qsort!不一定,要看要排多少結點,如果很少,簡單排序法就很OK了。看具體情況了。
    排序可能是對OPEN表整體進行排序,也可以是對後續展開的子結點排序,排序的目的就是要使程式有啟發性,更快的搜出目標解。

    如果估值函式只考慮結點的某種效能上的價值,而不考慮深度,比較有名的就是有序搜尋(Ordered-Search),它著重看好能否找出解,而不看解離起始結點的距離(深度)。
    如果估值函式考慮了深度,或者是帶權距離(從起始結點到目標結點的距離加權和),那就是A*,舉個問題例子,八數碼問題,如果不考慮深度,就是說不要求最少步數,移動一步就相當於向後多展開一層結點,深度多算一層,如果要求最少步數,那就需要用A*。
    簡單的來說A*就是將估值函式分成兩個部分,一個部分是路徑價值,另一個部分是一般性啟發價值,合在一起算估整個結點的價值。

    從A*的角度看前面的搜尋方法,如果路徑價值為0就是有序搜尋,如果路徑價值就用所在結點到起始結點的距離(深度)表示,而啟發值為0,那就是BFS或者DFS,它們兩剛好是個反的,BFS是從OPEN表中選一個深度最小的進行展開,
    而DFS是從OPEN表中選一個深度最大的進行展開。當然只有BFS才算是特殊的A*,所以BFS可以求要求路徑最短的問題,只是沒有任何啟發性。 下文稍後,會具體談A*搜尋演算法思想。

BFS、DFS、Kruskal、Prim、Dijkstra演算法時間複雜度
      上面,既然提到了A*演算法與廣度、深度優先搜尋演算法的聯絡,那麼,下面,也順便再比較下BFS、DFS、Kruskal、Prim、Dijkstra演算法時間複雜度吧:     
      一般說來,我們知道,BFS,DFS演算法的時間複雜度為O(V+E),
      最小生成樹演算法Kruskal、Prim演算法的時間複雜度為O(E*lgV)。
      而Prim演算法若採用斐波那契堆實現的話,演算法時間複雜度為O(E+V*lgV),當|V|<<|E|時,E+V*lgV是一個較大的改進。
            //|V|<<|E|,=>O(E+V*lgV) << O(E*lgV),對吧。:D
      Dijkstra 演算法,斐波納契堆用作優先佇列時,演算法時間複雜度為O(V*lgV + E)。
           //看到了吧,與Prim演算法採用斐波那契堆實現時,的演算法時間複雜度是一樣的。

      所以我們,說,BFS、Prime、Dijkstra 演算法是有相似之處的,單從各演算法的時間複雜度比較看,就可窺之一二。


A*搜尋演算法的思想
      ok,既然,A*搜尋演算法作為是一種好的、高效的尋路演算法,咱們就來想辦法實現它吧。
      實現一個演算法,首先得明確它的演算法思想,以及演算法的步驟與流程,從我之前的一篇文章中,可以瞭解到:
      A*演算法,作為啟發式演算法中很重要的一種,被廣泛應用在最優路徑求解和一些策略設計的問題中。
而A*演算法最為核心的部分,就在於它的一個估值函式的設計上:

        f(n)=g(n)+h(n)

      其中f(n)是每個可能試探點的估值,它有兩部分組成:
一部分,為g(n),它表示從起始搜尋點到當前點的代價(通常用某結點在搜尋樹中的深度來表示)。
另一部分,即h(n),它表示啟發式搜尋中最為重要的一部分,即當前結點到目標結點的估值,

h(n)設計的好壞,直接影響著具有此種啟發式函式的啟發式演算法的是否能稱為A*演算法。

      一種具有f(n)=g(n)+h(n)策略的啟發式演算法能成為A*演算法的充分條件是:

 1、搜尋樹上存在著從起始點到終了點的最優路徑。
 2、問題域是有限的。
 3、所有結點的子結點的搜尋代價值>0。
 4、h(n)=<h*(n) (h*(n)為實際問題的代價值)。

      當此四個條件都滿足時,一個具有f(n)=g(n)+h(n)策略的啟發式演算法能成為A*演算法,並一定能找到最優解。
      對於一個搜尋問題,顯然,條件1,2,3都是很容易滿足的,而條件4: h(n)<=h*(n)是需要精心設計的,由於h*(n)顯然是無法知道的,所以,一個滿足條件4的啟發策略h(n)就來的難能可貴了。

      不過,對於圖的最優路徑搜尋和八數碼問題,有些相關策略h(n)不僅很好理解,而且已經在理論上證明是滿足條件4的,從而為這個演算法的推廣起到了決定性的作用。


A*搜尋演算法的應用
      ok,咱們就來應用A*搜尋演算法實現八數碼問題,下面,就是其主體程式碼,由於給的註釋很詳盡,就不再囉嗦了,有任何問題,請不吝指正:

//節點結構體
typedef struct Node
{
 int data[9];
 double f,g;
 struct Node * parent;
}Node,*Lnode;

//OPEN CLOSED 表結構體
typedef struct Stack
{
 Node * npoint;
 struct Stack * next;
}Stack,* Lstack;

//選取OPEN表上f值最小的節點,返回該節點地址
Node * Minf(Lstack * Open)
{
 Lstack temp = (*Open)->next,min = (*Open)->next,minp = (*Open);
 Node * minx;
    while(temp->next != NULL)
 {
  if((temp->next ->npoint->f) < (min->npoint->f))
  {
   min = temp->next;
   minp = temp;
  }
  temp = temp->next;
 }
 minx = min->npoint;
 temp = minp->next;
 minp->next = minp->next->next;
 free(temp);
 return minx;
}

//判斷是否可解
int Canslove(Node * suc, Node * goal)
{
 int a = 0,b = 0,i,j;
 for(i = 1; i< 9;i++)
  for(j = 0;j < i;j++)
  {
   if((suc->data[i] > suc->data[j]) && suc->data[j] != 0)
    a++;
   if((goal->data[i] > goal->data[j]) && goal->data[j] != 0)
    b++;
  }
  if(a%2 == b%2)
   return 1;
  else 
   return 0;
}

//判斷節點是否相等 ,1相等,0不相等
int Equal(Node * suc,Node * goal)
{
 for(int i = 0; i < 9; i ++ )
  if(suc->data[i] != goal->data[i])return 0;
  return 1;
}

//判斷節點是否屬於OPEN表 或 CLOSED表,是則返回節點地址,否則返回空地址
Node * Belong(Node * suc,Lstack * list)
{
 Lstack temp = (*list) -> next ;
 if(temp == NULL)return NULL;
 while(temp != NULL)
 {
  if(Equal(suc,temp->npoint))return temp -> npoint;
  temp = temp->next;
 }
 return NULL;
}

//把節點放入OPEN 或CLOSED 表中
void Putinto(Node * suc,Lstack * list)
{
    Stack * temp;
 temp =(Stack *) malloc(sizeof(Stack));
 temp->npoint = suc;
 temp->next = (*list)->next;
 (*list)->next = temp;
}

///////////////計算f值部分-開始//////////////////////////////
double Fvalue(Node suc, Node goal, float speed)
{//計算f值
 double Distance(Node,Node,int);
 double h = 0;
 for(int i = 1; i <= 8; i++)
  h = h + Distance(suc, goal, i);
 return h*speed + suc.g; //f = h + g(speed值增加時搜尋過程以找到目標為優先因此可能不會返

回最優解)                                       
}
double Distance(Node suc, Node goal, int i)
{//計算方格的錯位距離
 int k,h1,h2;
 for(k = 0; k < 9; k++)
 {
  if(suc.data[k] == i)h1 = k;
  if(goal.data[k] == i)h2 = k;
 }
 return double(fabs(h1/3 - h2/3) + fabs(h1%3 - h2%3));
}
///////////////計算f值部分-結束//////////////////////////////

///////////////////////擴充套件後繼節點部分的函式-開始/////////////////
int BelongProgram(Lnode * suc ,Lstack * Open ,Lstack * Closed ,Node goal ,float speed)
{//判斷子節點是否屬於OPEN或CLOSED表 並作出相應的處理
 Node * temp = NULL;
 int flag = 0;
 if((Belong(*suc,Open) != NULL) || (Belong(*suc,Closed) != NULL))
 {
  if(Belong(*suc,Open) != NULL) temp = Belong(*suc,Open);
  else temp = Belong(*suc,Closed);
  if(((*suc)->g) < (temp->g))
  {
   temp->parent = (*suc)->parent;
   temp->g = (*suc)->g;
   temp->f = (*suc)->f;
   flag = 1;
  }
 }
 else
 {
  Putinto(* suc, Open);
  (*suc)->f = Fvalue(**suc, goal, speed);
 }
 return flag; 
}

void Spread(Lnode * suc, Lstack * Open, Lstack * Closed, Node goal, float speed)
{//擴充套件後繼節點總函式
 int i;
 Node * child;
 for(i = 0; i < 4; i++)
 {
  if(Canspread(**suc, i+1))                   //判斷某個方向上的子節點可否擴充套件
  {
   child = (Node *) malloc(sizeof(Node));  //擴充套件子節點
   child->g = (*suc)->g +1;                //運算元節點的g值
   child->parent = (*suc);                 //子節點父指標指向父節點
   Spreadchild(child, i);                  //向該方向移動空格生成子節點
   if(BelongProgram(&child, Open, Closed, goal, speed)) // 判斷子節點是否屬

於OPEN或CLOSED表 並作出相應的處理
    free(child);
  }
 }
}
///////////////////////擴充套件後繼節點部分的函式-結束//////////////////////////////////

Node * Process(Lnode * org, Lnode * goal, Lstack * Open, Lstack * Closed, float speed)
{//總執行函式
 while(1)
 {
  if((*Open)->next == NULL)return NULL;  //判斷OPEN表是否為空,為空則失敗退出
  Node * minf = Minf(Open);              //從OPEN表中取出f值最小的節點
  Putinto(minf, Closed);                 //將節點放入CLOSED表中
  if(Equal(minf, *goal))return minf;     //如果當前節點是目標節點,則成功退出
        Spread(&minf, Open, Closed, **goal, speed);   //當前節點不是目標節點時擴充套件當前節點的後繼

節點
 }
}

int Shownum(Node * result)
{//遞迴顯示從初始狀態到達目標狀態的移動方法
 if(result == NULL)return 0;
 else
 {
  int n = Shownum(result->parent);
  for(int i = 0; i < 3; i++)
  {
   printf("/n");
   for(int j = 0; j < 3; j++)
   {
    if(result->data[i*3+j] != 0)
     printf(" %d ",result->data[i*3+j]);
    else printf("   ");
   }
  }
  printf("/n");
  return n+1;
 }
}

完。July、二零一一年三月十日。