On this page
ข้อ 58 · LC216 Combination Sum III (ผลรวมชุดค่า III) 🟡
หาทุก combination ของเลข k ตัวจาก 1-9 ที่ไม่ซ้ำและ sum (รวม) ได้ n ด้วย backtracking พร้อม pruning (ตัดกิ่ง)
โจทย์ (LC216): กำหนดจำนวนเต็ม k กับ n มา ให้หาทุก combination ที่ใช้ได้ของเลขจำนวนเต็ม k ตัว ซึ่ง sum แล้วได้เท่ากับ n โดยมีเงื่อนไขว่าใช้ได้เฉพาะเลข 1 ถึง 9 เท่านั้น และแต่ละเลขใช้ได้อย่างมากหนึ่งครั้ง ให้ return list ของทุก combination ที่ใช้ได้ (ห้ามมี combination ซ้ำกัน ลำดับใน list ไม่สำคัญ)
- Input:
- k = 3, n = 7
- Output:
- [[1,2,4]]
- Explanation:
- 1 + 2 + 4 = 7 และไม่มี combination อื่นที่ใช้ได้อีกแล้ว
- Input:
- k = 3, n = 9
- Output:
- [[1,2,6],[1,3,5],[2,3,4]]
- Explanation:
- แต่ละชุดเรียงน้อยไปมาก sum ได้ 9 ทั้งหมด และไม่ซ้ำกัน
- Input:
- k = 4, n = 1
- Output:
- []
- Explanation:
- เลือกเลข 4 ตัวไม่ซ้ำจาก 1-9 ผลรวมน้อยที่สุดที่ทำได้คือ 1+2+3+4 = 10 ซึ่งมากกว่า 1 อยู่แล้ว จึงไม่มี combination ที่ใช้ได้
- 2 <= k <= 9
- 1 <= n <= 60
แนวทาง — ต้องใช้อะไร & คิดยังไง
ยังเป็น backtracking แต่โจทย์นี้เพิ่มเงื่อนไขสองอย่างที่ต้องคุมพร้อมกัน: count (จำนวน) ตัวต้องเป็น k พอดี และ sum ต้องเป็น n พอดี เราจึงพก remaining (ค่าที่เหลือต้องเติมให้ครบ n) และ compare (เทียบ) length ของ path กับ k
ประเด็นสำคัญคือกัน combination ซ้ำ เช่น [1,2,4] กับ [4,2,1] ต้องนับเป็นชุดเดียว วิธีที่สะอาดที่สุดคือบังคับให้ choose จากน้อยไปมากเสมอ โดยส่งค่า start บอกว่าเลขถัดไปต้องเริ่มจากตัวไหน ทำให้แต่ละ combination ออกมา sorted (เรียง) อยู่แล้ว จึงไม่มีทางได้ชุดที่เป็นการ permute (สลับลำดับ) ของกันและกัน
นอกจากนี้ยังทำ pruning (ตัดกิ่ง) ได้: ถ้าเลขที่กำลังจะ choose มากกว่า remaining แล้ว ตัวถัด ๆ ไปยิ่งใหญ่กว่า จึงหยุด loop ทันทีด้วย break ไม่ต้องเสียเวลา iterate ต่อ
- initialize result และ path เขียนฟังก์ชัน backtrack(start, remaining)
- ถ้า len(path) == k แปลว่า choose ครบจำนวนแล้ว: ถ้า remaining == 0 พอดี ให้เก็บ copy (สำเนา) ของ path ลง result แล้ว return ไม่ว่ากรณีใด
- iterate num จาก start ถึง 9: ถ้า num > remaining ให้ break (pruning)
- ไม่งั้น choose (append num) → explore (backtrack(num+1, remaining-num)) → unchoose (pop)
- เริ่มด้วย backtrack(1, n)
ต้อง compare สองเงื่อนไขพร้อมกันตอนเก็บคำตอบ: count ครบ k ตัว และ remaining เหลือ 0 พอดี ถ้าเช็คแค่ sum โดยไม่ดู count หรือกลับกัน จะได้คำตอบผิด อีกจุดคือส่ง num+1 (ไม่ใช่ start+1 หรือ num) ตอน explore เพื่อไม่ให้ choose เลขซ้ำและไล่จากน้อยไปมาก
ไล่ทีละสเต็ป
ลองไล่ k = 3, n = 7 ดูการเดินของ start และ remaining (แสดงเฉพาะ path ที่นำไปสู่คำตอบและที่ถูก prune):
| path | start | remaining | เกิดอะไร |
|---|---|---|---|
| [] | 1 | 7 | เลือก 1 |
| [1] | 2 | 6 | เลือก 2 |
| [1,2] | 3 | 4 | เลือก 3 → remaining 1, ยังไม่ครบ k แต่ทางตัน |
| [1,2,3] | 4 | 1 | ครบ k แต่ remaining=1 ≠ 0 ทิ้ง ถอยไปลอง 4 |
| [1,2,4] | 5 | 0 | ครบ k และ remaining=0 → เก็บ [1,2,4] |
▶ เฉลยละเอียด (ลองเองก่อนนะ)
def combination_sum3(k, n):
result = []
path = []
def backtrack(start, remaining):
if len(path) == k: # เลือกครบ k ตัวแล้ว
if remaining == 0: # ผลรวมพอดี = คำตอบที่ใช้ได้
result.append(path[:])
return # ครบ k แล้วไม่ว่ายังไงก็หยุด
for num in range(start, 10): # เลือกเลขจาก start ถึง 9
if num > remaining: # ตัดกิ่ง: ตัวนี้และตัวถัด ๆ ใหญ่เกิน
break
path.append(num) # choose
backtrack(num + 1, remaining - num) # explore: ตัวถัดไปเริ่มที่ num+1
path.pop() # unchoose
backtrack(1, n)
return result
print(combination_sum3(3, 7)) # [[1, 2, 4]]
print(combination_sum3(3, 9)) # [[1, 2, 6], [1, 3, 5], [2, 3, 4]]
print(combination_sum3(4, 1)) # [][[1, 2, 4]]
[[1, 2, 6], [1, 3, 5], [2, 3, 4]]
[]เรา build combination ทีละตัว โดย parameter (พารามิเตอร์) start คุมไม่ให้ย้อนไป choose เลขที่เล็กกว่าตัวล่าสุด ทำให้ combination ที่ได้ sorted จากน้อยไปมากเสมอ จึงไม่มีทาง choose เลขซ้ำและไม่ได้ชุดที่เป็นเพียงการ permute ของกันและกัน ส่วน remaining คือค่าที่ยังต้องเติมให้ครบ n เมื่อ choose เลข num เราก็ subtract (ลบ) มันออกจาก remaining แล้ว explore ต่อ
จุดที่ทำให้เร็วขึ้นคือ pruning เมื่อ num > remaining เราหยุดทั้ง loop ด้วย break ได้เลย เพราะ range sorted จากน้อยไปมาก ถ้าตัวนี้ใหญ่เกินไปแล้ว ตัวถัด ๆ ยิ่งใหญ่กว่า จึงไม่มีทางเป็นคำตอบ ถ้าเปลี่ยน break เป็น continue ก็ยังได้คำตอบถูกแต่จะช้าลงเพราะเสียเวลา iterate ตัวที่ไม่มีทางเวิร์ก
Time O(C(9,k) · k) จำนวน combination ที่เป็นไปได้มากสุดคือการ choose k ตัวจาก 9 (มีเพดานตายตัวเพราะ choose ได้แค่ 1-9) และการ copy แต่ละชุดใช้ O(k) · Space O(k) จาก depth ของ recursion และขนาด path
โจทย์ combination (choose ชุดโดยไม่สน order/ลำดับ) กันซ้ำด้วย parameter start ที่บังคับให้ไล่จากน้อยไปมาก และเร่งความเร็วด้วย pruning เมื่อรู้ว่าทางข้างหน้าเป็นไปไม่ได้แล้ว