Notes & software courses · Free to learn
Aph's Blog
On this page

ข้อ 12 · LC11 Container With Most Water 🟡

👋 อ่านฟรีทั้งหมดบน Aph's Blog — เนื้อหาภาษาไทย ทำตามทีละหน้าใน sidebar ได้เลย หากมีข้อเสนอแนะหรืออยากให้เพิ่มหัวข้อไหน บอกได้เสมอ

ดูเหมือนโจทย์เรขาคณิต แต่จริง ๆ คือ 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 ปริมาณน้ำนั้น — ห้ามเอียงภาชนะ

Example 1
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
Example 2
Input:
height = [1, 1]
Output:
1
Explanation:
มีแค่สองเส้น กว้าง = 1, สูง = min(1,1) = 1, พื้นที่ = 1
Constraints (ข้อจำกัด)
  • n == height.length
  • 2 <= n <= 10^5
  • 0 <= height[i] <= 10^4
LeetCode 11 — Container With Most Water example diagram
รูปจากโจทย์ LeetCode: เส้นแนวตั้งแทน array [1,8,6,2,5,4,8,3,7] — พื้นที่สีฟ้าคือน้ำที่กักได้มากที่สุด = 49

Understanding the Area Formula

ก่อนกระโดดไปเฉลย ต้องชัดก่อนว่าคำนวณพื้นที่ยังไง

ถ้าเลือกเส้นที่ตำแหน่ง i กับ j (โดย i < j) น้ำที่กักได้คือ

Area = width × height
     = (j − i) × min(height[i], height[j])

ความสูงใช้ min() เพราะน้ำล้นออกทางเส้นที่เตี้ยกว่า
— เติมสูงกว่าเส้นเตี้ยไม่ได้
💡 Observation ที่เป็นกุญแจทั้งข้อ

พื้นที่ถูกจำกัดด้วยเส้นที่เตี้ยกว่าเสมอ — observation เดียวนี้คือทั้งหมดที่ต้องเข้าใจเพื่อเห็นว่าทำไม two pointers ถึงเวิร์ก

Step 1 — Brute Force

เริ่มจากวิธีตรง ๆ ตามปกติ เช็คทุกคู่เส้นที่เป็นไปได้ แล้วเก็บค่ามากสุด

Brute Force — O(n²) time, O(1) spacepython
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]))
Output
49
MetricBrute ForceImpact
Time ComplexityO(n²)ลูปซ้อนสองชั้น — n = 10,000 ≈ 100 ล้านครั้ง
Space ComplexityO(1)โอเค ไม่มีโครงสร้างเพิ่ม
LeetCode ResultTLEเคสใหญ่รันไม่ทัน — 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 — นี่คือโอกาสเดียวที่พื้นที่อาจโต
พื้นที่โตได้ไหม?ได้ — ถ้าความสูงใหม่มากพอจนชดเชยความกว้างที่เสียไป
🔑 CONCLUSION

ขยับตัวชี้ที่ชี้เส้นเตี้ยกว่าเสมอ การขยับฝั่งสูงไม่มีทางทำให้พื้นที่โต การขยับฝั่งเตี้ยคือทางเดียวที่มี 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
Stepleftrighth[left]h[right]WidthHeightAreamaxAreaMove
108178188left++ (1 < 7)
21887774949right−− (8 > 7)
31783631849right−− (8 > 3)
41688584049right−− (tie)
51584441649right−− (8 > 4)
61485351549right−− (8 > 5)
7138222449right−− (8 > 2)
8128616649right−− (8 > 6)
91149left >= 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พับไว้ด้านใน — คลิกเมื่อพร้อมดู
คำตอบสำหรับวางใน LeetCode — หัวท้ายเดินเข้าหากัน · O(n) time · O(1) spacepython
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
นี่คือแบบที่ 1 ของหน้าแนวคิด

ตัวชี้เริ่มที่หัวและท้ายแล้วเดินเข้าหากัน และทุกครั้งที่ขยับ เราตอบได้ว่าทำไมคู่ที่ทิ้งไปไม่มีทางดีกว่า ซึ่งตรงกับเงื่อนไขในส่วนที่ 3 ของหน้าแนวคิดพอดี · สังเกตว่าข้อนี้ไม่ต้องเรียงข้อมูลก่อน เพราะเหตุผลในการตัดทิ้งมาจากความสูงที่จำกัดพื้นที่ ไม่ได้มาจากลำดับของค่า

