Back to Blog
알고리즘재귀귀납적사고하노이탑

0x0B. 재귀 - 함수의 재귀적 호출

재귀 함수의 개념과 귀납적 사고, 거듭제곱/하노이탑/Z 문제 풀이를 정리한다.

0x0B 재귀


0x00 알고리즘 설명

재귀 : 하나의 함수에서 자기 자신을 다시 호출해 작업을 수행하는 알고리즘

수학적 귀납법

수학적 귀납법

도미노를 예로 들면, "1번 도미노가 쓰러진다.", "k번 도미노가 쓰러지면 k+1번 도미노도 쓰러진다"가 참이니까 모든 도미노가 쓰러진다는 것이다. 즉 지금까지 당연하게 생각하던 절차지향적인 사고를 탈피해야 한다.

절차지향적 사고

절차지향적 사고

귀납적 사고

귀납적 사고

재귀 함수의 조건 : 반드시 특정 입력(base condition or base case)에 대해서는 자기 자신을 호출하지 않고 종료되어야 한다. 모든 입력은 base condition으로 수렴해야 한다.

재귀의 대한 정보

  1. 함수의 인자로 어떤 것을 받고 어디까지 계산한 후 자기 자신에게 넘겨줄지 명확하게 정해야 함
  2. 모든 재귀 함수는 반복문만으로 동일한 동작을 하는 함수를 만들 수 있음
  3. 재귀는 반복문으로 구현했을 때에 배해 코드가 간결하지만 메모리/시간에서는 손해를 봄
  4. 한 함수가 자기 자신을 여러 번 호출하게 되면 비효율적일 수 있음

EX_ 피보나치 :

피보나치

이미 계산한걸 또 계산하는 것이 빈번하다. 시간복잡도 O(1.618^n)로 n에 대한 지수함수 만큼의 시간이 걸림

  1. 재귀함수가 자기 자신을 부를 때 스택 영역에 계속 누적이 됨

→ 일부 컴파일 환경에서는 스택 영역의 메모리가 별도로 1MB로 제한되어 있기도 한다. 채점 사이트에서도 swexpertacademy.com에는 제한이 걸려있다. 따라서 스택 메모리가 작게 제한된 곳에서 문제를 푸는데 풀이가 재귀를 깊게 들어가는 풀이라면 어쩔 수 없이 재귀 대신 반복문으로 문제를 풀어야한다. 스택 메모리에는 지역 변수도 들어간다.


0x01 연습문제1 - 거듭제곱

BOJ 1629번 : 곱셈

#include <bits/stdc++.h>
using namespace std;

using ll = long long;
ll pow(ll a, ll b ,ll c) {
    if(b == 1) return a % c; // base condition
    ll val = pow(a, b/2, c);
    val = val * val % c;
    if(b % 2 == 0) return val;
    return val * a % c;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);

    int a, b, c;
    cin >> a >> b >> c;
    cout << pow(a, b, c) << "\n";
}

시간복잡도 : O(log b)

→ base condition을 잘 잡아뒀고 k승의 결과를 토대로 2k, 2k+1승의 결과를 계산할 수 있으니 마치 도미노를 쓰러트리는 것처럼 모든 결과가 잘 계산될 것이다로 함수를 이해하는 귀납적인 사고!


0x02 연습문제2 - 하노이 탑

BOJ 11729번 : 하노이 탑 이동 순서

  1. n-1개의 원판을 기둥 1에서 기둥2로 옮긴다.
  2. n번 원판을 기둥 1에서 기둥 3으로 옮긴다.
  3. n-1개의 원판을 기둥 2에서 기둥 3으로 옮긴다.

→ 원판이 n-1개일 때 옮길 수 있으면 원판이 n개일 때에도 옮길 수 있다.

원판이 1개일 때 원판을 내가 원하는 곳으로 옮길 수 있다. 원판이 k일 때 옮길 수 있으면 원판이 k+1개일 때도 옮길 수 있다.

  1. 함수의 정의 void func(int a, int b, int n) : 원판 n개를 a번 기둥에서 b번 기둥으로 옮기는 방법을 출력하는 함수
  2. base condition m = 1일 때 cout << a << " " << b << "\n";
  3. 재귀 식 n-1개의 원판을 기둥 a에서 기둥 6-a-b로 옮긴다. → func(a, 6-a-b, n-1); n번 원판을 기둥 a에서 기둥 b로 옮긴다. → cout << a << " " << b << "\n"; n-1개의 원판을 기둥 6-a-b에서 기둥 b로 옮긴다. → func(6-a-b, b, n-1);
#include <bits/stdc++.h>
using namespace std;

void hanoi(int a, int b, int n) {
    if(n == 1) {
        cout << a << " " << b << "\n";
        return;
    }
    hanoi(a, 6-a-b, n-1);
    cout << a << " " << b << "\n";
    hanoi(6-a-b,  b, n-1);
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);

    int n;
    cin >> n;
    cout << (1<<n)-1 << "\n";
    hanoi(1, 3, n);
}

