拼图问题
bfs
中的八数码问题
例题
在一个 3×33×3 的网格中,1∼8 1∼8 这 88 个数字和一个 x
恰好不重不漏地分布在这 3×3 3×3 的网格中。
例如:
在游戏过程中,可以把 x
与其上、下、左、右四个方向之一的数字交换(如果存在)。
我们的目的是通过交换,使得网格变为如下排列(称为正确排列):
例如,示例中图形就可以通过让 x
先后与右、下、右三个方向的数字交换成功得到正确排列。
交换过程如下:
现在,给你一个初始网格,请你求出得到正确排列至少需要进行多少次交换。
分析
看完了题目,这让我想起,我们之前可能玩过一种拼图的游戏,在玩游戏的过程中我们会得到一张比较好看的图片,但是它缺了一个口!然后呢,别人将这个图片给打乱了,就像9宫格这样打乱,让我们一个一个对着这个口移动回去。
在移动的过程中我们会发现,我们不是计算机,不可以很快的判断出我们要移动几步,因此我们会先按照我们的感觉,对x
这个缺口的上下左右依次进行移动,看看移动之后是否会更快地把这个拼图给拼回去。但是我们在拼图的时候也可能会出现重复的情况,比如我们移动了若干步,最后我们还是回到了起点,这样的话,我们之前移动的那些步数不就直接没有用了吗?
所以,为了让我们的计算机不犯这种低级错误,我们得让我们的计算机知道我们之前走过哪些步骤,这就需要stl
中的unordered_map
来发挥作用了,我们可以把我们的状态用string
类型的字符串来表示,int
类型用来记录这种已经出现过的步数是什么时候走过的,同时为了管理我们的步数,我们还要添加一个stl
中的queue
容器来存储我们的步数
以下便是我们的初始状态
#include<iostream>
#include<algorithm>
#include<queue>
#include<unordered_map>
using namespace std;
const int N = 100010;
int main()
{
string start;
for (int i = 0; i < 9; i++)
{
char ch[2];
cin >> ch;
start += *ch;
}
cout << bfs(start) << endl;
}
我们用string
类型的字符串数组来存储我们一开始的棋盘格局,例如string start = "123x46758"
,这样子也可以与我们的map
保持一致,下面的for
循环便是我们对初始状态的赋值
最后对于bfs(start)
的返回值进行输出就得到结果啦!
bfs函数的算法设计
queue<string> q;
unordered_map<string, int> d;
q.push(start);
d[start] = 0;
int dx[4] = { -1,0,1,0 }, dy[4] = { 0,1,0,-1 };
string end = "12345678x";
上面的q
,d
是我们定义的两个stl
容器,q.push(start)
就是把我们的初始状态放到队列中,d[start]
是用来记录这是我们第几步得到的结果,如上述的0就是我们的初始状态。
玩游戏时我们要一一试错,计算机是很蠢的,它也要一一试错,这时我们就要定义偏移量了,下面的dx
,dy
数组就是给我们调偏移量的,最后我们也要让电脑知道它有没有调错是吧,那我们还要设置一个string
字符串end
来记录结束标志,一旦我们找到了解那就直接结束了。
进入队列
while (q.size())
{
//入队
auto t = q.front();
q.pop();
//如果t的状态与end状态一致,返回数值
if (t == end) return d[t];
int distance = d[t];
//找到x的位置
int k = t.find('x');
//技巧:从一维数组转成二维将(k/竖直方向上的数),(k%水平方向上的数)
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)
{
//将x值的位置与另外一个值交换
swap(t[a * 3 + b], t[k]);
//如果没有换过(没有重复之前的状态)
if (!d.count(t))
{
d[t] = distance + 1;
q.push(t);
}
//状态恢复
swap(t[a * 3 + b], t[k]);
}
}
}
return -1;
首先,我们定义一个auto
类型的变量t
来更好地运用队列,引用队列中的第一个元素后,q.pop()
把第一个元素出列,由于队列容器不支持我们拿出来它的中间值,所以我们这样做更加方便一些
每次我们先判断是否达到了预期效果,假如我们一开始就成功了,那就不用动图片任何一步就可以返回值了
我们在移动九宫格的时候是根据x
的位置来进行移动的,同理,我们要让电脑找到我们x
的位置在哪里,不然的话我们根本移动不了好吧,我们用k
记录我们x
的位置,用distance
来记录当前步数所对应的值,比如在这个示例中,开始不是结束状态,我们不能一下子返回值,我们要拿一个distance
来记录,由于我们一开始定义的string
是一维数组的形式,这使得我们可以更好的找到x
的位置
下面的方法就很有技巧性了,我们在一维数组中找到了x
的位置,那怎么在二维数组上还原呢?有个技巧
小技巧
设一个一维数组的容量为100,把这个一维数组用二维数组4*25的形式表现出来,我们在一维数组上找的x
的位置在72这个位置上(72指的是数组下标,指的是a[72]
),那么对应的二维数组上的位置就是[72/25][72%25]
的位置上,也就是[2][22]
这个位置,是不是很巧妙?
那么二维数组是怎么还原成一维数组呢?我们现在已经可以把一维数组转变成二维数组了,那么我们也可以通过这种方法把二维数组下标重新变成一维的形式,比如说在二维数组上的a[1][15]
,二维数组的形式还是425,那么二维转一维就是`a[125+15],也就是
a[40]`
遍历每一处
现在我们进行傻瓜式移动,对x
上下左右进行移动,只要它,欸,跟之前的不重复,没越界,我们就记录它好吧,我们设a
,b
是x
移动后的坐标
只要不越界,我们就进入第一处循环,交换x
与那个数字的位置
再到map
地图中找一下有没有重复
没有的话,就再进入下一层if
我们的d[t]
也要在上一步distance
距离的情况下加1(我们移了一步嘛),然后进队q.push(t)
状态恢复
再来一次swap
恢复状态,不影响后面的发挥
得出结果
要么return d[t]
,要么return -1
比如这个
引入
—acwing算法基础课 (yxc主讲)