] > recursive call

第 4 回 再帰処理

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

4-1. 再帰処理

同じ関数の中で自分自身の関数を呼び出すことができます。 これを再帰処理と言います。 問題を解決する際に、問題を分解し、分解した部分に同じ解法を適用できるよ うな場合があります。 その時、この再帰処理は有効です。 例えば、前々回まで取り上げていた「階乗」の計算では、 n の階乗は for 文を用いて ましたが、次のような漸化式でも書けます。

{ n! = n ( n - 1 )! 0! = 1

これを C の関数にすると次のようになります。


int factor(int n){
  if(n==0){return 1;}
  else{ return n*factor(n-1); }
}

この関数の場合、 for 文で単純に書けるので、再帰処理にする必要はありま せん。 しかし、組合せの数ではこの再帰処理が有効です。 実は、先日紹介している factor をもとに計算する組合せの数では 20C2 = 20*19 2*1 = 190 もオーバフローして計算できません。これは、内部で factor を使い、 20! > 1018 を計算しているからです。 結果は 190 という小さい値でも、途中で 20! という計算がおかしくなるほど 巨大な数を使ってしまうので、正常に計算できなくなります。 そこで、組合せの数は factor を使わずに計算する必要があります。

そこで、パスカルの三角形を使って計算する方法を考えます。 パスカルの三角形とは次のような数表のことです。

        1   1
      1   2   1
    1   3   3   1
  1   4   6   4   1
1   5  10  10   5   1

各値は上の左右の値を足したものになっています。 そして、その値は組合せの数を表しています。 一番上は 1C0 1C1 を、 二列目は 2C0 , 2C1 , 2C2 を表しています。 もともと、n 個のものから m 個のものを取り出す組合せの数は、 ある n 個の中の 1 つのものに注目して、 (1) そのものを選んだ時の組合せの数と (2)それを選ばなかった時の組合せの数 を足したものになるはずです。 つまり、それは (1)n-1 個の中から m-1 個を取り出す組合せの数と (2)n-1 個の中から m 個を取り出す組合せの数 の和になります。 式で書くと nCm = n-1 Cm-1 + n-1 Cm となります。 なお、 nC0 = 1 , nCn = 1 とします。 これを C 言語で計算させるプログラムは次の通りです。


int combination(int n, int m){
  if(m==0){ return 1; }
  else if(n==m){ return 1; }
  else{ return combination(n-1,m-1)+combination(n-1,m); }
}

このようにすると計算の途中で行われるのは足し算のみなので巨大な値は扱い ません。 従って、大きな値でも求めることができるようになります。

演習4-1

上の combination を使って、組合せの数 5C2 , 20C2 を求めなさい。

演習4-2

フィボナッチ数列は次のように定義されます。 f(20)を求めなさい。

f(0)=1 , f(1)=1 , f(n)= f(n-1)+ f(n-2)

なお、この計算では一度計算した値を何度も計算し直すので効率が悪いです。 例えば、グローバルな配列変数を用意し初期値を 0 としておいて、 0 だった らその値を計算して配列変数に代入し、 0 でなければ配列変数の値を使うよ うにすると f(40) 程度まで瞬時に求められるようになります。

また、 f(n) を求める時、f(i) が何回計算されるか、さらに、再帰処理が行わ れるかを n の関数で求めなさい。

演習4-3

アッカーマン関数は次のように定義されます。 この関数は原始帰納関数という関数のクラスでは計算できないほど値が大きく なる関数です。 この関数に対し、小さなパラメータの値を計算し、a(0,m), a(1,m), a(2,m), a(3,m) などがどのような関数になるか考えなさい。

{ a(0,m) = m+1 a(n,0) = a(n-1,1) a(n,m) = a(n-1, a(n,m-1))

演習4-4

ハノイの塔とは重ねた円盤を動かすパズルです。 大きさが一回りずつ違う円盤を大きい順に下から並べます。 そして、次のルールでその円盤の山を移動させます。

  1. 一度には一枚しか動かせない。
  2. 移動可能な場所は、現在位置を含み三箇所
  3. 小さい円盤の上に大きい円盤を置くことはできない

以下に 3 枚での例を示します。

abc
1 =
==
===
--a--



--b--



--c--
2
==
===
--a--


=
--b--



--c--
move 1 from a to b
3

===
--a--


=
--b--


==
--c--
move 2 from a to c
4

===
--a--



--b--

=
==
--c--
move 1 from b to c
5


--a--


===
--b--

=
==
--c--
move 3 from a to b
6

=
--a--


===
--b--


==
--c--
move 1 from c to a
7

=
--a--

==
===
--b--



--c--
move 2 from c to b
8


--a--
=
==
===
--b--



--c--
move 1 from a to b

この解法を再帰的に考えます。 もし、hanoi(n-1,'a','b','c') が n-1 枚の円盤を a から b へ移動する解法 を出力するとします(c はもう一つの領域)。 すると、 n 枚の円盤を a から b へ移動する解法は次のように考えられます。

  1. まず、 n の上に乗っている n-1 枚の円盤を a から c へ移動します。つ まり hanoi(n-1,'a','c','b')
  2. そして、n を a から b へ移動します。 move n from a to b
  3. 最後に c にある n-1 枚の円盤を b に移します。 hanoi(n-1,'c','b','a')

このようにすると hanoi(n,'a','b','c') の解法を出力します。 hanoi(3,'a','b','c') と hanoi(4,'a','b','c') の解法を求めなさい。


4-2. C++ の値呼び出し

関数の呼び出し方には、理論上三種類あります。

値呼び出し
呼び出された関数の中で、ローカル変数に引数の値をコピーして使う。
変数呼び出し
引数で指定した変数を、呼び出された関数は使用できる。
名前呼び出し
引数に式を書いた場合、その引数の値が必要かどうかを関数の中で判断す るため、式のまま関数に引き継ぐ。

このうち、C 言語では値呼び出ししか出来ないことは前回説明しました。 しかし、C++ 言語では変数呼び出し(C++ 用語では参照呼び出し) ができます。 関数定義の際、引数の定義で変数の前に & を付けると参照呼び出しであ ることが指定できます。 次のプログラムは C 言語ではコンパイルできませんが、 C++ ではコンパイル でき、予想通りの動作をします。


#include <stdio.h>
void swap(int &a, int &b){
  int c;
  c=a;
  a=b;
  b=c;
  return;
}
main(){
  int a=3,b=4;
  printf("%d, %d\n",a,b);
  swap(a,b);
  printf("%d, %d\n",a,b);
}

4-3. タートルグラフィックのクラス

実用的なクラスの例として、多少高級なグラフィックのパッケージを考え ます。 ここで示すタートルグラフィックというのは、次のようなコマンド群からなる ものです。

これを点から点へ線を引くことしかできない低レベルなグラフィックから実現 することを考えます。 クラス Turtle のメソッドとして次のものが考えられます。

クラスの宣言部は次のようになるはずです。


class Turtle {
private:
  double x;
  double y;
  double angle;
public:
  Turtle();
  ~Turtle();
  home(int _x, int _y);
  forward(double n);
  turn(double d);
  skip(double n);
};

このうち、コンストラクタ Turtle::Turtle() はグラフィックの初期化、デス トラクタ Turtle::~Turtle() はグラフィックの後処理をします。

演習4-5

以下の空欄を埋めて Turtle クラスのメソッドを実装しなさい。


#include <math.h>
#include グラフィック 
// 次の節で Windows 用に改造します。
void Turtle::home(int _x, int _y){
   // x, y に値を代入
}
void Turtle::forward(double n){
  double newx, newy;
  // newx, newy を計算
  line(x,y,newx,newy);
  // x,y に newx, newy を代入
}
void Turtle::turn(double n);
  // angle に n を足す(ラジアンに直す)
}
void Turtle::skip(double n){
  double newx, newy;
  // newx, newy を計算
  // x,y に newx, newy を代入
}

4-4. Windows のプログラミング

オブジェクト指向言語である Smalltalk で導入された GUI(グラフィックユー ザインタフェース) を Apple 社は真似て Lisa や Macintosh を作りました。 さらに Microsoft 社は Macintosh を真似て Windows を作ったため、 Windows の UIもオブジェクト指向になっています。 しかし、内部での処理はオブジェクト指向になってなく、次のようなプログラ ミング技術(イベントドリブン)を使っています。 Windows のアプリケーションソフトは、常に動き続けているわけではありませ ん。 Windows が必要に応じて送ってくる「イベント」を処理しては終了するという ことを繰り返します。 ユーザは画面上のオブジェクトにマウスやキーボードを使ってメッセージを送 りますが、これを Windows はイベントとして処理をし、アプリケーションに 送ります。

一方、 Windows のソフトを作る際、ウィンドウのデザインなどはパラメータを指定し て作ります。 そのため、リソースファイルというファイルを作り、リソースコンパイラで別 にコンパイルしてリンクします。

このため、Windows のソフトは次の形になります。


#include <Windows>
int WINAPI WinMain(引数){
  リソースの指定と本当のアプリケーションの関数を登録;
  終了;
}
void 本当のアプリケーション(イベント)
{
  switch(イベント){
  case メッセージ1:{
    メッセージ1 が来た時の処理;
    終了;
  }
  case メッセージ2:{
    メッセージ2 が来た時の処理;
    終了;
  }
  ...
  }
}

Windows のイベントは UINT 型です。今回は次の 3 つのイベントの処理を説 明します。

WM_INITDIALOG
一番最初に呼び出される時に受けるイベント。ここでウィンドウを作った り、変数を初期化したりする。
WM_PAINT
画面を表示しなければならなくなった時(ウィンドウが開いたとか、上に 乗っているウィンドウが無くなったとか)受けるイベント。 ここに表示したい画面を書くプログラムを置く。
WM_COMMAND
ボタンを押されるなど、ユーザからの指示により発生するイベント。 イベントの内容はさらに引数で与えられる。 特に、 IDCANCEL はウィンドウの閉じるボタン(X)で発生するイベントであり、 アプリケーションの終了操作を行わねばならない。

さて、前節で説明したタートルグラフィックを Windows で実現するには、今 説明した Windows のプログラミングに従い、その上で Turtle クラスを結合 します。 そして、実際にグラフィックを表示させる部分は WM_PAINT イベントの処理で 書きます。 なお、Windows ではウィンドウやグラフィックの対象などいろいろな資源が ハンドルと呼ばれる一種の変数により参照されます。 また、現在の点という概念がタートルグラフィックとは別に存在しており、 MoveToEX という現在点を移動させる関数と LineTo という新しい点まで線を 引く関数が用意されています。 以下に四角を画面に表示するプログラムを示します。HWND や POINT などは Windows で管理しなければならないウィンドウや点を示します。


//turtle.h
#include <Windows.h>
class Turtle {
private:
  HWND hwnd;
  PAINTSTRUCT ps;
  POINT sp;
  double x,y;
  double angle;
public:
  Turtle(HWND);
  ~Turtle();
  void forward(double);
  void turn(double);
  void home(int,int);
  void skip(double);
};


//turtle.cpp
#include <Windows.h>
#include <cmath>
#include "turtle.h"
using std::cos;
using std::sin;
Turtle::Turtle(HWND h){
  hwnd=h;
  BeginPaint(hwnd, &ps);
}
Turtle::~Turtle(){
  EndPaint(hwnd, &ps);
}
void Turtle::forward(double n){
  double dx,dy;
  dx=n*cos(angle);
  dy=n*sin(angle);
  LineTo(ps.hdc,(int)(x+=dx),(int)(y+=dy));
}
void Turtle::turn(double dangle){
  angle += dangle * 3.141592 / 180;
}
void Turtle::home(int _x, int _y){
  x=(double)_x;
  y=(double)_y;
  angle=0.0;
  MoveToEx(ps.hdc, _x, _y, &sp);
}
void Turtle::skip(double n){
  double dx,dy;
  dx=n*cos(angle);
  dy=n*sin(angle);
  MoveToEx(ps.hdc, (int)(x+=dx), (int)(y+=dy), &sp);
}


//box.h
#define XSIZE 100
#define YSIZE 100


//box.cpp
#include <Windows.h>
#include "box.h"
#include "turtle.h"

BOOL CALLBACK box( HWND, UINT, WPARAM, LPARAM );
int x_size, y_size;

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE, LPSTR, int)
{
  DialogBox( hInstance, "DLG_DATA", HWND_DESKTOP, (DLGPROC)box ) ;
  return 0;
}

