11. Container With Most Water
給一排高度不同的直線,任選兩根當作水桶的兩片壁,問能裝的最大水量。水量 = 兩根的距離(底邊)× 兩根中較矮的那根(水位)。
思路
先從暴力解出發:枚舉所有的配對,,然後取最大值。這個一定寫得出來,而且它給了一個好起點 —— 從暴力解往下優化才有東西可以講。
接著看核心想法,這是一句俗諺:水桶的最大容量決定於最矮的一邊。
題目給出的正是水桶高度,只是俗諺中的水桶,底部的面積都一樣;這個題目裡面,我們有不同的水桶底邊與水桶壁高度的組合。
所以我們就從左右兩端向中間逼近。逼近的關鍵觀察是:不管移動哪一個指針,底邊都一定變短。 底邊只會變差,那唯一還能變好的因子就是高度 —— 於是策略很清楚:把矮的那邊丟掉,賭中間會出現更高的壁。
為什麼丟掉矮的那邊不會漏掉答案
這是這題真正的核心 —— 「怎麼知道往中間收的時候沒跳過真正的答案?」理由是一個交換論證:
假設現在 height[left] < height[right]。對任何夾在中間的 j(left < j < right),配對 (left, j) 的面積是 min(height[left], height[j]) × (j - left):
- 底邊一定比現在小,因為
j - left < right - left - 水位一定不超過現在,因為
min(height[left], height[j]) ≤ height[left],而height[left]正是現在的水位
兩個因子都不可能變好,所以 left 不可能出現在任何比現在更好的答案裡,可以安全丟掉。反過來說,如果丟掉的是高的那邊,這個論證就不成立了 —— 高的那邊還可能跟中間某根更矮但距離仍夠遠的柱子組出更大面積。
想通這一段,這題就從「我記得這個 trick」變成「我能證明它是對的」。
解題方向
class Solution:
def maxArea(self, height: List[int]) -> int:
left = 0
right = len(height) - 1
max_area = 0
while left < right:
width = right - left
max_area = max(max_area, min(height[left], height[right]) * width)
if height[left] < height[right]:
left += 1
else:
right -= 1
return max_area
每一輪都先算完當前的面積再移動指針,順序不能顛倒 —— 否則會漏掉當前這組配對。
兩邊一樣高的時候往哪邊移? 這份程式碼走 else 分支移右邊。移左邊也可以,答案完全相同:兩邊一樣高時,上面那個論證對兩邊同時成立,所以丟哪一邊都不會漏答案。
補充
同樣是對撞指針的題:167. Two Sum II(和太小就移左、太大就移右)、15. 3 Sum(固定一個數再對撞剩下的區間)、125. Valid Palindrome(兩端往中間比對)。共同點都是「有一個單調的量,讓你每一步都能安全丟掉一邊」,見 Two Pointers 模板。
別跟 42. Trapping Rain Water 搞混。 兩題的圖看起來幾乎一樣,但問的是完全不同的事:
| 問什麼 | 答案的形狀 | |
|---|---|---|
| 11 Container With Most Water | 挑兩根柱子當桶壁,最多裝多少 | 一個配對 |
| 42 Trapping Rain Water | 整片地形的所有凹陷總共積多少水 | 每一格的貢獻之和 |
11 可以丟掉矮的那邊,因為只需要找一個最佳配對;42 每一格都要算,所以需要記住「左右兩側的最高值」,那是另一套解法(前綴最大值或單調棧)。
複雜度
- 時間 — 兩個指針各自單向移動,總共走完陣列一次
- 空間 — 只有兩個索引和一個最大值
其中 是柱子的數量。作為對照,暴力枚舉所有配對是 時間、 空間。