On this page
ข้อ 62 · LC790 Domino and Tromino Tiling (ปูกระเบื้องโดมิโน) 🟡
count (นับ) จำนวนวิธีปู board 2xn ด้วย domino และ tromino ด้วย DP recurrence และการ mod เลขใหญ่
โจทย์ (LC790): มี tile (กระเบื้อง) สองแบบ ได้แก่ domino (โดมิโน) ขนาด 2 x 1 และ tromino (โทรมิโน) รูปตัว L ซึ่งทั้งสองแบบ rotate (หมุน) ได้ กำหนดจำนวนเต็ม n มา ให้หาจำนวนวิธีปู board (กระดาน) ขนาด 2 x n ทั้งหมด (คำตอบอาจมีขนาดใหญ่มาก ให้ตอบเป็นเศษจากการหารด้วย 10^9 + 7) การปูสองแบบจะถือว่าต่างกันก็ต่อเมื่อมีคู่ช่องที่ติดกันแบบ 4 ทิศคู่ใดคู่หนึ่ง ที่การปูแบบหนึ่งมี tile ปิดครบทั้งคู่ แต่อีกแบบไม่ครบ
- Input:
- n = 1
- Output:
- 1
- Explanation:
- ปูโดมิโนแนวตั้งหนึ่งอันได้แบบเดียว
- Input:
- n = 3
- Output:
- 5
- Explanation:
- มีวิธีปูที่ต่างกันทั้งหมด 5 แบบ (ผสมโดมิโนและโทรมิโนในรูปแบบต่าง ๆ)
- Input:
- n = 4
- Output:
- 11
- 1 <= n <= 1000
- ตอบเป็นเศษจากการหารด้วย 10^9 + 7
คำตอบต้อง mod 1000000007 (คือ 10^9 + 7) เพราะจำนวนวิธีโตเร็วมากจนเลขใหญ่มหาศาล และต้อง mod ทุกก้าวระหว่างคำนวณ ไม่ใช่แค่ตอนท้าย
แนวทาง — ต้องใช้อะไร & คิดยังไง
โจทย์นี้เป็น DP 1 มิติ (state = จำนวน column ที่ปูเสร็จ) แต่ transition หายากด้วยการมองตรง ๆ เพราะการวาง tromino ทำให้เกิดขอบหยัก ต้อง analyze (วิเคราะห์) กรณีปิด column สุดท้ายอย่างละเอียด ผลลัพธ์ยุบลงมาเป็น recurrence ลัดที่ prove (พิสูจน์) ได้: dp[n] = 2 * dp[n-1] + dp[n-3]
state คือ dp[i] = จำนวนวิธีปู board 2 x i ให้เต็มพอดี base case คือ dp[0] = 1 (board ว่างมีหนึ่งวิธีคือไม่ทำอะไร), dp[1] = 1, dp[2] = 2 ในทางปฏิบัติเรามักจำ recurrence นี้ไว้เลยเพราะ proof (การพิสูจน์) เต็ม ๆ ค่อนข้างยาว
- define MOD = 10**9 + 7
- ดัก n <= 2 return n ตรง ๆ (n=1 → 1, n=2 → 2)
- initialize dp ยาว n+1 ตั้ง dp[0], dp[1], dp[2] = 1, 1, 2
- iterate i จาก 3 ถึง n: dp[i] = (2 * dp[i-1] + dp[i-3]) % MOD
- return dp[n]
ลืมใส่ mod ทุกก้าว ถ้า mod แค่ตอนท้าย เลขระหว่างทางจะใหญ่มาก (ในภาษาอื่นจะ overflow ส่วน Python จะช้าเพราะเลขยักษ์) และต้องตั้ง base case ให้ครบสามตัว (dp[0], dp[1], dp[2]) เพราะ recurrence อ้างถึง dp[n-3]
ไล่ทีละสเต็ป
ไล่จนถึง n = 4 ด้วย recurrence dp[i] = 2*dp[i-1] + dp[i-3]:
| i | recurrence | dp[i] |
|---|---|---|
| 0 | base | 1 |
| 1 | base | 1 |
| 2 | base | 2 |
| 3 | 2*dp[2] + dp[0] = 2*2 + 1 | 5 |
| 4 | 2*dp[3] + dp[1] = 2*5 + 1 | 11 |
▶ เฉลยละเอียด (ลองเองก่อนนะ)
def num_tilings(n):
MOD = 10**9 + 7
if n <= 2:
return n # n=1 -> 1, n=2 -> 2
dp = [0] * (n + 1)
dp[0], dp[1], dp[2] = 1, 1, 2 # base case
for i in range(3, n + 1):
dp[i] = (2 * dp[i - 1] + dp[i - 3]) % MOD
return dp[n]
print(num_tilings(3)) # 5
print(num_tilings(4)) # 11
print(num_tilings(30)) # 3123421825
11
312342182การหา transition dp[n] = 2*dp[n-1] + dp[n-3] มาจากการวิเคราะห์ว่าตอนปิดคอลัมน์สุดท้ายทำได้กี่แบบ: วางโดมิโนแนวตั้งหนึ่งอันปิดคอลัมน์เดียว (ต่อจาก dp[n-1]), วางโดมิโนแนวนอนสองอันปิดสองคอลัมน์ (ต่อจาก dp[n-2]), หรือวางโทรมิโนที่ทำให้เกิดขอบหยัก การรวมกรณีขอบหยักทั้งหมดยุบลงมาได้เป็นสูตรกระชับข้างต้น ในการแก้โจทย์จริง เรามักจำสูตรนี้ไว้เลย
จุดที่พลาดบ่อยคือลืมใส่ mod ทุกก้าว ถ้าใส่แค่ตอนท้าย เลขระหว่างทางจะใหญ่มาก และต้องตั้ง base case ให้ครบสามตัว (dp[0], dp[1], dp[2]) เพราะสูตรอ้างถึง dp[n-3] ถ้าตั้งไม่ครบ ตอน i = 3 จะไปอ่าน dp[0] ที่ต้องเป็น 1 ไม่ใช่ 0
Time O(n) ไล่ตารางรอบเดียว · Space O(n) จากลิสต์ dp (ลดเหลือ O(1) ได้ด้วยการเก็บแค่สามค่าล่าสุด)
โจทย์นับวิธี (counting) มักมี transition เป็นผลบวกของหลายกรณีการปิดสถานะสุดท้าย และเมื่อจำนวนโตเร็วโจทย์จะให้ mod ด้วยจำนวนเฉพาะใหญ่ — จำไว้ว่าต้อง mod ทุกก้าวเสมอ ไม่ใช่แค่ตอนคืนค่า