BOOL CALLBACK box( HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
  static HWND hwnd_gr;

  switch(msg){
  case WM_INITDIALOG:{
    HDC hdc = GetDC(hwnd);
    TEXTMETRIC tm;
    GetTextMetrics(hdc, &tm);
    ReleaseDC(hwnd, hdc);
    y_size = (int) (YSIZE/8.0*(float)tm.tmHeight);
    x_size = (int) (XSIZE/4.0*(float)tm.tmAveCharWidth);
    return TRUE;
  }
  case WM_PAINT:{
    Turtle t(hwnd);
    t.home(x_size/2,y_size/2);
    t.forward(10);
    t.turn(90);
    t.forward(10);
    t.turn(90);
    t.forward(10);
    t.turn(90);
    t.forward(10);
    return TRUE;
  }
  case WM_COMMAND:
    switch(LOWORD(wp)){
    case IDCANCEL:
      EndDialog( hwnd, -1);
      return TRUE;
    }
  }
  return FALSE;
}


//box.rc
#include <windows.h>
#include "box.h"

DLG_DATA DIALOG DISCARDABLE 10, 10, XSIZE, YSIZE
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Box"
FONT 10, "system"
BEGIN
END


# Makefile
TARGET = box
CXX = g++
RESC = windres
RES_INCS = 
LIBS = -lgdi32
.SUFFIXES: .ro .rc
.rc.ro:
	$(RESC) $(RES_INCS) -i $< -o $@ -O coff

