过河问题(图、BFS)
描述:
原题:3只羊和3只狮子过河,有1艘船只能容纳2只动物,当河岸上狮子数大于羊数,羊就会被吃掉,找到运输方法,让所有动物都过河。
类似推广:野人传教士过河;羊狼过河;有些问题描述时会加一个农夫(渡船人),但农夫往往不是影响因素。
思路:
看到类似问题,想到状态搜索,搜索方式一般有两种:DFS和BFS,其都是对所有状态的一种搜索,直到搜索到目标状态
定义状态:
1)题目意思是,原来岸边有3狮3羊,最后要安全渡河,变成0狮0羊,那么状态必有狮子数和羊数;
2)什么导致状态发生变化,是用船运送,那船在此岸还是彼岸?一开始船肯定在此岸,最后运输完成后,船必然是在彼岸,故船在哪个岸应该也要作为状态;
于是,定义状态state:
- state = (m, n, k)
- m: 羊数,0 <= m <= 3
- n: 狮子数,0 <= n <= 3
- k: 船在哪个岸,k = {0, 1}, 1表示在A岸(也就是此岸);0表示在B岸(也就是在彼岸)
那么,最终就是搜索A岸从(3,3,1)—> (0,0,0)
注:理论上可能的状态一共有32种,即 4 * 4 * 2 = 32
定义决策(让状态发生变化的行为):
根据题意,船运输的羊和狮子的数量满足:
1)总数不超过2:m + n <= 2
2)羊数不小于狮子数:m >= n
那么,策略一共有以下5种:
(1,0), (0,1), (1,1), (2,0), (0,2)
例如,(1,0)表示运输羊1只狮子0只
BFS
就是从当前状态开始,遍历所有可能的决策,进行状态跳转,如下图:
1)搜索在向广度拓展,所以是典型的BFS搜索(如果是DFS,就是向深度拓展);
2)可以根据记忆(空间换时间),将已经发现过的状态剪枝,不重复搜索;
3)判断状态是否合法:
- A岸 和 B岸的狮子数不大于羊数;
- A岸 或 B岸的狮子数或羊数不能超过边界;
4)根据船在A岸还是B岸,修改状态跳转逻辑:
- 船在A岸,则A岸狮子、羊的数量减少,B岸狮子、羊的数量增加;
- 船在B岸,则A岸狮子、羊的数量增加,B岸狮子、羊的数量减少;
show the code
以下是python编写的算法,基于pythonds包
详细可看注释,个人认为还是比较清晰的
from pythonds.graphs import Graph
from pythonds.basic import Queue
def solution():
'''
3只羚羊和3只狮子过河问题:
1艘船只能容纳2只动物
当河岸上狮子数大于羚羊数,羚羊就会被吃掉
找到运输方法,让所有动物都过河
'''
# 定义合法的运输操作(i, j) , 例如(1, 0)表示运送羚羊1只,狮子0只
opt = [(1, 0), (0, 1), (1, 1), (2, 0), (0, 2)]
# 定义状态state(m, n, k), m表示羚羊数,n表示狮子数,k表示船在此岸还是彼岸
# stateA 表示A岸(此岸)的状态;stateB 表示B岸(彼岸)的状态;
# 初始状态
stateA = (3, 3, 1)
stateB = (0, 0, 0)
# BFS搜索
mygraph = Graph()
myqueue = Queue()
myqueue.enqueue((stateA, stateB))
sequence = [] # 剪枝记录(最后发现,有效状态只有15种)
sequence.append((stateA))
while True:
stateA, stateB = myqueue.dequeue()
if stateA == (0, 0, 0):
break
for o in opt:
# 一次从某岸到另一岸的运输
if stateA[2] == 1:
stateA_ = (stateA[0] - o[0], stateA[1] - o[1], stateA[2] - 1)
stateB_ = (stateB[0] + o[0], stateB[1] + o[1], stateB[2] + 1)
else:
stateB_ = (stateB[0] - o[0], stateB[1] - o[1], stateB[2] - 1)
stateA_ = (stateA[0] + o[0], stateA[1] + o[1], stateA[2] + 1)
# 运输后
if stateA_[0] and stateA_[0] < stateA_[1]: # 此岸在有羊的情况下,如果狼大于羊,则吃掉
continue
elif stateB_[0] and stateB_[0] < stateB_[1]: # 彼岸在有羊的情况下,如果狼大于羊,则吃掉
continue
elif stateA_[0] < 0 or stateA_[0] > 3 or stateA_[1] < 0 or stateA_[1] > 3: # 边界
continue
else:
# 剪枝
if stateA_ in sequence:
continue
else:
sequence.append(stateA_)
myqueue.enqueue((stateA_, stateB_))
mygraph.addEdge(stateA, stateA_, o)
return mygraph, sequence
if __name__ == '__main__':
g, sq = solution()
# 建立父子关系
for v_n in sq:
v = g.getVertex(v_n)
for nbr in v.getConnections():
if nbr.getColor() == 'white':
nbr.setPred(v)
nbr.setColor('gray')
v.setColor('black')
target = g.getVertex(sq[-1])
# 回溯,显示决策路径
while target.getPred():
predv = target.getPred()
print(target.id, '<--', predv.getWeight(target), '--', predv.id)
target = predv
编译结果:
(0, 0, 0) <-- (1, 1) -- (1, 1, 1)
(1, 1, 1) <-- (1, 0) -- (0, 1, 0)
(0, 1, 0) <-- (0, 2) -- (0, 3, 1)
(0, 3, 1) <-- (0, 1) -- (0, 2, 0)
(0, 2, 0) <-- (2, 0) -- (2, 2, 1)
(2, 2, 1) <-- (1, 1) -- (1, 1, 0)
(1, 1, 0) <-- (2, 0) -- (3, 1, 1)
(3, 1, 1) <-- (0, 1) -- (3, 0, 0)
(3, 0, 0) <-- (0, 2) -- (3, 2, 1)
(3, 2, 1) <-- (1, 0) -- (2, 2, 0)
(2, 2, 0) <-- (1, 1) -- (3, 3, 1)
查看sequence后,可以发现,BFS搜索的有效状态其实只有15种,然而在其他博客中(以下)描述有16种有效状态,其中,A岸状态**(0,1,1)**是唯一的不同,why?
sequence记录的状态如下:
(3, 3, 1)
(3, 2, 0)
(2, 2, 0)
(3, 1, 0)
(3, 2, 1)
(3, 0, 0)
(3, 1, 1)
(1, 1, 0)
(2, 2, 1)
(0, 2, 0)
(0, 3, 1)
(0, 1, 0)
(1, 1, 1)
(0, 2, 1)
(0, 0, 0)
原因:BFS搜索到(0,0,0)就停止了,而(0,1,1)是(0,0,0)的跳转状态,所采取的动作为(0,1),即从B岸又送了一只狮子过来,所以理论上这个状态是合法的,但实际搜索时是搜不到的。
参考博客:
https://www.cnblogs.com/guanghe/p/5485800.html