"อัลกอริทึมที่แก้ปัญหาโดยการ ลดขนาดของปัญหา ลงจนกลายเป็นปัญหาย่อยประเภทเดียวกันที่มีขนาดเล็กลง"
ในทางโปรแกรมมิ่ง คือฟังก์ชันที่ เรียกตัวเอง (Self-call) เพื่อทำงานที่ง่ายขึ้น
เงื่อนไขที่ทำให้ฟังก์ชัน หยุดเรียกตัวเอง เพื่อป้องกันการทำงานไม่รู้จบ (Infinite Loop)
การลดขนาดปัญหาและ เรียกฟังก์ชันเดิม ด้วยพารามิเตอร์ที่เล็กลง
\(F_0 = 0\)
\(F_1 = 1\)
0, 1, 1, 2, 3, 5, 8, 13...
แผนภูมิแสดงการเรียกฟังก์ชัน (Nodes = Function Calls)
"แต่ละโหนดจะแตกกิ่งก้านตามจำนวนครั้งที่เรียกตัวเองภายในฟังก์ชัน"
สังเกต: จำนวนการเรียกจะเพิ่มขึ้นเป็นทวีคูณ (Exponential)
"Overlapping Subproblems"
การคำนวณ ปัญหาย่อยเดิมซ้ำๆ หลายครั้ง
เช่น การคำนวณ \(F_2\) ซ้ำกันหลายจุดในต้นไม้ของ \(F_7\)
\(O(2^n)\)
\(O(n)\)
Space ขึ้นอยู่กับความลึกของ Stack ที่เรียกใช้งาน
"จดบันทึกผลลัพธ์ของปัญหาย่อยลงในอาร์เรย์ (Array) หลังจากคำนวณครั้งแรก เพื่อเรียกใช้ใหม่ได้ทันที"
เปลี่ยนจาก \(O(2^n)\) เป็น \(O(n)\) !
หลักการ "First make it work, then make it fast"
ฝึกพื้นฐานการนิยามความสัมพันธ์แบบเวียนเกิด (Recurrence Relation)
เรียนรู้การใช้ Memoization เพื่อแก้ปัญหา TLE ในกรณีที่ปัญหาย่อยทับซ้อนกัน
ฝึกควบคุมจำนวนการเรียกฟังก์ชันแทนการใช้ Loop ปกติ
โจทย์: จงเขียนฟังก์ชันเวียนเกิดเพื่อหา "ผลรวมของเลขหลัก" ทั้งหมดของจำนวนเต็ม \(n\)
ตัวอย่าง: ถ้า \(n = 126\) ผลรวมคือ \(1+2+6 = 9\)
for หรือ whilesumDigits(n) = (n % 10) + sumDigits(n / 10)
จงหาค่า \(F_n\) เมื่อกำหนด \(n \le 10^6\)
ความท้าทาย: หากใช้ Naive Recursion จะติด TLE แน่นอน เพราะความซับซ้อนคือ \(O(2^n)\)
"เราต้องใช้ Memoization เพื่อลดความซับซ้อนให้เหลือ \(O(n)\)"
memo ด้วยค่า -1 ก่อนเริ่มทำงานทุกครั้ง
จงพิมพ์คำว่า "Hello World!" จำนวน \(N\) บรรทัด โดยห้ามใช้คำสั่งวนซ้ำ (Loop) ใดๆ ทั้งสิ้น
โจทย์นี้ใช้เพื่อทดสอบความเข้าใจเรื่องความลึกของการเรียก (Recursion Depth) และการทำงานของ Stack
ในภาษา C++ ความลึกของ Recursion ที่มากเกินไป (เช่น \(N > 10^6\)) อาจทำให้เกิด Stack Overflow ได้
\[\binom{n}{0} = 1\]
\[\binom{n}{n} = 1\]
จำนวนวิธีเลือกของ \(k\) ชิ้น จากทั้งหมด \(n\) ชิ้น โดยไม่สนใจลำดับ
"โครงสร้างนี้สัมพันธ์โดยตรงกับ สามเหลี่ยมปาสกาล"
"ห.ร.ม. ของ \(a\) และ \(b\) คือ ห.ร.ม. ของ \(b\) และเศษที่เหลือจากการหาร \(a\) ด้วย \(b\)"
กรณีฐาน: \(gcd(a, 0) = a\)
นี่เป็นหนึ่งในอัลกอริทึมเวียนเกิดที่มีประสิทธิภาพสูงที่สุด (\(O(\log(\min(a, b)))\))
การหาค่า \(x^n\) ในเวลา \(O(\log n)\) แทนที่จะเป็น \(O(n)\)
หาก \(n\) เป็นเลขคู่: \(x^n = (x^{n/2})^2\)
หาก \(n\) เป็นเลขคี่: \(x^n = x \times (x^{(n-1)/2})^2\)
"ใช้แนวคิด Divide and Conquer ในการลดขนาดกำลังลงครึ่งหนึ่งในทุกขั้นตอน"
คำนวณการเลือกโดยใช้ความสัมพันธ์แบบเวียนเกิดและ Memoization
ฝึกมองความสัมพันธ์ของตัวเลขในเชิงสมการเวียนเกิดเบื้องต้น
เขียนฟังก์ชัน ห.ร.ม. แบบ Recursive ตามหลักของ Euclid
โจทย์: จงคำนวณค่า \(\binom{n}{k}\) โดยใช้นิยามเวียนเกิด:
\(\binom{n}{k} = \binom{n-1}{k-1} + \binom{n-1}{k}\)
หากไม่ใช้ Memoization ฟังก์ชันจะเรียกซ้ำมหาศาลจนติด Time Limit Exceeded ได้ง่ายมาก
*หมายเหตุ: สำหรับค่า n ขนาดใหญ่มาก ควรระวังเรื่อง Integer Overflow
โจทย์: เรารู้ค่า \(R1\) และค่าเฉลี่ย \(S\) จงหาค่า \(R2\) ที่ทำให้ \(S = (R1 + R2) / 2\)
แม้จะแก้ด้วยสมการตรงๆ ได้ แต่ให้ลองวิเคราะห์ว่าคำตอบเปลี่ยนแปลงอย่างไรเมื่อค่าเฉลี่ยเพิ่มขึ้น (Recursive Thinking)
"จงเขียนฟังก์ชัน ห.ร.ม. แบบเวียนเกิด (Recursive GCD) ที่รับค่า \(a\) และ \(b\) แล้วคืนค่าตัวหารร่วมที่มากที่สุด"
Hint: อัลกอริทึมนี้ทำงานในเวลา \(O(\log n)\) โดย \(n = \min(a, b)\)
__gcd(a, b) ได้ทันทีสำหรับการแข่งขันจริง
คำนวณ \(x^n \pmod M\) สำหรับค่า \(n\) ขนาดมหาศาลโดยใช้เวลา \(O(\log n)\)
ฝึกการใช้ Bit Manipulation ร่วมกับการเวียนเกิดเพื่อสลับลำดับบิต
โจทย์: รับจำนวนเต็ม \(x, n,\) และ \(M\) จงหาค่าของ \((x^n) \pmod M\)
Constraints: \(n \le 10^{18}\) และ \(M \le 10^9\)
การวนลูป \(10^{18}\) ครั้งจะทำให้ติด TLE ทันที เราต้องลดจำนวนการคูณให้เหลือเพียง \(\approx 60\) ครั้งโดยการแบ่งครึ่งกำลังในทุกขั้นตอน
"Time Complexity: \(O(\log n)\) "
โจทย์: รับจำนวนเต็ม \(N\) ในฐานสิบ แปลงเป็นฐานสอง แล้ว "กลับลำดับบิต" จากหลังมาหน้า จากนั้นแสดงผลลัพธ์เป็นฐานสิบ
ฝึกการใช้ Bitwise Operators: &, >>, <<, |
(n & 1) เพื่อดึงบิตขวาสุด (LSB)
(ans << 1) | bit
หมายเหตุ: n >> 1 คือการเลื่อนบิตไปทางขวา (หารสอง)
"ทุกครั้งที่มีการเรียกฟังก์ชัน คอมพิวเตอร์จะเก็บข้อมูลลงใน Stack Memory หากเรียกซ้อนกันลึกเกินไป (เช่น \(10^6\) ชั้น) โปรแกรมจะแครชทันที"
ทางแก้: ตรวจสอบ Base Case ให้แม่นยำ หรือเปลี่ยนไปใช้ Iteration ในกรณีที่จำเป็น
ความซับซ้อนทางพื้นที่ (Space) ของ Recursion คือ...
ไม่ได้ขึ้นอยู่กับจำนวนโหนดทั้งหมด แต่ขึ้นอยู่กับ กิ่งที่ลึกที่สุด ใน Recursion Tree ณ ขณะหนึ่ง
• Base Case: ต้องมีและต้องไปถึงได้เสมอ
• Memoization: เครื่องมือสำคัญในการเปลี่ยน Exponential เป็น Linear
• Math Functions: \(n!\), \(F_n\), \(gcd\), และ \(\binom{n}{k}\) คือแม่แบบของการคิดแบบเวียนเกิด
• Big O: ต้องวิเคราะห์ทั้งเวลา (Time) และพื้นที่ (Stack Space)
ช่วงบ่ายเตรียมพบกับ: Complete Search & Backtracking
ฝึกสร้างปัญหาย่อยจากการตัดสินใจ "เลือก" หรือ "ไม่เลือก"
ฝึกการจัดการสถานะ (State) และการคืนค่าสถานะเมื่อค้นหาเสร็จ
ประยุกต์ใช้การลองทุกความเป็นไปได้ (Brute Force) กับปัญหาการคำนวณ
โจทย์: จงสร้างสับเซต (Subset) ทั้งหมดของเซต \(\{0, 1, \dots, n-1\}\) โดยใช้การเรียกตัวเอง (Recursion)
ตัวอย่าง: ถ้า \(n = 3\) ต้องได้ชุดข้อมูล 8 ชุด (รวมเซตว่าง)
สำหรับสมาชิกแต่ละตัว เรามี 2 ทางเลือกเสมอ: ใส่ลงในสับเซตปัจจุบัน หรือ ไม่ใส่
search(k+1) สองครั้ง (ครั้งแรกไม่ใส่ \(k\), ครั้งที่สองใส่ \(k\) เข้าไปในเวกเตอร์)
จงสร้างวิธีเรียงสับเปลี่ยน (Permutation) ทั้งหมดของตัวเลข \(1\) ถึง \(n\)
ความต่าง: จำนวนวิธีคือ \(n!\) ซึ่งมากกว่าสับเซตมากเมื่อ \(n\) เพิ่มขึ้น
"เราต้องใช้ Array เพื่อจำว่าเลขตัวไหนถูก 'เลือก' ไปแล้วในลำดับปัจจุบัน"
มีเลข 4 อยู่สี่ตัว (\(4 \dots 4 \dots 4 \dots 4\)) จงเติมเครื่องหมาย \(\{+, -, *, /\}\) ลงในช่องว่าง 3 ช่อง เพื่อให้ได้ผลลัพธ์ตามที่โจทย์กำหนด