5.再帰的アルゴリズム 5.1 再帰の考え方 ■再帰的(recursive) ある事象が,自分自身を含んでいたり,それを用いて定義されて いること。 ■自然数の定義 (a)1は自然数である。 (b) ある自然数の直後の整数は自然数である。 このような定義を再帰的定義(recursive definition)といいます。 数学的帰納法は再帰的 数学的帰納法で定義されるような問題は, 再帰的となります。 例えば,階乗値を求める問題 ⅰ) f(0) = 1 ⅱ) n > 0に対して f(n) = n × f(n-1) は再帰的です。 木構造の探索も再帰のひとつ 最も簡単な2分木の探索を例にとると, ⅰ) 自ノードと等しければ探索完了 ⅱ) 左部分木を探索 ⅲ) 右部分木を探索 再帰的関数呼出 階乗の再帰版の動き fact(3) 3 6 long fact(long n) { if(n == 0) return 1; else return(n * fact( n -1)); } 2 2 long fact(long n) { if(n == 0) return 1; else return(n * fact( n -1)); } 1 1 long fact(long n) { if(n == 0) return 1; else return(n * fact( n -1)); } 0 1 long fact(long n) { if(n == 0) return 1; else return(n * fact( n -1)); } 直接的な再帰と間接的な再帰 直接的な再帰: 自分と同じ関数を呼び出す。 関数A A(・・) 関数A A(・・) 間接的な再帰: 別の関数を呼出し, その関数が自分と同じ関数を呼び出す。 関数A B(・・) 関数A A(・・) 関数B A(・・) 5.2 再帰的プログラムの動き 末尾再帰(Tail Recursion) Returnを追ってみる。 3を乗じて新しい値とする。 fact(3) 3 6 long fact(long n) { if(n == 0) return 1; else return(n * fact( n -1)); } 2 2を乗じて新しい値とする。 2 long fact(long n) { if(n == 0) return 1; else return(n * fact( n -1)); } 1 1を乗じて新しい値とする。 1 long fact(long n) { if(n == 0) return 1; else return(n * fact( n -1)); } 0 初期値を1とする。 1 long fact(long n) { if(n == 0) return 1; else return(n * fact( n -1)); } Tail Recursionをループに private long fact2(long n) { long r=1; for(long i=1;i <= n;i++) r *= i; return r; } 真に再帰的な場合 2回以上再帰呼び出しを行う関数を真に(genuinely)再帰的である といいます。 真に再帰的な関数の動きを見るために, 次のプログラムを例にとる。 private void test(int n) { if(n <= 0 ) return; test(n - 1); MessageBox.Show(n.ToString()); test(n - 2); } 真に再帰的な場合(2) Test(4)が実行された場合の表示の順序は,以下のようになる。 ⑤ Test(3);4を表示;test(2); ③ ⑦ Test(2);3を表示;test(1); ② Test(1);2を表示;test(0); ④ Test(1);2を表示;test(0); ⑥ Test(0);1を表示;test(-1); Test(0);1を表示;test(-1); ① Test(0);1を表示;test(-1); return return return return return return 2分木の場合の通りがけ順と同じような表示順序になる。 return return 再帰的プログラムのループ化 末尾再帰は,簡単にループ化できるが, 以下の下線部は再帰のまま残ってしまう。 private void test(int n) { int nn = n; while (nn > 0 ) { test(nn - 1); MessageBox.Show(nn.ToString()); nn = nn - 2; } } スタックを使ってループ化 次のようにスタックを使えば,ループ化が可能 private void test(int n) { int[] stk = new int[20]; int p = 0; int nn = n; bool flag = true; while(flag) { while (nn > 0 ) { stk[p++] = nn; nn = nn - 1; } if(p == 0) flag = true; { nn = stk[--p]; MessageBox.Show(nn.ToString()); nn = nn - 2; } } } N=4の場合 スタックの動き 表示 1 2 3 4 3 4 2 3 4 1 2 3 4 2 3 4 3 4 4 ① ② ③ ④ ⑤ ⑥ ⑦ 表示 1 4 1 2 1 4 4 ⑧ ⑨ ⑩ 2 1 2 2 ⑪ ⑫ ⑬ ⑭ 5.3 ハノイの塔 重なった円盤を3本柱の間で移動する問題(3枚の場合) (より小さい円盤が必ず上にくるように重ねる) 1 2 3 5.3 ハノイの塔 3枚の場合の手順(その1) (1) 1 2 3 1 2 3 (2) 5.3 ハノイの塔 3枚の場合の手順(その2) (3) 1 2 3 5.3 ハノイの塔 3枚の場合の手順(その3) (4) 1 2 3 1 2 3 (5) 5.3 ハノイの塔 3枚の場合の手順(その4) (6) 1 2 3 1 2 3 (7) 5.3 ハノイの塔 N枚の場合 1 2 3 2 3 (1)1から2にN-1枚を移動。 1 5.3 ハノイの塔 1から3に1枚を移動。 1 2 3 2 3 N-1枚を2から3に移動。 1 プログラム例 フォーム上に,textBox1,listBox1, button1を貼り付けます。 枚数をtextBox1で指定して,button1をクリックすれば, listBox1に手順が表示されるものとします。 移動元をA,移動先をB,残る塔をCとすると,次の表の通りとなります。 A B C A+B+C 1 2 3 6 1 3 2 6 2 3 1 6 2 1 3 6 3 1 2 6 3 2 1 6 表に示すとおり, A+B+C=6 の関係が成り立ちますから, C = 6-(A+B) とすることができます。 プログラム例 private void MoveTo(int n,int x, int y) { string S=“第”+ n.ToString() + “盤を” + x.ToString() + “軸から” + y.ToString() + "に移動"; listBox1.Items.Add(S); } private void Hanoi(int n, int x, int y) { if(n>0) // n = 0 のとき何もしない { Hanoi(n-1,x, 6-x-y); // n - 1 枚を x → 中間的な塔 MoveTo(n,x,y); // 最も大きい円盤を x → y Hanoi(n-1,6-x-y, y); // n - 1 枚を 中間的な塔 → y } } プログラム例 private void button1_Click(object sender, System.EventArgs e) { listBox1.Items.Clear(); int n = int.Parse(textBox1.Text); Hanoi(n,1,3); } 8王妃問題(8 Queen problem) 5.4 8王妃問題 8×8のチェス盤上に,8個の王妃が互いに取りあうことのないように配置する チェスの王妃 : 縦・横・斜めライン上のコマをとることができる。 (いわば,将棋の飛車と角の働きを併せ持つ) 0 1 2 0 1 2 3 4 5 6 7 ● 3 4 5 6 7 8王妃問題(8 Queen problem) [例] 8×8のチェス盤上に,8個の王妃が互いに取りあうことのないように配置する チェスの王妃 : 縦・横・斜めライン上のコマをとることができる。 (いわば,将棋の飛車と角の働きを併せ持つ) 0 1 2 3 4 5 6 7 0 ● ● 1 2 3 4 5 6 7 ● ● ● ● ● ● 方針 [1]各列に,王妃は1個だけ配置する。 [2]各行に,王妃は1個だけ配置する。 ただし,たとえば,以下のような例も組合せとして検査することになる。 0 1 2 3 4 5 6 0 7 0 ● 0 ● 1 2 3 4 1 2 3 4 5 6 7 5 6 7 1 2 3 4 5 6 7 5.4 8王妃問題 [1]各列に,王妃は1個だけ配置する。 [2]各行に,王妃は1個だけ配置する。 斜め方向限定用のフラグA 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 5.4 8王妃問題 斜め方向限定用のフラグB 7 6 5 4 3 2 1 0 8 9 10 11 12 13 14 5.4 8王妃問題 フォーム定義 コマンドボタン Name:button1 ラベル Name:label1 , label2 ,…,label8 Name:label9 , label10,…,label16 Name:label17, label18,…,label24 ・ ・ ・ Name:labe49 , label50,…,label56 Name:label57, label58,…,label64 プログラム ①データ宣言,解の表示 private private private private private private int 解番号=0; bool[] 行配置済み=new bool[8]; bool[] 対角配置済みA=new bool[15]; bool[] 対角配置済みB=new bool[15]; int[] 列位置= new int[8]; Label[,] LabelDT= new Label[8,8]; // // // // 各行に王妃が配置済みかどうかを示す 対角/に王妃が配置済みかどうかを示す 対角\に王妃が配置済みかどうかを示す 各列の王妃の位置 private void 表示() { int i,j; for(j=0;j<8;j++) { for(i=0;i<8;i++) LabelDT[j,i].Text=""; LabelDT[j,列位置[j]].Text="●"; } 解番号++; MessageBox.Show(解番号.ToString() + "番目の解表示"); } プログラム ②解の設定 private void 設定(int i) { int j; for (j =0; j<8; j++) { if(!行配置済み[j] && !対角配置済みA[i+j] && !対角配置済みB[i-j+7]) { 列位置[i]=j; if (i==7) 表示(); else { 行配置済み[j]=対角配置済みA[i+j]=対角配置済みB[i-j+7]=true; 設定(i+1); 行配置済み[j]=対角配置済みA[i+j]=対角配置済みB[i-j+7]=false; } } } } プログラム ③初期化 private void 初期化() { int i, j, X, ID;bool Flag;string Name; X=-8; for(i=0;i<8;i++) // ラベルの2次元配列化 { X +=8; for(j=0;j<8;j++) { ID=X+j+1; Name="label" + ID.ToString(); Flag=true; foreach(Control myControl in this.Controls) if(myControl.Name==Name) { Flag=false; LabelDT[i,j]=(Label) myControl; break; } if(Flag) MessageBox.Show("初期化に失敗しました"); } } // 列位置,フラグ類の初期化 for(i=0;i<8;i++) 列位置[i]=0; for(i=0;i<8;i++) 行配置済み[i]=false; for(i=0;i<15;i++) 対角配置済みA[i]= 対角配置済みB[i]=false; } プログラム ④button1_Click,Form1_Load private void button1_Click(object sender, System.EventArgs e) { 解番号=0; 設定(0); MessageBox.Show(解番号.ToString() + "個の解が求められました."); } private void Form1_Load(object sender, System.EventArgs e) { 初期化(); } 騎士巡歴(Knight‘s Tour)問題 5.5 騎士巡歴問題 N×Nのマス目上のすべてを チェスのナイトが訪れるような順序を求める問題 チェスのナイト : 桂馬方向に進むことができる (いわば,将棋の桂馬と似た働き) 0 1 0 ● 1 ● 2 3 ● 4 ● 2 3 4 ● ● y ● ● 5.5 騎士巡歴問題 騎士巡歴(Knight‘s Tour)問題 最初(1,1)の位置にいるものとします。 0 1 0 1 2 3 4 2 3 4 5.5 騎士巡歴問題 現在の位置からの騎士のジャンプ先については,以下のような表を作成しておく。 行先表[0].X= 2; 行先表[1].X= 1; 行先表[2].X=-1; 行先表[3].X=-2; 行先表[4].X=-2; 行先表[5].X=-1; 行先表[6].X= 1; 行先表[7].X= 2; 行先表[0].Y= 1; 行先表[1].Y= 2; 行先表[2].Y= 2; 行先表[3].Y= 1; 行先表[4].Y=-1; 行先表[5].Y=-2; 行先表[6].Y=-2; 行先表[7].Y=-1; これらの行き先に,すでにコマがあれば,失敗とみなし, 最大到着数(25コマ)に達すれば成功とする。 プログラム フォーム定義 コマンドボタン Name:button1 ラベル Name:label1 , label2 ,…,label5 Name:label6 , label7 ,…,label10 Name:label11,label12,…,label15 Name:labe16 , label17,…,label20 Name:label21, label22,…,label25 Name:label26 実行例 プログラム プログラム ①データ宣言 public const int NSIZE = 5; public Label[,] LabelDT = new Label[NSIZE+1,NSIZE+1]; public int[,] 盤面 = new int[NSIZE+1,NSIZE+1]; public int 到達最大数 = NSIZE*NSIZE; public int 解番号 = 0; public int 到達数 = 0; public struct 行先 { public int X; public int Y; } public 行先[] 行先表 = new 行先[8]; プログラム ②初期設定 private void 初期設定() { int i,j, ID; string labelName; int X=-5; for(i=1;i<6;i++) // 盤面用ラベルの2次元配列化 { X=X+5; for(j=1;j<6;j++) { ID = X +j; labelName="label" + ID.ToString(); foreach( Control myControl in Controls) if(myControl.Name==labelName) { LabelDT[i,j]=(Label) myControl; break; } } } 行先表[0].X= 2; 行先表[0].Y= 1; // 行き先表の設定 行先表[1].X= 1; 行先表[1].Y= 2; 行先表[2].X=-1; 行先表[2].Y= 2; 行先表[3].X=-2; 行先表[3].Y= 1; 行先表[4].X=-2; 行先表[4].Y=-1; 行先表[5].X=-1; 行先表[5].Y=-2; 行先表[6].X= 1; 行先表[6].Y=-2; 行先表[7].X= 2; 行先表[7].Y=-1; } プログラム ③表示と解の設定 private void 表示() { int i,j; for(i=1;i<6;i++) for(j=1;j<6;j++)LabelDT[i,j].Text=盤面[i,j].ToString(); 解番号++; MessageBox.Show(解番号.ToString() +"番目の解が見つかりました"); } private void Try(int X,int Y) { int i; if(X<1 || X>NSIZE || Y<1 || Y>NSIZE) return; if(盤面[X,Y]>0) return; 到達数++; 盤面[X,Y] = 到達数; if(到達数>=到達最大数) 表示(); else for(i=0;i<8;i++) Try(X+行先表[i].X,Y+行先表[i].Y); 盤面[X,Y]=0; 到達数--; } プログラム ④Form1_Load,button1_Click private void Form1_Load(object sender, System.EventArgs e) { 初期設定(); } private void button1_Click(object sender, System.EventArgs e) { int i,j; for(i=1; i<=NSIZE;i++)for(j=1;j<=NSIZE;j++)盤面[i,j]=0; 解番号=0; 到達数=0; Try(1,1); lblMessage.Text="解の数=" +解番号.ToString()+"通り"; MessageBox.Show(解番号.ToString() +"個の解が見つかりました"); }
© Copyright 2025 ExpyDoc