每周一算法:A*(A Star)算法

题目链接

八数码难题

题目描述

3 × 3 3\times 3 3×3 的棋盘上,摆有八个棋子,每个棋子上标有 1 1 1 8 8 8 的某一数字。棋盘中留有一个空格,空格用 0 0 0 来表示。空格周围的棋子可以移到空格中。要求解的问题是:给出一种初始布局(初始状态)和目标布局(为了使题目简单,设目标状态为 123804765 123804765 123804765),找到一种最少步骤的移动方法,实现从初始布局到目标布局的转变。

输入格式

输入初始状态,一行九个数字,空格用 0 0 0 表示。

输出格式

只有一行,该行只有一个数字,表示从初始状态到目标状态需要的最少移动次数。保证测试数据中无特殊无法到达目标状态数据。

样例 #1

样例输入 #1

283104765

样例输出 #1

4

提示

样例解释

图中标有 0 0 0 的是空格。绿色格子是空格所在位置,橙色格子是下一步可以移动到空格的位置。如图所示,用四步可以达到目标状态。

并且可以证明,不存在更优的策略。

广度优先搜索

算法思想

根据题目描述,输入一个棋盘的初始状态,求从初始状态到目标状态需要的最少移动次数,可以用广度优先搜索求解,基本思想如下:

  • 将初始状态 start \text{start} start的移动步数设为 0 0 0,然后将其加入队列
  • 只要队列不为空
    • 从队首取出一个状态 state \text{state} state
    • 如果 state = end \text{state}=\text{end} state=end结束状态,则搜索结束,返回到达的 state \text{state} state移动步数
    • 否则,找到 state \text{state} state中字符 0 0 0的位置,向相邻方向进行扩展
      • 如果扩展到一个新的状态,则计算扩展到新状态的步数,并将新状态加入队列

代码实现

#include <bits/stdc++.h>
using namespace std;
queue<string> q;
unordered_map<string,int> dis;

int dx[]={-1, 1, 0, 0}, dy[]={0, 0, -1, 1};

int bfs(string start)
{
    string end = "123804765";
    dis[start] = 0;
    q.push(start);
    while(!q.empty())
    {
        string state = q.front(); q.pop();
        int step = dis[state];
        if(state == end) return step;
        int k = state.find('0'); //在当前状态的字符串中找到字符0
        int x = k / 3, y = k % 3; //将字符串中的位置转换为矩阵中的坐标
        for(int i = 0; i < 4; i ++)
        {
            int a = x + dx[i], b = y + dy[i]; 
            if(a < 0 || a >= 3 || b < 0 || b >= 3) continue; //越界
            swap(state[k], state[a * 3 + b]); //将数字字符与0进行交换,转移到新状态
            if(!dis.count(state)) //没有访问过
            { 
                dis[state] = step + 1; //转移到state状态的最小步数
                q.push(state); //入队
            }
            swap(state[k], state[a * 3 + b]); //恢复现场,交换回来,为下次转移做准备
        }
    }
    return -1;
}

int main()
{
    string start;
    char c;
    for(int i = 0; i < 9; i ++)
    {
        cin >> c;
        start += c;
    }
    cout << bfs(start) << endl;
    return 0;
}

A*算法

通过BFS可以发现,对每个状态都可以将 0 0 0向上右下左四个方向进行扩展,在最坏情况下要搜索的状态空间为 4 9 4^9 49,指数级别,搜索的效率比较低。在这种情况下,可以使用A*算法进行求解。

A*(A Star)算法是一种很常用的路径查找和图形遍历算法,它有较好的性能和准确度。

A*算法通过下面的函数来计算每个状态的优先级:

f ( n ) = g ( n ) + h ( n ) f(n)=g(n) + h(n) f(n)=g(n)+h(n)
其中:

  • f ( n ) f(n) f(n)是当前状态 n n n综合优先级。当选择下一个要扩展的状态时,我们总会选取综合优先级最高(值最小)的状态。
  • g ( n ) g(n) g(n)是状态距离起点(初始状态)的代价
  • h ( n ) h(n) h(n)是状态 n n n距离终点(目标状态)的预计代价,这也就是A*算法的启发函数

算法思想

