商人们怎样安全过河


问题(智力游戏)

随从们密约,在河的任一岸,一旦随从的人数比商人多,就杀人越货。
乘船渡河的方案由商人决定。
商人们怎样才能安全过河?

已知条件

  • 3名商人
  • 3名随从
  • 小船至多可载2人
  • 两岸均需满足:随从人数 ≤ 商人人数(若某岸商人人数为0,则随从人数可任意)

问题分析

这是一个多步决策过程

  • 决策:每一步(此岸到彼岸或彼岸到此岸)船上的人员。
  • 要求:在安全的前提下(两岸的随从数不比商人多),经有限步使全体人员过河。

建模过程

我们利用DFS和BFS来进行数学建模,用DFS找出所有的符合题意且合法的状态,再利用BFS找出最短路径(图论),在BFS中记录从起点到终点的路径,一种合法的路径 --> 之前的路径,利用哈希表记录。

设置状态 (左岸商人, 左岸随从, 船的方向)
设置起点 (3, 3, '左')
设置终点 (0, 0, '右')

在遍历到目标的时候,执行函数ok,还原路径。

代码

# (左岸商人, 左岸随从, 船位置)
# (m_left, c_left, boat)
# 目标:(0, 0, '右')
import sys
from collections import deque
sys.setrecursionlimit(10000)

# 检测是否符合题意
def check(m_left, c_left):
    m_right = 3 - m_left
    c_right = 3 - c_left

    # 左岸
    if 0 < m_left < c_left:
        return False

    # 右岸
    if 0 < m_right < c_right:
        return False

    return True

# 随从可以单独过河
# 列举所有状态
res = []

# 访问标记
vis = set()

"""
深度优先搜索部分
"""

def dfs(m_left, c_left, boat):
    state = (m_left, c_left, boat)
    if state in vis:
        return
    if not check(m_left, c_left):
        return
    vis.add(state)
    res.append(state)
    for m, c in [(1, 0), (0, 1), (1, 1), (2, 0), (0, 2)]:
        if boat == '左':
            new_m = m_left - m
            new_c = c_left - c
            new_boat = '右'
        else:
            new_m = m_left + m
            new_c = c_left + c
            new_boat = '左'
        # 排除非法情况
        if 0 <= new_m <= 3 and 0 <= new_c <= 3:
            dfs(new_m, new_c, new_boat)

# 获取所有状态
dfs(3, 3, '左')
print("所有合法状态:")
print(res)
print("开始寻找最短路径(BFS)......")

"""
广度优先搜索部分
"""
# BFS
res_set = set(res)
ans = []
vis.clear()
parent = {}

def ok():
    path = []
    cur = (0, 0, '右')
    while cur != (3, 3, '左'):
        path.append(cur)
        cur = parent[cur]
    path.append((3, 3, '左'))
    path.reverse()
    print("最短路径:")
    for p in path:
        print(p)

q = deque([(3, 3, '左')])
vis.add((3, 3, '左'))

while q:
    cur = q.popleft()
    m_left, c_left, boat = cur
    if m_left == 0 and c_left == 0 and boat == '右':
        ok()
        break
    for m, c in [(1, 0), (0, 1), (1, 1), (2, 0), (0, 2)]:
        if boat == '左':
            new_m = m_left - m
            new_c = c_left - c
            new_boat = '右'
        else:
            new_m = m_left + m
            new_c = c_left + c
            new_boat = '左'
        if 0 <= new_m <= 3 and 0 <= new_c <= 3:
            nxt = (new_m, new_c, new_boat)
            if nxt in res_set and nxt not in vis:
                vis.add(nxt)
                parent[nxt] = cur
                q.append(nxt)

输出

所有合法状态:
[(3, 3, '左'), (3, 2, '右'), (2, 2, '右'), (3, 2, '左'), (3, 1, '右'), (3, 0, '右'), (3, 1, '左'), (1, 1, '右'), (2, 2, '左'), (0, 2, '右'), (0, 3, '左'), (0, 1, '右'), (1, 1, '左'), (0, 0, '右'), (0, 1, '左'), (0, 2, '左')]
开始寻找最短路径(BFS)......
最短路径:
(3, 3, '左')
(2, 2, '右')
(3, 2, '左')
(3, 0, '右')
(3, 1, '左')
(1, 1, '右')
(2, 2, '左')
(0, 2, '右')
(0, 3, '左')
(0, 1, '右')
(1, 1, '左')
(0, 0, '右')

因此所有的合法状态得出:

(3, 3, '左') (3, 2, '右') (2, 2, '右') (3, 2, '左')
(3, 1, '右') (3, 0, '右') (3, 1, '左') (1, 1, '右')
(2, 2, '左') (0, 2, '右') (0, 3, '左') (0, 1, '右')
(1, 1, '左') (0, 0, '右') (0, 1, '左') (0, 2, '左')

最短的合法路径:

(3, 3, '左') → (2, 2, '右') → (3, 2, '左') → (3, 0, '右') → 

 → (3, 1, '左') → (1, 1, '右') → (2, 2, '左') → (0, 2, '右') → 

 → (0, 3, '左') → (0, 1, '右') → (1, 1, '左') → (0, 0, '右')

最终结论

一共需要11
过河过程(共 11 步):

1 商 1 从过河  
1 商返回  
2 从过河  
1 从返回  
2 商过河  
1 商 1 从返回  
2 商过河  
1 从返回  
2 从过河  
1 商 1 从返回  
1 商 1 从过河  

完成,全部过河。