1. 程式人生 > >A*與IDA*的奇妙之旅

A*與IDA*的奇妙之旅

因為A*卡了一天QAQ

那麼,A*是什麼呢?

A*

A*是對於bfs的優化,啟發式搜尋。

例如下圖:
在這裡插入圖片描述

不錯,在這張圖上,小人去找電腦,用bfs的話:

在這裡插入圖片描述

黃色就是bfs的搜尋範圍。。。不要問我為什麼選黃色

Dij不會打全稱

在這裡插入圖片描述

那麼,A*是怎樣的呢?
在這裡插入圖片描述

沒看出區別?我都看不出區別

那麼,這張圖呢?
A*
在這裡插入圖片描述

像這種圖,如果用bfs與Dij都會。。。
但是A*的啟發式搜尋就體現在這。

首先定義函式:
\[F(x)=G(x)+H(x)\]
\(G(x)\)表示已經走過的路,\(H(x)\)表示期望走的路。

不難看,\(G(x)\)很容易想到,但是\(H(x)\)就十分難受了(╯﹏╰)。

例如,如果只允許你走上下左右,那麼切莫雪夫距離當期望值再好不過了。

\(H(x)\)一定要比實際值小,大的話反而容易淡化\(G(x)\)的作用。

建一個小根堆,將找到的\(F(x)\)存進去,這個堆叫"OPEN"
每次取出堆頂,然後把堆頂刪除,同時把他丟到"CLOSE"列表裡。
我們要承偌丟到"CLOSE"列表裡的數不再更新。

然後搜尋上下所有四格:

已經在"CLOSE"裡面的,不去理他。
不在"OPEN"裡面的,加進來。
已經在“OPEN”裡的,判斷一下是否更優,更新。這裡貌似沒用

當搜到終點時,結束。

已經在“OPEN”裡的,判斷一下是否更優,更新。

這句話有沒有用?

樣例
當八個方向都可以時,就不大一樣了,尤其是當走的代價不一樣時,這個的作用就很明顯了。

為什麼終點搜到就可以退出了?大概因為\(H(x)\)都是估計\(x\)\(終點\)的吧(而且\(H(x)\)也比真實值小的情況下)。

~~不過A*的具體步驟還是看情況定~~

那麼,對於\(H(x)\)還有一個要注意的點。
首先不用說,小於實際值(否則\(G(x)\)的作用被淡化的話你就廢了)。
但是如果\(G(x)\)取的不好,反而會負優化,所以\(G(x)\)尤其重要,尤其是\(G(x)\)剛好等於實際值時,A*就跑得飛快了!

而且A還有個優化,當許多\(F(x)\)都相同時,取\(G(x)\)

最大大的那一個,這樣能讓A避免擴充套件了多條路徑。

IDA*:

BFS加迭代加深?

迭代加深是什麼:

在DFS的搜尋裡面,可能會面臨一個答案的層數很低,但是DFS搜到了另為一個層數很深的分支裡面導致時間很慢,但是又卡BFS的空間,這個時候,可以限制DFS的搜尋層數,一次又一次放寬搜尋的層數,知道搜尋到答案就退出,時間可觀,結合了BFS和DFS的優點於一身。

bool  bk;
void  dfs(int  limit,int  x)
{
    if(...)
    {
        bk=true;
        return  ;
    }
    if(x==limit)return  ;
    for(...)dfs(limit,x+1);
}
for(int  i=1;i<=100;i++)
{
    dfs(i,0);
    if(bk==true)return  0;
}

這也是個簡潔的迭代加深,簡稱ID演算法。

那麼IDA=A+ID?
不是。難道12=1+2?
在這裡插入圖片描述

不過,特別舒服的事情是IDA是對與A的優化,甚至比A*還好打!

IDA*就是將\(F(x)\)搬到了IDDFS上然後加了個可行性剪枝

例題

這題目,不大難,難在了\(H(x)\)怎麼做。

由於騎士要從現有狀態恢復到目標狀態,DFS穩了!
首先DFS不能讓馬走,否則狀態多,應該讓空格走,狀態會少很多

又要求15步,IDDFS,穩了!雖然我還是傻愣愣的將0到15每次都迭代加深。

