写在前面
书接上文:传教士与野人问题深度优先搜索算法(DFS)-Python实现
在研究并实现了深度优先搜索算法(DFS)以后,这篇文章将研究使用广度优先算法(BFS)解决传教士与野人问题。
关于搜索问题及传教士与野人问题的要素与抽象,已经在前文有详细解释,在这篇广度优先算法的文章里,使用的状态(State)、动作(Action)、转换模型、目标测试(Goal test)和路径代价(Cost)都与DFS相同。在本文就不再赘述,可以点开上文链接,阅读"算法思路"之前所有的内容,以便更好地理解本文。
……
……
……
算法思路
本文将使用递归实现广度优先搜索算法(BFS),使用graph = {}来记录图,键(key)为父节点,值(value)为子节点,一个父节点可以带着很多子节点,一个子节点也可以在很多的父节点的后面,因此非常符合Python字典的特点,使用起来非常方便。算法的思路如下:
输入
用户输入信息,其中包括N和K的值,用以后续的计算。随即生成初始状态[N, K, 0, 0, 1]。
生成动作
生成可行的动作[m, c],其中要求m + c ≤ K && (m ≥ c || m == 0)。
对m和c从0到K开始遍历,将所有可行的动作加入到列表actions当中:[[m1, c1], [m2, c2], ...]
开始搜索建图
初始化一个队列q(FIFO, first in first out)用来存储需要遍历的节点。
首先将起点[n, n, 0, 0, 1]入队,开始循环。在循环内,先出队一个状态,如果这个状态是终点状态的话,就继续出下一个状态。其他情况下就是在到终点的路上,需要对该状态执行其中一个动作,然后判断是否符合条件。
条件为:
- 状态中的各个人数都大于0;
- 满足不被吃的条件。
条件如果符合,那么将该节点加入到graph的字典中,key值跟value值就是当前节点跟执行完动作产生的节点。
在符合上面条件的前提下,如果该节点没有遍历过,那么将该节点再入队,等待遍历。
然后就是再执行不同的动作,再寻找能到的节点,重复上面的判断及入图、入队操作。
直到该节点所有动作执行完毕,本次循环结束,就可以再次出队下一个节点。
等队空时,循环结束。
输出路径
输出路径使用的方法是递归的方法。从初始状态开始,在graph中进行深度优先搜索。
以初始状态为key值,遍历每个value,将value继续作为key值,然后同样遍历其value值。在此过程中记录下路径。
找到终点后,将该路径输出,然后return上一层,继续找。直到最后搜索完毕return。
结束
代码(Python)
"""
作者:Zhanyu_Guo
创建日期:2020.10.25
更新日期:2020.11.03
文件名:CrossRiverBFS.py
"""
# 用以测试运行时间
import time
"""globals"""
n = 0 # 传教士与野人各自的人数N
q = [] # 作为队列
actions = [] # 动作,即变化的方式,表示方式[M, C](传教士、野人)
path = [] # 递归查找的单次路径
paths = [] # 存放多个路径
checkList = [] # 已经遍历过的点
graph = {} # 图
# 判断状态是否满足条件,同时建图
def ok(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
# 满足上述条件为有效点,加入到图中
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[:]
# 判断是否符合条件
if ok(state_b, temp):
# 入队
if temp not in q:
q.append(temp)
pass
pass
pass
pass
pass
# 深度搜索寻找路径
def find_path(state):
global n
# 走到重复的状态
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)
# 逐个探索
for i in range(len(graph[state])):
find_path(graph[state][i])
path.pop()
pass
pass
def main():
global n
# 输入
n = int(input("输入人数N:"))
k = int(input("输入载客量K:"))
# 初始状态
state = [n, n, 0, 0, 1]
q.append(state)
# 生成动作
# i:移动传教士和野人之和,从1到k
for i in range(1, k + 1):
# 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
# 生成完毕
# 记录开始时间
start = time.perf_counter()
# 建图
mapping()
# 计算总时长
total = time.perf_counter() - start
print(total)
# 搜索路径
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__':
try:
main()
except Exception as e:
print(e)
pass