$(TARGET).exe: $(TARGET).o $(TARGET).ro turtle.o
	$(CXX) -o $@ $^ $(LIBS)

turtle.o: turtle.h
$(TARGET).ro: $(TARGET).h
$(TARGET).o: $(TARGET).h

なおターゲットファイル一つだけの名前を取り出すために $< という変数 を使用しています。 これにより box.rc を参照でき、正しくリソースファイルを作ることが出来ます。

4-5. 再帰処理によるグラフィック

このタートルグラフィックと再帰処理を用いてグラフィックを描きましょう。 木のような図形を考えます。 幹を一本描き、その先から短い幹が左右に出るような図形を考えます。 この図形を描く関数を tree(int length) とします。 するとこの関数は次のような手順で描けます。

  1. length 分だけ幹を描きます。
  2. 左に曲がり、 tree(length / ○) を描く。
  3. 同じ場所で右に曲がり、 tree(length / ○) を描く。

ここで、左右にそれぞれ 30 度曲がることにします。また、長さは毎回 1/2 になることにします。 そして、上の手順にあるように左右で同じ地点から出発しなければなりません ので tree の手続きが終ったら元の位置に戻ることにします。 この時向きは始めの向きから 180 度曲がった方向を向いているものとします。 まとめると次のようになります。

  1. length が 1 より小さければ 180 度回転して終了する。
  2. length 分だけ幹を描きます。
  3. 左に 30 度曲がり、 tree(length / 2) を描く。
  4. 左に 120 度曲がり、 tree(length / 2) を描く。
  5. 左に 30 度曲がり、 length 分だけ進みます。
