A_Star算法是一个主要用bfs实现的寻路算法,当数据量庞大的时候往往有更高的时间效率,可以看成是Dijkstra算法的升级版。
“盲目搜索会浪费很多时间和空间, 所以我们在路径搜索时, 会首先选择最有希望的节点, 这种搜索称之为 "启发式搜索"”(本段话来源于)
特点:时间效率高,但是空间会呈指数增长
A_Star算法时间效率的提升主要为每个数据增加上了优先级,每次更新距离选取优先级最高的点来进行更新(也就是“最有希望的节点”)优先级由这个函数来决定:
g(n)是从起点到n点的距离
h(n)是从n点到终点的预期距离,被称为启发函数(顾名思义就是通过预期来给定最有希望的花费)
A_Star最核心就在于如何去选择启发函数:
1.如果网格图中只能上下左右四个方向移动时:曼哈顿距离
用公式来说就是:A(a,b) B(c,d) 曼哈顿距离
2.如果能向周围上下左右斜上歇下八个方向移动时:对角距离
function heuristic(node)
local dx = abs(node.x-goal.x)
local dy = abs(node.y-goal.y)
return D * (dx + dy) + (D2 - 2 * D) * min(dx,dy)
end
3.如果能向任意方向移动时:欧几里得距离
换句话说就是两点之间的直线距离
其次要注意的是,关于启发函数的选取必须要保证:,即从起点到点n的最短距离加上点n到终点的预期距离必须小于等于从起点到终点的最短距离。其次,如果启发函数h(n)的值一直为0,那么就会退化成Dijkstra算法,甚至从时间空间上来说不如Dijkstra算法
关于A_Star算法有个非常经典的八数码问题,类似于数字华容道:题目链接
题目首先给出一个字符串,例如:
//2 3 4 1 5 x 7 6 8
我们要做的本质上就是怎么移动x把它变成:
//1 2 3 4 5 6 7 8 x
首先说结论:如果给定的字符串是有解的,那么这个字符串的逆序对数量一定是一个偶数
理由如下:
首先我们最后要让字符串变成"12345678x",这串数字的逆序对数量毫无疑问为0,那也就是说我们的最终答案要让字符串的逆序对数量变成0.
其次我们通过观察可以发现:
/*
2 3 4 2 3 x
1 5 x --> 1 5 4
7 6 8 7 6 8
2 3 4 1 5 x 7 6 8
|
2 3 x 1 5 4 7 6 8
*/
不论当我们怎样移动x的位置,移动之后的结果在一个字符串上得到的结果都是将x的位置往前或者往后移动两位,也就是说移动之后的结果只会将移动前的字符串逆序对数量+2或者-2或者不变,对逆序对的奇偶性没有产生任何影响。这也就说明了如果逆序对数量为奇数的话,不论我们怎么移动,都不会将逆序对数量变成0,也就无解
知道了这些条件,我们用A_Star算法就可以解决这题:
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
#include <unordered_map>
#define x first
#define y second
using namespace std ;
typedef pair<int, string> PIS ; //预期值(启发函数) 字符串
int dx[4] = {0, 0, 1, -1} ; //上下左右四个方向
int dy[4] = {1, -1, 0, 0} ;
char op[] = "rldu" ; //操作字符,根据上面的方向来写的
int h(string s) { //用来计算启发函数,也就是曼哈顿距离 f(n)
int res = 0 ;
for (int i = 0 ; i < s.size() ; i ++) {
if (s[i] != 'x') {
int t = s[i] - '1' ;
res += abs(t / 3 - i / 3) + abs(t % 3 - i % 3) ; //曼哈顿距离
}
}
return res ;
}
string bfs(string start) {
string end = "12345678x" ; //最终状态
unordered_map<string, int>dis ; //dis表示从起点到n点的最短距离
unordered_map<string, pair<char, string>> prev; //prev用来记录路径
priority_queue<PIS, vector<PIS>, greater<PIS>> heap ; //heap是优先队列,根据第一个键值(启发函数)从小到大排序
dis[start] = 0 ;
heap.push({h(start), start}) ;
while (heap.size()) {
auto t = heap.top() ;
heap.pop() ;
string tmp = t.y ;
if (tmp == end) //如果达到了最终状态直接break
break ;
int x, y ;
for (int i = 0 ; i < 9 ; i ++) { //获取'x'的坐标
if (tmp[i] == 'x') {
x = i / 3, y = i % 3 ;
break ;
}
}
string backup = tmp ;
for (int i = 0 ; i < 4 ; i ++) {
int tx = x + dx[i], ty = y + dy[i] ;
if (tx < 0 || tx >= 3 || ty < 0 || ty >= 3)
continue ;
tmp = backup ;
swap(tmp[tx * 3 + ty], tmp[x * 3 + y]) ;
if (!dis.count(tmp) || dis[tmp] > dis[backup] + 1) { //如果dis没有出现过tmp,或者dis[tmp]有更优解
dis[tmp] = dis[backup] + 1 ;
prev[tmp] = {op[i], backup} ;
heap.push({dis[tmp] + h(tmp), tmp}) ;
}
}
}
string res ;
while (end != start) { //回溯返回答案
res += prev[end].x ;
end = prev[end].y ;
}
reverse(res.begin(), res.end()) ;
return res ;
}
int main() {
char c ;
string str, seq ; //seq用来计算逆序对的数量
while (cin >> c) {
if (c != 'x')
seq += c ;
str += c ;
}
int cnt = 0 ;
for (int i = 0 ; i < 8 ; i ++) { //计算逆序对的数量
for (int j = i ; j < 8 ; j ++) {
if (seq[j] > seq[i])
cnt ++ ;
}
}
if (cnt & 1) //如果是奇数直接unsolvable
puts("unsolvable") ;
else
cout << bfs(str) << '\n' ;
return 0 ;
}
用这串代码是可以过掉AcWing上面的八数码题,但是HDU上面的八数码是过不了了
前面我们说过A_Star算法的一个弊端就是空间会呈指数增长,如果数据量太庞大,我们内存可能会超限。所以说我们需要用别的方法来优化空间复杂度。
这就用到了一个知识叫做"康托展开":
康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。 (百度)
康托展开运算:
a表示的是当前数字往右有多少比它大的数,例如:
//4 5 3 1 2
1.第一位是4,往右边数总共有三个数比它小,所以就是3×4!
2.第二位数字是5,往右边数总共有三个数比它小,所以就是3×3!
3.第三位数字是3,往右边数总共有两个数比它小,所以就是2×2!
4.第四位是1,往右边数没有数字比它小,所以就是0×1!
5.第五位数字是2,往右边数没有数字比它小,所以就是0×0!
最终X=3×4!+3×3!+2×2!+0×1!+0×0!=94
这样就得到了45312这个全排列到自然数的一个映射
同时康托也能逆展开:
1.94 / 4! = 3 ······ 22 三个数比它小:4
2.22 / 3! = 3 ······ 4 三个数比它小:5
3.4 / 2! =2 两个数比它小:3
··················
通过这样的思路我们就能将一个康托展开映射成的数,反过来推出原来的排列结果:45312
通过康托展开我们就可以构建哈希表来对空间复杂度进行压缩,学习这块的知识时我看了很多人的代码,其中我觉得这个大佬的代码写的最为好看:大佬链接
其中他的手写类好看的让我欲罢不能,所以我大体也是照着他的模样来学习的,我改动了字符串处理代码和函数的传参类型,我觉得会更好看一点,代码如下:
#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std ;
const int n = 9 ;
const int fac[n] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320} ; //预处理阶乘
const int dx[4] = {0, 0, 1, -1} ;
const int dy[4] = {1, -1, 0, 0} ;
char op[] = "rldu" ; //路径
const int N = 362880 ;//最大的hash值+1,也就是排列876543210的hash值+1
const int aim = 46233; //123456780的hash值
char step[N] ; //路径
bool vis[N] ; //记录有没有被hash过
int mp[N] ; //实际路径
int parent[N] ;
int goal_state[9][2] = { //预处理坐标
{0, 0}, {0, 1}, {0, 2},
{1, 0}, {1, 1}, {1, 2},
{2, 0}, {2, 1}, {2, 2}
};
int *solve(string s) { //处理字符串
static int a[n] ;
int cnt = 0;
for (int i = 0 ; i < s.size() ; i ++) {
if (s[i] == 'x') {
a[cnt ++] = 0 ;
continue ;
}
if (s[i] >= '0' && s[i] <= '9') {
a[cnt ++] = s[i] - '0' ;
}
}
return a ;
}
int getcnt(int *a) { //统计逆序对数量
int cnt = 0;
for (int i = 0 ; i < 9 ; i ++) {
if (a[i] == 0)
continue ;
for (int j = i + 1 ; j < 9 ; j ++) {
if (a[j] == 0)
continue ;
if (a[j] < a[i])
cnt ++ ;
}
}
return cnt ;
}
int cantor(int *s) { //康托展开
int res = 0, cnt = 0 ;
for (int i = 0 ; i < n ; i ++) {
cnt = 0 ;
for (int j = i + 1 ; j < n ; j ++) {
if (s[i] > s[j])
cnt ++ ;
}
res += fac[n - 1 - i] * cnt ;
}
return res ;
}
int h(int *s) { //曼哈顿距离
int pos, res = 0 ;
for (int i = 0 ; i < 3 ; i ++) {
for (int j = 0 ; j < 3 ; j ++) {
pos = i * 3 + j ;
if (s[pos] == 0)
continue ;
res += abs(i - goal_state[s[pos] - 1][0]) + abs((j - goal_state[s[pos] - 1][1]));
}
}
return res ;
}
class Chess {
public:
int hash, f ;
Chess(const int &hash, const int &f) : hash(hash), f(f) { //康托哈希值,花费值
}
bool operator<(const Chess &t)const {
if (f == t.f)
return mp[hash] > mp[t.hash] ;
return f > t.f ;
}
};
void reverseCantor(int hash, int s[], int &space) { //逆康托展开
bool visited[n] = {} ;
int tmp ;
for (int i = 0 ; i < n ; i ++) {
tmp = hash / fac[n - 1 - i] ;
for (int j = 0 ; j < n ; j ++ ) {
if (!visited[j]) {
if (tmp == 0) {
s[i] = j ;
if (j == 0)
space = i ;
visited[j] = true ;
break ;
}
tmp -- ;
}
}
hash %= fac[n - i - 1] ;
}
}
void printPath() { //回溯打印路径
char queue[31] ;
int t = 0 ;
int c = aim ;
while (parent[c] != -1) {
queue[t] = step[c] ;
t ++ ;
c = parent[c] ;
}
for (int i = t - 1 ; i >= 0 ; i --) {
cout << queue[i] ;
}
puts("") ;
}
void A_star(int start, int f) {
priority_queue<Chess> q ;
q.push(Chess(start, f)) ;
int preHash, hash, state[n], space ;
while (!q.empty()) {
Chess preChess = q.top() ;
preHash = preChess.hash ;
if (preHash == aim) {
printPath() ;
return ;
}
q.pop() ;
reverseCantor(preHash, state, space) ;
for (int i = 0 ; i < 4 ; i ++) {
int tx = space / 3 + dx[i], ty = space % 3 + dy[i] ;
if (tx < 0 || tx >= 3 || ty < 0 || ty >= 3)
continue ;
int tz = tx * 3 + ty ;
state[space] = state[tz] ;
state[tz] = 0 ;
hash = cantor(state) ;
if (!vis[hash]) {
step[hash] = op[i] ;
mp[hash] = mp[preHash] + 1 ;
vis[hash] = true ;
parent[hash] = preHash ;
q.push(Chess(hash, mp[hash] + h(state))) ;
} else if (mp[hash] > mp[preHash] + 1 ) {
step[hash] = op[i] ;
mp[hash] = mp[preHash] + 1 ;
parent[hash] = preHash ;
q.push(Chess(hash, mp[hash] + h(state))) ;
}
state[tz] = state[space] ;
state[space] = 0 ;
}
}
puts("unsolvable") ;
}
int main() {
string s ;
while (getline(cin, s)) {
int *a = solve(s) ;
if (getcnt(a) & 1) {
puts("unsolvable") ;
continue ;
}
int hash = cantor(a) ;
memset(vis, false, sizeof(vis)) ;
memset(mp, 0, sizeof(mp)) ;
vis[hash] = true ;
parent[hash] = -1 ;
mp[hash] = 0 ;
A_star(hash, h(a)) ;
}
return 0;
}
水平有限,欢迎指正