修道士(Missionaries)和野人(Cannibals)问题
修道士野人过河问题是人工智能领域一个典型的问题,其经典描述为如下。
在河的左岸有N个传教士(M)、N个野人(C)和一条船(Boat),修道士们想用这条船把所有人都运过河去,但有以下条件限制:
(一)状态空间
在此问题的求解中,选择使用三元组(M, C, S)表示问题的状态,式中M表示起始岸修道士人数,C表示起始岸野人人数,S为0-1变量,表示船的位置,当S为0时表示船在起始岸,为1时表示船在终点岸。如:(0,3,0)表示起始岸有三个野人,没有修道士,船在起始岸。
于是修道士野人问题可以描述为: 从(3,3,0)到(0,0,1)的状态转换。在此问题的状态空间中共有32 种状态,其中12种不合理状态:如(1,0,1)说明右岸有2个M,3个C;4种不可能状态:如(3,3,0)说明所有M和C都在左岸,而船在右岸,所以可用的状态共16种,组成合理的状态空间。可能的问题状态分别为:(0, 0, 1), (0, 1, 1), (0, 2, 1), (1, 0, 1), (1, 1, 0), (1, 1, 1), (1, 2, 0), (1, 2, 1), (1, 3, 0), (2, 0, 1), (2, 1, 0), (2, 1, 1), (2, 2, 0), (2, 2, 1), (2, 3, 0), (3, 1, 0), (3, 2, 0), (3, 3, 0)。
(二)操作集
在此问题种定义Operation操作算符,用二元组(M, C)表示,式中M表示一次划船过程中船上修道士人数,C表示穿上野人人数。并规定,每一次使用操作算符对问题状态进行操作运算后,问题状态的S必须改变,即船必须从一岸驶向另一岸。例如,当对初始问题状态(3,3,0)使用(1,1)算符后,问题状态变成(2,2,1),这表示一个修道士和一个野人划船驶到终点岸,此时船停留在重点岸。根据左右两岸和穿上都不能出现野人人数大于修道士人数的约束,可以得到可用的操作算符共有5种,分别是(0, 1), (0, 2), (1, 0), (1, 1), (2, 0)。
(一)存储结构
1.基本问题状态和操作算符使用列表存储。由于本问题仅需使用到简单的数据插入、提取等功能,因此可用选用Python提供的基本数据结构列表完成。使用列表表示问题的状态空间为:[[0, 0, 1], [0, 1, 1], [0, 2, 1], [1, 0, 1], [1, 1, 0], [1, 1, 1], [1, 2, 0], [1, 2, 1], [1, 3, 0], [2, 0, 1], [2, 1, 0], [2, 1, 1], [2, 2, 0], [2, 2, 1], [2, 3, 0], [3, 1, 0], [3, 2, 0], [3, 3, 0]];操作集为:[[0, 1], [0, 2], [1, 0], [1, 1], [2, 0]];
2.节点关系使用字典存储。在找到目标节点后,需要回溯从开始节点走到当前节点的路径,这需要存储节点间的关系。这里使用字典存储节点间的亲属关系,子节点为键,父节点为值。
(二)算法基本思想
1.首先将问题初始状态(initial_state)节点放入OPEN表中(用于临时存储);
2.从OPEN表中取出首节点,判断是否为目标节点。若为目标节点,则任务完成,输出结果;若不是目标节点,则进行下一步;
3.对从OPEN表中取出的不是目标节点的当前节点进行可扩展性判断,之后根据所使用的搜索策略将其子节点存入OPEN表;若当前节点不可扩展,则程序结束,问题无解。
(三)搜索策略
本次使用广度优先算法、有界深度优先算法和全局启发式搜索算法分别对问题进行求解。其中全局启发式搜索算法所使用的启发式函数为f(n) = d(n) + w(n), d(n)为当前节点深度,w(n)为未渡河人数;有界深度优先算法设置深度阈值为25。
(四)函数设计与调用关系
1.函数设计
此程序共设计并使用到10个自定义函数,其中:
(1)create_vertex()和create_edges()用于初始化问题条件,生成相应的节点和边;
(2)move()用于对问题状态执行操作算符并控制移动过程中各状态的合理性(如避免出现不合理状态,控制船只状态等);
(3)whether_expandable()用于判断当前节点是否可扩展;
(4)get_d(), get_w()和search_heuristic()共同实现启发式搜索。get_d()用于获取当前节点深度,get_w()用于计算当前节点距离目标节点的距离,即还有多少人尚未过河;
(5)search_depth()深度优先搜索算法;
(6)search_breadth()广度优先搜索算法;
(7)heap_adjust(), heap_create()和heap_sort()用于实现堆排序功能,此功能用于给生成的子节点进行排序。
2.函数调用关系
程序中函数调用关系为:
(1) main()->create_vertex(), create_edges(), 搜索算法(三选一);
(2) search_breadth()->whether_expandable()->move();
(3) search_ depth ()->whether_expandable()->move();
(4) search_heuristic()->whether_expandable()->move();
search_heuristic()->get_d(), get_w();
search_heuristic()->heap_sort()->heap_adjust(), heap_create()。
(一) 启发式索搜
可用的操作算符为: [[0, 1], [0, 2], [1, 0], [1, 1], [2, 0]]
可能出现的顶点有 16 种, 分别为: [[0, 0, 1], [0, 1, 1], [0, 2, 1], [1, 0, 1], [1, 1, 0], [1, 1, 1], [1, 2, 0], [1, 2, 1], [1, 3, 0], [2, 0, 1], [2, 1, 0], [2, 1, 1], [2, 2, 0], [2, 2, 1], [2, 3, 0], [3, 1, 0], [3, 2, 0], [3, 3, 0]]
渡河成功!路径为:
[3, 3, 0] ->[2, 2, 1] ->[3, 2, 0] ->[3, 0, 1] ->[3, 1, 0] ->[1, 1, 1] ->[2, 2, 0] ->[0, 2, 1] ->[0, 3, 0] ->[0, 1, 1] ->[1, 1, 0] ->[0, 0, 1]
(二) 广度优先搜索
可用的操作算符为: [[0, 1], [0, 2], [1, 0], [1, 1], [2, 0]]
可能出现的顶点有 16 种, 分别为: [[0, 0, 1], [0, 1, 1], [0, 2, 1], [1, 0, 1], [1, 1, 0], [1, 1, 1], [1, 2, 0], [1, 2, 1], [1, 3, 0], [2, 0, 1], [2, 1, 0], [2, 1, 1], [2, 2, 0], [2, 2, 1], [2, 3, 0], [3, 1, 0], [3, 2, 0], [3, 3, 0]]
渡河成功!路径为:
[3, 3, 0] ->[3, 1, 1] ->[3, 2, 0] ->[3, 0, 1] ->[3, 1, 0] ->[1, 1, 1] ->[2, 2, 0] ->[0, 2, 1] ->[0, 3, 0] ->[0, 1, 1] ->[0, 2, 0] ->[0, 0, 1]
(三) 有界深度搜索
可用的操作算符为: [[0, 1], [0, 2], [1, 0], [1, 1], [2, 0]]
可能出现的顶点有 16 种, 分别为: [[0, 0, 1], [0, 1, 1], [0, 2, 1], [1, 0, 1], [1, 1, 0], [1, 1, 1], [1, 2, 0], [1, 2, 1], [1, 3, 0], [2, 0, 1], [2, 1, 0], [2, 1, 1], [2, 2, 0], [2, 2, 1], [2, 3, 0], [3, 1, 0], [3, 2, 0], [3, 3, 0]]
渡河成功!路径为:
[3, 3, 0] ->[3, 1, 1] ->[3, 2, 0] ->[3, 0, 1] ->[3, 1, 0] ->[1, 1, 1] ->[2, 2, 0] ->[0, 2, 1] ->[0, 3, 0] ->[0, 1, 1] ->[0, 2, 0] ->[0, 0, 1]
(一)main文件
from initial import create_edges
from initial import create_vertex
from search_breadth import search_breadth
from search_depth import search_depth
from search_heuristic import search_heuristic
capacity = 2 # 小船容载量,过低则可能导致问题无解
Missionaries = 3 # 修道士的人数
Cannibals = 3 # 野人人数
init_state = [Missionaries, Cannibals, 0] # 问题初始状态,表示三个修道士、三个野人和小船都在初始岸
layer = 25 # 设置有界深度算法深度阈值
set_of_operation = create_edges(capacity) # 生成所有可用的运算符,即图中存在的边
set_of_vertex = create_vertex(Missionaries, Cannibals) # 生成所有可能的问题状态,即图中存在的顶点
# search_breadth(init_state, set_of_operation) # 利用广度优先算法求解
# search_depth(init_state, set_of_operation, layer) # 利用有界深度算法求解
search_heuristic(init_state, set_of_operation) # 利用有界深度算法求解
(二)initial文件
def create_vertex(missionary, cannibal): # 生成问题中所有可能的状态,即所有顶点
init_state = [missionary, cannibal, 0] # 初始状态
set_of_state = [] # 存储状态集的列表
count = 0
for i in range(missionary+1): # 生成所有可能的状态,即所有顶点
for j in range(cannibal+1):
if init_state[0] == 0 or init_state[0] >= init_state[1]:
if i != 0 and j != 0:
set_of_state.append([i, j, 0])
if i != missionary and j != cannibal:
set_of_state.append([i, j, 1])
count += 1
print('可能出现的顶点有', count, '种, 分别为:', set_of_state)
def create_edges(capacity): # 生成所有的运算子,即边
set_of_operation = [] # 存储运算子的列表
for i in range(capacity + 1):
for j in range(capacity + 1):
if i + j <= capacity and (i >= j or i == 0):
if i == 0 and j == 0:
continue
set_of_operation.append([i, j])
print('可用的操作算符为:', set_of_operation)
return set_of_operation
(三)move文件
def move(vertex, edge, init_missionary=3, init_cannibal=3):
if vertex[2] == 1:
missionary = vertex[0] + edge[0]
cannibal = vertex[1] + edge[1]
state_of_boat = 1 - vertex[2]
else:
missionary = vertex[0] - edge[0]
cannibal = vertex[1] - edge[1]
state_of_boat = 1 - vertex[2]
if missionary != 0 and missionary < cannibal:
return False
elif (init_missionary - missionary) != 0 and ((init_missionary - missionary) < (init_cannibal - cannibal)):
return False
elif missionary < 0 or cannibal < 0 or (init_missionary - missionary) < 0 or (init_cannibal - cannibal) < 0:
return False
else:
return [missionary, cannibal, state_of_boat]
(四)heap_sort文件
def heap_adjust(lists, pos, length): # 堆排序(升序排列,构建大根堆):
max_ = pos
lchild = 2*pos+1 # 由于lists下表从0开始,所以左右孩子下标为2*pos+1,2*pos+2
rchild = 2*pos+2
if max_ < length // 2: # 注意符号是<,堆调整时,必定是从(length//2)-1开始
if lchild < length and lists[lchild][3] > lists[max_][3]:
max_ = lchild
if rchild < length and lists[rchild][3] > lists[max_][3]:
max_ = rchild
if max_ != pos: # 如果max_未发生改变,说明不需要调整
lists[max_], lists[pos] = lists[pos], lists[max_]
heap_adjust(lists, max_, length) # 递归调整
def heap_create(lists, length):
for i in range(length // 2)[::-1]:
heap_adjust(lists, i, length)
def heap_sort(lists):
length = len(lists)
heap_create(lists, length)
for i in range(length)[::-1]:
lists[0], lists[i] = lists[i], lists[0] # 首尾元素互换,将最大的元素放在列表末尾
heap_adjust(lists, 0, i) # 从头再调整,列表长度-1(尾元素已经完成排序,所以列表长度-1)
(五)search_breadth文件
from whether_expandable import whether_expandable
def search_breadth(init_state, set_of_operation): # 广度优先搜索算法
open_list = []
relation = {}
open_list.append(init_state)
while 1:
if open_list == []: # 判断open表是否为空
print("失败!open表为空,不存在可用节点,无解。")
return
vertex = open_list[0]
open_list = open_list[1: -1] # 更新open表
if vertex == [0, 0, 1]: # 当前节点为目标节点,打印输出
result = [] # 存储整个路径
result.append(vertex)
res = [] # 存储路径中的单个节点
print("渡河成功!路径为:")
while res != init_state:
res = relation[str(result[-1])]
if res:
result.append(res)
else:
break
for i in result[::-1]:
if i != result[0]:
print(i, '->', end='')
else:
print(i)
return
else: # 当前节点不是目标节点时
if vertex != init_state:
sons = whether_expandable(vertex, set_of_operation, relation[str(vertex)])
else:
sons = whether_expandable(vertex, set_of_operation, [0, 0, 0])
if sons: # 判断当前节点是否可扩展
for i in sons:
relation[str(i)] = vertex # 用字典存储节点间的亲属关系,子节点为键,父节点为值
open_list.append(i)
(六)search_depth文件
from whether_expandable import whether_expandable
def search_depth(init_state, set_of_operation, layer): # 深度有界搜索算法
open_list = []
relation = {}
open_list.append(init_state)
layer_count = 0
while 1:
layer_count += 1
if layer_count > layer: # 使用layer来判断迭代深度,当深度超过预设值时,停止循环
print("深度超过最大限度(%d),未找到解!", layer)
break
if open_list == []: # 判断open表是否为空
print("失败!open表为空,不存在可用节点,无解。")
return
vertex = open_list[0]
open_list = open_list[1: -1] # 更新open表
if vertex == [0, 0, 1]:
result = [] # 存储整个路径
result.append(vertex)
res = [] # 存储路径中的单个节点
print("渡河成功!路径为:")
while res != init_state:
res = relation[str(result[-1])]
if res:
result.append(res)
else:
break
for i in result[::-1]:
if i != result[0]:
print(i, '->', end='')
else:
print(i)
return
else:
if vertex != init_state:
sons = whether_expandable(vertex, set_of_operation, relation[str(vertex)])
else:
sons = whether_expandable(vertex, set_of_operation, [0, 0, 0])
if sons: # 判断当前节点是否可扩展
for i in sons:
relation[str(i)] = vertex # 用字典存储节点间的亲属关系,子节点为键,父节点为值
open_list.insert(0, i)
(七)search_heuristic文件
from whether_expandable import whether_expandable # 启发函数为f(n) = d(n) + w(n), d(n)为当前节点深度,w(n)为未渡河人数
from heap_sort import heap_sort
def get_d(vertex, init_state, relation): # 用于计算当前节点的深度
result = [] # 存储整个路径
result.append(vertex)
res = [] # 存储路径中的单个节点
d = 0
# print("渡河成功!路径为:")
while res != init_state:
d += 1
res = relation[str(result[-1])]
if res:
result.append(res)
else:
break
return d
def get_w(vertex): # 用于计算当前节点距离目标节点的距离,即还有多少人尚未过河
return vertex[0] + vertex[1] + 1 - vertex[2]
def search_heuristic(init_state, set_of_operation):
open_list = []
relation = {}
open_list.append(init_state)
while 1:
if open_list == []: # 判断open表是否为空
print("失败!open表为空,不存在可用节点,无解。")
return
vertex = open_list[0]
open_list = open_list[1: -1] # 更新open表
if vertex == [0, 0, 1]:
result = [] # 存储整个路径
result.append(vertex)
res = [] # 存储路径中的单个节点
print("渡河成功!路径为:")
while res != init_state:
res = relation[str(result[-1])]
if res:
result.append(res)
else:
break
for i in result[::-1]:
if i != result[0]:
print(i, '->', end='')
else:
print(i)
return
else:
if vertex != init_state:
sons = whether_expandable(vertex, set_of_operation, relation[str(vertex)])
else:
sons = whether_expandable(vertex, set_of_operation, [0, 0, 0])
if sons: # 判断当前节点是否可扩展
sort_list = []
for i in sons:
relation[str(i)] = vertex # 用字典存储节点间的亲属关系,子节点为键,父节点为值
i.append(get_d(i, init_state, relation) + get_w(i)) # 使用启发函数对生成的子节点进行标注,并将标注的权值加到子节点列表内
sort_list.append(i)
heap_sort(sort_list)
for i in sort_list:
i = i[:-1]
open_list.append(i)
(八)whether_expandable文件
from move import move
def whether_expandable(vertex, set_of_operation, pre_vertex): # 判断当前节点是否可扩展
sons = []
for operation in set_of_operation:
m = move(vertex, operation)
if m:
if m != pre_vertex: # 扩展得到的子节点不应该是当前节点的父节点,即应当避免重复
sons.append(m)
if sons == []:
return False
else:
return sons