Step 5 — Complexity Analysis

MetricBrute ForceTwo PointersExplanation
TimeO(n²)O(n)left เดินไปขวาอย่างเดียว, right เดินไปซ้ายอย่างเดียว — รวมไม่เกิน n ก้าว
SpaceO(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 CaseInputExpectedHandled?
มีแค่ 2 เส้น[1, 1]1Yes — รอบเดียว left=0, right=1, area=1
สูงเท่ากันหมด[5, 5, 5, 5]15Yes — กว้าง 3 × สูง 5 = 15
เรียงลดลง[6, 5, 4, 3]9Yes — left อยู่ที่ 0, right ขยับเข้ามาทีละก้าว
สูงกลางอย่างเดียว[1, 100, 1]2Yes — คู่หัวท้าย กว้าง 2 × สูง 1 = 2
คำตอบอยู่ก้าวแรก[8, 1, 1, 8]24Yes — ก้าวแรกจับได้เลย max_water=24
✅ ไม่ต้องเขียน special case

two pointers ตัวนี้จัดการ edge cases ทั้งหมดได้โดยไม่ต้องมี logic พิเศษแยก

Step 8 — อธิบายในสัมภาษณ์ยังไง

ข้อนี้เป็นของโปรดที่ Amazon, Google, Microsoft นี่คือลำดับที่ควรพูด

  1. Restate: “หาสองเส้นที่ maximize กว้าง × ความสูงที่เตี้ยกว่า”
  2. Brute force: “ลองทุกคู่ O(n²) ได้ แต่ช้าไป ต้องการ O(n)”
  3. Intuition: “พื้นที่ถูกจำกัดด้วยเส้นเตี้ย ถ้าขยับฝั่งสูง ความกว้างลดและความสูงไม่มีทางดีขึ้น → พื้นที่ไม่มีทางโต ดังนั้นต้องขยับฝั่งเตี้ยเสมอ”
  4. Algorithm: “เริ่มปลายทั้งสองข้างเพื่อได้กว้างสุด ขยับฝั่งเตี้ยเข้ามาทีละก้าว เก็บพื้นที่มากสุดตลอดทาง”
  5. Code: เขียน ~15 บรรทัด
  6. Complexity: “O(n) time, O(1) space — แต่ละตัวชี้ขยับได้ไม่เกิน n ครั้ง”
  7. Edge cases: ยาว 2, สูงเท่ากันหมด, คำตอบอยู่ก้าวแรก
💡 สิ่งที่ interviewer อยากเห็น

อยากเห็นว่าคุณพิสูจน์ได้ว่าทำไม 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

python
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
TimeSpaceLeetCodeDifficulty
O(n)O(1)Accepted ✓Medium

Wrapping Up — จำสามอย่างนี้พอ

ข้อนี้เป็นโจทย์ที่ “อ๋อ” ดังมาก พอเข้าใจว่าพื้นที่ถูกจำกัดด้วยเส้นเตี้ย และการขยับฝั่งสูงจึงเป็นการเสียของฟรี — เฉลย two pointers ก็ชัดขึ้นทันที

  1. Area = กว้าง × min(height[left], height[right])
  2. ขยับตัวชี้ที่ชี้เส้นเตี้ยกว่าเข้ามาเสมอ
  3. อย่าหยุดกลางคัน — วิ่งจน left ชน right เพราะค่าสูงสุดอาจอยู่ก้าวไหนก็ได้

สามข้อนี้บวกพิสูจน์ว่าทำไมมันเวิร์ก คือสิ่งที่ต้องพกเข้าห้องสัมภาษณ์

ต่อยอด

พี่น้องที่ใช้ opposite-ends คล้ายกัน: LC42 Trapping Rain Water (คนละโจทย์ — รวมน้ำระหว่างแท่งหลายอัน), LC167 Two Sum II (เรียงแล้ว ตัดสินใจขยับจากผลรวม), LC15 3Sum