คู่มือโอลิมปิกคอมพิวเตอร์

Computer Olympiad Guide for Thai High School Students

Recursion

แนวคิด

  • Recursion คือกระบวนการเรียกตัวเอง

  • ตามปกติแล้วแบ่งกรณีเป็น

    • base case

    • recursion case

    โดยที่จะย่อยปัญหาให้เล็กลงจนกระทั่งแก้ได้ด้วย base case

การเขียนโปรแกรม

จะเขียนกระบวนการนั้นด้วยฟังก์ชัน และการเรียกตัวเองจะเป็นการเรียกฟังก์ชันที่เขียนขึ้นมาภายในฟังก์ชันนั้น

ตัวอย่างการเขียน recursive function ในภาษา C++ โดยยังไม่คำนึงถึงการนำไปใช้แก้โจทย์ปัญหา

void recursive_function() {
    if (base condition)
        return; // base case
    else
        recursive_function(); // recursion case
}

วิเคราะห์ประสิทธิภาพ

จะใช้วิธีการนับจำนวนครั้งที่จะมีการเรียกฟังก์ชันเกิดขึ้นตามพารามิเตอร์ของฟังก์ชันนั้น

โจทย์ตัวอย่าง - Factorial

เนื้อหาโจทย์

จงหาค่าของ n! โดย n! = n × (n − 1) × (n − 2) × ⋯ × 1

แนวคิด

โจทย์นี้สามารถใช้โครงสร้าง loop ในการแก้ปัญหาได้ แต่นำมาเป็นตัวอย่างเริ่มต้นในการใช้โครงสร้างของ recursion ในการแก้ปัญหา

โดยมองค่าของ n! เป็น recursive function ดังนี้

$$f(n) = \begin{cases} 1\text{ if }n \leq 1 \\ n \times f(n - 1)\text{ else} \end{cases}$$

รหัสเทียม
#include <bits/stdc++.h>

using namespace std;

int factorial(int n) {
  if (n <= 1) { // base case
    return n;
  } else {
    return n * factorial(n - 1);
  }
}

int main() {
  cout << factorial(4);
  return 0;
}

จะได้ output

24

โจทย์ตัวอย่าง - Fibonacci

เนื้อหาโจทย์

จงหาค่าของเลขฟิโบนัชชีลำดับที่ n โดยที่ เลขฟิโบนัชชีลำดับที่ n จะมีค่าเท่ากับผลบวกของเลขฟิโบนัชชีลำดับที่ n − 1 และ n − 2

แนวคิด

โจทย์นี้สามารถใช้โครงสร้าง loop ในการแก้ปัญหาได้เช่นกัน แต่นำมาเป็นอีกตัวอย่างหนึ่งในการใช้โครงสร้างของ recursion ในการแก้ปัญหา

โดยมองค่าของเลขฟิโบนัชชีลำดับที่ n เป็น recursive function ดังนี้

$$f(n) = \begin{cases} n\text{ if }n \leq 1 \\ f(n - 1) + f(n - 2)\text{ else} \end{cases}$$

รหัสเทียม
#include <bits/stdc++.h>

using namespace std;

int fibo(int n) {
  if (n <= 1) { // base case
    return 1;
  } else {
    return fibo(n - 1) + fibo(n - 2);
  }
}

int main() {
  cout << fibo(4);
  return 0;
}

จะได้ output

3

โจทย์ตัวอย่าง - Permutation

เนื้อหาโจทย์

จงหาลำดับที่มีสมาชิก n ตัว โดยที่แต่ละสมาชิกมีค่าน้อยกว่า r ทั้งหมดที่เป็นไปได้ (จำเป็นต้องใช้ array)

แนวคิด

เป็นการค้นทุกวิธีที่เป็นไปได้ (complete search) ซึ่งจะนำ recursive function เข้ามาใช้ โดยเริ่มต้นด้วยการสร้างลำดับที่ยังไม่มีสมาชิกใด ๆ และค่อย ๆ เติมสมาชิกเข้าไปในลำดับนั้น

base case จะเป็นเงื่อนไขเมื่อลำดับมีสมาชิกครบ n

recursive case จะเป็นการเติมสมาชิกลำดับถัดไปด้วยทุกค่าที่เป็นไปได้

รหัสเทียม
#include <bits/stdc++.h>

using namespace std;

int a[10];

void perm(int d, int r, int n) {
  if (d == n) { // base case
    for (int i = 0; i < n; i++) {
      cout << a[i] << " ";
    }
    cout << "\n";
    return;
  }
  for (int i = 0; i < r; i++) {
    a[d] = i;
    perm(d + 1, r, n); // recursive case
  }
}

int main() { perm(0, 3, 3); }

จะได้ output

0 0 0
0 0 1
...
2 2 2

ปัญหาที่พบบ่อย

Overflow

กรณีไม่มี base case หรือ base case ไม่ครอบคลุม จะทำให้เกิดการ overflow ได้

#include <bits/stdc++.h>

using namespace std;

int factorial(int n) { return n * factorial(n - 1); }

int main() {
  cout << factorial(4);
  return 0;
}

จะเกิด error เช่น:

"./a.out" terminated by signal SIGSEGV (Address boundary error)

โจทย์แนะนำ

  • หาลำดับ permutation โดยมีเงื่อนไขที่แตกต่างกัน

    • จำนวน 5 หลัก และแต่ละหลักมีค่าไม่เกิน 6

    • ตัวเลขก่อนหน้าต้องน้อยกว่าหรือเท่ากับตัวถัดไป

    • ตัวเลขห้ามซ้ำ

  • กลับข้อความ (reverse string) ห้ามใช้ loop, array หรือ string

    • กลับทั้งข้อความ เช่น: “iamastring” เป็น “gnirtsamai”

    • กลับเฉพาะภายในคำที่คั่นด้วยเว้นวรรค เช่น: “i am a string” เป็น “i ma a gnirts”

  • Programming.in.th

    • 0019 Perket

    • 0039 Food

  • cses.fi

    • 2205 Gray Code

    • 2165 Tower of Hanoi

    • 1622 Creating Strings

    • 1623 Apple Division