(算法提高课)搜索-A*

文章讲述了如何通过A*算法解决八数码问题,利用曼哈顿距离作为启发式估价,确保最终序列逆序对为偶数,实现最少移动次数的网格排列。
摘要由CSDN通过智能技术生成

179. 八数码

在一个 3×3的网格中,1∼8这 8个数字和一个 X 恰好不重不漏地分布在这 3×3的网格中。
例如:
1 2 3
X 4 6
7 5 8
在游戏过程中,可以把 X 与其上、下、左、右四个方向之一的数字交换(如果存在)。
我们的目的是通过交换,使得网格变为如下排列(称为正确排列):
1 2 3
4 5 6
7 8 X
例如,示例中图形就可以通过让 X 先后与右、下、右三个方向的数字交换成功得到正确排列。
交换过程如下:
1 2 3 1 2 3 1 2 3 1 2 3
X 4 6 4 X 6 4 5 6 4 5 6
7 5 8 7 5 8 7 X 8 7 8 X
把 X 与上下左右方向数字交换的行动记录为 u、d、l、r。
现在,给你一个初始网格,请你通过最少的移动次数,得到正确排列。
输入格式

输入占一行,将 3×3的初始网格描绘出来。
例如,如果初始网格如下所示:
1 2 3
x 4 6
7 5 8
则输入为:1 2 3 x 4 6 7 5 8
输出格式
输出占一行,包含一个字符串,表示得到正确排列的完整行动记录。
如果答案不唯一,输出任意一种合法方案即可。
如果不存在解决方案,则输出 unsolvable。
输入样例:
2 3 4 1 5 x 7 6 8
输出样例
ullddrurdllurdruldr

这个题型算法分析与设计课程中讲过

难点一:如何保证有解
八数码问题有解等价于:将字符按行读取之后,读出的序列的逆序对的数量为偶数(该结论对所有n为奇数的 n ∗ n n*n nn 方格数码成立)
因为本题只能上下左右四个方向移动(将x看作空位的话)

  • 其中,某个单元向右移动或者向左移动时,并不改变最后按行取出的序列数字顺序,此时序列对的改变数目为0
  • 若单元格向上(向下)移动,此时该单元格在按行取出的序列中,将向前移动两个字符(向后移动两个字符)。和每个字符将形成一个奇数的序列对数目改变(要么+1,要么-1),两个奇数之和必然为偶数;
  • 初始序列(正确序列)的逆序对数量为0,每次移动造成序列对偶数的改变,因此最后序列对必然为偶数

其中逆序对的定义为:在给定序列中,若 i < j i<j i<j 且有 a [ i ] > a [ j ] a[i] > a[j] a[i]>a[j],此时构成一个逆序对


难点二:
本题中的估价函数当前状态中的每个数字与它目标状态的曼哈顿距离之和
( x i , y i ) 和 ( x j , y j ) (x_i,y_i)和(x_j,y_j) (xi,yi)(xj,yj)的曼哈顿距离: D ( i , j ) = ∣ x i − x j ∣   +   ∣ y i − y j ∣ D(i,j) = |x_i - x_j| ~+~|y_i - y_j| D(i,j)=xixj + yiyj