木を描く手順

void tree(Turtle &t, int length){
  if(length < 1){
    t.turn(180);
    return;
  }
  t.forward(length);
  t.turn(30); tree(t, length/2);
  t.turn(120); tree(t, length/2);
  t.turn(30); t.skip(length);
  return;
}

演習4-6

上の tree を完成させなさい。

演習4-7

Koch 曲線とは次のような曲線です。この曲線を描きなさい。

コッホ曲線

4-6. Java での再帰グラフィック

Java ではグラフィックを扱うのに AWT と Swing の二種類の方法があります。 AWT は古い実装で機種に依存していましたが、 Swing は機種に依存しないグ ラフィックインターフェースライブラリを実現しています。 したがって、ここでは Swing を使うことにします。 ここで、上に示した C++ のタートルグラフィックと同等の機能を実現するた めのサンプルを以下に示します。


import java.awt.*;
import java.awt.geom.*;
import javax.swing.*;
class Test {
    public static void main(String[] args){
	DrawFrame frame = new DrawFrame();
	frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	frame.show();
    }
}
class DrawFrame extends JFrame {
    public DrawFrame(){
	setTitle("Turtle Graphics");
	setSize(WIDTH, HEIGHT);
	DrawPanel panel = new DrawPanel();
	Container contentPane = getContentPane();
	contentPane.add(panel);
    }
    public static final int WIDTH = 100;
    public static final int HEIGHT = 100;
}
class DrawPanel extends JPanel {
    public void paintComponent(Graphics g){
	super.paintComponent(g);
	Turtle t = new Turtle((Graphics2D) g);
	t.forward(10);
	t.turn(90);
	t.forward(10);
	t.turn(90);
	t.forward(10);
	t.turn(90);
	t.forward(10);	
    }
}
class Turtle {
    double x,y,angle;
    Graphics2D g;
    Turtle(Graphics2D _g){
	x=(double)DrawFrame.WIDTH/2;
	y=(double)DrawFrame.HEIGHT/2;
	angle=0;
	g=_g;
    }
    public void home(double _x, double _y){
	x=_x;
	y=_y;
	angle=0;
    }
    public void forward(double n){
	double dx=n*Math.cos(angle);
	double dy=n*Math.sin(angle);
	g.draw(new Line2D.Double(x,y,x+dx,y+dy));
	x+=dx;
	y+=dy;
    }
    public void turn(double d){
	angle+=d*Math.PI/180;
    }
    public void skip(double n){
	double dx= n*Math.cos(angle);
	double dy= n*Math.sin(angle);
	x+=dx;
	y+=dy;
    }
}

