【学时总结】 ◆学时 · I◆ A*算法
【學時·I】A*算法
■基本策略■
——A*(A Star)無非就是BFS的升級,當BFS都超時的時候……
同樣以隊列為基礎結構,BFS使用FIFO隊列(queue),而A*則使用優先隊列(priority_queue)。與BFS的優化極其相似,但一般的BFS優化只是相當于使用了一個最優性剪枝,偶爾不會起到足夠的優化所以就TLE了。
所以A*算法改進了其優先級的判定方法,使用了一個啟發函數(沒錯,就是這么水的名字),它可以“樂觀”地預估出一個從當前狀態到達目標狀態的代價,且此預估值必然小于等于實際值,否則A*算法就會出錯。
■一般的搜索題■
這是A*的解題范圍和其他搜索算法可以實現的解題范圍的重疊域
◆永恒的經典◆ 八數碼問題
· 以下引用自POJ 1077
The 15-puzzle has been around for over 100 years; even if you don't know it by that name, you've seen it. It is constructed with 15 sliding tiles, each with a number from 1 to 15 on it, and all packed into a 4 by 4 frame with one tile missing. Let's call the missing tile 'x'; the object of the puzzle is to arrange the tiles so that they are ordered as:1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 x
where the only legal operation is to exchange 'x' with one of the tiles with which it shares an edge. As an example, the following sequence of moves solves a slightly scrambled puzzle:
1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4
5 6 7 8 5 6 7 8 5 6 7 8 5 6 7 8
9 x 10 12 9 10 x 12 9 10 11 12 9 10 11 12
13 14 11 15 13 14 11 15 13 14 x 15 13 14 15 x
r-> d-> r->
The letters in the previous row indicate which neighbor of the 'x' tile is swapped with the 'x' tile at each step; legal values are 'r','l','u' and 'd', for right, left, up, and down, respectively.
Not all puzzles can be solved; in 1870, a man named Sam Loyd was famous for distributing an unsolvable version of the puzzle, and frustrating many people. In fact, all you have to do to make a regular puzzle into an unsolvable one is to swap two tiles (not counting the missing 'x' tile, of course).
In this problem, you will write a program for solving the less well-known 8-puzzle, composed of tiles on a three by three arrangement.
· 解析
這道題顯然是一道搜索題,不過有一點變形……就難了許多。當然,這道題用BFS或者雙向BFS能過,但是A*算法實際運行時間更優。
1.啟發函數
作為整個A*算法的精華,這個函數顯得十分重要,那么下面給出2個思路:
思路A: H()-當前狀態有多少個元素不在正確位置; G()-當前搜索的深度,即用了的操作數; 最后一次操作將會在一次操作中將2個元素歸位,所以啟發函數F()=H()-1+G();
評價: 這個啟發函數一定正確,但是通常與真實值相差過大,不是特別優;
思路B: H()-當前狀態中位置不對的元素(不包含空位子)與正確位置的曼哈頓距離之和; G()-當前搜索的深度,即用了的操作數; 由于將一個元素歸位至少需要H()次操作;
評價: 估測值與真實值較為接近,且不會超過真實值;
2.優先隊列的定義
優先隊列是A*算法所依賴的數據結構。STL的priority_queue能夠實現自動排序,但有一個缺點——當需要輸出路徑時,STL的隊列并沒有提供訪問刪除了的元素的函數,因此無法通過記錄“父親”狀態來輸出路徑,這時候就需要手寫優先隊列……QwQ
現在暫時不考慮這種尷尬情況,就當是只輸出最短方案操作次數。那么我們需要一個結構體:
這樣定義了優先級過后就可以利用STL直接排序了~
3.如果代碼不懂可以看這篇Blog:
Eight 八數碼問題
· 源代碼
僅供參考,如有不足(我知道寫得復雜了點)請評論指出
/*Lucky_Glass*/ #include<cstdio> #include<cstring> #include<algorithm> #include<queue> using namespace std; #define MOD 1000003struct Node {int pri,code,whe,dep;}Push; bool operator <(Node A,Node B) {return A.pri+A.dep>B.pri+B.dep;} int num[10]; int MOve[4]={-3,-1,3,1}; long long ten_n[]={1,10,100,1000,10000,100000,1000000,10000000,100000000,1000000000,10000000000}; vector<int> vis[MOD];inline int H(int n) {int res=0;for(int i=9;i>0;i--,n/=10){int v=n%10==0? 9:n%10;int x1=(i-1)/3+1,y1=i%3==0? 3:i%3;int x2=(v-1)/3+1,y2=v%3==0? 3:v%3;res+=abs(x1-x2)+abs(y1-y2);}return res; } inline long long change(int n,int a,int b) {long long A=n%ten_n[9-a+1]/ten_n[9-a],B=n%ten_n[9-b+1]/ten_n[9-b],Ret;Ret=n-A*ten_n[9-a]-B*ten_n[9-b];Ret=Ret+B*ten_n[9-a]+A*ten_n[9-b];return Ret; } inline bool Find(int n) {int m=n%MOD;for(int i=0;i<vis[m].size();i++)if(vis[m][i]==n)return true;vis[m].push_back(n);return false; }int main() { // freopen("in.txt","r",stdin);priority_queue<Node> que;for(int i=0,j=0;i<9;i++){scanf("%d",&num[j]);Push.code=Push.code*10+num[j];if(num[j]) j++;else Push.whe=i+1;}int End=0;for(int i=0,x;i<9;i++)scanf("%d",&x),End=10*End+x;Push.pri=H(Push.code);que.push(Push);while(!que.empty()){Node Top=que.top();que.pop();for(int i=0;i<4;i++){Push=Top;Push.whe+=MOve[i];Push.dep++;if(Push.whe<=0 || Push.whe>9 || (i%2 && (Push.whe-1)/3!=(Top.whe-1)/3)) continue;Push.code=(int)change(Push.code,Push.whe,Top.whe);if(Find(Push.code)) continue;if(Push.code==End){printf("%d",Push.dep);return 0;}Push.pri=H(Push.code);que.push(Push);}}puts("-1");return 0; }◆真正的難題◆ 15數碼問題
BFS真的過不了了……
UVA 15-Puzzle Problem
· 解析
這兩道題唯一的區別就是數據規模,8數碼的可能情況不超過9!種,但是15數碼的可能情況是在16!種以內!所以只加上一般的判重并不能起到什么優秀的作用,所以用到了A*算法,啟發函數和原來一樣。但是……
人無完人,A*算法也是有缺陷的 QwQ
正如雙向BFS,A*算法雖然在一般情況下較快,而這一般情況就是有解的情況。如果無解,雙向搜索會退化為兩個不相交的圓,既浪費空間又浪費時間;而A*算法會退化為普通的BFS(需要搜索完所有解),且比普通BFS慢——每次插入的時間復雜度為 O(log siz)。15數碼仍然有無解的情況,所以為了避免超時(枚舉出所有情況肯定會超時啊),我們需要預判:
bool If_ans(int brd[][4]) {int sum=0,siz=0,x,y,tmp[17]={};for(int i=0;i<4;i++)for(int j=0;j<4;j++){tmp[siz++]=brd[i][j];if(!brd[i][j]) x=i,y=j;}for(int i=0;i<16;i++)for(int j=i+1;j<16;j++)if(tmp[j]<tmp[i] && tmp[j])sum++;if((sum+x)%2==0) return false;return true; }上面這段代碼的意思就是——將4*4的15數碼矩陣除去0后,每行相接形成一個鏈狀表(tmp),求出逆序對的個數(sum),如果加上0的行數(行數從0開始)的和是二的倍數,則無解,否則有解。其實就是下面這樣:
最為權威的英文證明:Workshop Java - Solvability of the Tiles Game
· 源代碼
這段代碼并不算優秀,實際上利用了 Uva 的數據漏洞,真正的解是 IDA*,之后再說
/*Lucky_Glass*/ #include<cstdio> #include<cstring> #include<algorithm> #include<set> #include<queue> #include<iostream> using namespace std;const int mov[4][2]={{0,1},{0,-1},{1,0},{-1,0}}; const int fin[18][2]={{3,3},{0,0},{0,1},{0,2},{0,3},{1,0},{1,1},{1,2},{1,3},{2,0},{2,1},{2,2},{2,3},{3,0},{3,1},{3,2},{3,3}}; const char chr[]="RLDU";struct state {int pri,dep,x,y,brd[4][4];string ans;bool operator <(const state &cmp)const {return pri+dep>cmp.pri+cmp.dep;} };inline int Dis(int x,int y,int fx,int fy){return abs(x-fx)+abs(y-fy);} int Get_pri(int brd[][4]) {int sum=0;for(int i=0;i<4;i++)for(int j=0;j<4;j++){if(!brd[i][j] || (i==fin[brd[i][j]][0] && j==fin[brd[i][j]][1])) continue;sum+=Dis(i,j,fin[brd[i][j]][0],fin[brd[i][j]][1]);}return 4*sum; } bool If_ans(int brd[][4]) {int sum=0,siz=0,x,y,tmp[17]={};for(int i=0;i<4;i++)for(int j=0;j<4;j++){tmp[siz++]=brd[i][j];if(!brd[i][j]) x=i,y=j;}for(int i=0;i<16;i++)for(int j=i+1;j<16;j++)if(tmp[j]<tmp[i] && tmp[j])sum++;if((sum+x)%2==0) return false;return true; } bool A_Star(state srt) {priority_queue<state> que;state Top,Pus;que.push(srt);while(!que.empty()){Top=que.top();que.pop();for(int i=0;i<4;i++){Pus=Top;Pus.x+=mov[i][0];Pus.y+=mov[i][1];if(Pus.x<0 || Pus.x>3 || Pus.y<0 || Pus.y>3) continue;swap(Pus.brd[Pus.x][Pus.y],Pus.brd[Top.x][Top.y]);Pus.dep++;if(Pus.dep>50) continue;Pus.ans+=chr[i];Pus.pri=Get_pri(Pus.brd);if(!Pus.pri) {cout<<Pus.ans<<endl;return true;}if(Pus.ans.size()>=2){int f1=Pus.ans.size()-1,f2=Pus.ans.size()-2;if((Pus.ans[f1]=='U' && Pus.ans[f2]=='D') || (Pus.ans[f1]=='D' && Pus.ans[f2]=='U') || (Pus.ans[f1]=='L' && Pus.ans[f2]=='R') || (Pus.ans[f1]=='R' && Pus.ans[f2]=='L'))continue;}que.push(Pus);}}return false; }int main() {int t;scanf("%d",&t);while(t--){state srt;for(int i=0;i<4;i++)for(int j=0;j<4;j++){scanf("%d",&srt.brd[i][j]);if(!srt.brd[i][j]) srt.x=i,srt.y=j;}if(!If_ans(srt.brd)){printf("This puzzle is not solvable.\n");continue;}srt.dep=0;srt.pri=0;if(!A_Star(srt)) printf("This puzzle is not solvable.\n");}return 0; }■k-短路問題■
其他的搜索拿這個題型沒轍了 (`?ω?′)
◆一道版題◆ k-th shortest POJ 2449
(題目長在超鏈接里)
· 解析
其實就是標準的k短路。如果是一般的搜索肯定會TLE的,那么為什么A*可以完成呢?原因如下:
與之前的算法不同,我們在原來的A*算法中會有判重,并舍棄優先級低的情況;但是由于K短路中,同一條路徑可能走多次,以達到第k短的路徑。比如下面:
但是這樣也造成了一些麻煩,有時候是無解的:
然后回到A*算法——由于我們按優先級排序,所以我們當前的隊頭元素一定是現在整個隊列中最優的情況(前提是啟發函數寫對了);所以當我們第n次搜索到終點時,當前的路徑就是第n短路徑,為了避免意外,我們還可以先把到達終點的情況push到隊列里,當整個操作完畢再次以到達終點的情況為隊頭時,說明它的確是第n短的。
根據這一性質,我們可以在隊頭元素是終點時,記錄是第幾次訪問到終點,如果恰好是第n次,則返回答案——注意:此時雖然訪問到終點,但還要把這種情況push進隊列,否則反例見上圖。
別急著寫代碼,有一個小坑。由于我們是判斷隊頭是否為終點,所以當起點終點重合時,按題意應該不算最短路徑,但當程序進入BFS時會記錄一次。所以我們給出特判——當起點終點重合時,k++。
·源代碼
/*Lucky_Glass*/ #include<cstdio> #include<cstring> #include<algorithm> #include<queue> #include<vector> using namespace std;#define POI 1000struct Line{int v,len;}; struct state {int u,dep,pri;bool operator <(const state cmp)const{if(cmp.pri==pri) return cmp.dep<dep;else return cmp.pri<pri;} }; vector<Line> lec[POI+5],dis_lec[POI+5]; int n_poi,n_edg,srt,fin,kth,INF; int dis[POI+5];inline Line Make_Line(int v,int l){return Line{v,l};};void SPFA() {bool vis[POI+5]={};memset(dis,0x3f,sizeof dis);INF=dis[0];queue<int> que;dis[fin]=0;que.push(fin);while(!que.empty()){int Fro=que.front();que.pop();vis[Fro]=false;for(int i=0;i<dis_lec[Fro].size();i++){int Pus=dis_lec[Fro][i].v;if(dis[Pus]>dis[Fro]+dis_lec[Fro][i].len){dis[Pus]=dis[Fro]+dis_lec[Fro][i].len;if(!vis[Pus]) que.push(Pus),vis[Pus]=true;}}} } int A_Star() {if(srt==fin) kth++;if(dis[srt]==INF) return -1;priority_queue<state> que;que.push(state{srt,0,dis[srt]});int tot=0;while(!que.empty()){state Top=que.top();que.pop();if(Top.u==fin){tot++;if(tot==kth) return Top.dep;}for(int i=0;i<lec[Top.u].size();i++){state Pus=Top;Pus.dep+=lec[Top.u][i].len;Pus.u=lec[Top.u][i].v;Pus.pri=dis[Pus.u]+Pus.dep;que.push(Pus);}}return -1; }int main() {scanf("%d%d",&n_poi,&n_edg);for(int i=0,u,v,l;i<n_edg;i++)scanf("%d%d%d",&u,&v,&l),lec[u].push_back(Line{v,l}),dis_lec[v].push_back(Line{u,l});scanf("%d%d%d",&srt,&fin,&kth);SPFA();printf("%d\n",A_Star());return 0; }The End
Thanks for reading!
-Lucky_Glass
轉載于:https://www.cnblogs.com/LuckyGlass-blog/p/9060353.html
總結
以上是生活随笔為你收集整理的【学时总结】 ◆学时 · I◆ A*算法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 索引补充
- 下一篇: Netty 高性能之道 - Recycl