5章 再帰呼出しと分割統治 杉原厚吉.(2001).「データ構造とアルゴリズム」. 東京:共立出版. 2011/06/21-24 情報知能学科「アルゴリズムとデータ構造」 担当:白井英俊 1 5.1 再帰呼び出し • 複雑な問題を分割して解くことの代表的なものが 再帰 ― 小問題が大問題の類似形の場合に有 効 • 再帰呼び出し(recursive call) ― 手続き(関数)の 中で自分自身(その関数)を呼び出すこと • 特長:ある種の手続きを簡潔に表現できる 特に、問題自身が再帰的な場合に有効 木構造、配列、グラフ構造など、構造が「再帰的」になっているとき など ただし、どんな問題でも再帰がよい、というわけではない 2 再帰的な構造、効率の悪い問題 • Hanoiの塔:再帰的な構造をしていて、計算量が 大きな問題の典型例 (右図はWikipediaより拝借) 問題:n枚の円盤を3本あるうちの一つの柱から別 な柱に移す手順を求める 入力:円盤の枚数、柱の名称(a,b,c) 出力:円盤を移す手順 3 プログラム Hanoi def hanoi(n,a,b,c) # n枚の円盤をaからcへ移す if (n==1) # 円盤が一枚ならば print ’move the disk from ’,a,’ to ’,c,”\n” else hanoi(n-1,a,c,b) # n-1枚の円盤をaからbに移す print ’move the disk from ’,a,’ to ’,c,”\n” hanoi(n-1,b,a,c) # n-1枚の円盤をbからcに移す end # if end # def 4 ハノイの塔の計算量 • 円盤をn枚移すのにかかる計算量を f(n) とすると、このアルゴリズムは f(n) = 2*f(n-1)+α という計算量が必要である。(円盤を1枚移す手間を定数「 α 」として いる) したがって、f(n) は O(2n) g(n)=f(n)+α とおくと g(n) = 2*g(n-1) となるので g(n)は2の等比数列 これはアルゴリズムのせいではなく、問題自体が難しい (指数オーダーの計算量を必要とする)から =>アルゴリズムが簡潔に書けるからといって、そのアル ゴリズムの効率がよいということにはならない 5 ハノイの塔の計算量 円盤をn枚移すのにかかる計算量を f(n) とすると、 f(n) は O(2n) 検証: (1枚移す計算量を1とする) O(2n) 1枚移す: 2枚移す: 3枚移す: …. n枚移す: f(1) = 1 f(2) = 2*f(1)+1 = 3 f(3) = 2*f(2)+1 = 7 = 21 -1 = 22 -1 = 23 -1 f(n) = 2*f(n-1)+1 = 2n -1 6 5.2 分割統治法 • 再帰呼び出しによる効率のよいアルゴリズムの例: マージソート(merge sort) • 入力: n = 2k (kは正整数)個の数の集合S(配列とす る) • 出力: Sの要素を昇順に並べたもの(配列とする) Sの最小要素 Sの最大要素 • 手続き: |S|=2ならば、[min(S),max(S)]を返す さもなくば、Sをn/2個ずつに分け、それぞれをマー ジソート(mergeSort)する。この二つの結果を、昇順 に並べ(merge)、それを返す。 7 マージソートの実現(0) アルゴリズムから、mergeSortの全体像は: def mergeSort(s) n = s.size if (n == 2) # |S|=2の場合の手続きを書く---(1) else # それ以外の場合の手続きを書く---(2) end # if end # def 8 マージソートの実現(1) (1) |S|=2のときに[min(S),max(S)]を返す これは簡単。条件文の使い方さえ分かっていれば… def mergeSort(s) n = s.size return s if n==1 # 一般性を考慮 if (n == 2) # s[0]とs[1]を比較して、 # [小さい方の要素、大きい方の要素] を返す else # 次で考える end # if end # def 9 マージソートの実現のために アルゴリズムを実現するには、以下の作業をするための「アル ゴリズム」が必要: 2個の要素を持つ配列 (1) |S|=2のときに[min(S),max(S)]を返す (2) Sをn/2個ずつに分け、それぞれをマージ ソート(mergeSort)し、二つのマージソートが 返した結果を合わせて、昇順に並べる (mergeする) マージソートの実現(2) def mergeSort(s) n = s.size return s if n==1 # 一般性を考慮 if (s.size == 2) # できているはず… else mid = (n-1)/2 # 真中の要素の番号 x = mergeSort(s[0 .. mid]) y = mergeSort(s[(mid+1) .. (n-1)]) return merge(x,y) # xとyを合わせて昇順に end # if end # def 注:Rubyでは、配列xのインデックスi番目の要素から インデックスj番目の要素までを x[i..j] により配列として取り出せる 11 mergeSortの動き(1) 0 1 2 3 4 5 6 7 mergeSort([20, 10, 5, 15, 3, 7, 13, 18]) def mergeSort(s) n = s.size mid = (n-1)/2 x = mergeSort(s[0 .. mid]) y = mergeSort(s[(mid+1) .. (n-1)]) return merge(x,y) 8 3 mergeSortの動き(2) 0 1 2 3 mergeSort([20, 10, 5, 15]) def mergeSort(s) n = s.size 4 mid = (n-1)/2 x = mergeSort(s[0 .. mid]) y = mergeSort(s[(mid+1) .. (n-1)]) return merge(x,y) 1 mergeSortの動き(3) 0 1 mergeSort([20, 10]) def mergeSort(s) n = s.size if (s.size == 2) 2 true # [最小の要素、最大の要素]を返す return else … end [10, 20] mergeSortの動き(4) 0 1 mergeSort([ 5, 15]) def mergeSort(s) n = s.size if (s.size == 2) 2 true # [最小の要素、最大の要素]を返す return else … end [ 5, 15] mergeSortの動き(2続) 0 1 2 3 mergeSort([20, 10, 5, 15]) def mergeSort(s) n = s.size mid = (n-1)/2 [10, 20] [5,15] y = mergeSort(s[(mid+1) .. (n-1)]) x = mergeSort(s[0 .. mid]) return merge(x, y) マージ(merge)とは? ・「再帰法」は、今作ろうとしている手続き (mergeSort)が小さな問題に対しては「ちゃん と正しい値を返してくれる」と考えて作る方法。 だから、以下は「ソートされた数の配列が返さ れると仮定してよい」: mergeSort(s[0..mid]) # 配列sの前半 mergeSort(s[(mid+1)..(n-1)]) # 配列sの後半 とすると、考えなければならない問題は、返した結果を合わ せて、昇順に並べる方法 17 マージ(merge) • 再帰法の仮定から mergeSort(s[0..mid]) mergeSort(s[(mid+1)..(n-1)]) は、それぞれソートされた配列を返す。ここで x= mergeSort(s[0..mid]) [1,3,5,9,11,15,18,20] y= mergeSort(s[(mid+1)..(n-1)]) [2,4,6,8,10,12,14,16] とすると、 マージmerge(x, y) は、x,yを入力として、 [1,2,3,4,5,6,8,9,10,11,12,14,15,16,18,20] というような昇順の配列を返す! 18 マージ(merge)関数を作る def merge(x,y) result = [ ] # マージした結果の記録用 xlen = x.size ‐1; ylen = y.size ‐1 # xとyの最後の要素のインデックス xc = 0; yc = 0 # xとyそれぞれ「比較する」要素の番号 while (xc <= xlen && yc <= ylen ) # 未検査要素あり # x[xc]とy[yc]を比較し小さい方をresultに入れるなど # 適切なプログラムを書く end # while # xかyに未検査のものがある場合の処理を書く end # def 19 mergeの動き(1) merge ( [10, 20] , [5, 15] ) xlen ylen 0 1 2 5 15 y ylen = y.size ‐1 def merge(x, y) 0 1 result = [ ] 10 20 x xlen = x.size ‐1; xc = 0; yc = 0 (xc <= xlen && yc <= ylen ) xwhile の残りの要素をすべ yc xc 比較(小さい方をresultへ) # x[xc]とy[yc]を比較しresultに入れる てresultに入れる # 適切なプログラムを書く ] result end #[ while > < mergeSortの動き(2) 0 1 2 3 mergeSort([20, 10, 5, 15]) def mergeSort(s) n = s.size mid = (n-1)/2 [10, 20] [5,15] y = mergeSort(s[(mid+1) .. (n-1)]) x = mergeSort(s[0 .. mid]) return merge(x, y) [5, 10, 15, 20] mergeの動き(2) merge ([ 3, 7] , [13, 18] ) xlen ylen 0 x 2 1 3 7 0 1 y 13 18 y の残りの要素をす xc 比較(小さい方をresultへ) べてresultに入れる yc < result [ ] mergeSortの動き(1続) 0 1 2 3 4 5 6 7 mergeSort([20, 10, 5, 15, 3, 7, 13, 18]) def mergeSort(s) n = s.size mid = (n-1)/2 x = mergeSort(s[0 .. mid]) y = mergeSort(s[(mid+1) .. (n-1)]) return merge(x,y) 8 3 [5,10,15,20] [3,7,13,18] mergeの動き(3) xlen 0 1 2 3 ylen 0 1 2 3 5 10 20 , [ 3, 3 7 13 18] 18 ) 10, 15 15, 20] 7, 13, merge ( [ 5, yc xc 比較(小さい方をresultへ) < > result 4 [ xの残りの要素をすべ てresultに入れる ] マージソートの完成 • mergeとmergeSortをちゃんと書くことができれ ば、次のような結果が得られるだろう: (mergeSortTemplate.rb参照)ーーー課題 mergeSort([20, 10, 5, 15, 3, 7, 13, 18, 2, 6, 4, 1, 19]) の出力結果: [1, 2, 3, 4, 5, 6, 7, 10, 13, 15, 18, 19, 20] 25 分割統治法 • 入力データの大きさをnとすると、そのデータ を大きさ n/b の問題 a 個に分割し、それぞれ マージソートの計算 を再帰的に解いた結果を利用して、元の問題 量はO(n*log n) の解を作るという方法(教科書、p.51)で、かつ 計算時間 f(n) が n=1の時、 f(n) = c (cは定数) n ≧2の時、 f(n)=a*f(n/b) + cn (a,b,cは定数) となるもの(マージソートでは a=b=2) 26 ハノイの塔とマージソートの比較 ハノイの塔: hanoi(n,a,b,c) if (n == 1) 1枚円盤をaからcに移動 else hanoi(n-1,a,c,b) 1枚円盤をaからcに移動 hanoi(n-1,b,a,c) end 計算量 O(2n) O(n*log n) マージソート:mergeSort(s) if (s.size == 1) return s elsif (s.size == 2) return [s.min, s.max] else x=mergeSort(sの前半分) y=mergeSort(sの後半分) return merge(x,y) end 27 ハノイの塔とマージソートの比較 • ハノイの塔は再帰法を使っていても、その計 算量は O(2n) • マージソートでは、O(n*log n) この違いは何か? 半分! 答え: 元の問題に対し、一定の比で小さな問題に して解く(マージソート)か、一定の差で小さな 問題にして解く(ハノイの塔)か。(p.53-54) 28 再帰呼出しを使わない方がよい例 • フィボナッチ数列(Fibonacci sequence) 定義: 非負整数nに対するフィボナッチ数F(n) とは n=0 または n=1 のとき、1 それ以外: F(n) = F(n-1)+F(n-2) 1 1 2 3 5 8 13 21 34 55 89 144 … この形から、「ハノイの塔」タイプであることが明らか 29 フィボナッチ数F(n)を求める • 先の定義を単純にプログラムすると以下のとおり: def fibo1(n) if (n==0 || n==1) return 1 else return fibo1(n-1)+fibo1(n-2) end # if end #def これを用いて fibo1(30)や fibo1(50)を計算させてみよう 30 フィボナッチ数F(n)の別な求め方 • 実は再帰を使わず、F(1),F(2),F(3),… の順にF(n)まで計算すれば、ずっと効率がよい …計算量のオーダーはO(n) def fibo2(n) return 1 if (n==0 || n==1) x = 1; y = 1 for i in 2..n z = x+y ; x = y; y = z end # for return z これを用いて end # def fibo2(30)や fibo2(50)を計算させてみよう 31 実は。。。 • 教科書p.56にあるように、フィボナッチ数F(n) はO(log n)で求めることができる。 n 1 1 A(n) 1 0 FF(n(n)1) A(n 1) FF((10)) A(n 1)11 とすると ここで F (n) F (1) 1 A(n 1) A(n 1) F (n 1) F (0) 1 1 1 1 1 2 1 1 0 1 0 1 0 2 1 2 1 5 3 1 0 1 0 3 2 これを用いて F(10)やF (30)を計算しよう 32 まとめ:ハノイの塔の問題とフィボナッチ 数の問題の違い • ハノイの塔の問題で求めなければならないのは、「 実際に円盤を移すこと」。円盤を移す回数が計算量 となる。これは再帰を使わなくても同じ値になる。 指示手順を求めるという場合、具体的に「どの円盤 をどこに移すか」という指示一つ一つが、「円盤を移 すこと」と同じなので、指示の個数=円盤を移す個数 、となる。だからいずれにせよ 2nに比例した計算量 • フィボナッチ数(関数F)を求める問題では、nが与えら れた時にF(n)の値が求められればよい。これは定義 通りに再帰を使わずに、 O(log n)という効率的な求め 方がある。 33
© Copyright 2024 ExpyDoc