修道士野人问题的python求解

修道士野人问题的python求解

  • 问题描述
    • 状态空间表示
    • 程序设计
    • 执行结果
    • 源代码(Python3.5)

问题描述

修道士(Missionaries)和野人(Cannibals)问题
修道士野人过河问题是人工智能领域一个典型的问题,其经典描述为如下。
在河的左岸有N个传教士(M)、N个野人(C)和一条船(Boat),修道士们想用这条船把所有人都运过河去,但有以下条件限制:

  1. 修道士和野人都会划船,但船每次最多只能运K个人;
  2. 在任何岸边野人数目都不能超过修道士,否则修道士会被野人吃掉。
    上述约束可以表述为:
    ① M≧C 任何时刻两岸、船上都必须满足传教士人数不少于野人数(M=0时除外,既没有传教士);
    ② M+C≦K 船上人数限制在K以内。

状态空间表示

(一)状态空间
在此问题的求解中,选择使用三元组(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]

源代码(Python3.5)

(一)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

你可能感兴趣的:(人工智能)