关于 A ∗ A^* A算法:
可以理解为加了启发式剪枝的BFS。
启发式剪枝,是一种估算当前状态到终点状态所需的步数,我们称估算的方法叫做估价函数。设当前点为 u u u,已经走了 f ( u ) f(u) f(u)步,估价函数为 h ( u ) h(u) h(u),实际到终点的距离为 g ( u ) g(u) g(u),则必须满足以下性质: h ( u ) ≤ g ( u ) h(u) \leq g(u) h(u)g(u)
所以我们要以 f ( u ) + h ( u ) f(u)+h(u) f(u)+h(u)为关键字进行排序(有点类似于堆化的 D i j k s t r a Dijkstra Dijkstra

简单来说 A ∗ A^* A算法在处理队列的元素上进行了优化

  1. 拿出队头并扩展元素
  2. 估计扩展元素到终点的距离
  3. 将扩展后的元素进行从小到大的排序,并且小的作为对头(这样做大大减少了几乎无意义的数据带来的影响)

应用的环境:

  1. 有解(无解时,仍然会把所有空间搜索,会比一般的bfs慢,因为优先队列的操作是logn的)
  2. 边权非负,如果是负数,那么终点的估值有可能是负无穷,终点可能会直接出堆

性质:除了终点以外的其他点无法在出堆或者入堆的时候确定距离,只能保证终点出堆时是最优的。


关于题目本身:
自己理解的本题其实不算严格意义上的按层来拓展,尤其是代码的第55行。在之前的题目中通常会设置一个vis数组或者直接通过map的count方法来判定某个元素是否被访问过,而如果被访问过通常会直接continue,因为如果按层次遍历则第一次被遍历到的一定是到源点路径最短的

而本题的拓展规则是要与目标序列靠齐,为了减小搜索宽度,本题添加了一个估价函数,用曼哈顿距离来刻画当前序列和目标序列的相似程度,并且先搜索与目标序列相似度最高的序列

所以本题不算从源点开始的不断广搜。而是每次从队首取出与目标序列相似度最高的序列,进行以这个序列为元序列往下一层的广搜,并将这些序列入队;再从队首取出相似度最高的序列,再进行往下一层的广搜…所以会出现需要更新的情况。因为每次队首选出的元序列本身,它的dis就不一定的递增的(以前的广搜相当于将其与源点的距离作为第一标准放入了优先队列)
但是第一次更新到end的时候,一定是距离最短的时候,对此结论可做以下解释:
设终点出队的时候的距离为 d i s dis dis,且 d i s > d i s b e s t dis>disbest dis>disbest(最优解)
设在最优路径上存在一点 u u u,最优路径为起点——> u u u——>终点
d i s t 优 dist_{优} dist = d i s t [ u ] + g ( u ) ≥ d i s t [ u ] + f ( u ) = dist[u] + g(u) \geq dist[u] + f(u) =dist[u]+g(u)dist[u]+f(u)
            = > d i s t [ e n d ] > d i s t 优 > = d i s t [ u ] + f ( u ) ~~~~~~~~~~~ => dist[end] > dist_优 >= dist[u] + f(u)            =>dist[end]>dist>=dist[u]+f(u)
说明优先队列中存在一个比出堆元素更小的值(end已经出队作为答案而正确路径u还未出队),这就矛盾了。
所以说终点第一次出堆时就是最优的。

#include<bits/stdc++.h>
using namespace std;
//这里将第一个设置为int类型方便后续存放在小根堆中
//若优先队列中存储的为复合结构,如pair,默认情况下,排序规则是先按照pair的first的属性降序排列,如果first相等,则按照second属性降序排序
//而本题中的第一考虑元素就是first中存储的曼哈顿距离
typedef pair<int, string> PIS;

//估价函数计算,计算当前序列中的所有点和目标序列中的曼哈顿距离
int f(string state)
{
    int dis = 0;
    for(int i = 0;i < state.size();i ++){
        if(state[i] != 'x'){
            int t = state[i] - '1';   //t为当前位置的数字,i为应该在该位置的数字
            dis += abs(i / 3 - t / 3) + abs(i % 3 - t % 3);
        }
    }
    return dis;
}

string bfs(string start)
{
    int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
    char op[] = {'u', 'd', 'l', 'r'};
    string end = "12345678x";

    unordered_map<string, int> dis;   //用来存储距离,以及从起点开始变化的步数,每移动一次在原有的dis上++
    unordered_map<string, pair<string, char> > prev;  //当前序列-->前序列,到当前序列的操作字符  的一组映射
    priority_queue<PIS, vector<PIS>, greater<PIS> > heap;  //建立一个小根堆,以第一个PIS的第一个估价函数来优化队列

    dis[start] = 0;  heap.push({f(start), start});  //初始化

    while(!heap.empty()){
        PIS now = heap.top(); heap.pop();
        string state = now.second;  //记录当前拓展的元序列
        int step = dis[state];  //记录到起点的距离(按层拓展)

        if(state == end) break;

        int x, y; //找到当前的空格点
        for(int i = 0;i < state.size();i ++){
            if(state[i] == 'x'){
                x = i / 3;
                y = i % 3;
                break;
            }
        }
        string init = state;
        for(int i = 0;i < 4;i ++){
            int sx = x + dx[i], sy = y + dy[i];
            if(sx < 0 || sy < 0 || sx >= 3 || sy >= 3) continue;
            state = init;
            swap(state[x * 3 + y], state[sx *3 + sy]);
            //如果当前序列没有被拓展过且其值需要更新
            if(!dis.count(state) || dis[state] > step + 1){
                dis[state] = step + 1;
                prev[state] = {init, op[i]};
                heap.push({dis[state] + f(state), state});
            }
        }
    }

    string ans = "";
    while(end != start){
        ans += prev[end].second;
        end = prev[end].first;
    }
    reverse(ans.begin(), ans.end());
    return ans;
}
int main()
{
    string start, seq, c;
    while(cin >> c){   //读取含有空格的输入
        start += c;
        if(c != "x") seq += c;
    }

    int cnt = 0;  //统计逆序对的数量
    for(int i = 0;i < 8;i ++){
        for(int j = i + 1;j < 8;j ++){
            if(seq[i] > seq[j]) cnt ++;
        }
    }
    if(cnt % 2)  puts("unsolvable");
    else cout << bfs(start) << endl;

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值