再帰呼び出し


再帰呼び出しとは

再帰呼び出しとは、ある関数fの中で、自分自身(関数f)を呼び出すような ことを言う。 入門書では、次のような例で説明されることが多い。

#include <stdio.h>

int factorial(int n)
{
    int r;

    if (n == 0) {
        r = 1;
    } else {
        r = n * factorial(n-1);
    }

    return r;
}

int main(void)
{
    printf("%d\n", factorial(10));

    return 0;
}
factorial.c

このプログラムを実行すると、

のように入れ子状に呼び出されることになる。 次のプログラムを動かして確認してみよう。

#include <stdio.h>

int factorial(int n)
{
    int r;

    printf("enter (n=%d)\n", n);

    if (n == 0) {
        r = 1;
    } else {
        r = n * factorial(n-1);
    }

    printf("exit (n=%d, r=%d)\n", n, r);

    return r;
}

int main(void)
{
    printf("%d\n", factorial(10));

    return 0;
}
factorial2.c

ローカル変数の値が一般にスタック上に取られるのは、 再帰呼び出しを実現するためである。 単にその関数内の変数を他の部分で使われているものと区別するだけなら、 変数名を(コンパイラ内部で自動的に)何か他と衝突しない別のものに 置換するだけでよい。

ところで、この階乗を計算する関数は、言うまでも無く再帰呼び出しなど 使わなくても実現できるし、再帰を使わない方が効率が良い。 そこで、もう少し再帰を使う意味がある例を示す。

ハノイの塔

再帰アルゴリズムがぴったり過ぎる例。

n枚移動するには、
  1. n-1枚をワークスペースによけておいて、
  2. 一番下にあったn枚目を目的の場所に移動、
  3. よけておいたn-1枚を目的の場所に移動、
という手順で解ける。これをそのままプログラムする。

#include <stdio.h>

void hanoi(int from, int to, int work, int n)
{
    if (n == 1) {
        printf("move %d to %d\n", from, to);
        return;
    }
    hanoi(from, work, to, n-1);
    hanoi(from, to, work, 1);
    hanoi(work, to, from, n-1);
}

int main(int argc, char **argv)
{
    int n;

    sscanf(argv[1], "%d", &n);

    hanoi(0, 1, 2, n);

    return 0;
}
hanoi.c

こんな簡単なプログラムで解けてしまう。

hanoi2.c は、移動の様子を図示するように改造したもの。

8queenパズル

チェスのクイーンを8x8のチェス盤に8個、互いに取られないように配置する パズル。

クイーンは、

のように八方に動ける。動ける場所に他のクイーンがないような配置とは、 例えば

のようなものである。

このような配置は、全部で92通り、回転や裏返しを除くと12通り存在する。

8 queenパズルを解く

定式化

各列に一つしかクイーンを置けず、また必ず一つクイーンを おくのは明らかなので、その位置を0~7の数値で表すと、盤面の状態は 8個の数値で表せる。例えば上の盤面は、

(4 0 3 5 7 1 6 2)

という数値で表せる。これをboard[i]という配列に格納すると、 問題は、

を満たすような数字の組を求める問題と見倣せる。

解法

これをいきなり解くのは難しいので、問題を分割する。 すなわち、 のような関数群を用意したと考える。このとき、最後の関数は 簡単なものになろう。n番目の関数は、n+1番目の関数がちゃんと動くならば、 n列目を固定してn+1番目の関数を呼ぶ操作をn列目の全ての可能性について 繰り返せば良い。

再帰呼出し

上のアルゴリズムを実装するには、上記8個の関数を書けばよい。

#include <stdio.h>

int board[8];

void solve7()
{
    int bi, j, f;

    for (bi=0; bi<8; bi++) {
        f = 0;
        for (j=0; j<7; j++) {
            if (bi == board[j]) f=1;
            if (7 - j == bi - board[j]) f=1;
            if (7 - j == board[j] - bi) f=1;
        }
        if (f == 1) continue;

        board[7] = bi;

        for (j=0; j<8; j++) {
            printf("%d ", board[j]);
        }
        printf("\n");
    }
}