<<는 left shift, 따라서 2^n-1로 옮긴 횟수를 의미


0x03 연습문제3 - Z

BOJ 1074번 : Z

Z 문제
  1. 함수의 정의 void func(int n, int r, int c)
  2. base condition n = 0일 때 return 0;
  3. 재귀 식 (r,c)가 1번 사각형일 때 → return func(n-1, r, c); (r,c)가 2번 사각형일 때 → return half x half + func(n-1, r, c-half); (r,c)가 3번 사각형일 때 → return 2 x half x half + func(n-1, r-half, c); (r,c)가 4번 사각형일 때 → return 3 x half x half + func(n-1, r-half, c-half);
#include <bits/stdc++.h>
using namespace std;

int func(int n, int r, int c) {
    if(n == 0) return 0; // base condition

    int half = 1<<(n-1);
    if(r < half && c < half) return func(n-1, r, c); // 1번 사각형
    else if(r < half && c >= half) return half*half + func(n-1, r, c-half); // 2번 사각형
    else if(r >= half && c < half) return 2*half*half + func(n-1, r-half, c); // 3번 사각형
    else return 3*half*half + func(n-1, r-half, c-half); // 4번 사각형
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);

    int n, r, c;
    cin >> n >> r >> c;
    cout << func(n, r ,c) << "\n";
}

문제 풀이

이번 강의에 해당하는 문제는 총 10개였다.

연습문제는 강의를 들으면서 잠시 멈춰놓고 해결하였고, 나머지 체크표시있는 기본문제, 응용문제를 풀어보면서 재귀와 귀납적 사고를 길러보려고 한다.

Note_ 1780번 종이의 개수, 2630번 색종이 만들기, 1992번 쿼드트리 : 이 기본문제 3문제는 비슷한 유형의 문제였다. 첫번째 문제를 풀때는 오래걸렸지만 2,3번째 문제는 거기서 응용이 되는 문제여서 확실히 감을 잡을 수 있었다.

  1. 함수의 정의 void func(int n, int a, int b) // n과 시작점 (a,b)
  2. base condition 3^n × 3^n의 데이터가 모두 같으면 해당하는 배열칸을 증가 후 return
  3. 재귀 식
n /= 3
for(int i=0; i<3; i++)
  for(int j=0; j<3; j++)
    func(n/3, a + i*n, b + j*n)

2447번 별찍기 - 11 : 재귀함수로 별찍기란 쉽지 않았다. 그래도 이 문제까지는 어떻게든 아이디어는 떠올렸지만 구현에 있어서 제한이 있었고 코드를 참고해서 풀어낼 수 있었다.

먼저 2차원 배열을 선언하고 공백으로 초기화 ( char board[102][102] )

  1. 함수의 정의 void func(int n, int a, int b) // n과 시작점 (a,b)
  2. base condition n==1이 되면 해당하는 칸을 '*'로 변경 후 return
  3. 재귀 식
for(int i=0; i<3; i++)
  for(int j=0; j<3; j++) {
    if(i==1 && j==1) continue;
    func(n / 3, x + n / 3 * i, y + n / 3 * j);
  }
별찍기 11

2448번 별찍기 - 12 : 다음 별찍기 응용문제는 더 어려웠다 ,, 많이 고민을 해봤지만 전혀 방법이 떠오르지 않았다. 그래서 코드를 참고하며 다시 공부하고 구현해보았다.

먼저 똑같이 2차원 배열을 선언한다.

  1. 함수의 정의 void func(int n, int a, int b) // n과 삼각형의 위쪽 꼭짓점 위치 (a,b)
  2. base condition n==3이 되면 해당칸을 기준으로 높이가 3인 삼각형을 만들고 return
  3. 재귀 식 위쪽 삼각형 → func(n/2, a, b) 왼쪽아래 삼각형 → func(n/2, a + n/2, b - n/2) 오른쪽아래 삼각형 → func(n/2, a + n/2, b + n/2)
별찍기 12

리뷰

재귀함수는 많이 접해보긴 했지만 절차지향적 사고로 문제를 풀어왔고, 익숙하지 않았다.

당연히 재귀함수 코드를 보면 절차지향적으로 생각하여 코드이해에 많은 시간을 뺏겼고 재귀, 말만 들어도 아직 두려운 상태였다.

이렇게 재귀함수에 대해 잘 강의해주셔서 머리에 쏙쏙 들어오고 귀납적 사고로 생각하려고 많이 노력하였다.

기본문제와 응용문제를 풀어보면서 더 많은 생각을 할 수 있었고 머리가 아픈 시간들이였다 ,,ㅎ

재귀에 대해 다시 한번 다듬고, 다음은 0X0C 백트래킹으로 돌아오겠다.

출처: 바킹독님의 실전알고리즘 강의