算法分析:大中小三個水桶分水問題


題目要求:有一個容積為8升的水桶里裝滿了水,另外還有一個容積為3升的空桶和一個容積為5升的空桶,如何利用這兩個空桶等分8升水?附加條件是三個水桶都沒有體積刻度,也不能使用其它輔助容器。

一直以為只有一種方法2333,想了一個早上,再結合網上大神的解釋(https://blog.csdn.net/orbit/article/details/6596521)(http://www.it610.com/article/5031244.htm),才發現方法很多。。。。終於弄明白了一點。

涉及python算法參考(python七大查找算法):https://www.cnblogs.com/lsqin/p/9342929.html

                             解決問題的思路

        如果用人的思維方式,那么解決這個問題的關鍵是怎么通過倒水湊出確定的1升水或能容納1升水的空間,考察三只水桶的容積分別是3、5和8,用這三個數做加減運算,可以得到很多組答案,例如:

3 – (5 - 3) = 1

這個策略對應了上面提到的第一種解決方法,而另一組運算:

(3 + 3)- 5 = 1

則對應了上面提到的第二種解決方法。

        但是計算機並不能理解這個“1”的重要性,很難按照人類的思維方式按部就班地推導答案,因此用計算機解決這個問題,通常會選擇使用“窮舉法”。為什么使用窮舉法?因為這不是一個典型意義上的求解最優解的問題,雖然可以暗含一個求解倒水次數最少的方法的要求,但就本質而言,常用的求解最優解問題的高效的方法都不適用於此問題。如果能夠窮舉解空間的全部合法解,然后通過比較找到最優解也是一種求解最優解的方法。不過就本題題意而言,並不關心什么方法最快,能求出全部等分水的方法可能更符合題意。

        如果我們把某一時刻三個水桶中存水的容積稱為一個狀態,則問題的初始狀態是8升的水桶裝滿水,求解的解出狀態(最終狀態)是8升水桶中4升水,5升水桶中4升水。窮舉法的實質就是把從初始狀態開始,根據某種狀態變化的規則搜索全部可能的狀態,每當找到一個從初始狀態到最終狀態的變化路徑,就可以理解為找到了一種答案。這樣的狀態變化搜索的結果通常是得到一棵狀態搜索樹,根節點是初始狀態,葉子節點可能是最終狀態,也可能是某個無法轉換到最終狀態的中間狀態,狀態樹有多少個最終狀態的葉子節點,就有多少種答案。根據以上分析結果,解決本問題的算法關鍵有三點:首先,建立算法的狀態模型;其次,確定狀態樹的搜索算法(暗含狀態轉換的規則);最后,需要一些提高算法效率的手段,比如應用“剪枝”條件避免重復的狀態搜索,還要避免狀態的循環生成導致搜索算法在若干個狀態之間無限循環。

 

狀態和動作的數學模型

 

        建立狀態模型是整個算法的關鍵,這個狀態模型不僅要能夠描述靜止狀態,還要能夠描述並記錄狀態轉換動作,尤其是對狀態轉換的描述,因為這會影響到狀態樹搜索算法的設計。所謂的靜止狀態,就是某一時刻三個水桶中存水的容積,我們采用長度為3的一維向量描述這個狀態。這組向量的三個值分別是容積為8升的桶中的水量、容積為5升的桶中的水量和容積為3升的桶中的水量。因此算法的初始狀態就可以描述為[8 ,0, 0],則終止狀態為[4, 4, 0]。

 

        對狀態轉換的描述就是在兩個狀態之間建立關聯,在本算法中這個關聯就是一個合法的倒水動作。某一時刻三個水桶中的存水狀態,經過某個倒水動作后演變到一個新的存水狀態,這是對狀態轉換的文字描述,對算法來講,倒水狀態描述就是“靜止狀態”+“倒水動作”。我們用一個三元組來描述倒水動作:{from, to, water},from是指從哪個桶中倒水,to是指將水倒向哪個桶,water是此次倒水動作所倒的水量。本模型的特例就是第一個狀態如何得到,也就是[8, 0, 0]這個狀態對應的倒水動作如何描述?我們用-1表示未知的水桶編號(上帝水桶),因此第一個狀態對應的倒水動作就是{-1, 1, 8}。應用本模型對前面提到的第一種解決方法進行狀態轉換描述,整個過程如圖(1)所示:

圖1 一個解決方法的狀態轉換圖

 

狀態樹搜索算法

 

        確定了狀態模型后,就需要解決算法面臨的第二個問題:狀態樹的搜索算法。一個靜止狀態結合不同的倒水動作會遷移到不同的狀態,所有狀態轉換所展示的就是一棵以狀態[8, 0, 0]為根的狀態搜索樹,圖(2)畫出了這個狀態搜索樹的一部分,其中一個用不同顏色標識出來的狀態轉換過程(狀態樹的一個分支)就是本問題的一個解:

 

圖2狀態樹一部分的展示

 

      狀態樹的搜索就是對整個狀態樹進行遍歷,這中間其實暗含了狀態的生成,因為狀態樹一開始並不完整,只有一個初始狀態的根節點,當搜索(也就是遍歷)操作完成時,狀態樹才完整。樹的遍歷可以采用廣度優先遍歷算法,也可以采用深度優先遍歷算法,就本題而言,要求解所有可能的等分水的方法,暗含了要記錄從初始狀態到最終狀態,所以更適合使用深度優先遍歷算法。狀態樹的遍歷暗含了一個狀態生成的過程,就是促使狀態樹上的一個狀態向下一個狀態轉換的驅動過程,這是一個很重要的部分,如果不能正確地驅動狀態變化,就不能實現狀態樹的遍歷(搜索)。

 

        建立狀態模型一節中提到的動作模型,就是驅動狀態變化的關鍵因子。對一個狀態來說,它能轉換到哪些新狀態,取決於它能應用哪些倒水動作,一個倒水動作能夠在原狀態的基礎上“生成”一個新狀態,不同的倒水動作可以“生成”不同的新狀態。由此可知,狀態樹遍歷的關鍵是找到三個水桶之間所有合法的倒水動作,用這些倒水動作分別“生成”各自相應的新狀態。遍歷三個水桶的所有可能動作,就是對三個水桶任取兩個進行全排列(常用的排列組合算法可以參考《排列組合算法》一文),共有6種水桶的排列組合,也就是說有6種可能的倒水動作。將這6種倒水動作依次應用到當前狀態,就可以“生成”6種新狀態,從而驅動狀態發生變化(有些排列並不能組合出合法的倒水動作,關於這一點后面“算法優化”部分會介紹)。

 

算法優化和避免狀態循環(看不懂的地方)

 

        從圖(2)可以看出來,對於三個水桶這樣小規模的題目,其整個狀態樹的規模也是相當大的,更何況是復雜一點的情況,因此類似本文這樣對搜索整個狀態樹求解問題的算法都不得不面對一個算法效率的問題,必須要考慮如何進行優化,減少一些明顯不必要的搜索,加快求解的過程。

 

        前文講過,狀態搜索的核心是對三個水桶進行兩兩排列組合得到6種倒水動作,但是並不是每種倒水動作都是合法的,比如,需要倒出水的桶中沒有水的情況和需要倒進水的桶中已經滿的情況下,都組合不出合法的倒水動作。除此之外,因為水桶是沒有刻度的,因此倒水動作也是受限制的,也就是說合法的倒水動作只能有兩種結果:需要倒出水的桶被倒空和需要倒進水的桶被倒滿。加上這些限制之后,每次組合其實只有少數倒水動作是合法的,可以驅動當前的狀態到下一個狀態。利用這一點,就可以對狀態樹進行“剪枝”,避免對無效(非法)的狀態分支進行搜索。

 

        除了通過“剪枝”提高算法效率,對於深度優先的狀態搜索還需要防止因狀態的循環生成造成深度優先搜索無法終止的問題。狀態的循環生成有兩種表現形式:一種是在兩個桶之間互相倒水;另一種就是圖(2)中展示的一個例子,[3, 5, 0] -> [3, 2, 3] -> [6, 2, 0] -> [3, 5, 0]形成一個狀態環。要避免出現狀態環,就需要記錄一次深度遍歷過程中所有已經搜索過的狀態,形成一個當前搜索已經處理過的狀態表,每當生成一個新狀態,就先檢查是否是狀態表中已經存在的狀態,如果是則放棄這個狀態,回溯到上一步繼續搜索。如果新狀態是狀態表中沒有的狀態,則將新狀態加入到狀態表,然后從新狀態開始繼續深度優先遍歷。在這個過程中因重復出現被放棄的狀態,可以理解為另一種形式的“剪枝”,可以使一次深度優先遍歷很快收斂到初始狀態。

--------------------- 本文來自 吹泡泡的小貓 的CSDN 博客 ,全文地址請點擊:https://blog.csdn.net/orbit/article/details/6596521?utm_source=copy


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM