A*啟發式搜尋 其實是兩種搜尋方法的合成( A*搜尋演算法 + 啟發式搜尋),但要真正理解A*搜尋演算法,還是得先從啟發式搜尋演算法談起。
何為啟發式搜尋
啟發式搜尋演算法有點像廣度優先搜尋,不同的是,它會優先順著有啟發性和具有特定資訊的節點搜尋下去,這些節點可能是到達目標的最好路徑。我們稱這個過程為最優(best-first)或啟發式搜尋。
簡單來說,啟發式搜尋就是對取和不取都做分析,從中選取更優解(或刪去無效解)
由於概念過於抽象,我們使用例題講解。
例題【NOIP2005 普及組】 採藥:
題目大意:有$ N $ 種物品和一個容量為 \(W\)的揹包,每種物品有重量\(wi\)和價值\(ui\) 兩種屬性,要求選若干個物品(每種物品只能選一次)放入揹包使揹包中物品的總價值最大且揹包中物品的總重量不超過揹包的容量。
很明顯這是一個01揹包問題,很容易讓我們想到使用動態規劃和貪心演算法。
但在啟發式演算法的思路是:
我們寫一個估價函式 f,可以剪掉所有無效的 0 枝條(就是剪去大量無用不選枝條)。
估價函式 f 的執行過程如下:
我們在取的時候判斷一下是不是超過了規定體積(可行性剪枝)。
在不取的時候判斷一下不取這個時,剩下的藥所有的價值 + 現有的價值是否大於目前找到的最優解(最優性剪枝)。
例題程式碼:
#include <algorithm>
#include <cctype>//isdigit
#include <cstdio>
int n, tot, ans;
struct node
{
int c, v;//體積與價值
double cost;
};
using namespace std;
const int N = 10000;
node a[N + 5];
int read()/*快讀大法吼啊!比cin scanf都快*/
{
int x = 0; short w = 0; char ch = 0;
while (!isdigit(ch)) { w |= ch == '-'; ch = getchar(); }
while (isdigit(ch)) { x = (x << 3) + (x << 1) + (ch ^ 48); ch = getchar(); }
return w ? -x : x;
}
//所有的啟發式搜尋都會有一個估價函式。下面是這一題的估價函式。
inline int f(int t, int v) {
int tot = 0;
for (int i = 1; t + i <= n; i++)
{
if (v >= a[t + i].c)
{
v -= a[t + i].c;
tot += a[t + i].v;
}
else
return (int)(tot + v * a[t + i].cost);
}
return tot;
}
bool operator<(const node &a, const node &b){
return a.cost > b.cost;
} /*等價於
bool cmp(node a,node b){
return a.cost>b.cost;
}
*/
void DFS(int now, int cv, int cp){
ans = max(ans, cp);
if (now > n)
{
return;
}
if (f(now, cv) + cp > ans)
DFS(now + 1, cv, cp);
if (cv - a[now].c >= 0)
DFS(now + 1, cv - a[now].c, cp + a[now].v);
}
int main()
{
tot = read(), n = read();
for (int i = 1; i <= n; i++)
{
a[i].c = read(), a[i].v = read();
a[i].cost = 1.0 * a[i].v / a[i].c;
}
sort(a + 1, a + 1 + n);//由於我們過載了<,所以就不需要cmp函式
DFS(0, tot, 0);
printf("%d", ans);
return 0;
}
A*搜尋演算法
A*搜尋演算法,俗稱A星演算法,作為啟發式搜尋演算法中的一種,這是一種在圖形平面上,有多個節點的路徑,求出最低通過成本的演算法。常用於遊戲中的NPC的移動計算,或線上遊戲的BOT的移動計算上。該演算法像Dijkstra演算法一樣,可以找到一條最短路徑;也像BFS一樣,進行啟發式的搜尋。
A*演算法最為核心的部分,就在於它的一個估值函式的設計上:
定義起點\(s\) ,終點 \(t\)。
從起點(初始狀態)開始的距離函式 \(g(x)\)。
到終點(最終狀態)的距離函式 \(h(x),h*(x)\) 。
定義每個點的估價函式$ f(n)=g(n)+h(n)$ 。
\(A*\)演算法每次從 優先佇列 中取出一個 最小的,然後更新相鄰的狀態。
如果 \(h <= h*\),則 A*演算法能找到最優解。
上述條件下,如果 滿足三角形不等式,則 A*演算法不會將重複結點加入佇列 。
其實……$h = 0 $ 時就是 DFS 演算法, 並且邊權為 時就是 BFS 。
例題 八數碼
題目大意:在 $ 3$ x \(3\)的棋盤上,擺有八個棋子,每個棋子上標有 1 至 8 的某一數字。棋盤中留有一個空格,空格用 0 來表示。空格周圍的棋子可以移到空格中,這樣原來的位置就會變成空格。給出一種初始佈局和目標佈局(為了使題目簡單,設目標狀態為
123
804
765
)
,找到一種從初始佈局到目標佈局最少步驟的移動方法。
$h $函式可以定義為,不在應該在的位置的數字個數。
容易發現\(h\) 滿足以上兩個性質,此題可以使用 A*演算法求解。
程式碼實現:
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
#include <set>
using namespace std;
const int dx[4] = {1, -1, 0, 0}, dy[4] = {0, 0, 1, -1};
int fx, fy;
char ch;
struct matrix {
int a[5][5];
bool operator<(matrix x) const {
for (int i = 1; i <= 3; i++)
for (int j = 1; j <= 3; j++)
if (a[i][j] != x.a[i][j]) return a[i][j] < x.a[i][j];
return false;
}
} f, st;
int h(matrix a) {
int ret = 0;
for (int i = 1; i <= 3; i++)
for (int j = 1; j <= 3; j++)
if (a.a[i][j] != st.a[i][j]) ret++;
return ret;
}
struct node {
matrix a;
int t;
bool operator<(node x) const { return t + h(a) > x.t + h(x.a); }
} x;
priority_queue<node> q;
set<matrix> s;
int main() {
st.a[1][1] = 1;
st.a[1][2] = 2;
st.a[1][3] = 3;
st.a[2][1] = 8;
st.a[2][2] = 0;
st.a[2][3] = 4;
st.a[3][1] = 7;
st.a[3][2] = 6;
st.a[3][3] = 5;
for (int i = 1; i <= 3; i++)
for (int j = 1; j <= 3; j++) {
scanf(" %c", &ch);
f.a[i][j] = ch - '0';
}
q.push({f, 0});
while (!q.empty()) {
x = q.top();
q.pop();
if (!h(x.a)) {
printf("%d\n", x.t);
return 0;
}
for (int i = 1; i <= 3; i++)
for (int j = 1; j <= 3; j++)
if (!x.a.a[i][j]) fx = i, fy = j;
for (int i = 0; i < 4; i++) {
int xx = fx + dx[i], yy = fy + dy[i];
if (1 <= xx && xx <= 3 && 1 <= yy && yy <= 3) {
swap(x.a.a[fx][fy], x.a.a[xx][yy]);
if (!s.count(x.a)) s.insert(x.a), q.push({x.a, x.t + 1});
swap(x.a.a[fx][fy], x.a.a[xx][yy]);
}
}
}
return 0;
}
另外例題 K短路 同樣可以用A*演算法可以思考下如何AC