A* 算法
一、A*算法
先看一下优先队列BFS算法,该算法维护了一个优先队列,不断从队列中取出当前状态最小的状态进行扩展,每个状态第一次从队列中被取出时,就得到了从初态到该状态的最小代价;
但是一个状态的当前代价最小,只能说明从起始状态到该状态的代价最小,而在未来的搜索中,从该状态到目标状态可能会花费很大的代价,而也有可能当前代价较大,但未来到目标状态的代价可能会很小;
为了提高搜索效率,可以对未来产生的代价进行预估,则搜索到一种状态时,计算出当前状态到目标状态的代价的估计值,用 “当前代价 + 估计代价” 最小的状态进行扩展;
二、估价函数
1、估价函数
设 xxx 为当前状态, f(x)f(x)f(x) 为对 xxx 的评估函数,则有
f(x)=g(x)+h(x)
f(x) = g(x) + h(x)
f(x)=g(x)+h(x)
其中,
g(x)g(x)g(x) 表示从初始状态到 xxx 的实际代价;
h(x)h(x)h(x) 表示 xxx 到终点的最优路径的评估,为启发式信息;
则当 g(x)=0g(x) = 0g(x)=0 时,有 f(x)=h(x)f(x) = h(x)f(x)=h(x) 为贪心算法;
当 h(x)=0h(x) = 0h(x)=0 时,有 f(x)=g(x)f(x) = g(x)f(x)=g(x) 为普通的 BFSBFSBFS 算法;
2、设计估价函数
设计估价函数时,估价值不能大于实际的代价,估价应该比实际代价更优,否则最优解会被压在队列底部,无法取出,导致最终不会找出最优解;
A* 算法的关键在于能否设计出一个优秀的估价函数,估价函数应该在满足设计准则的前提下,尽可能地反映出未来实际代价的变化趋势和相对大小趋势;
三、例题
八数码问题
题目
在一个3*3的九宫格棋盘里,放有8个数码,数码的数字分别是1~8。棋盘中还有一个位置是空着的,用0表示。可以通过在九宫格里平移数码来改变状态(即空格位在九宫格内能上下左右移动)。数码在任何情况下都不能离开棋盘。给出8个数码的初始状态(没放数码的空格用0表示)和目标状态,问从初始状态到目标状态,最少需要经过多少次移动操作。
输入
两行 第一行9个数字,用空格隔开,表示初始状态 第二行9个数字,用空格隔开,表示目标状态
输出
一个数,即最短路径,如果没有答案,则输出-1
估价函数
考虑 A* 算法,估价函数,
每次移动会把一个数字与空格交换位置,这样至多把一个数字向它在目标状态中的位置移近一步,而从任意一个状态到目标状态的移动步数也不可能小于所有数字当前位置与目标位置的曼哈顿距离之和;
所以估价函数设置为所有数字在当前状态到最终状态中的位置的曼哈顿距离之和;
代码
#include <cstdio>
#include <cstring>
#include <map>
#include <cmath>
#include <queue>
#include <algorithm>
#define MAXN 5
using namespace std;
int a[MAXN][MAXN], b[MAXN][MAXN], sx, sy, ex, ey;
map < long long, bool > f1;
map < long long, int > f2;
int wayx[4] = { 0, 0, 1, -1 };
int wayy[4] = { 1, -1, 0, 0 };
struct node {
int x, y, z, a[MAXN][MAXN], tot;
bool operator < (const node t) const {
return t.tot < tot;
}
};
int h(int a[MAXN][MAXN], int b[MAXN][MAXN]) {
int tot = 0;
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
for (int l = 1; l <= 3; l++) {
for (int k = 1; k <= 3; k++) {
if (a[i][j] == a[l][k]) {
tot += abs(i - k) + abs(j - l);
}
}
}
}
}
return tot;
}
long long gethash(int a[MAXN][MAXN]) {
long long tot = 0, t = 1;
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
tot += a[i][j] * t;
t *= 10;
}
}
return tot;
}
int a_star(int sx, int sy, int ex, int ey, int a[MAXN][MAXN], int b[MAXN][MAXN]) {
priority_queue < node > q;
node t;
t.x = sx, t.y = sy, t.z = 0, t.tot = h(a, b);
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
t.a[i][j] = a[i][j];
}
}
q.push(t);
long long hs = gethash(a), he = gethash(b);
if (hs == he) return 0;
f1[hs] = true, f2[hs] = 0;
while (!q.empty()) {
node t = q.top();
q.pop();
int xr = t.x, yr = t.y;
t.z++;
for (int i = 0; i < 4; i++) {
int dx = xr + wayx[i], dy = yr + wayy[i];
if (dx >= 1 && dx <= 3 && dy >= 1 && dy <= 3) {
swap(t.a[xr][yr], t.a[dx][dy]);
long long tot = gethash(t.a);
if (tot == he) return t.z;
if (f1[tot] == false || (f1[tot] == true && t.z < f2[tot])) {
f1[tot] = true;
f2[tot] = t.z;
t.x = dx, t.y = dy, t.tot = t.z + h(t.a, b);
q.push(t);
}
swap(t.a[xr][yr], t.a[dx][dy]);
}
}
}
return -1;
}
int main() {
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
scanf("%d", &a[i][j]);
if (a[i][j] == 0) {
sx = i, sy = j;
}
}
}
for (int i = 1; i <= 3; i++) {
for (int j = 1; j <= 3; j++) {
scanf("%d", &b[i][j]);
if (b[i][j] == 0) {
ex = i, ey = j;
}
}
}
printf("%d", a_star(sx, sy, ex, ey, a, b));
return 0;
}
四、K 短路
题目
给定一张N个点(编号1,2…N),M条边的有向图,求从起点S到终点T的第K短路的长度,路径允许重复经过点或边。
分析
想到求最短路的 DijkstraDijkstraDijkstra 算法,其思路为用优先队列从起点搜索,则第一次搜索到终点,就为起点到终点的最短路;
则求K短路可用类似的方法,即当第K次搜索到终点时,就为第K短路;
由于题目给出了起点与终点,可考虑用A*算法优化;
估价函数
根据估价函数的设计准则,当前点 xxx 到 eee 点的估计距离应不大于 xxx 到 eee 点的实际距离,则把估价函数定为从 xxx 到 eee 的最短路长度;
则程序思路如下
-
输入时,正反向建两个图,用反向建的图处理结点 xxx 到终点 eee 点的最短路;
-
从起点开始A*搜索扩展状态,当第K次搜索到 eee 结点时,就得到路径长;
当起点为终点时,会把起点算作一次,所以次数+1;
代码
#include <cstdio>
#include <queue>
#include <vector>
#include <cstring>
#include <algorithm>
#define MAXN 1005
#define INF 0x3f3f3f3f
using namespace std;
int n, m, s, e, k;
struct edge {
int to, tot;
};
vector < edge > g1[MAXN], g2[MAXN];
int dis[MAXN];
bool vis[MAXN];
void SPFA (int s) {
memset(dis, INF, sizeof(dis));
dis[s] = 0;
queue < int > q;
q.push(s);
while (!q.empty()) {
int tot = q.front();
q.pop();
vis[tot] = false;
for (int i = 0; i < g2[tot].size(); i++) {
int v = g2[tot][i].to, z = g2[tot][i].tot;
if (dis[v] > dis[tot] + z) {
dis[v] = dis[tot] + z;
if (!vis[v]) {
vis[v] = true;
q.push(v);
}
}
}
}
}
struct node {
int to, z, tot;
bool operator < (const node t) const {
return t.tot < tot;
}
};
int a_star(int s, int e, int k) {
if (dis[s] == INF) return -1;
if (s == e) k++;
int tot = 0;
priority_queue < node > q;
node t;
q.push( node ( { s, 0, 0 + vis[s] } ) );
while (!q.empty()) {
t = q.top();
q.pop();
if (t.to == e) tot++;
if (tot == k) return t.z;
for (int i = 0; i < g1[t.to].size(); i++) {
q.push( node ( { g1[t.to][i].to, t.z + g1[t.to][i].tot, t.z + g1[t.to][i].tot + dis[g1[t.to][i].to] } ) );
}
}
return -1;
}
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= m; i++) {
int a, b, l;
scanf("%d %d %d", &a, &b, &l);
g1[a].push_back( edge ( { b, l } ) );
g2[b].push_back( edge ( { a, l } ) );
}
scanf("%d %d %d", &s, &e, &k);
SPFA(e);
printf("%d", a_star(s, e, k));
return 0;
}
本文详细介绍了A*算法,包括其与BFS的区别、估价函数的设计,并通过八数码问题和K短路问题展示了A*算法的具体应用。估价函数在A*算法中的关键作用是预估未来代价,确保搜索效率。同时,文章提供了八数码问题的估价函数实现,以及K短路问题中使用A*算法优化求解的思路。
1921

被折叠的 条评论
为什么被折叠?



