前言
传教士(missionaries)与食人者(cannibals)问题是出处于第一篇从分析角度处理问题表述的论文,故在人工智能中很有名。上周我用暴力搜索来进行了求解,这次基于python,使用启发式算法来寻找一个最优渡河方案,希望对读者有所帮助。若您认为本文有不足之处,欢迎留言。
一、问题描述
若干传教士和食人者都在河的一岸(以下称为原岸),有一条能载1-2个人的船。请设法使所有人都渡河到河的对岸,并要求在任何地方都不能出现食人者多于传教士的情况。
二、主要函数
1.生成可行转移情况
def Actions(m):# m 为船的最大载客量
actions = np.array([]).astype(int)# 记录可行的转移
p = 0 # 记录可行转移个数
for ms in range(m+1):
for cs in range(m+1):
if (ms+cs)>=1 and (ms+cs) <= m :
actions = np.append(actions,[1,ms,cs])# 船的位置在这里设为 # 2.生成可行转移1 ,可方便后续计算
p+=1
actions = np.reshape(actions,(p,3))
print("可行的转移有%d个"%p)
print("分别是:\n",actions)
return p,actions
以3个传教士和3个食人者为例运行以下代码:
import numpy as np
ms_0,cs_0 = 3,3# 初始传教士和食人者的数量
state_0 = np.array([1,ms_0,cs_0])# 初态
state_z = np.array([0,0,0])# 终态
m = 2
p,actions = Actions(m)
可得到以下可行转移情况:
2.启发式搜索
def trans_path(state_0,state_z,actions):
# 初态,终态,可行状态,可行转移
i = 0 # 队头指针
queue = np.array([state_0])# 队列
arrived = False # 作为是否已到达终点的标记
number = np.array([])# 记录每个状态展开的状态数,用于后续画树状图
while True:
w = queue[i][0]# 1 表示在原岸,0 表示在对岸
state_s,d_list = np.array([]),np.array([])# 搜索状态和距离记录
if w == 1:# 若船在原岸
for row in range(len(actions)):# 宽度遍历
state_T = queue[i] - actions[row]
# 验证状态是否符合规则
if all(state_T>=0):
if ((state_T[1]==0)|(state_T[1]==3)) or (state_T[1]==state_T[2]):# 符合规则
appeared = False
for state in queue:
if all(state_T == state):
appeared = True
if not appeared:
d = state_T.sum()
state_s = np.append(state_s,state_T)
d_list = np.append(d_list,d)# 对应状态离终点的距离
if d == 0:# 判断是否到达终点,若无就记录
arrived = True
break
if arrived:
number = np.append(number,len(d_list))# # 记录展开的状态数
queue = np.append(queue,state_s).reshape((-1,3))
path_cost = queue.shape[0]# 搜索代价
return path_cost,queue,number
state_s = np.reshape(state_s,(-1,3))
queue = np.append(queue,state_s[d_list.argsort()]).reshape((-1,3))# 添加新状态
number = np.append(number,len(d_list))# # 记录展开的状态数
i+=1 # 移动指针
else:# 若船在对岸
for row in range(len(actions)):# 宽度遍历
state_T = queue[i] + actions[row]
# 验证状态是否符合规则
if all(state_T<= 3):
if ((state_T[1]==0)|(state_T[1]==3)) or (state_T[1]==state_T[2]):# 符合规则
appeared = False
for state in queue:
if all(state_T == state):
appeared = True
if not appeared:
d = state_T.sum()
state_s = np.append(state_s,state_T)
d_list = np.append(d_list,d)# 对应状态离终点的距离
state_s = np.reshape(state_s,(-1,3))
queue = np.append(queue,state_s[d_list.argsort()]).reshape((-1,3))# 添加新状态
number = np.append(number,len(d_list))# # 记录展开的状态数
i+=1 # 移动指针
运行函数则可得搜索代价和状态。
path_cost,state_ed,number = trans_path(state_0,state_z,actions)
print("搜索代价为:",path_cost)
print("搜索到的状态有和对应展开的节点数:")
for i in range(len(number)):
print(state_ed[i],int(number[i]))
for i in range(len(number),len(state_ed)):
print(state_ed[i])
输出结果:
根据对应的展开节点数,我们还可以画出启发式搜索的树图。
三、优缺点分析
启发式算法,不需要列出所有的转移状态,只要事先知道初态和终态,以及中间可行的转移状态需要满足的条件即可。
只是一般启发式算法一次只能得到一个最优解。
四、完整代码
import numpy as np
def Actions(m):# m 为船的最大载客量
actions = np.array([]).astype(int)# 记录可行的转移
p = 0 # 记录可行转移个数
for ms in range(m+1):
for cs in range(m+1):
if (ms+cs)>=1 and (ms+cs) <= m :
actions = np.append(actions,[1,ms,cs])# 船的位置在这里设为 # 2.生成可行转移1 ,可方便后续计算
p+=1
actions = np.reshape(actions,(p,3))
print("可行的转移有%d个"%p)
print("分别是:\n",actions)
return p,actions
def trans_path(state_0,state_z,actions):
# 初态,终态,可行状态,可行转移
i = 0 # 队头指针
queue = np.array([state_0])# 队列
arrived = False # 作为是否已到达终点的标记
number = np.array([])# 记录每个状态展开的状态数,用于后续画树状图
while True:
w = queue[i][0]# 1 表示在原岸,0 表示在对岸
state_s,d_list = np.array([]),np.array([])# 搜索状态和距离记录
if w == 1:# 若船在原岸
for row in range(len(actions)):# 宽度遍历
state_T = queue[i] - actions[row]
# 验证状态是否符合规则
if all(state_T>=0):
if ((state_T[1]==0)|(state_T[1]==3)) or (state_T[1]==state_T[2]):# 符合规则
appeared = False
for state in queue:
if all(state_T == state):
appeared = True
if not appeared:
d = state_T.sum()
state_s = np.append(state_s,state_T)
d_list = np.append(d_list,d)# 对应状态离终点的距离
if d == 0:# 判断是否到达终点,若无就记录
arrived = True
break
if arrived:
number = np.append(number,len(d_list))# # 记录展开的状态数
queue = np.append(queue,state_s).reshape((-1,3))
path_cost = queue.shape[0]# 搜索代价
return path_cost,queue,number
state_s = np.reshape(state_s,(-1,3))
queue = np.append(queue,state_s[d_list.argsort()]).reshape((-1,3))# 添加新状态
number = np.append(number,len(d_list))# # 记录展开的状态数
i+=1 # 移动指针
else:# 若船在对岸
for row in range(len(actions)):# 宽度遍历
state_T = queue[i] + actions[row]
# 验证状态是否符合规则
if all(state_T<= 3):
if ((state_T[1]==0)|(state_T[1]==3)) or (state_T[1]==state_T[2]):# 符合规则
appeared = False
for state in queue:
if all(state_T == state):
appeared = True
if not appeared:
d = state_T.sum()
state_s = np.append(state_s,state_T)
d_list = np.append(d_list,d)# 对应状态离终点的距离
state_s = np.reshape(state_s,(-1,3))
queue = np.append(queue,state_s[d_list.argsort()]).reshape((-1,3))# 添加新状态
number = np.append(number,len(d_list))# # 记录展开的状态数
i+=1 # 移动指针
ms_0,cs_0 = 3,3# 初始传教士和食人者的数量
state_0 = np.array([1,ms_0,cs_0])# 初态
state_z = np.array([0,0,0])# 终态
m = 2
p,actions = Actions(m)
path_cost,state_ed,number = trans_path(state_0,state_z,actions)
print("搜索代价为:",path_cost)
print("搜索到的状态有和对应展开的节点数:")
for i in range(len(number)):
print(state_ed[i],int(number[i]))
for i in range(len(number),len(state_ed)):
print(state_ed[i])