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)\)
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;
}
因為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。