このように Swing では一番外側の Window はフレームと呼ばれ、 JFrame の サブクラスのインスタンスとして生成されます。 そして、フレームの構成要素である contentPain と呼ばれる描画領域に部 品を貼り付けることで描画します。 ここでは Graphics オブジェクト一つだけ貼り付け、 Graphic オブジェクト に Line2D オブジェクトを送ることで線を描きます。

4-7. Squeak での再帰グラフィック

Squeak には MIT で非集中システムを研究するために作られた StarLogo (http://www.media.mit.edu/) と同等の機能を実現した StarSqueak というカテゴリがあります。 これは大量のタートルを発生させ並行して動くもので、蟻などの生物の群の動 きや、交通モデルなどのシミュレーションにより解析を行うものです。 Logo はもともと教育用に作られた言語で、タートルグラフィックとリスト処 理を実装したものです。

Squeak でタートルを一つ発生させるクラスと、それを初期化して、四角を描 くメソッドは以下のとおりになります。


Object subclass: #TestTurtle
	instanceVariableNames: 'aTurtle '
	classVariableNames: ''
	poolDictionaries: ''
	category: 'Practice-Practice'!

createTurtle
	"Test StarSqueakTurtle"
	| aMorph |
	aMorph ← StarSqueakMorph new openInWorld.
	aMorph makeTurtles: 1.
	aMorph turtlesDo: [ :each | aTurtle ← each ].
	aTurtle goto: 50@50;
		     heading: 0.
box: size
	"draw a box with size"
	|  |
	aTurtle penDown.
	4 timesRepeat: [
	aTurtle forward: size;
			 turnRight: 90. ].

そして木を描くには以下のようにします。 オブジェクトメソッドからそのメソッドを呼び出すには、自分自身を示す self オブジェクトにメッセージを送ります。


treeOfSize: size withDegree: aDegree
	"draw a tree of size with aDegree"
	(aDegree = 0 ) ifTrue: [ aTurtle turnRight: 180]
		       ifFalse:[
	    aTurtle penDown;
		     forward: size;
		     turnRight: 30.
            self treeOfSize: size /2 withDegree: aDegree - 1.
            aTurtle turnRight: 120.
	    self treeOfSize: size /2 withDegree: aDegree - 1.
            aTurtle turnRight: 30;
		     penUp;
		     forward:  size.]

これを WorkSpace で以下のように呼び出すことで木を描くことができます。


t ← TestTurtle new.
t createTurtle.
t treeOfSize: 32 withDegree: 5.

一方コッホ曲線は以下のようになります。


kochOfSize: size withDegree: aDegree
	"draw a Koch spline of size with aDegree"
	(aDegree > 0 ) ifFalse: [ anTurtle penDown; forward: size]
				   ifTrue:[
	aTurtle penDown. 
	self kochOfSize: size / 3 withDegree: aDegree - 1.
	aTurtle turnLeft: 60.
	self kochOfSize: size / 3 withDegree: aDegree - 1.
	aTurtle turnRight: 120.
	self kochOfSize: size / 3 withDegree: aDegree - 1.
	aTurtle turnLeft: 60.
	self kochOfSize: size / 3 withDegree: aDegree - 1.].

これを次のように呼び出します。


t ← TestTurtle new.
t createTurtle.
t kochOfSize: 32 withDegree: 5.

坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科