題目
有兩個容量分別爲 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
- 如果目標大於兩個瓶子容量和或目標爲0,特殊處理
- 如果目標大於兩個瓶子容量,則減去大容量作爲新目標容量,即原目標容量是新目標容量加上大容量
- 遞歸裏面不斷通過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的整數次組合操作,即使用一個二元一次方程進行求解判斷,將題目抽象爲數學問題,可以理解爲:
- 已知x,y,z三個正整數
- 判斷是否存在兩個整數a,b,使得線性方程可以滿足:,即在以ab爲變量構成的二維座標系中,直線方程是否經過整數座標
- a,b的約束條件爲:不能同時大於等於1(同時等於1除外),即要檢查的座標點不考慮第一象限中(1,b)(a,1)的那一部分
在數論中有個貝祖定理,是這麼說的:
- 若x,y是整數,且x和y的最大公約數爲k,一定存在整數a和b,使得成立
- 進一步,等式有解的充要條件是:z是k的整數倍
- 另外,等式有解時必然有無窮多個整數解
從貝祖定理知道,本題目可以轉換爲判斷,z是否能夠整除x和y的最大公約數。
- ab的約束條件首先剔除(思路中的第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種操作方式:
- 把X倒滿
- 把Y倒滿
- 把X倒空
- 把Y倒空
- 把X倒入Y,可能把X倒空,也可能把Y倒滿
- 把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