然後,我們再判斷一下,在最有情況下,當一個位置的馬與目標狀態不一樣時,那麼就cnt++,期望這個馬可以一個迴歸原位,不過在IDDFS裡面的判斷就不是\(if(x+ex()>limit)\)了(\(ex()\)代表期望函式)。

而是if(x+ex()>limit+1)為什麼呢?因為在ex()裡面我把空格也計算了一遍,而在最後一步,是可以將空格與馬同時迴歸原位的,所以要讓limit+1。

那麼,程式碼君:

#include<cstdio>
#include<cstring>
#include<algorithm>
using  namespace  std;
int  st[7][7]=
{
    {3,3,3,3,3,3,3},
    {3,1,1,1,1,1,3},
    {3,0,1,1,1,1,3},
    {3,0,0,2,1,1,3},
    {3,0,0,0,0,1,3},
    {3,0,0,0,0,0,3},
    {3,3,3,3,3,3,3}
};//目標狀態 
int  map[7][7];//記錄當前狀態 
inline  int  ex()//期望函式 
{
    int  tot=0;
    for(int  i=1;i<=5;i++)
    {
        for(int  j=1;j<=5;j++)
        {
            if(map[i][j]!=st[i][j])tot++;
        }
    }
    return  tot;
}
inline  void  wap(int  &x,int  &y){x^=y^=x^=y;}//交換函式swap 
int  dx[]={-2,-2,-1,1,2,2,1,-1};
int  dy[]={-1,1,2,2,1,-1,-2,-2};//方向 
bool  dfs(int  limit,int  x,int  mx,int  my)
{
    if(x==limit)
    /*果已經到界限,判斷一下,
    為什麼不用擔心x<limit的情況呢,如果有,在上幾次DFS就已經解決了。 */
    {
        return  !ex();
    }
    if(x+ex()>limit+1)return  false;//IDA*的可行性剪枝 
    for(int  t=0;t<=7;t++)//空格的發個方向 
    {
        int  tx=mx+dx[t],ty=my+dy[t];//新的空格位置 
        if(tx<1  ||  tx>5   ||  ty<1  ||  ty>5)continue;//判斷 
        wap(map[mx][my],map[tx][ty]);//交換 
        if(dfs(limit,x+1,tx,ty))// dfs(limit,x+1,tx,ty)==true
        {
            //找到了答案 
            wap(map[mx][my],map[tx][ty]);//交換 
            return  true;
        }
        wap(map[mx][my],map[tx][ty]);//回朔 
    }
    return  false;
}
int  main()
{
    int  T;scanf("%d",&T);
    while(T--)
    {
        char  ss[10];
        int  tx=0,ty=0;
        for(int  i=1;i<=5;i++)//構建map 
        {
            scanf("%s",ss+1);
            for(int  j=1;j<=5;j++)
            {
                if(ss[j]=='1')map[i][j]=1;
                else  if(ss[j]=='0')map[i][j]=0;
                else  map[i][j]=2,tx=i,ty=j;//記錄空格位置 
            }
        }
        bool  bk=false;
        for(int  i=0;i<=15;i++)//0代表一開始就是目標狀態 
        {
            bk=dfs(i,0,tx,ty);
            if(bk)//bk==true;
            {
                printf("%d\n",i);
                break;//輸出並break 
            }
        }
        if(!bk)printf("-1\n");//判斷 
    }
    return  0;
}

A*的例題(比較難)

因為luogu卡A*,給出bzoj的連結。

不就是計算k最大能是多少
反向建邊,\(D(x)\)代表\(n\)\(x\)的最短路

\[H(x)=D(x)\]

首先有個優化,設\(kk=能量/D(1),(k≤kk)\)為什麼?因為就算你條條最短路,你最多也只能擴充套件kk條!

那麼的話呢,我們從起點開始,每次將周圍的點塞進堆裡面,這裡什麼時候丟進CLOSE列表呢,僅當一個數被訪問了kk次。

而且這裡一個點是可以多次被丟進OPEN列表裡的,總之A*的步驟就不太一樣就對了。

每次取出堆頂然後更新,當訪問到終點時記錄一下,然後。。。

程式碼君!可惡的BZOJ,卡我priority_queue,逼我手寫

