นากแม่น้ำสีมิ้นต์ต้นฉบับกำลังลากเส้นน้ำสองสีจากขอบคนละด้านขึ้นเนินจนพื้นที่ที่ไปถึงซ้อนกัน

LeetCode #417 Pacific Atlantic: เดินน้ำย้อนจากสองมหาสมุทร

พี่โชว์Engineering

Pacific Atlantic Water Flow ให้ตารางความสูง น้ำไหลจากช่องหนึ่งไปเพื่อนบ้านที่สูงไม่กว่าเดิม ขอบบน/ซ้ายติด Pacific และขอบล่าง/ขวาติด Atlantic ต้องคืนทุกช่องที่น้ำไหลถึงทั้งสองฝั่งครับ

ถ้าเริ่มปล่อยน้ำจากทุกช่อง เราทำงานเดิมซ้ำมาก เคล็ดลับคือกลับทิศ: เริ่มจากทะเลแล้วเดิน “ขึ้นเขา” ไปยังช่องที่สูงเท่ากันหรือสูงกว่า ช่องที่ทั้งสองทะเลเดินย้อนมาถึงคือคำตอบ

วิธีที่ 1: DFS/BFS จากทุก cell

สำหรับแต่ละช่อง เดินตามทิศน้ำจนรู้ว่าถึงขอบใด เวลา worst case O((mn)²) และมี visited ต่อจุด เหมาะแค่ทำ oracle บนตารางเล็กเพื่อทดสอบวิธีเร็ว

วิธีที่ 2: Reverse multi-source BFS สองรอบ — ค่าเริ่มต้นที่แนะนำ

ใส่ขอบ Pacific ทั้งหมดเป็น source ชุดแรก และขอบ Atlantic เป็น source ชุดที่สอง เดินไปเพื่อนบ้านที่สูงไม่น้อยกว่าช่องปัจจุบัน แต่ละรอบแตะ cell/edge คงที่ จึงเวลา O(mn), พื้นที่ O(mn) แล้ว intersect visited สองชุด ไม่แก้ input และไม่เสี่ยง recursion depth

วิธีที่ 3: DFS สองรอบ

แนวคิดเดียวกับ BFS แต่ใช้ recursion โค้ดสั้นและเหมาะภาษา/constraints ที่ stack เพียงพอ ใน Python ตารางยาวแคบอาจชน recursion limit จึงควรเลือก iterative BFS เป็น default

วิธีที่ 4: Frontier เดียวพร้อม bitmask

เก็บบิตว่า cell ถูกถึงจากทะเลใด แล้วขยาย state ใน queue เดียว ลดจำนวนโครงสร้างบางส่วน แต่ logic การ dedupe ซับซ้อนขึ้นและไม่ได้เปลี่ยน O(mn) เหมาะเมื่อ memory layout สำคัญและมี benchmark รองรับ

เงื่อนไขเลือกเหตุผล
ตารางทั่วไปReverse BFS สองรอบชัดและ O(mn)
ตารางเล็กมาก/ทำ oracleเดินจากทุก cellตรงนิยาม ตรวจวิธีเร็วได้
ภาษา stack ใหญ่Reverse DFSโค้ดกระชับ
memory เข้มงวดbitmask ต่อ cellรวมสถานะสองชุด
input มาเป็น streamต้องเก็บอย่างน้อย frontier/แถวการเชื่อมสี่ทิศย้อนกลับต้องเห็นเพื่อนบ้าน
แถว/คอลัมน์เรียงอาจ prune ได้เฉพาะกรณีconstraints ไม่รับประกัน
อนุญาตแก้ inputทำ marker ใน grid ได้แต่ทำให้ API มี side effect ไม่จำเป็น
from collections import deque
import unittest


def pacific_atlantic(heights):
    if not heights or not heights[0]:
        return []
    rows, cols = len(heights), len(heights[0])

    def reachable(starts):
        seen = set(starts)
        queue = deque(seen)
        while queue:
            row, col = queue.popleft()
            for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
                nr, nc = row + dr, col + dc
                if not (0 <= nr < rows and 0 <= nc < cols):
                    continue
                if (nr, nc) in seen or heights[nr][nc] < heights[row][col]:
                    continue
                seen.add((nr, nc))
                queue.append((nr, nc))
        return seen

    pacific = [(0, col) for col in range(cols)] + [(row, 0) for row in range(rows)]
    atlantic = [(rows - 1, col) for col in range(cols)] + [(row, cols - 1) for row in range(rows)]
    both = reachable(pacific) & reachable(atlantic)
    return [list(cell) for cell in sorted(both)]


class WaterFlowTests(unittest.TestCase):
    def test_classic_grid(self):
        grid = [
            [1, 2, 2, 3, 5],
            [3, 2, 3, 4, 4],
            [2, 4, 5, 3, 1],
            [6, 7, 1, 4, 5],
            [5, 1, 1, 2, 4],
        ]
        expected = [[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]]
        self.assertEqual(pacific_atlantic(grid), expected)

    def test_single_cell_reaches_both(self):
        self.assertEqual(pacific_atlantic([[7]]), [[0, 0]])

    def test_empty(self):
        self.assertEqual(pacific_atlantic([]), [])


if __name__ == "__main__":
    unittest.main()

Edge cases และ trade-off

ขอบมุมถูกใส่ซ้ำได้แต่ set จัดการให้ ตารางค่าเท่ากันทั้งหมดจะ reachable ทั้งหมด เงื่อนไขย้อนทิศต้องเป็น neighbor >= current; เขียนกลับด้านเพียงตัวเดียวผลจะผิดเงียบ ๆ การ sort ผลในตัวอย่างมีเพื่อให้ test deterministic ไม่จำเป็นต่อ LeetCode

Recap

  • กลับโจทย์จาก “น้ำไปทะเล” เป็น “ทะเลเดินขึ้นเขา”
  • ใช้ multi-source จากขอบทั้งสองชุด
  • intersect visited เพื่อหาคำตอบ
  • ทดสอบ 1×1, ค่าเท่ากัน และตารางว่างในโค้ด reusable
  • ค่าเริ่มต้นภายใต้ constraints ของโจทย์: reverse BFS สองรอบ