3種解法 - 兩水壺拼水問題


題目

有兩個容量分別爲 x升 和 y升 的水壺以及無限多的水。請判斷能否通過使用這兩個水壺,從而可以得到恰好 z升 的水?

如果可以,最後請用以上水壺中的一或兩個來盛放取得的 z升 水。

你允許:

  • 裝滿任意一個水壺
  • 清空任意一個水壺
  • 從一個水壺向另外一個水壺倒水,直到裝滿或者倒空

示例 1: (From the famous “Die Hard” example)

輸入: x = 3, y = 5, z = 4
輸出: True

示例 2:

輸入: x = 2, y = 6, z = 5
輸出: False


解法一(暴力法)

思路:目標容量是通過當前兩個瓶子相互減,通過遞歸相減遍歷出所有可能組合的,沒去探尋規律,因此稱爲暴力法,不過其搜索的方式也可以稱爲深度優先搜索。如果遞歸找到返回True,如果遍歷所有可能後沒找到,則返回False

  1. 如果目標大於兩個瓶子容量和或目標爲0,特殊處理
  2. 如果目標大於兩個瓶子容量,則減去大容量作爲新目標容量,即原目標容量是新目標容量加上大容量
  3. 遞歸裏面不斷通過x和y對當前差值進行處理
  • 時間複雜度:O(x*y)
  • 空間複雜度:O(x*y)

遞歸的寫法,在測試用例數值比較大的時候會超出遞歸深度,因此最前面兩行是修改了Python默認的遞歸深度。(如果不修改深度,超出深度的測試用例爲:104597,104623,123)。

# author: [email protected]
import sys 
sys.setrecursionlimit(1000000) 
class Solution:
    def deIn(self,s,x,y,z,t):
        if z == t:# 找到
            return True
        if s.__contains__(t):# 已經出現過,則不再遞歸
            return False
        s.add(t)
        return self.deIn(s,x,y,z,abs(x-t)) or self.deIn(s,x,y,z,abs(y-t))

    def canMeasureWater(self, x: int, y: int, z: int) -> bool:
        if z > x + y:
            return False
        if z in (0,x,y):#簡單特例
            return True
        if z > max(x,y):
            z = z - max(x,y)
        s = set()
        return self.deIn(s,x,y,z,max(x,y)-min(x,y))

解法二(直線方程)

思路:由於每次倒水操作都是全部倒掉或者倒滿,都可以看做是對x,y的整數次組合操作,即使用一個二元一次方程進行求解判斷,將題目抽象爲數學問題,可以理解爲:

  1. 已知x,y,z三個正整數
  2. 判斷是否存在兩個整數a,b,使得線性方程可以滿足:ax+by=zax + by = z,即在以ab爲變量構成的二維座標系中,直線方程是否經過整數座標
  3. a,b的約束條件爲:不能同時大於等於1(同時等於1除外),即要檢查的座標點不考慮第一象限中(1,b)(a,1)的那一部分

在數論中有個貝祖定理,是這麼說的:

  1. 若x,y是整數,且x和y的最大公約數爲k,一定存在整數a和b,使得ax+by=kax+by=k成立
  2. 進一步,等式ax+by=zax+by=z有解的充要條件是:z是k的整數倍
  3. 另外,等式有解時必然有無窮多個整數解

從貝祖定理知道,本題目可以轉換爲判斷,z是否能夠整除x和y的最大公約數。

  1. ab的約束條件首先剔除(思路中的第3點)
  2. 輾轉相除得到最大公約數
  3. 判斷是否能整除最大公約數
class Solution:
# author: [email protected]
    def canMeasureWater(self, x: int, y: int, z: int) -> bool:
        if z > x + y:#剔除約束條件
            return False
        if z in (0,x,y):#簡單特例
            return True
        x, y = max(x,y), min(x,y)
        while y > 0:#輾轉相除 得到 最大公約數
            y, x = x % y, y        
        return z % x == 0

解法三(深度\廣度優先搜索)

思路:每次把每種可能的操作都執行一遍,如果滿足條件則返回True,否則每次把操作結果存入棧,每次取棧頂元素後持續查找,直到將每次操作的可能性都遍歷完,本算法使用棧來達到深度優先搜索算法,也可以使用隊列來達到廣度優先搜索算法。

每次操作最多有以下6種操作方式:

  1. 把X倒滿
  2. 把Y倒滿
  3. 把X倒空
  4. 把Y倒空
  5. 把X倒入Y,可能把X倒空,也可能把Y倒滿
  6. 把Y倒入X,可能把Y倒空,也可能把X倒滿

爲了減少重複遍歷,引入visited集合進行過濾來判斷,這種方式跟解法一類似,也可以理解成暴力法,即基於具體規則的暴力破解。本解法使用循環來代替遞歸,用於避免超出Python的默認最大遞歸深度,當然也可以類似解法一使用遞歸的方式實現。

class Solution:
# author: [email protected]
    def canMeasureWater(self, x: int, y: int, z: int) -> bool:
        stack = [(0,0)]
        visited = set()
        while stack:
            cx,cy = stack.pop()
            if z in (cx,cy,cx+cy):
                return True
            if (cx,cy) in visited:
                continue
            visited.add((cx,cy)) #加入標籤減少重複遍歷
            stack.append((x,cy))#1. 把X倒滿
            stack.append((cx,y))#2. 把Y倒滿
            stack.append((0,cy))#3. 把X倒空
            stack.append((cx,0))#4. 把Y倒空
            stack.append((cx-min(cx,y-cy),cy+min(y-cy,cx)))#5. 把X倒入Y,可能把X倒空,也可能把Y倒滿
            stack.append((cx+min(x-cx,cy),cy-min(cy,x-cx)))#6. 把Y倒入X,可能把Y倒空,也可能把X倒滿
        return False

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章