用途
- 用于最小步数模型,可以在庞大的搜索空间中减少搜索范围;
- 需要注意的是,如果没有路径可以到达终点,那么A*算法还是会搜索所有的状态.这种情况下,A*算法每选择一个状态的时间复杂度为
O(logn)
,而普通BFS只需要O(1)
- 其次,A*算法一般应用于非负权边图;如果含负权边且不存在负环,则也可以应用(待定)
原理:
- A*算法的核心在于估价函数,用于估算当前状态到终点的步数;
- 假设实际距离为
g(state)
,估计距离为f(state)
,则估价函数必须满足:f(state) <= g(state)
; - 越接近则算法效果越好,如果估计值全部为0,则退化为dijkstra算法
- A*算法使用小根堆,按照
d(u) + f(u)
排序 (假设到达状态u已用步数为d(u))
性质
1.当终点状态第一次出队,即表示找到了最小步数
证明-反证法
- 假设不是最小步数,即有:
d(end) > d(优)
- 说明堆中尚存在一条最优路径上的点u满足
d(优) = d(u) + g(u) >= d(u) + f(u)
(堆中至少包含起点) - ->
d(end) > d(优) >= d(u) + f(u)
, 这与小根堆的性质产生矛盾,说明假设错误
2.除终点外,每一点可能被更新多次;
3.A*算法不能保证到每一点(除终点外)在入队/出队时距离是最优的,这一点与dijkstra算法区分
算法步骤
while(q.size())
{
优先队列中堆顶元素出队;
if(堆顶元素==终点状态) break;
使用堆顶元素更新其他状态,对更新的状态进行估价,并将其入队
}
例题 -八数码
题解
- 估计函数:计算当前状态下每一数字距离其正确位置的曼哈顿距离之和
- 八数码问题是否有解:计算对应字符串中逆序对的个数,如果为偶数则有解,反之无解。大概的解释是:每一次变化对于逆序对的影响必定是偶数个,且正解中逆序对的个数也是偶数
代码
#include <iostream>
#include <cstring>
#include <queue>
#include <unordered_map>
#include <algorithm>
using namespace std;
typedef pair<int , string> PIS;
string start;
int dx[] = {-1 , 1 , 0 , 0} , dy[] = {0 , 0 , -1 , 1};
//估价函数
int f(string str)
{
int res = 0;
for(int i = 0; i < 9; i ++)
{
if(str[i] !='x')
{
int t = str[i] - '1';
res += abs(i/3 - t/3) + abs(i%3 - t%3);
}
}
return res;
}
string bfs()
{
string over = "12345678x";
string op = "udlr";
priority_queue<PIS , vector<PIS> , greater<PIS> > heap;
unordered_map<string , int> dist;
unordered_map<string , pair<char,string>> prev;
heap.push({f(start) , start});
dist[start] = 0;
while(heap.size())
{
PIS t = heap.top();
heap.pop();
string state = t.second;
if(state == over) break;
int d = dist[state];
int u = state.find('x');
int a = u / 3 , b = u % 3;
for(int i = 0; i < 4; i++)
{
int x = a + dx[i] , y = b + dy[i];
if(x < 0 || x >= 3 || y < 0 || y >= 3) continue;
swap(state[u] , state[x*3 + y]);
if(!dist.count(state) || dist[state] > d + 1)
{
dist[state] = d + 1;
prev[state] = {op[i] , t.second};
heap.push({f(state) + dist[state] , state});
}
swap(state[u] , state[x*3 + y]);
}
}
string res;
//对状态转移进行回溯,记录操作(udlr)
while(over != start)
{
res += prev[over].first;
over = prev[over].second;
}
reverse(res.begin() , res.end());
return res;
}
int main()
{
string t;
for(int i = 0; i < 9; i++)
{
char a; cin >> a;
start += a;
if(a != 'x') t += a;
}
int cnt = 0;
for(int i = 0; i < 8; i++)
for(int j = i + 1; j < 8; j++)
if(t[i] > t[j]) cnt++;
if(cnt & 1) puts("unsolvable");
else cout << bfs() << endl;
return 0;
}
例2-第k短路
题解
1.终点第几次出队,即表示第几短路,证明与最短路类似
2.估价函数:反向建边,跑一遍dijkstra算法,即可以求出与真实值相同的估计值
代码
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
#define x first
#define y second
typedef pair<int,int> PII;
typedef pair<int,PII> PIII;
const int N = 1010 , M = 200010;
int e[M] , ne[M] , w[M] , sh[N] , eh[N] , idx;
int dist[N] , cnt[N] , n , m;
bool st[N];
int S , T , K;
void add(int l ,int r , int v , int h[])
{
e[idx] = r , w[idx] = v , ne[idx] = h[l] , h[l] = idx ++;
}
void dijkstra()
{
memset(dist , 0x3f , sizeof dist);
dist[T] = 0;
priority_queue<PII , vector<PII> , greater<PII> > heap;
heap.push({0 , T});
while(heap.size())
{
PII t = heap.top();
heap.pop();
int d = t.x , u = t.y;
if(st[u]) continue;
for(int i = eh[u]; ~i; i = ne[i])
{
int v = e[i];
if(dist[v] > dist[u] + w[i])
{
dist[v] = dist[u] + w[i];
heap.push({dist[v] , v});
}
}
st[u] = true;
}
}
int astar()
{
priority_queue<PIII , vector<PIII> , greater<PIII> > heap;
heap.push({dist[S] , {0 , S}});
while(heap.size())
{
PIII t = heap.top();
heap.pop();
int d = t.y.x , u = t.y.y;
cnt[u]++;
if(cnt[T] == K) return d;
for(int i = sh[u]; ~i; i = ne[i])
{
int v = e[i];
int tmp = d + w[i];
if(cnt[v] < K) //对于在极端情况下可能会有问题,这里是对正确性和时间做了下权衡。
heap.push({tmp + dist[v] , {tmp , v}});
}
}
return -1;
}
int main()
{
cin >> n >> m;
memset(sh , -1, sizeof sh);
memset(eh , -1, sizeof eh);
for(int i = 0; i < m; i++)
{
int l , r , v;
cin >> l >> r >> v;
add(l ,r , v , sh) , add(r , l , v , eh);
}
cin >> S >> T >> K;
if(S == T) K++; //注意特殊数据:S==T,则 K++剔除自己走到自己的情况
dijkstra();
printf("%d
" , astar());
return 0;
}
参考资料
Acwing-算法提高课-搜索章节:
https://www.acwing.com/activity/content/introduction/16/