On this page
- โจทย์พูดว่าอะไร
- Understanding the Area Formula
- Step 1 — Brute Force
- Step 2 — Building the Intuition
- The Proof — ทำไมต้องขยับฝั่งเตี้ยเสมอ
- พิสูจน์แบบตัดคู่ทิ้ง (แบบที่ interviewer ชอบถาม)
- Step 3 — Full Dry Run
- Step 4 — The Optimal Solution
- Step 5 — Complexity Analysis
- Why It’s O(n) — ไล่เหตุผลทีละข้อ
- Step 6 — Common Wrong Approaches
- Wrong 1 — ขยับทั้งสองฝั่งพร้อมกันทุกก้าว
- Wrong 2 — ขยับฝั่งสูง
- Wrong 3 — เริ่มจากเส้นสูงสุดแล้วขยายออก
- Step 7 — Edge Cases
- Step 8 — อธิบายในสัมภาษณ์ยังไง
- ข้อนี้ต่างจาก Two Sum II ยังไง
- Final Solution at a Glance
- Wrapping Up — จำสามอย่างนี้พอ
ข้อ 12 · LC11 Container With Most Water 🟡
ดูเหมือนโจทย์เรขาคณิต แต่จริง ๆ คือ two pointers รอบเดียว — ของยากไม่ใช่โค้ด (แค่ ~10 บรรทัด) แต่คือทำไมขยับฝั่งเตี้ยถึงถูก
โจทย์นี้มีอะไรแอบซ่อนอยู่ พออ่านครั้งแรกเหมือนเรขาคณิต แต่พอเห็นมุมมองที่ถูก มันกลายเป็น two pointers ที่เดินรอบเดียวจบ
ของยากไม่ใช่โค้ด — โค้ดสั้นมาก ของยากคือสร้าง intuition ว่าทำไมวิธีนี้ถึงถูก นั่นคือสิ่งที่หน้านี้โฟกัส เริ่มจากศูนย์เลย
เรียบเรียงตามบทสอนของ Daily Dev Notes (LeetCode 11 — Full Solution Explained) รวม intuition / พิสูจน์ / dry run / วิธีคิดผิด / วิธีพูดในสัมภาษณ์ — รูปตัวอย่างใช้ของโจทย์ LeetCode โดยตรง
โจทย์พูดว่าอะไร
ให้ integer array height ความยาว n มีเส้นแนวตั้ง n เส้น จุดปลายของเส้นที่ i อยู่ที่ (i, 0) และ (i, height[i])
หาเส้นสองเส้นที่รวมกับแกน x แล้วได้ภาชนะจุน้ำได้มากที่สุด แล้ว return ปริมาณน้ำนั้น — ห้ามเอียงภาชนะ
- Input:
- height = [1, 8, 6, 2, 5, 4, 8, 3, 7]
- Output:
- 49
- Explanation:
- เลือกเส้นที่ index 1 (สูง 8) กับ index 8 (สูง 7)
กว้าง = 8 − 1 = 7
สูง = min(8, 7) = 7
พื้นที่ = 7 × 7 = 49
- Input:
- height = [1, 1]
- Output:
- 1
- Explanation:
- มีแค่สองเส้น กว้าง = 1, สูง = min(1,1) = 1, พื้นที่ = 1
- n == height.length
- 2 <= n <= 10^5
- 0 <= height[i] <= 10^4

