這篇博文中,同樣是一個很簡單的數學問題,但是解決起來比上一個的問題要複雜一些。在這次模型求解中,我會使用兩種方法,一種是純粹的數學方法,另一種是通過計算機程序來計算,通過計算機求解我們可以求解一些規模更大的問題。由於這篇文章篇幅我預計會比較長,爲了不混淆,上一篇文章《椅子能在不平的地面上放平嗎?》中的延伸問題我會再寫一篇文章單獨解答。
問題引出
問題: 三名商人各帶一個隨從過河,一隻小船隻能容納兩個人,隨從們約定,只要在河的任何一岸,一旦隨從人數多於商人人數就殺人越貨,但是商人們知道了他們的約定,並且如何過河的大權掌握在商人們手中,商人們該採取怎樣的策略才能安全過河呢?
這次的問題是一個很經常遇到的過河問題,其實對於該類問題,我們經過邏輯思考就可以得到答案。但是通過數學模型的建立,我們可以得到一個通用的解答,並且通過計算機的計算我們可以大大擴大問題的規模。
問題分析
因爲這個問題已經理想化了,所以我們無需對模型進行假設,該問題可以看作一個多步決策問題。
每一步,船由此岸劃到彼岸或者由彼岸劃回此岸,都要對船上的人員進行決策(此次渡河船上可以有幾名商人和幾名隨從),在保證安全(兩岸的隨從都不比商人多)的前提下,在有限次的決策中使得所有人都到對岸去。
因此,我們要做的就是要確定每一步的決策,達到渡河的目標。
建立模型
記第 k 次過河前此岸的商人數爲 xk , 隨從數爲 yk , k = 1, 2, 3…, xk ,yk = 0, 1, 2, 3
定義狀態: 將二維向量 sk = ( xk , yk ) 定義爲狀態
將安全渡河狀態下的狀態集合定義爲允許狀態集合, 記爲
記第 k 次渡河船上的商人數爲 uk , 隨從數爲 vk
定義決策: 將二維向量 dk = (uk , vk) 定義爲決策
允許決策集合 記作
因爲小船容量爲2,所以船上人員不能超過2,而且至少要有一個人划船,由此得到上式。
由我們定義的狀態 sk 和決策 dk ,我們可以發現它們之間是存在聯繫的:
k 爲奇數是表示船由此岸划向彼岸,k 爲偶數時表示船由彼岸劃回此岸
狀態 sk 是隨着決策 dk 變化的,規律爲:
我們把上式稱爲狀態轉移律,因此渡河方案可以抽象爲如下的多步決策模型:
求決策 dk ∈ D(k = 1,2,…,n) , 使狀態 sk ∈ S 按照轉移率,初始狀態 s1 = (3,3) 經有限步 n 到達狀態 sn+1 = (0,0)
到這裏,整個數學模型就已經非常清晰了,接下來要做的就是求解模型得出結果。
求解模型
在這個模型的求解中,我將會使用兩種方法,一種是數學圖解法,用於解決和當前題目一樣的規模比較小的問題,優點是比較簡便,但是對於規模比較大的問題就無能爲力了,比如說有50個商人攜帶50個隨從過河,第二種方法是通過計算機編程,使用程序來解決該問題,即使問題規模增大,我們也可以利用計算機強大的計算能力來解決。
數學圖解法
我們首先在 xOy 平面座標系中畫出如下方格,方格中的點表示狀態 s = (x,y)
起始狀態(下圖綠色點) s1 = (3,3) , 終止狀態(下圖紅色點) sn+1 = (0,0)
允許決策 dk 表示的是在方格中的移動,根據允許決策 dk 的定義,它每次的移動範圍爲1~2格,並且 k 爲奇數時向左或下方或左下方移動,k 位偶數時向右或上方或右上方移動。
於是,這個問題就變成了,根據允許決策 dk ,在方格中在狀態(方格點)之間移動,找到一條路徑,使得能從起始狀態(上圖綠色點) s1 = (3,3) ,到達終止狀態(上圖圖紅色點) sn+1 = (0,0)
在下圖中,我們給出了一種方案,我們可以很清楚的看到該方案絕對不是最佳方案(渡河次數最少),它只是給出了一種方案,而且我們看來是一種極其不優化的方案,但是可以很清楚地看出圖解法是如何工作的。
根據上圖,我們得出的方案如下:
- d1:兩個隨從劃到對岸
- d2:一個隨從劃回來
- d3:兩個隨從劃到對岸
- d4:一個隨從劃回來
- d5:兩個商人劃到對岸
- d6:一個商人和一個隨從劃回來
- d7:兩個商人劃到對岸
- d8:一個隨從劃回來
- d9:兩個隨從劃到對岸
- d10:一個商人劃回來
- d11:一個商人和隨從劃到對岸
最終商人們安全渡河
程序求解
我們看到上面介紹的圖解法對於小規模問題很直觀也很簡單,但是無法應對大規模的問題,於是我們採用編程的方法來再次解決上述問題,這次我使用的編程語言爲Python.
創建允許狀態集合
對於允許狀態集合,我們要去使用算法對其進行計算,所謂允許狀態無非就是,河岸兩邊的商人們都是安全的:
- 一種情況是:兩岸的商人人數都比隨從人數多(對於隨從和商人人數相同的情況就是河的任一岸,商人人數等於隨從人數)
- 另一情況爲:所有商人都在河的任何一岸,此時另一岸沒有任何商人,對於隨從的人數在河的任一岸的數量不論是多少,此時都是安全的
按照以上方法編程如下:
'''創建允許狀態集合'''
def allowset(self):
allowset = []
for i in range(self.merchants + 1):
for j in range(self.servants + 1):
if i == 0:
allowset.append([i,j])
elif i == self.merchants:
allowset.append([i,j])
elif (i >= j and ((self.merchants-i) >= (self.servants-j))):
allowset.append([i,j])
return allowset
創建允許決策集合
對於創建允許決策集合,它和船的容量是相關的,只要每次渡河的商人數量和隨從數量小於等於船的容量即可,代碼如下:
'''創建允許決策集合'''
def allowaction(self):
allowactionset = []
for i in range(self.capacity + 1):
for j in range(self.capacity + 1):
if (i+j) <= self.capacity and (i + j) != 0:
allowactionset.append([i,j])
return allowactionset
如何渡河
對於如何渡河問題我採取的是一種隨機的方法,對於當前安全狀態,隨機選擇一種決策進行試探,如果採取該決策可以到達安全狀態,則採用,如此循環,直到到達目的地。如果採取該策略不能到達安全狀態,則再次隨機選擇一種策略。
代碼如下:
def solve(self,allowactionset,allowstate):
count = 1;
current = (self.merchants,self.servants)
while current != [0,0]:
move = allowactionset[random.randint(0,len(allowactionset)-1)]
temp = [current[0]+((-1)**count)*move[0],current[1]+((-1)**count)*move[1]]
if(temp in allowstate):
current = [current[0]+((-1)**count)*move[0],current[1]+((-1)**count)*move[1]]
if(count % 2 == 1):
print "[%d]個商人,[%d] 個隨從從此岸劃到對岸" %(move[0],move[1])
elif(count % 2 == 0):
print "[%d]個商人,[%d] 個隨從從對岸劃回此岸" %(move[0],move[1])
count = count + 1
完整代碼
有了以上算法之後,我們就可以使用計算機來解決一些較大規模的問題了,完整代碼如下:
# -*- coding: utf-8 -*-
# Copyright (c) 2015 Jason Luo @ SDU
"""解決商人安全過河問題"""
import random
class Boat(object):
def __init__(self, merchants, servants, capacity):
self.merchants = merchants
self.servants = servants
self.capacity = capacity
print "Initialize: [%d] merchants and [%d] servants" %(merchants, servants)
'''創建允許狀態集合'''
def allowset(self):
allowset = []
for i in range(self.merchants + 1):
for j in range(self.servants + 1):
if i == 0:
allowset.append([i,j])
elif i == self.merchants:
allowset.append([i,j])
elif (i >= j and ((self.merchants-i) >= (self.servants-j))):
allowset.append([i,j])
return allowset
'''創建允許決策集合'''
def allowaction(self):
allowactionset = []
for i in range(self.capacity + 1):
for j in range(self.capacity + 1):
if (i+j) <= self.capacity and (i + j) != 0:
allowactionset.append([i,j])
return allowactionset
'''渡河'''
def solve(self,allowactionset,allowstate):
count = 1;
current = (self.merchants,self.servants)
while current != [0,0]:
move = allowactionset[random.randint(0,len(allowactionset)-1)]
temp = [current[0]+((-1)**count)*move[0],current[1]+((-1)**count)*move[1]]
if(temp in allowstate):
current = [current[0]+((-1)**count)*move[0],current[1]+((-1)**count)*move[1]]
if(count % 2 == 1):
print "[%d]個商人,[%d] 個隨從從此岸劃到對岸" %(move[0],move[1])
elif(count % 2 == 0):
print "[%d]個商人,[%d] 個隨從從對岸劃回此岸" %(move[0],move[1])
count = count + 1
'''主方法'''
def main():
boat = Boat(3,3,2)
allowstate = boat.allowset()
print "允許狀態集合爲:"
print allowstate
actionset = boat.allowaction()
print "允許決策集合爲:"
print actionset
boat.solve(actionset,allowstate)
if __name__ == '__main__':
main()
運行結果
爲了縮短運行結果的篇幅,我們同樣採取小規模問題來驗證算法的正確性,這裏還是採用原問題規模,運行結果如下:
Initialize: [3] merchants and [3] servants
允許狀態集合爲:
[[0, 0], [0, 1], [0, 2], [0, 3], [1, 1], [2, 2], [3, 0], [3, 1], [3, 2], [3, 3]]
允許決策集合爲:
[[0, 1], [0, 2], [1, 0], [1, 1], [2, 0]]
[1]個商人,[1] 個隨從從此岸劃到對岸
[2]個商人,[0] 個隨從從此岸劃到對岸
[2]個商人,[0] 個隨從從對岸劃回此岸
[2]個商人,[0] 個隨從從此岸劃到對岸
[0]個商人,[2] 個隨從從此岸劃到對岸
算法評價
該算法可以解決問題,但是有很多的不足,首先,由此算法得到的結果是隨機的,它只是一個可行解,並不是最優解,並且其中很可能存在重複的步驟,對於一些超大規模的問題,它會產生許多重複的計算,其中會存在許多重複與環,還有許多可以改進的方法。這裏我提出一種改進方法的思路,留待思考:我們可以藉助圖論中的深度優先算法來改進該問題從而得到最優解。如果不一定需要最優解的話,我們還可以在該算法上應用一個隊列的數據結構,記錄曾經在當前狀態採取的策略,從而避免採取重複決策。
參考資料
本文的版權歸作者 羅遠航 所有,採用 Attribution-NonCommercial 3.0 License。任何人可以進行轉載、分享,但不可在未經允許的情況下用於商業用途;轉載請註明出處。感謝配合!