传教士与野人问题深度优先搜索算法(DFS)-Python实现

写在前面

在大三专业课——人工智能基础,完成了一个算法作业。不得不说,在这次的作业上,我花了挺多功夫的,但也因此对这个作业涉及的算法很有心得。这次DFS的搜索算法的核心部分是由我自己编写的,仅仅在“船的状态”上参考借鉴了一篇非常具有启发性且通俗易懂的文章——C语言实现野人与传教士过河问题

十分感谢这篇文章给我提供的思路。

问题描述

三个传教士和三个野人在河的一岸,有一条能载一个人或者两个人的船。请设法使所有人都渡到河的另一岸,要求在任何地方野人数都不能多于传教士的人数。

问题抽象

一般的搜索问题,需要五个要素。

  1. 状态(State):出发时的初始状态、期望到达时的目标状态和每一次动作后的当前状态。在本例中用左岸传教士人数、左岸野人人数、右岸传教士人数、右岸野人人数、船的位置组成列表来表示状态:[ML, CL, MR, CR, B],其中人数用数字表示数目,船的位置表示的方式很特殊:左岸为1,右岸为-1。
  2. 动作(Action):指一个状态可以执行的动作。在本例中即每次摆渡时,船载两种人分别的人数,比如说:[2, 1]表示载2个传教士,1个野人。
  3. 转换模型:描述每个动作产生的结果,即Result(State, Action),求出下一个状态。在本例中,[3, 3, 0, 0, 1]在动作[1, 1]造成的结果为[2, 2, 1, 1, -1],所以通过模型Result(),将[3, 3, 0, 0, 1]转换为了[2, 2, 1, 1, -1]。
  4. 目标测试(Goal test):确定当前状态是否为目标状态。在本例中,[0, 0, M, C, -1]为目标状态,当每次经过转换模型后,应该对得到的状态进行目标测试。
  5. 路径代价(Cost):即每条路径的代价。在本例中,假设每次划船的代价是相同的,就可以用状态转移的次数来表示代价,次数越少的解越优。

在对搜索问题五要素进行解释的同时,我们也对本例的问题进行了相应的举例。在下面以N=3, K= 2(N为传教士和野人分别的人数,K为船的最大载客量)为例,再次总结一下问题抽象的思路:

  1. 状态:初始状态为[3, 3, 0, 0, 1],目标状态为[0, 0, 3, 3, -1],中间状态为[ML, CL, MR, CR, B],应该满足:0 ≤ ML, CL, MR, CR ≤ N = 3。
  2. 动作:K = 2的时候动作一共有:[1, 0], [0, 1], [1, 1], [2, 0], [0, 2]。其中[m, c]表示载m个传教士,c个野人。
  3. 转换模型:通过当前状态和动作,得出下个状态为[ML - B*action[0], MR + B*action[0], CL - B*action[1], CR - B*action[1], -B]。从中可以看出,船的位置B决定了两岸人数的增加或是减小。
  4. 目标测试:测试当前状态是不是目标状态[0, 0, 3, 3, -1];
  5. 路径代价:每变换一次状态,记一次。

对于第3点,以状态[3, 3, 0, 0, 1]和动作[0, 2]为例,得到的下个状态应该为:

[3-1*0, 3-1*2, 0+1*0, 0+1*2, -1*1] = [3, 1, 0, 2, -1]

可以看到,船的位置(±1)决定了左岸和右岸到底是增加还是减少,然后下一个状态必然跟上一个状态的船的位置相反,可以按这个规则继续计算。

做作业的时候,顺便画了状态空间:

算法思路

本文将使用递归实现深度优先搜索算法(DFS),使用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], ...]

进入递归

递归形式实现的深度优先搜索是从一个点不断地寻找子节点,直到不满足条件再回来,找另外一个方向。就如同一个人走迷宫,采取一直右转的策略(当然不能走曾经走过的路,不然就一直绕了),然后终于无法右转了,只好退回去左转,然后继续右转试探。

递归当中最重要的是对于return的条件判断,有如下几个条件如果满足就需要回到上一个点:

  1. 到达终点。这时候不需要继续往终点的下一个状态寻找,直接返回到前一个点,寻找另外一条路。
  2. 与前面已经走过的点重复。因为在这个问题下,如果出现重复的点,那么必然没有必要。在上一次就能到这一个状态,走过若干步又到了这个状态,中间若干步根本没有必要走。所以此时退回,找另外的路。
  3. 不符合状态的要求。在这个问题下,要求传教士人数不为零的时候,传教士人数要大于野人人数。

如果上面的要求都符合,就不必return,将该点加入到用于存储图的字典当中。紧接着进行下一状态的建立(通过与action进行运算),然后再次进入递归。如果所有的action都试完了,return。

输出路径

输出路径使用的方法是递归的方法。从初始状态开始,在graph中进行深度优先搜索。

以初始状态为key值,遍历每个value,将value继续作为key值,然后同样遍历其value值。在此过程中记录下路径。

找到终点后,将该路径输出,然后return上一层,继续找。直到最后搜索完毕return。

结束

递归彻底结束后,输出一下总共找到多少条路径。

代码(Python)

"""
作者:Zhanyu_Guo
创建日期:2020.10.25
更新日期:2020.11.03
文件名:CrossRiverDFS.py
"""
# 用以测试运行时间
import time
"""globals"""
n = 0       # 传教士与野人各自的人数N
path = []   # 递归查找的单次路径
paths = []  # 存放多个路径
graph = {}  # 图

# 已走过状态的列表
# 表示方式[ML, CL, MR, CR, B](左传教士、左野人、右传教士、右野人、船)
# 船的位置(1:左岸,-1:右岸)决定了数目的增减
stateList = []

# 动作,即变化的方式,表示方式[M, C](传教士、野人)
actions = []


# 判断状态是否满足条件,同时建图
def ok(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 len(stateList) - 1:
        state_b = stateList[-2][:]
        if tuple(state_b) in graph.keys() and tuple(state) not in graph[tuple(state_b)]:
            graph[tuple(state_b)].append(tuple(state))
        else:
            graph[tuple(state_b)] = [tuple(state)]
            pass
        pass

    # 判断是否与前面状态重复
    for p in stateList[:-1]:
        if p[0] == state[0] and p[1] == state[1] and p[4] == state[4]:
            return False
        pass

    return True


def mapping(state):
    # 判断并建图
    if not ok(state):
        return

    # 到达目标状态[0, 0, n, n, -1]
    if state[0] == 0 and state[1] == 0:
        return

    # 执行动作,找到下一个有效点
    tmp = [0] * 5
    for action in actions:
        # 船的位置(1:左岸,-1:右岸)决定了数目的增减
        tmp[0] = state[0] - action[0] * state[4]
        tmp[1] = state[1] - action[1] * state[4]
        tmp[2] = state[2] + action[0] * state[4]
        tmp[3] = state[3] + action[1] * state[4]
        tmp[4] = -state[4]
        stateList.append(tmp[:])
        mapping(tmp)
        stateList.pop()
        pass

    return


# 深度搜索寻找路径
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:"))

    # 初始状态
    s = [n, n, 0, 0, 1]
    stateList.append(s)

    # 生成动作
    # 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(s)

    # 计算总时长
    total = time.perf_counter() - start
    print(total)

    # 搜索路径
    find_path(tuple(s))

    # 路径条数
    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)


if __name__ == '__main__':
    try:
        main()
        pass
    except Exception as e:
        print(e)
        pass
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Guo_Zhanyu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值