1. 问题描述:
如图所示: 有9只盘子,排成1个圆圈。其中8只盘子内装着8只蚱蜢,有一个是空盘。
我们把这些蚱蜢顺时针编号为 1~8。每只蚱蜢都可以跳到相邻的空盘中,也可以再用点力,越过一个相邻的蚱蜢跳到空盘中。 请你计算一下,如果要使得蚱蜢们的队形改为按照逆时针排列,并且保持空盘的位置不变(也就是1-8换位,2-7换位,...),至少要经过多少次跳跃?
输出
输出一个整数表示答案
来源:http://oj.ecustacm.cn/problem.php?id=1318
2. 思路分析:
① 分析题目可以知道我们已知一个初始的盘面状态,需要求解到达目标盘面状态的最少步数,根据这个原状态到目标状态的最少步数的特点可知可以使用bfs(宽度优先搜索),bfs可以求解出从原状态到达目标状态的最少步数,分析题目可以设置原状态为"012345678",目标状态为"087654321",其中0表示空盘子的位置,整个过程其实可以看成是空盘子与周围的元素在交换位置。将状态设置为str字符串类型这样可以在搜索的时候直接比较原状态与目标状态对应的字符串是否相等。当前状态中0的位置可以与从当前0的位置算起的左右两边的第一个元素或者第二个元素交换,可以通过下面的图来理解具体的过程,其实就是0的位置与0周围的第一个或第二个位置的元素交换,所以在使用bfs搜索的时候可以尝试加入周围的邻接状态有四个(0与周围元素交换位置之后的状态),为了方便处理可以使用一个方法来往队列中加入周围的四个邻接状态,方法中可以传递当前的0的位置与可以交换的元素的位置。为了避免在搜索的过程中出现重复状态,可以使用set集合对状态进行去重,只有当当前的状态在set集合中不存在的时候才可以加入当前的队列和set集合中。
② 使用bfs搜索都是固定的套路,因为使用的是python语言所以使用collections.deque()声明一个双端队列,这样在弹出队列节点的时候效率会更高一点,一开始的时候往队列中加入初始节点,节点为元组类型(python中的元组一个很方便的点是可以封装多个属性而且可以通过索引取出具体的元素),元组中的属性包括当前的状态(表示当前盘子与蚱蜢的的位置)对应的字符串,当前盘子的位置,到达当前状态需要的步数,使用字符串表示状态这样在弹出队列节点的时候可以直接比较当前状态是否与目标状态是否相等。一开始的时候弹出队首节点,判断当前的状态与目标状态是否相等如果原状态与目标状态不相等那么继续执行循环并且尝试加入当前状态相关的四个邻接状态,因为python中的字符串是不可以修改的所以我们在交换元素的时候需要将当前的字符串转为列表,然后交换元素的位置之后将列表通过join()方法变为字符串类型加入到队列中。
③ 因为题目中蚱蜢与盘子是围成一个圈的,所以我们在交换元素的时候需要通过取余操作实现列表元素的滚动,比如"012345678"中0可以与8交换位置,8表示左边的-1位置,所以实际上0应该与(-1 + 9)% 9 = 8也就是第八个元素的位置,其实取余的操作就是模拟围成一个圈的过程,这也是这道题目与其他得题目不同的一个点。
3. 代码如下:
import collections
# 使用一个函数来往队列中加入当前的邻接状态这样会更容易处理
def insertQueue(queue: collections.deque, dir: int, poll: tuple, vis: set):
pos = poll[1] # 0元素的位置
status = poll[0] # 当前的状态
# 因为题目中的状态是滚动的所以需要对9取余实现滚动, insertPos表示与0交换的元素的索引
insertPos = (pos + dir + 9) % 9
# 将字符串转为列表才可以交换元素, 交换列表中的元素之后然后再通过join方法将列表中的元素转为字符串
t = list(status)
t[pos], t[insertPos] = t[insertPos], t[pos]
addStatus = "".join(t)
if addStatus not in vis:
# set集合去重加入当前得到的状态
vis.add(addStatus)
queue.append((addStatus, insertPos, poll[2] + 1))
if __name__ == '__main__':
# 因为求解的是最少的步数, 所以可以使用广度优先搜索解决
queue = collections.deque()
# 第一个参数为初始状态, 第二个参数是空盘子的位置, 第三个参数为到达当前状态的步数
queue.append(("012345678", 0, 0))
# 标记列表
vis = set()
vis.add("012345678")
while queue:
poll = queue.popleft()
# 到达了目标状态那么输出最少步数即可
if poll[0] == "087654321":
print(poll[2])
break
# 可以向左边或者是右边跳一个或者是两个位置, -1表示相对于当前的0的左边的位置, 1表示相对于当前的0的右边的位置(分别对应四个邻接状态)
insertQueue(queue, -2, poll, vis)
insertQueue(queue, -1, poll, vis)
insertQueue(queue, 1, poll, vis)
insertQueue(queue, 2, poll, vis)