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

ข้อ 58 · LC216 Combination Sum III (ผลรวมชุดค่า III) 🟡

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

หาทุก combination ของเลข k ตัวจาก 1-9 ที่ไม่ซ้ำและ sum (รวม) ได้ n ด้วย backtracking พร้อม pruning (ตัดกิ่ง)

โจทย์ (LC216): กำหนดจำนวนเต็ม k กับ n มา ให้หาทุก combination ที่ใช้ได้ของเลขจำนวนเต็ม k ตัว ซึ่ง sum แล้วได้เท่ากับ n โดยมีเงื่อนไขว่าใช้ได้เฉพาะเลข 1 ถึง 9 เท่านั้น และแต่ละเลขใช้ได้อย่างมากหนึ่งครั้ง ให้ return list ของทุก combination ที่ใช้ได้ (ห้ามมี combination ซ้ำกัน ลำดับใน list ไม่สำคัญ)

Example 1
Input:
k = 3, n = 7
Output:
[[1,2,4]]
Explanation:
1 + 2 + 4 = 7 และไม่มี combination อื่นที่ใช้ได้อีกแล้ว
Example 2
Input:
k = 3, n = 9
Output:
[[1,2,6],[1,3,5],[2,3,4]]
Explanation:
แต่ละชุดเรียงน้อยไปมาก sum ได้ 9 ทั้งหมด และไม่ซ้ำกัน
Example 3
Input:
k = 4, n = 1
Output:
[]
Explanation:
เลือกเลข 4 ตัวไม่ซ้ำจาก 1-9 ผลรวมน้อยที่สุดที่ทำได้คือ 1+2+3+4 = 10 ซึ่งมากกว่า 1 อยู่แล้ว จึงไม่มี combination ที่ใช้ได้
Constraints (ข้อจำกัด)
  • 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 ต่อ

  1. initialize result และ path เขียนฟังก์ชัน backtrack(start, remaining)
  2. ถ้า len(path) == k แปลว่า choose ครบจำนวนแล้ว: ถ้า remaining == 0 พอดี ให้เก็บ copy (สำเนา) ของ path ลง result แล้ว return ไม่ว่ากรณีใด
  3. iterate num จาก start ถึง 9: ถ้า num > remaining ให้ break (pruning)
  4. ไม่งั้น choose (append num) → explore (backtrack(num+1, remaining-num)) → unchoose (pop)
  5. เริ่มด้วย 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):

pathstartremainingเกิดอะไร
[]17เลือก 1
[1]26เลือก 2
[1,2]34เลือก 3 → remaining 1, ยังไม่ครบ k แต่ทางตัน
[1,2,3]41ครบ k แต่ remaining=1 ≠ 0 ทิ้ง ถอยไปลอง 4
[1,2,4]50ครบ k และ remaining=0 → เก็บ [1,2,4]
▶ เฉลยละเอียด (ลองเองก่อนนะ)
เฉลย (Python) — โค้ดนี้รันได้จริงpython
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))  # []
Output
[[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

💡 สรุป pattern

โจทย์ combination (choose ชุดโดยไม่สน order/ลำดับ) กันซ้ำด้วย parameter start ที่บังคับให้ไล่จากน้อยไปมาก และเร่งความเร็วด้วย pruning เมื่อรู้ว่าทางข้างหน้าเป็นไปไม่ได้แล้ว