#include<cstdio>
#include<cstring>
#include<algorithm>
using  namespace  std;
//邊目錄 
struct  bian
{
    int  y,next;
    double  c;
}v1[210000]/*正向建圖*/,v2[210000]/*反向建圖*/;int  last1[6000],last2[6000],vlen;
int  n,m;double  QWQ;//能量 
inline  void  ins(int  x,int  y,double  c)
{
    vlen++;
    v1[vlen].y=y;v1[vlen].c=c;v1[vlen].next=last1[x];last1[x]=vlen;
    v2[vlen].y=x;v2[vlen].c=c;v2[vlen].next=last2[y];last2[y]=vlen;
}
//A*判斷時所用 
struct  node
{
    int  d;//d表示在哪個點 
    double  f,h;//f表示總估值,h表示實際走的長度 
}now,fut;//在A*裡面當中轉的時候用 
inline  bool  operator<(node  x,node  y){return  x.f==y.f?x.h>y.h:x.f<y.f;}//過載 
//手寫堆。。。
node  a[1510000];int  len,ans=0;
inline  void  insert(node  x)//插入 
{
    a[++len]=x;//放進最後一個 
    int  i=len/2,j=len;
    while(i>=1)//向上調整 
    {
        if(a[j]<a[i])
        {
            node  tt=a[j];a[j]=a[i];a[i]=tt;//交換 
            j=i;i=j/2;
        }
        else  break;//退出 
    }
}
inline  void  del()//刪除堆頂 
{
    a[1]=a[len];len--;//將a[len]移到堆頂,達到將a[1]刪除的目的 
    int  i=1,j=2;
    while(j<=len)//向下調整 
    {
        if((j+1)<=len  &&  a[j+1]<a[j])j++;//找兩個兒子最小的來做對比 
        if(a[j]<a[i])//對比 
        {
            node  tt=a[j];a[j]=a[i];a[i]=tt;//交換 
            i=j;j=i*2;
        }
        else  break;//退出 
    }
}
//SPFA
double  d[6000];
int  list[6000],head,tail;
bool  bol[6000];
void  spfa()
{
    for(int  i=1;i<n;i++)d[i]=999999999.0;
    head=1;tail=2;list[head]=n;bol[n]=true;//初始化 
    while(head!=tail)
    {
        int  x=list[head++];bol[x]=false;
        if(head==n+1)head=1;//迴圈佇列優化 
        for(int  k=last2[x];k;k=v2[k].next)
        {
            int  y=v2[k].y;
            if(d[x]+v2[k].c<d[y])//判斷 
            {
                d[y]=d[x]+v2[k].c;
                if(!bol[y])
                {
                    bol[y]=true;
                    list[tail++]=y;if(tail==n+1)tail=1;
                }
                //別看了,沒有酸辣粉(SLF優化) 
            }
        }
    }
}
//A*
int  cnt[6000];
void  A_star(int  kk)//計算k最大為多少 
{
    now.d=1;now.h=0;now.f=d[1];
    insert(now);//插入起點 
    while(len)//len!=0,堆不為空 
    {
        now=a[1];del();//刪除堆頂 
        cnt[now.d]++;//計算 次數 
        if(now.f>QWQ)break;//最小的F值都大於能量值,只能退出了。 
        if(now.d==n)//到達了終點 
        {
            ans++;
            QWQ-=now.f;//消耗能量 
        }
        if(cnt[now.d]>kk)continue;//CLOSE列表 
        for(int  k=last1[now.d];k;k=v1[k].next)
        {
            fut.d=v1[k].y;fut.h=now.h+v1[k].c;fut.f=fut.h+d[fut.d];//加入新的點 
            insert(fut);//加入進去 
        }
    }
}
int  main()
{
    scanf("%d%d%lf",&n,&m,&QWQ);
    for(int  i=1;i<=m;i++)
    {
        int  x,y;double  c;scanf("%d%d%lf",&x,&y,&c);
        ins(x,y,c);
    }
    spfa();A_star(QWQ/d[1]);//一個優化
    printf("%d\n",ans);//輸出 
    return  0;
}
//強行加註釋。。。 

終於寫完了 QWQ。