void solve6()
{
    int bi, j, f;

    for (bi=0; bi<8; bi++) {
        f = 0;
        for (j=0; j<6; j++) {
            if (bi == board[j]) f=1;
            if (6 - j == bi - board[j]) f=1;
            if (6 - j == board[j] - bi) f=1;
        }
        if (f == 1) continue;

        board[6] = bi;

        solve7();
    }
}

void solve5()
{
    int bi, j, f;

    for (bi=0; bi<8; bi++) {
        f = 0;
        for (j=0; j<5; j++) {
            if (bi == board[j]) f=1;
            if (5 - j == bi - board[j]) f=1;
            if (5 - j == board[j] - bi) f=1;
        }
        if (f == 1) continue;

        board[5] = bi;

        solve6();
    }
}

void solve4()
{
    int bi, j, f;

    for (bi=0; bi<8; bi++) {
        f = 0;
        for (j=0; j<4; j++) {
            if (bi == board[j]) f=1;
            if (4 - j == bi - board[j]) f=1;
            if (4 - j == board[j] - bi) f=1;
        }
        if (f == 1) continue;

        board[4] = bi;

        solve5();
    }
}

void solve3()
{
    int bi, j, f;

    for (bi=0; bi<8; bi++) {
        f = 0;
        for (j=0; j<3; j++) {
            if (bi == board[j]) f=1;
            if (3 - j == bi - board[j]) f=1;
            if (3 - j == board[j] - bi) f=1;
        }
        if (f == 1) continue;

        board[3] = bi;

        solve4();
    }
}

void solve2()
{
    int bi, j, f;

    for (bi=0; bi<8; bi++) {
        f = 0;
        for (j=0; j<2; j++) {
            if (bi == board[j]) f=1;
            if (2 - j == bi - board[j]) f=1;
            if (2 - j == board[j] - bi) f=1;
        }
        if (f == 1) continue;

        board[2] = bi;

        solve3();
    }
}

void solve1()
{
    int bi, j, f;

    for (bi=0; bi<8; bi++) {
        f = 0;
        for (j=0; j<1; j++) {
            if (bi == board[j]) f=1;
            if (1 - j == bi - board[j]) f=1;
            if (1 - j == board[j] - bi) f=1;
        }
        if (f == 1) continue;

        board[1] = bi;

        solve2();
    }
}

void solve0()
{
    int bi;

    for (bi=0; bi<8; bi++) {
        board[0] = bi;
        solve1();
    }
}

int main(void)
{
    solve0();

    return 0;
}
queen0.c

これを見るとほとんど同じような関数ばかりが並んでいることが分かる。 このような書き方ではバグが混入しやすいし、何よりも8クイーンでなく Nクイーンを解くようなプログラムを書くことが出来ない。

そこで、全て一つの関数で書いて、引数でその関数が何番目の関数かを 指定することにする。

#include <stdio.h>

#define N 8

int board[N];

void solve(int i)
{
    int bi, j, f;

    for (bi=0; bi<N; bi++) {
        f = 0;
        for (j=0; j<i; j++) {
            if (bi == board[j]) f=1;
            if (i - j == bi - board[j]) f=1;
            if (i - j == board[j] - bi) f=1;
        }
        if (f == 1) continue;

        board[i] = bi;

        if (i == N - 1) {
            for (j=0; j<N; j++) {
                printf("%d ", board[j]);
            }
            printf("\n");
        } else {
            solve(i+1);
        }
    }
}

int main(void)
{
    solve(0);

    return 0;
}
queen1.c

極めて単純になり、かつNクイーンへの拡張も容易になったことが 分かるであろう。

このようなアルゴリズムを「バックトラック法」と呼ぶ。


再帰呼び出し /