题目: 野人与传教士渡河问题:3个野人与3个传教士打算乘一条船到对岸去,该船一次最多能运2个人,在任何时候野人人数超过传教士人数,野人就会把传教士吃掉,如何用这条船把所有人安全的送到对岸?在实现基本程序的基础上实现N个野人与N个传教士问题的所有解的求解,N由用户输入。
注:我的上机实验都是采用python语言,通过pycharm实现
野人和传教士
1.1 了解宽度优先搜索
问题一野人和传教士的过河问题,要求使用宽度优先搜索的方法进行实现。基于这个要求需要先了解什么是宽度优先搜索。在老师的课上已经对宽度优先搜索有了基础的了解。在盲目搜索中包括,宽度优先搜索:搜索是以接近起始节点的程度依次扩展节点的,且这种节点是逐层进行的,在对下一层的任一节点进行搜索进行搜索之前,必须搜索完本层的所有节点。宽度优先搜索中假设每一次操作的代价都相等的情况下,能够保证在搜索树中找到一条通向目标节点的最短路径。
在书上的八数码难题的宽度优先搜索树图中可以看到,宽度优先搜索是逐层一一进行的,在对下一层的任一节点进行搜索进行搜索之前,必须搜索完本层的所有节点。而之后的深度优先搜索树图中是沿着一条路径进行下去的,知道深度界限为止。
为了更深刻的了解宽度优先搜索,我还通过《算法图解》这本书以及网上的关于BFS广度优先算法的相关资料进行学习。
宽度优先搜索(BFS, Breadth First Search)是一个针对图和树的遍历算法。发明于上世纪50年代末60年代初,最初用于解决迷宫最短路径和网络路由等问题。
如何确定最短路径,需要两个步骤:
- 使用图建立问题模型
- 使用宽度优先搜索解决问题
- 首先是关于图的了解:
图模拟一组连接,由节点和边构成。一个节点可能与众多节点相连,这些节点被称为邻居。图用于模拟不同的东西是如何相连的。
- 其次是关于宽度优先搜索:
宽度优先搜索是一种用于图的查找算法,可以解决两类问题,一个是,从节点a出发,有前往节点b的路径吗:另一个是,从节点a出发,前往节点b的哪条路径最短。这次的野人与传教士的问题属于第二种类型的问题。
在广度优先搜索的执行过程中,搜索范围从起点开始逐渐向外延伸,即先检查一度关系,再检查二度关系,以此类推。只有按添加顺序查找时,才能实现这样的目的,基于按添加顺序这个目的,可以采用可实现这个的数据结构——队列。
队列是一种先进先出的数据结构,亦可满足按添加顺序查找这一目的。
1.2实现算法
知道图和宽度搜索这两个确定路径的基本步骤后,便是实现。
- 做出流程图
图 1 野人与传教士问题流程图
- 首先是图的实现:
需要使用代码来实现图。图中每个节点都与邻近节点相连,需要一种结构让你能够表示这种关系,它就是散列表。散列表让你能够将键映射到值。在这里,你要将节点映射到其所有邻居。散列表是无序的,因此添加键—值对的顺序无关紧要。
图 2 散列表表示映射关系
graph[]表示一个数组,包含了state状态的节点的所有节点。通过graph[]记录图,已达到图的实现。
其中状态(State):出发时的初始状态、期望到达时的目标状态和每一次动作后的当前状态。用左岸传教士人数、左岸野人人数、右岸传教士人数、右岸野人人数、船的位置组成列表来表示状态:[ML, CL, MR, CR, B],其中人数用数字表示数目,船的位置表示的方式很特殊:左岸为1,右岸为-1。
- 其次是实现算法:
- 编写条件符合函数
在野人与传教士这个题目已知下,需要满足:
- 左右野人与传教士的人数不能小于0
- 在左岸,传教士的人数在不为0的情况下不能小于左边野人的数目,也就是state[0] > state[1] 同时 state[0] 不为0,右边也是如此,由于在题目中已有船的载客量为2的条件,故不需要考虑在船上的情况。
- 在符合条件的情况下不断更新图中的节点,通过graph[]实现。
2.创建一个队列
初始化一个队列q用来存储需要遍历的节点。前文已经介绍过了,队列是一种先进先出的数据结构,亦可满足按添加顺序查找这一目的。
在这部分不得不说野人或者传教士移动的动作,actions。题目的要求是船最多依次两人,最少需要一人摆渡。所以动作一共有:[1, 0], [0, 1], [1, 1], [2, 0], [0, 2]共五种。其中[m, c]表示载m个传教士,c个野人。
图 3 actions的情况
这里使用的是循环的方法,只有五种情况也当然可以用一一列举。
有了移动的动作,通过当前状态和动作,得出下个状态为[ML - B*action[0], MR + B*action[0], CL - B*action[1], CR - B*action[1], -B]。从中可以看出,船的移动动作可以不断更新下一个新的状态,用temp表示新的状态,但不是最终的可行状态。
图 4 移动后状态的更新
将起点[n, n, 0, 0, 1]入队,开始循环。在循环内,先出队一个状态,如果这个状态是终点状态的话,就继续出下一个状态。其他情况下就是在到终点的路上,需要对该状态执行其中一个动作,然后判断是否符合条件。
图 5 船移动后,符合条件的temp状态入队
在这里的条件就是前面的条件符合函数以及同时不能重复放在队列中。符合条件符合函数的话,那么将该节点加入到graph的dict中,key值跟value值就是当前节点跟执行完动作产生的节点。在符合上面条件的前提下,如果该节点没有遍历过,那么将该节点再入队,等待遍历。然后就是再执行不同的动作,再寻找能到的节点,重复上面的判断及入图、入队操作。
直到该节点所有动作执行完毕,本次循环结束,就可以再次出队下一个节点。等队空时,循环结束。
3. 输出路径
输出路径使用的方法是递归的方法。从初始状态开始,在graph中进行搜索。以初始状态为key值,遍历每个value,将value继续作为key值,然后同样遍历其value值。在此过程中记录下路径。找到终点后,将该路径输出,然后return上一层,继续找。直到最后搜索完毕return。
图 6 路径的搜索
4. 使用列表以计录检查过的点,否则会无限循环
仅当没有没有被遍历过才继续下一步的转移动作以及路径的搜索。遍历过的状态也就是节点,遍历后将该状态节点加入搜索队列同时其邻居也加入,依次类推,将反复循环。可以通过checkList列表计录储遍历过的节点。
图 7 使用checkList存储遍历过的节点
5. 通过算法的实现,画出状态搜索图
通过状态搜索图,可以看到从头到尾写出来的有两个,另一个重复的也可以写出两种一样的,故也有两种。一共四种,与运行出来的答案一致。
图 8 野人与传教士的状态搜索图
结果
输出的结果包括几次路径的每次动作后的状态,可以清楚的看到各部分的变化情况。
1.2结果分析与总结
在学习这方面的开始便想到了在之前的上课的过程通过状态空间图来表示八数码难题。在盲目搜索这一部分中也有提及宽度优先搜索。在书上的八数码难题的宽度优先搜索树图中可以看到,宽度优先搜索是逐层一一进行的,在对下一层的任一节点进行搜索进行搜索之前,必须搜索完本层的所有节点。而之后的深度优先搜索树图中是沿着一条路径进行下去的,知道深度界限为止。
仅仅依靠书本上的相关知识是远远不够的。网络上的相关资料很多,但是看了很久也没有透彻的理解这个算法的思想,便拿出了《算法图解》这本书,仔细的看了宽度优先搜索这一章节,通过图片详细的讲解了宽度优先算法的思想,应用以及该有的流程。
学习过之后对代码的实现更加脉络清晰,但是想要学会并且理解这些还是需要每个函数,一步步的试错,看着错误的原因,慢慢的尝试。才真正明白了宽度优先搜索这个算法。先是了解有关图,BFS是一种图形搜索算法,该算法是从起点开始搜索然后一层一层的向下搜索,如果找到目标或者搜索完了全部的节点则算法结束。再是队列实现顺序,在广度优先搜索的执行过程中,搜索范围从起点开始逐渐向外延伸,即先检查一度关系,再检查二度关系,以此类推。只有按添加顺序查找时,才能实现这样的目的,基于按添加顺序这个目的,可以采用可实现这个的数据结构——队列。
而关于搜索算法是利用计算机的高性能来有目的的穷举一个问题解空间的部分或所有的可能情况,从而求出问题的解的一种方法。DFS与BFS都是其中的算法。BFS靠队列实现,入队列,出队列。而DFS靠栈实现,压栈,出栈。DFS一次访问一条路,可以说是一条路走到黑,不行再换,而BFS一次访问多条路,一层层的向下搜索。而BFS可以自己控制队列的长度。不过运行BFS所需的内存会大于DFS需要的内存。
完整代码如下:
n = 0 # 传教士与野人各自的人数N
q = [] # 作为队列
actions = [] # 动作,即变化的方式,表示方式[M, C](传教士、野人)
path = [] # 递归查找的单次路径
paths = [] # 存放多个路径
checkList = [] # 已经遍历过的点
graph = {} # 图
# 判断状态是否满足条件,同时建图
def sc(state_b, state):
# 判断左右岸传教士的人数都不小于0
if state[0] < 0 or state[1] < 0 or state[2] < 0 or state[3] < 0:
return False
# 判断是否都满足不被吃的条件
if (state[0] < state[1] and state[0] != 0) or (state[2] < state[3] and state[2] != 0):
return False
# 满足上述条件为有效点,加入到图中
# tuple 是不可变的,这使得它可以作为 dict 的 key,也就是将键映射到值
if tuple(state_b) in graph.keys():
if tuple(state) not in graph[tuple(state_b)]:
graph[tuple(state_b)].append(tuple(state))
pass
else:
graph[tuple(state_b)] = [tuple(state)]
# 已经遍历过,不需要继续遍历,否则会无限循环
if state in checkList:
return False
return True
def mapping():
# 队列非空
while len(q) > 0:
# 出队
state_b = q.pop(0)
#为了记录遍历过的点,否则将会无限循环
checkList.append(state_b)
# 到达目标状态
if state_b[0] == 0 and state_b[1] == 0:
continue
# 移动后状态的更新
state_n = [0] * 5
for action in actions:
state_n[0] = state_b[0] - action[0] * state_b[4]
state_n[1] = state_b[1] - action[1] * state_b[4]
state_n[2] = state_b[2] + action[0] * state_b[4]
state_n[3] = state_b[3] + action[1] * state_b[4]
state_n[4] = -state_b[4]
temp = state_n[:]
# 在每次的状态更新时,判断新状态temp是否符合条件
if sc(state_b, temp):
#满足条件且不在队列中则入队
if temp not in q:
q.append(temp)
pass
pass
pass
pass
pass
# 宽度搜索寻找路径
def find_path(state):
# 走到重复的状态
if state in path:
path.append(state)
return
# 到达终点状态,记录路径
if state == (0, 0, n, n, -1):
path.append(state)
paths.append(path[:])
return
path.append(state)
# 在graph[]中,逐个探索
for i in range(len(graph[state])):
find_path(graph[state][i])
path.pop()
pass
pass
def main():
global n
# 输入
n = int(input("输入人数N:"))
# 初始状态
state = [n, n, 0, 0, 1]
q.append(state)
# 生成动作
# i:移动传教士和野人之和,从1到2
for i in range(1, 3):
# j:传教士的数目,从0到i
for j in range(i + 1):
# 如果满足传教士不少于野人或传教士为0,动作有效
if (j >= i - j) or (j == 0):
actions.append([j, i - j])
pass
pass
pass
# 生成完毕
# 建图
mapping()
# 搜索路径
find_path(tuple(state))
# 路径条数
num = 0
# 输出路径
for p in paths:
num += 1
print("第%d条路径:" % num)
str1 = "{:^6}{:^6}{:^6}{:^6}{:^6}"
print(str1.format("ML", "CL", "MR", "CR", "B"))
for i in p:
print(str1.format(i[0], i[1], i[2], i[3], i[4]))
pass
pass
# 结束
print("总共有%d条路径" % num)
pass
if __name__ == '__main__':
main()
print()
pass