Understanding the Area Formula
ก่อนกระโดดไปเฉลย ต้องชัดก่อนว่าคำนวณพื้นที่ยังไง
ถ้าเลือกเส้นที่ตำแหน่ง i กับ j (โดย i < j) น้ำที่กักได้คือ
Area = width × height
= (j − i) × min(height[i], height[j])
ความสูงใช้ min() เพราะน้ำล้นออกทางเส้นที่เตี้ยกว่า
— เติมสูงกว่าเส้นเตี้ยไม่ได้พื้นที่ถูกจำกัดด้วยเส้นที่เตี้ยกว่าเสมอ — observation เดียวนี้คือทั้งหมดที่ต้องเข้าใจเพื่อเห็นว่าทำไม two pointers ถึงเวิร์ก
Step 1 — Brute Force
เริ่มจากวิธีตรง ๆ ตามปกติ เช็คทุกคู่เส้นที่เป็นไปได้ แล้วเก็บค่ามากสุด
def max_area(height: list[int]) -> int:
max_water = 0
n = len(height)
for i in range(n):
for j in range(i + 1, n):
width = j - i
h = min(height[i], height[j])
area = width * h
max_water = max(max_water, area)
return max_water
print(max_area([1, 8, 6, 2, 5, 4, 8, 3, 7]))49| Metric | Brute Force | Impact |
|---|---|---|
| Time Complexity | O(n²) | ลูปซ้อนสองชั้น — n = 10,000 ≈ 100 ล้านครั้ง |
| Space Complexity | O(1) | โอเค ไม่มีโครงสร้างเพิ่ม |
| LeetCode Result | TLE | เคสใหญ่รันไม่ทัน — Time Limit Exceeded |
brute force ถูกทาง logic แต่ช้าเกินไป — n = 100,000 เส้นต้องเทียบกันประมาณหมื่นล้านคู่ เราต้องการ O(n)
Step 2 — Building the Intuition
นี่คือส่วนสำคัญที่สุดของทั้งหน้า อ่านช้า ๆ
เริ่มด้วยตัวชี้สองตัว — ตัวหนึ่งที่เส้นซ้ายสุด (index 0) อีกตัวที่เส้นขวาสุด (index n−1) นี่คือความกว้างมากที่สุดที่เป็นไปได้
คำนวณพื้นที่แล้ว ถามต่อ: ควรขยับ left หรือ right?
ถ้าขยับตัวชี้เข้ามาข้างใน ความกว้างจะลดลงเสมอ ดังนั้นถ้าอยากให้พื้นที่มีโอกาสโต ความสูงต้องเพิ่ม — แล้วเราควรขยับตัวไหน เพื่อให้ความสูงมีโอกาสเพิ่ม?
The Proof — ทำไมต้องขยับฝั่งเตี้ยเสมอ
สมมติเส้นซ้ายสูง 3 เส้นขวาสูง 8 พื้นที่ตอนนี้ถูกจำกัดที่ 3 (ฝั่งที่เตี้ยกว่า)
| ถ้าขยับ RIGHT (สูง 8) เข้ามา | |
|---|---|
| ความกว้าง | ลดลงเสมอ — เพราะขยับเข้ามา |
| ความสูงของเส้นขวาใหม่ | อะไรก็ได้ — แต่พื้นที่ยังถูกจำกัดด้วยเส้นซ้ายที่สูง 3 |
| พื้นที่โตได้ไหม? | ไม่ได้ — กว้างลด ความสูงยังติดเพดานที่ 3 มีแต่แย่ลงหรือเท่าเดิม |
| ถ้าขยับ LEFT (สูง 3) เข้ามา | |
|---|---|
| ความกว้าง | ลดลงเสมอ — เพราะขยับเข้ามา |
| ความสูงของเส้นซ้ายใหม่ | อาจมากกว่า 3 — นี่คือโอกาสเดียวที่พื้นที่อาจโต |
| พื้นที่โตได้ไหม? | ได้ — ถ้าความสูงใหม่มากพอจนชดเชยความกว้างที่เสียไป |
ขยับตัวชี้ที่ชี้เส้นเตี้ยกว่าเสมอ การขยับฝั่งสูงไม่มีทางทำให้พื้นที่โต การขยับฝั่งเตี้ยคือทางเดียวที่มี chance ได้พื้นที่ใหญ่ขึ้น — อัลกอริทึมทั้งก้อนคือ insight นี้ โค้ดและลูปเป็นแค่การ implement
นี่คือทั้งอัลกอริทึม ของอื่น ๆ — โค้ด ลูป — เป็นแค่การลงมือทำ insight เดียวนี้
พิสูจน์แบบตัดคู่ทิ้ง (แบบที่ interviewer ชอบถาม)
สมมติ height[left] < height[right] เราจะขยับ left เข้ามา แปลว่าทิ้งคู่ทั้งหมดที่ยังใช้เส้น left ตัวนี้เป็นผนังกับเส้นอื่นที่แคบกว่า right
คู่ปัจจุบัน: area = height[left] × (right − left)
พิจารณาคู่ (left, j) ใด ๆ ที่ left < j < right:
area' = min(height[left], height[j]) × (j − left)
กรณีที่ 1 — height[j] ≥ height[left]:
area' = height[left] × (j − left)
< height[left] × (right − left) เพราะ j < right
→ น้อยกว่าคู่ปัจจุบัน
กรณีที่ 2 — height[j] < height[left]:
area' = height[j] × (j − left)
< height[left] × (right − left) เพราะทั้งสูงและกว้างน้อยกว่า
→ น้อยกว่าคู่ปัจจุบัน
สรุป: ไม่มีคู่ไหนที่ใช้ left เป็นผนังแล้วจับกับ j ที่แคบกว่า
จะใหญ่กว่าคู่ปัจจุบันได้ → ทิ้ง left ได้ปลอดภัยกรณี height[right] เตี้ยกว่า ก็สมมาตรกัน — ทิ้งฝั่งขวาได้ด้วยเหตุผลเดียวกัน
Step 3 — Full Dry Run
ไล่ Example 1 ให้จบทั้งก้อน
height = [1, 8, 6, 2, 5, 4, 8, 3, 7]
index = [0, 1, 2, 3, 4, 5, 6, 7, 8]
maxArea = 0| Step | left | right | h[left] | h[right] | Width | Height | Area | maxArea | Move |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 8 | 1 | 7 | 8 | 1 | 8 | 8 | left++ (1 < 7) |
| 2 | 1 | 8 | 8 | 7 | 7 | 7 | 49 | 49 | right−− (8 > 7) |
| 3 | 1 | 7 | 8 | 3 | 6 | 3 | 18 | 49 | right−− (8 > 3) |
| 4 | 1 | 6 | 8 | 8 | 5 | 8 | 40 | 49 | right−− (tie) |
| 5 | 1 | 5 | 8 | 4 | 4 | 4 | 16 | 49 | right−− (8 > 4) |
| 6 | 1 | 4 | 8 | 5 | 3 | 5 | 15 | 49 | right−− (8 > 5) |
| 7 | 1 | 3 | 8 | 2 | 2 | 2 | 4 | 49 | right−− (8 > 2) |
| 8 | 1 | 2 | 8 | 6 | 1 | 6 | 6 | 49 | right−− (8 > 6) |
| 9 | 1 | 1 | — | — | — | — | — | 49 | left >= right → STOP |
Maximum area = 49 เจอที่ Step 2: เส้น index 1 (สูง 8) กับ index 8 (สูง 7) กว้าง 7 × สูง 7 = 49
สังเกต Step 4: สองฝั่งสูงเท่ากัน (ทั้งคู่เป็น 8) ขยับข้างไหนก็ได้ เพราะความสูงยังติดเพดานที่ 8 อยู่ดี แถมความกว้างลด — พื้นที่ไม่มีทางโตขึ้นจากคู่นี้
Step 4 — The Optimal Solution
หลังสร้าง intuition แล้ว โค้ดสั้นและสะอาดมาก
🔓 Python Solutionพับไว้ด้านใน — คลิกเมื่อพร้อมดู
class Solution:
def maxArea(self, height: List[int]) -> int:
left = 0 # เริ่มที่เส้นซ้ายสุด
right = len(height) - 1 # เริ่มที่เส้นขวาสุด
max_water = 0 # พื้นที่มากสุดที่เคยเจอ
while left < right:
# พื้นที่ = กว้าง x สูง โดยสูงถูกจำกัดด้วยเส้นที่เตี้ยกว่า
width = right - left
h = min(height[left], height[right])
max_water = max(max_water, width * h)
# ขยับตัวชี้ที่ชี้เส้น "เตี้ยกว่า" เข้ามา
# เพราะขยับฝั่งสูงมีแต่ทำให้แคบลงโดยความสูงไม่เพิ่ม
if height[left] < height[right]:
left += 1
else:
right -= 1
return max_waterตัวชี้เริ่มที่หัวและท้ายแล้วเดินเข้าหากัน และทุกครั้งที่ขยับ เราตอบได้ว่าทำไมคู่ที่ทิ้งไปไม่มีทางดีกว่า ซึ่งตรงกับเงื่อนไขในส่วนที่ 3 ของหน้าแนวคิดพอดี · สังเกตว่าข้อนี้ไม่ต้องเรียงข้อมูลก่อน เพราะเหตุผลในการตัดทิ้งมาจากความสูงที่จำกัดพื้นที่ ไม่ได้มาจากลำดับของค่า
Step 5 — Complexity Analysis
| Metric | Brute Force | Two Pointers | Explanation |
|---|---|---|---|
| Time | O(n²) | O(n) | left เดินไปขวาอย่างเดียว, right เดินไปซ้ายอย่างเดียว — รวมไม่เกิน n ก้าว |
| Space | O(1) | O(1) | ตัวแปรไม่กี่ตัว — left, right, max_water, area |
O(n) time และ O(1) space — n = 100,000 เส้น two pointers ทำ ~100,000 ก้าว ส่วน brute force ทำ ~10 พันล้าน นั่นคือความต่างระหว่าง 0.1ms กับ 100 วินาที
Why It’s O(n) — ไล่เหตุผลทีละข้อ
- left เริ่มที่ 0 แล้วเพิ่มอย่างเดียว
- right เริ่มที่ n−1 แล้วลดอย่างเดียว
- แต่ละรอบขยับพอดีหนึ่งตัว หนึ่งช่อง
- ลูปหยุดเมื่อ left >= right
- จำนวนรอบรวม ≤ n−1 (สองตัวไม่มีทางไขว้กัน)
- งานในรอบเป็น O(1) — ลบครั้งหนึ่ง, min ครั้งหนึ่ง, คูณครั้งหนึ่ง, เทียบครั้งหนึ่ง
- รวม: O(n) time
Step 6 — Common Wrong Approaches
Wrong 1 — ขยับทั้งสองฝั่งพร้อมกันทุกก้าว
บางคนคิดว่า “บีบเข้ามาทีละข้างพร้อมกัน” — ผิด เพราะจะข้ามคู่ที่ถูกต้อง คำตอบอาจต้องขยับแค่ซ้ายหลายก้าวติดกัน
ห้ามขยับสองตัวพร้อมกัน ดูว่าฝั่งไหนเตี้ยกว่า แล้วขยับแค่ฝั่งนั้น
Wrong 2 — ขยับฝั่งสูง
พลาดที่เจอบ่อยที่สุด บางคนคิดว่า “เส้นสูงมีส่วนกับความสูงมากกว่า เลยควรเก็บไว้…” หรือสับสนไปมาว่าขยับฝั่งไหน
กลับไปอ่านพิสูจน์ใน Step 2: ขยับฝั่งสูง → กว้างลด ความสูงยังติดเพดานเส้นเตี้ยเดิม → พื้นที่ไม่มีทางโต
พิสูจน์คือ: ขยับฝั่งสูงไม่มี upside เลย ขยับฝั่งเตี้ยคือทางเดียวที่มีโอกาสได้พื้นที่ใหญ่ขึ้น
Wrong 3 — เริ่มจากเส้นสูงสุดแล้วขยายออก
บางคนหาเส้นสูงสุดก่อนแล้วค่อยหาคู่ — ไม่เวิร์ก เพราะบ่อที่ดีที่สุดไม่จำเป็นต้องใช้เส้นที่สูงที่สุดเลย พื้นที่ขึ้นกับทั้งความสูงและความกว้าง บ่อกว้างที่สูงปานกลางชนะบ่อแคบที่สูงมากได้ (ใน Example 1 เส้นสูง 8 สองเส้นได้แค่ 40 < 49)
Step 7 — Edge Cases
| Edge Case | Input | Expected | Handled? |
|---|---|---|---|
| มีแค่ 2 เส้น | [1, 1] | 1 | Yes — รอบเดียว left=0, right=1, area=1 |
| สูงเท่ากันหมด | [5, 5, 5, 5] | 15 | Yes — กว้าง 3 × สูง 5 = 15 |
| เรียงลดลง | [6, 5, 4, 3] | 9 | Yes — left อยู่ที่ 0, right ขยับเข้ามาทีละก้าว |
| สูงกลางอย่างเดียว | [1, 100, 1] | 2 | Yes — คู่หัวท้าย กว้าง 2 × สูง 1 = 2 |
| คำตอบอยู่ก้าวแรก | [8, 1, 1, 8] | 24 | Yes — ก้าวแรกจับได้เลย max_water=24 |
two pointers ตัวนี้จัดการ edge cases ทั้งหมดได้โดยไม่ต้องมี logic พิเศษแยก
Step 8 — อธิบายในสัมภาษณ์ยังไง
ข้อนี้เป็นของโปรดที่ Amazon, Google, Microsoft นี่คือลำดับที่ควรพูด
- Restate: “หาสองเส้นที่ maximize กว้าง × ความสูงที่เตี้ยกว่า”
- Brute force: “ลองทุกคู่ O(n²) ได้ แต่ช้าไป ต้องการ O(n)”
- Intuition: “พื้นที่ถูกจำกัดด้วยเส้นเตี้ย ถ้าขยับฝั่งสูง ความกว้างลดและความสูงไม่มีทางดีขึ้น → พื้นที่ไม่มีทางโต ดังนั้นต้องขยับฝั่งเตี้ยเสมอ”
- Algorithm: “เริ่มปลายทั้งสองข้างเพื่อได้กว้างสุด ขยับฝั่งเตี้ยเข้ามาทีละก้าว เก็บพื้นที่มากสุดตลอดทาง”
- Code: เขียน ~15 บรรทัด
- Complexity: “O(n) time, O(1) space — แต่ละตัวชี้ขยับได้ไม่เกิน n ครั้ง”
- Edge cases: ยาว 2, สูงเท่ากันหมด, คำตอบอยู่ก้าวแรก
อยากเห็นว่าคุณพิสูจน์ได้ว่าทำไม greedy choice (ขยับฝั่งเตี้ย) ถึงถูก — พิสูจน์นั่นแหละ ไม่ใช่โค้ด ที่แยกคำตอบธรรมดาออกจากคำตอบที่ดี
ข้อนี้ต่างจาก Two Sum II ยังไง
| Two Sum II (LC 167) | Container With Most Water (LC 11) | |
|---|---|---|
| เป้าหมาย | หาคู่ที่บวกกันได้ target | หาคู่ที่พื้นที่มากสุด |
| เงื่อนไขหยุด | หยุดเมื่อ sum == target | ห้ามหยุดกลางคัน — ต้องไล่จน left ชน right |
| ตัดสินใจขยับ | ดูจาก sum เทียบ target | ขยับฝั่งเตี้ยเสมอ |
| ต้องเรียงก่อนไหม | ต้อง — บังคับเรียงแล้ว | ไม่ต้อง — ใช้กับ array ใดก็ได้ |
| ผลลัพธ์ | index ของคู่ | ค่าพื้นที่สูงสุด |
ใน Two Sum II เจอคำตอบแล้วหยุดได้เลย ใน Container With Most Water ห้ามหยุดกลางคัน — ต้องวิ่งจน left >= right เพราะค่าสูงสุดอาจอยู่ก้าวไหนก็ได้
Final Solution at a Glance
def max_area(height: list[int]) -> int:
left, right, max_water = 0, len(height) - 1, 0
while left < right:
area = (right - left) * min(height[left], height[right])
max_water = max(max_water, area)
if height[left] < height[right]:
left += 1
else:
right -= 1
return max_water| Time | Space | LeetCode | Difficulty |
|---|---|---|---|
| O(n) | O(1) | Accepted ✓ | Medium |
Wrapping Up — จำสามอย่างนี้พอ
ข้อนี้เป็นโจทย์ที่ “อ๋อ” ดังมาก พอเข้าใจว่าพื้นที่ถูกจำกัดด้วยเส้นเตี้ย และการขยับฝั่งสูงจึงเป็นการเสียของฟรี — เฉลย two pointers ก็ชัดขึ้นทันที
- Area = กว้าง × min(height[left], height[right])
- ขยับตัวชี้ที่ชี้เส้นเตี้ยกว่าเข้ามาเสมอ
- อย่าหยุดกลางคัน — วิ่งจน left ชน right เพราะค่าสูงสุดอาจอยู่ก้าวไหนก็ได้
สามข้อนี้บวกพิสูจน์ว่าทำไมมันเวิร์ก คือสิ่งที่ต้องพกเข้าห้องสัมภาษณ์
พี่น้องที่ใช้ opposite-ends คล้ายกัน: LC42 Trapping Rain Water (คนละโจทย์ — รวมน้ำระหว่างแท่งหลายอัน), LC167 Two Sum II (เรียงแล้ว ตัดสินใจขยับจากผลรวม), LC15 3Sum