搜索分為盲目搜索和啟發搜索
下面列舉OI常用的盲目搜索:
- dijkstra
- SPFA
- bfs
- dfs
- 雙向bfs
- 迭代加深搜索(IDFS)
下面列舉OI常用的啟發搜索:
- 最佳優先搜索(A)
- A*
- IDA*
那么什么是盲目,什么是啟發?
舉個例子,假如你在學校操場,老師叫你去國旗那集合,你會怎么走?
假設你是瞎子,你看不到周圍,那如果你運氣差,那你可能需要把整個操場走完才能找到國旗。這便是盲目式搜索,即使知道目標地點,你可能也要走完整個地圖。
假設你眼睛沒問題,你看得到國旗,那我們只需要向着國旗的方向走就行了,我們不會傻到往國旗相反反向走,那沒有意義。
這種有目的的走法,便被稱為啟發式的。
左圖為bfs,右圖為A

提供一個搜索可視化的鏈接https://qiao.github.io/PathFinding.js/visual/
搜索算法淺談
DFS
基礎中的基礎,幾乎所有題都可以出一檔指數級復雜度暴力分給DFS,同時他的實現也是目錄中提到的所有搜索算法中最簡單的
dfs的核心思想是:不撞南牆不回頭
舉個例子:

你現在在一號點,你想找到樹中與一號點連通的每一個點
那么我們考慮按照深度優先的順序去遍歷這棵樹,即,假設你當前在點x,如果和x連邊的點中有一個點y,滿足y比x深,即y是x的兒子,並且y還沒有被訪問過,那么我們就走到y,如果有多個y滿足條件,我們走到其中任意一個
如果沒有y滿足條件,我們返回x的父親
按照這個順序,我們就可以訪問到每個節點,並且每條邊會恰好被走兩次(從父親到兒子一次,從兒子到父親一次)
由於dfs的特性,它有時候會非常的浪費時間,為什么呢?
還是剛才這張圖:

如果我們把終點設在10號點,在dfs的過程中要先搜完一號點及其三個子樹才能到達終點
代碼大體框架:
void dfs(int k){
if(到達目的地或滿足條件)輸出解
for(int i=1;i<=算符種數;++i){
保存結果//有時候不需要
dfs(k+1);
回溯結果//有時候不需要
}
}
那么什么時候需要回溯呢?
我們先要了解回溯的目的:
我們在搜索的過程中,先選擇一種可能的情況向前搜索,一旦發現選擇的結果是錯誤的,就退一步重新選擇,這就需要回溯,向前搜索一步之后將狀態恢復成之前的樣子
所以在解題的過程中要判斷好是否需要回溯
bfs
bfs利用了一種線性數據結構,隊列
bfs的核心思想是:從廚師節點開始,生成第一層節點,檢查目標節點是否在目標節點中,若沒有再將第一層所有的節點逐一擴展,如此往復知道發現目標節點為止
我們再拿出徐瑞帆dalao的圖:

你現在還是在一號點,你還是想找到樹中與一號點連通的每一個點
我們初始的時候把一號點推入隊取出隊尾,然后只要當前隊列非空,我們就取出隊頭元素x,並將隊頭彈出
然后我們將x的所有兒子推入隊列
對於圖上的情況,我們將所有與x相連,並且還沒入過隊的點推入隊列
這樣我們就能夠訪問所有點
代碼大致框架:
void bfs(){
q.push(head);
while(!q.empty()){
temp=q.front;
q.pop();
if(temp為目標狀態)輸出解
if(temp不合法)continue;
if(temp合法)q.push(temp+Δ);
}
}
IDFS
我們已經學會了dfs和bfs
然而有的問題還是使我們無法進行搜索,因為你要進行搜索的圖可能是無限大的,每個點所連的邊也可能是無限多的,這就使得dfs和bfs都失效了,這時候我們就需要用到idfs
我們枚舉深搜的時候深度的上限,因為深度上限的限制,圖中的一些邊會被刪掉,而圖就變成了一個有限的圖,我們就可以進行dfs了
舉個栗子:

如果用普通的dfs,這顯然是一個無解的情況,你將會陷入無限的左子樹中
這時,我們設一個深度d,每次搜到第d層就返回搜其他的分支。如果在d層沒搜到答案則d++,從頭再搜
然而這個算法有一個很明顯的缺陷,有一些非答案點要重復搜好幾遍,這造成了極大的浪費
於是我們有了IDA*
A*
在看IDA* 之前,我們先了解A*
搜索算法經常運行效率很低,為了提高效率,我們可以使用A*算法
我們對每個點定義一個估價函數f(x)=g(x)+h(x)
g(x)表示從起始點到x的實際代價
h(x)表示估計的從x到結束點的代價,並要求h(x)小於等於從x到結束點的實際代價
那么每次我們從可行點集合中找到f(x)最小的x,然后搜索他
這個過程可以用優先隊列(即堆)實現
這樣的話可以更快地到達結束點,並保證到達結束點時走的是最優路徑
為什么要求h(x)小於等於實際代價呢?
因為如果h(x)大於實際代價的話,可能以一條非最優的路徑走到結束點,導致答案變大
舉個栗子:用A*做的八數碼難題
#include<map>
#include<queue>
#include<iostream>
#include<algorithm>
using namespace std;
int dx[]={-1,0,0,1},dy[]={0,-1,1,0};
int final[]={-1,0,1,2,5,8,7,6,3};
struct node
{
int state,g,h;
node(int _state,int _g)
{
state=_state;
g=_g;
h=0;
int tmp=state;
for(int i=8;i>=0;i--)
{
int a=tmp%10;tmp/=10;
if(a!=0)h+=abs((i/3)-(final[a]/3))+abs((i%3)-(final[a]%3));
}
}
};
bool operator<(node x,node y)
{
return x.g+x.h>y.g+y.h;
}
priority_queue<node>q;
map<int,bool>vis;
int main()
{
int n;
cin>>n;
q.push(node(n,0));
vis[n]=1;
while(!q.empty())
{
node u=q.top();
int c[3][3],f=0,g=0,n=u.state;q.pop();
if(u.state==123804765)
{
cout<<u.g<<endl;
return 0;
}
for(int i=2;i>=0;i--)
for(int j=2;j>=0;j--)
{
c[i][j]=n%10,n/=10;
if(!c[i][j])f=i,g=j;
}
for(int i=0;i<4;i++)
{
int nx=f+dx[i],ny=g+dy[i],ns=0;
if(nx<0||ny<0||nx>2||ny>2)continue;
swap(c[nx][ny],c[f][g]);
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
ns=ns*10+c[i][j];
if(!vis.count(ns))
{
vis[ns]=1;
q.push(node(ns,u.g+1));
}
swap(c[nx][ny],c[f][g]);
}
}
}
這是bfs做法

這是A*做法

很明顯,A*比bfs快多了
值得注意的是,A*只能在有解的情況下使用
IDA*
在進行IDFS的時候,我們也可以用A*進行搜索
如果在當前深度限制下搜到了結束狀態,我們就可以直接輸出答案
代碼大體框架:
//1代表牆,0代表空地,2代表終點
int G[maxn][maxn];
int n, m;
int endx, endy;
int maxd;
const int dx[4] = { -1, 1, 0, 0 };
const int dy[4] = { 0, 0, -1, 1 };
namespace ida
{
bool dfs(int x, int y, int d);
inline int h(int x, int y);
bool ida_star(int x, int y, int d)
{
if (d == maxd) //是否搜到答案
{
if (G[x][y] == 2)
return true;
return false;
}
int f = h(x, y) + d; //評估函數
if (f > maxd) //maxd為最大深度
return false;
//嘗試向左,向右,向上,向下走
for (int i = 0; i < 4; i++)
{
int next_x = x + dx[i];
int next_y = y + dy[i];
if (next_x > n || next_x < 1 || next_y > m || next_y < 1 || G[next_x][next_y] == 1)
continue;
if (ida_star(next_x, next_y, d + 1))
return true;
}
return false;
}
inline int h(int x, int y)
{
return abs(x - endx) + abs(y - endy);
}
}