A*算法与BFS类似,不同之处在于A*算法使用优先队列,选取 f ( n ) f(n) f(n)值最小(优先级最高)的状态作为下一个待扩展的状态。基本思想如下:

  • 将初始状态 start \text{start} start的移动步数设为 0 0 0,然后其综合优先级初始状态加入优先队列
  • 只要队列不为空
    • 取出优先队列中综合优先级最高(值最小)的状态 state \text{state} state
    • 如果 state = end \text{state}=\text{end} state=end结束状态,则搜索结束,返回到达的 state \text{state} state移动步数
    • 否则,找到 state \text{state} state中字符 0 0 0的位置,向相邻方向进行扩展
      • 如果扩展到一个新状态,或者到达该状态的步数减少,将状态的综合优先级和状态本身继续加入优先队列

启发函数

从算法的基本思想可以看出来,启发函数会影响A*算法的行为。

  • 在极端情况下,当启发函数 h ( n ) h(n) h(n)为0时,则将由 g ( n ) g(n) g(n)决定状态的优先级,此时算法就退化成了Dijkstra算法。
  • 如果 h ( n ) h(n) h(n)始终小于等于状态 n n n到终点的代价,则A*算法保证一定能够找到最短路径。但是当 h ( n ) h(n) h(n)的值越小,算法将遍历越多的状态,也就导致算法越慢。
  • 如果 h ( n ) h(n) h(n)完全等于状态 n n n到终点的代价,则A*算法将找到最佳路径,并且速度很快。可惜的是,并非所有场景下都能做到这一点。因为在没有达到终点之前,很难确切算出距离终点还有多远。
  • 如果 h ( n ) h(n) h(n)的值比状态 n n n到终点的代价要大,则A*算法不能保证找到最短路径,不过此时会很快。

通过调节启发函数我们可以控制算法的速度和精确度。因为在一些情况,可能未必需要最短路径,而是希望能够尽快找到一个路径即可,这也是A*算法比较灵活的地方。

对于网格形式的图,有以下这些启发函数可以使用:

  • 如果图形中只允许朝上下左右四个方向移动,则可以使用曼哈顿距离(Manhattan distance)。
  • 如果图形中允许朝八个方向移动,则可以使用对角距离。
  • 如果图形中允许朝任何方向移动,则可以使用欧几里得距离(Euclidean distance)。

代码实现

#include <bits/stdc++.h>
using namespace std;
typedef pair<int, string> PIS;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
string e = "123804765";
//启发函数取当前状态中每个数字与其目标位置的曼哈顿距离之和
int h(string state)
{
    int res = 0;
    for(int i = 0; i < state.size(); i ++)
    {
        if(state[i] != '0')
        {
            int k = e.find(state[i]); //当前数字在最终状态的位置
            res += abs(i / 3 - k / 3) + abs(i % 3 - k % 3); //累加每个数字到其正确位置的曼哈顿距离
        }
    }
    return res;
}
int astar(string start)
{
    priority_queue<PIS, vector<PIS>, greater<PIS>> heap; //小顶堆
    unordered_map<string, int> dis; //记录到达每一种状态的步数
    //g(start)返回起点到终点的预估距离
    heap.push({0 + h(start), start});
    dis[start] = 0;
    while(heap.size())
    {
        PIS t = heap.top(); heap.pop();
        string state = t.second;
        if(state == e) break; //终点第一次出队,搜索结束
        int step = dis[state];
        int k = state.find('0'); //找到0所在位置
        int x = k / 3, y = k % 3; 
        for(int i = 0; i < 4; i ++)
        {
            int a = x + dx[i], b = y + dy[i];
            if(a < 0 || a >= 3 || b < 0 || b >= 3) continue;
            swap(state[x * 3 + y], state[a * 3 + b]); //将0和目标交换
            if(!dis.count(state) || dis[state] > step + 1) //如果扩展到一个新的状态,或者能够缩短到state的距离
            {
                dis[state] = step + 1;
                heap.push({dis[state] + h(state), state}); //将综合优先级和状态加入优先队列
            }
            swap(state[x * 3 + y], state[a * 3 + b]);//恢复现场
        }
    }
    return dis[e];
}
int main()
{
    char c;
    string start;
    for(int i = 0; i < 9; i ++)
    {
        cin >> c;
        start += c;
    }
    cout << astar(start);
    return 0;
}

相关练习

洛谷P2324:骑士精神

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

少儿编程乔老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值