On this page
ข้อ 63 · LC62 Unique Paths (นับเส้นทางเดิน) 🟡
นับจำนวน path (เส้นทาง) ของ robot บน grid ที่เดินได้แค่ลงหรือขวา ด้วย 2D DP
โจทย์ (LC62): robot (หุ่นยนต์) อยู่ที่มุมบนซ้ายของ grid (กริด) ขนาด m x n พยายามเดินไปให้ถึงมุมล่างขวาของ grid โดยแต่ละก้าวเดินได้แค่ลงหนึ่งช่องหรือขวาหนึ่งช่องเท่านั้น ให้หาว่ามี unique path (เส้นทางที่ต่างกัน) ทั้งหมดกี่แบบ
- Input:
- m = 3, n = 7
- Output:
- 28
- Input:
- m = 3, n = 2
- Output:
- 3
- Explanation:
- grid 3 rows 2 columns มีทางที่ต่างกัน 3 แบบ: ลง→ลง→ขวา, ลง→ขวา→ลง, ขวา→ลง→ลง
- Input:
- m = 1, n = 1
- Output:
- 1
- Explanation:
- อยู่ที่เป้าหมายแล้ว มีหนึ่งทางคือไม่ต้องเดิน
- 1 <= m, n <= 100
- โจทย์สร้างเทสให้คำตอบไม่เกิน 2 × 10^9 (ระบุไว้ในเนื้อโจทย์ ไม่ใช่ในช่อง constraints)
แนวทาง — ต้องใช้อะไร & คิดยังไง
เป็น 2D DP แบบ grid define (นิยาม) dp[i][j] = จำนวน path จากมุมเริ่มมาถึงช่อง (i, j) การจะมาถึงช่องนี้ได้ ก้าวสุดท้ายต้องมาจากช่องบน (i-1, j) หรือช่องซ้าย (i, j-1) เท่านั้น จำนวน path จึงเท่ากับผลบวกของสองช่องนั้น
ถ้าคิดแบบ brute force คือลองเดินทุก path ด้วย recursion (การเรียกตัวเอง) จะช้ามาก (exponential) เพราะ path ซ้ำกันเยอะ (overlapping subproblems — ปัญหาย่อยซ้ำ) การเก็บ dp[i][j] ไว้ใช้ซ้ำจึงลดเหลือ O(m·n)
- สร้าง table dp ขนาด m x n initialize (ตั้งค่าเริ่มต้น) ทุกช่องเป็น 1
- row บนสุด (i=0) และ column ซ้ายสุด (j=0) มีทางเดียวเสมอ (เดินตรงทิศเดียว) จึงเป็น 1 อยู่แล้ว
- iterate i และ j จาก 1 ขึ้นไป: dp[i][j] = dp[i-1][j] + dp[i][j-1]
- return dp[m-1][n-1] (มุมล่างขวา)
ต้องเริ่ม i และ j จาก 1 เพื่อไม่ไปทับค่า base case ที่ขอบ (ถ้าเริ่มจาก 0 จะอ่าน dp[-1] ซึ่งวนไปท้าย array ผิด) และเพราะเราเติมจากบนลงล่าง ซ้ายไปขวา ตอนคำนวณ dp[i][j] ช่องบนและช่องซ้ายจึงถูกเติมเรียบร้อยแล้ว
ไล่ทีละสเต็ป
table m = 3, n = 3 หลังเติมเต็ม (แต่ละช่องคือผลบวกของช่องบน + ช่องซ้าย):
| j=0 | j=1 | j=2 | |
|---|---|---|---|
| i=0 | 1 | 1 | 1 |
| i=1 | 1 | 2 | 3 |
| i=2 | 1 | 3 | 6 ← คำตอบ |
▶ เฉลยละเอียด (ลองเองก่อนนะ)
def unique_paths(m, n):
# dp[i][j] = จำนวนทางเดินมาถึงช่อง (i, j)
dp = [[1] * n for _ in range(m)] # แถวบนสุด/คอลัมน์ซ้ายสุด = 1 ทาง
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i - 1][j] + dp[i][j - 1] # จากบน + จากซ้าย
return dp[m - 1][n - 1]
print(unique_paths(3, 7)) # 28
print(unique_paths(3, 2)) # 328
3transition คือ dp[i][j] = dp[i-1][j] + dp[i][j-1] เพราะทุก path ที่มาถึงช่อง (i, j) ก้าวสุดท้ายต้องเป็นการเดินลงมาจากช่องบน หรือเดินขวามาจากช่องซ้าย จำนวน path ทั้งหมดจึงเป็นผลบวกของทั้งสองแหล่ง เรา initialize ทั้ง table เป็น 1 ซึ่งจัดการ base case ให้เลย: row บนสุด (i=0) และ column ซ้ายสุด (j=0) มีทางเดียวเสมอเพราะเดินได้ทิศเดียว
จุดที่ต้องระวังคือลำดับการ iterate ต้องเริ่ม i และ j จาก 1 เพื่อไม่ไปทับค่า base case ที่ขอบ และเพราะเราเติมจากบนลงล่าง ซ้ายไปขวา ตอนคำนวณ dp[i][j] ช่องบนและช่องซ้ายจึงถูกเติมเรียบร้อยแล้ว
Time O(m·n) เติมทุกช่องใน table · Space O(m·n) จาก table dp (ลดเหลือ O(n) ได้ด้วยการเก็บแค่ row เดียว)
โจทย์ grid ที่เดินได้ทิศจำกัด ให้ define dp[i][j] เป็นคำตอบเมื่อมาถึงช่องนั้น แล้วรวมค่าจากช่องที่ก้าวเข้ามาได้ (บน/ซ้าย) — เป็นพื้นฐานของโจทย์ Minimum Path Sum, Unique Paths II และอื่น ๆ