Basic Algorithms • 再帰呼び出し • 分割統治とバランス化 • グラフ探索 • 列挙法 • 動的計画 再帰呼び出し サブルーチンを作る • サブルーチンとは、いわばプログラムの部品 • プログラム実行中、そこに行き一定の処理をして、またもとの 場所に帰ってこれる • 通常プログラムを作る際には、作業を階層的に「部品化」し、 設計をやりやすくする - 作業単位がわかる - 全体の見通しがよくなる • サブルーチンを実行することを、呼び出すという 再帰呼び出し • 通常の部品の概念では、部品の中に同じ部品が入ることはない • しかし、サブルーチンは、自分自身を呼び出せる (サブルーチンが自分自身を呼び出すことを再帰呼び出しという) • 自分を呼び出すと、サブルーチンの最初から、呼び出すところま でを繰り返すループになる for ループと同じものが作れる sub (int i){ printf (“%d\n”, i); // i の値を表示 if ( i<100 ) sub(i+1); } 練習問題 • 次のループを再帰呼び出しで書け。擬似コードでよい - 10 から 20 までを表示 - 50 から 20 までの偶数を表示 - 1,2,…,9,10,10,9,…,1という順番で数字を表示。ただし、ひ とつのサブルーチンで - m×n の長方形を “#” で表示 複数回の再帰呼び出し • for ループは単純に繰り返すだけだが、再帰呼び出しは、1つ のサブルーチンが自分を複数回呼び出すことができる sub (int i){ printf (“%d\n”, i); // i の値を表示 if ( i<100 ){ sub(i+1); sub(i+1); } } • どのように動くのだろうか? 1レベル目が1回、2レベル目が2回、3レベル目が4回、と 指数的に実行解数が増える 2分木のような計算構造 • 1つのサブルーチン実行を1つの点で書き、再帰的に呼び 出した関係があるところに線を引く • 2分木の構造が得られる • 3回呼び出せば3分木、 4回呼び出せば4分木、 • 呼び出しの回数が変化すれば、より複雑な構造になる いろいろな計算ができる 組合せ的な処理の例 • 例:8文字の○×△からなる文字列を全て表示 char s[9]; sub (int i){ if ( i = = 8 ){ s[8] = 0; printf (“%s\n”, s); // ○×△ の表示 } else { s[i] = ‘o’; sub(i+1); s[i] = ‘x’; sub(i+1); s[i] = ‘A’; sub(i+1); } } 組合せ的な検索 • 再帰呼び出しの計算構造は、(逆さにした)木のようである • このような、計算の構造を表した木を計算木、あるいは再帰 木という • このように、処理を分けることを分枝するという 分枝の例 • a[0],…,a[9] の組合せで、合計がちょうど b になるものを表示 int a[10], flag[10]; sub (int i, int s){ int j; if ( i = = 10 ){ if ( s != b ) return; for (j=0 ; j<10 ; j++ ) if (flag[j] = = 1) printf (“%d ”, a[j]); // 数 の表示 printf (“\n”); } else { flag[i] = 1; sub(i+1, s+a[i]); flag[i] = 0; sub(i+1, s); } } 分枝限定法 • 分枝操作を行うと、全ての組合せを検索できる • しかし、全ての組合せのうち、一部しか見る必要がないのであ れば、全て見るのは無駄 • 見る必要のない部分を省略したい • 分枝作業をした後、「見る必要のない再帰呼び出し」は呼び出 さないようにしよう • 必要(可能性)のない分枝を切ることを、限定操作という 限定操作をしながら分枝する方法を、分枝限定法という 限定操作の例 • a[0],…,a[9] の組合せで、合計が b 以下になるものを表示 int a[10], flag[10]; sub (int i, int s){ int j; if ( s > b ) return; // 限定操作! if ( i = = 10 ){ for (j=0 ; j<10 ; j++ ) if (flag[j] = = 1) printf (“%d ”, a[j]); // 数の表示 printf ("\n"); } else { flag[i] = 1; sub(i+1, s+a[i]); flag[i] = 0; sub(i+1, s); } } 分割統治とバランス化 問題の分割 • 人間が問題を解く(作業をする)場合、問題なりやることなりを 分割して、小さくしてやることがある 見通しを良くする、という点と、やりやすくする、という点 • 例えば、部屋の片づけのしかた - まず、各部屋にしまうものを分類 - 次に各部屋の中を、個別に整理 • さらに各部屋の片づけをする際に、本棚や机、それぞれにし まうものを集めてから、それぞれをやっつける • このように再帰的に問題を分割して解く方法を分割統治法と いう 分割統治法の構造 • 分割統治法を、手法としてしっかり見てみる • 問題が与えられると、 - まず、問題を分割できるように整形する - 問題を分割する(できた問題を子問題という) - 各子問題を解く(再帰呼び出し) - 各子問題の解を合わせて、もとの問題の解を作る • 問題によっては、必ずしも全ての手続きが必要なわけではない 分割統治法の例 • 先の、再帰呼び出しの例はだいたい分割統治法といってよい • 合計がちょうど b になる組合せを求める i 番目の数を含む組合せを求める問題と、そうでない組合 せを求める問題に分割している 整形と分割、統合に関して、複雑な処理が必要ない ソート • 分割統治法を使うと、効率良く解ける問題がいくつかある • 基本的な例がソート • 数値 a[0],…,a[n] を小さい順に並び替える問題を考える • 戦略として、次の手順を考える - まず、数値を a[0],…,a[k] と a[k+1],…,a[n]に分割する - それぞれをソートする - ソートしてできた列を合わせて、全体のソート列を得る (この操作をマージ(合併・併合)という) - この操作を再帰的に行うことで、ソートをする マージの例 •例題 1,9,5,3,7,6,2,0,8,4 - 1,9,5,3,7,6 と 2,0,8,4 に分割 - それぞれをソートする: 1,3,5,6,7,9 と 0,2,4,8 - マージする (両者を同時に先頭から見ていく) 1,3,5,6,7,9 と 0,2,4,8 0,1,2,3,4,5,6,7,8,9 •マージは線形時間でできる マージをするプログラムの例 •配列 a,b の数列をマージして、配列 c に格納 •配列 a,b の最後に、大きな数 HUGEが入っているとする { int ia, ib=0, ic=0; for ( ia=0 ; a[ia]<HUGE ; ia++){ while (b[ib]<a[ia] ){ c[ic] = b[ib]; ic++; ib++; } c[ic] = a[ia]; ic++; } } マージソートをするプログラムの例 •配列 a,b 両方に数列を格納しておくと、配列 a にソートした列が入る merge_sort (int *a, int *b, int s, int t){ int i1, i2, ia; if ( s = = t ) return; merge_sort (b, a, s, (s+t)/2 ); merge_sort (b, a, (s+t)/2, t); for ( i1=s,i2=(s+t)/2,ia=s ; i1<(s+t)/2 ; i1++){ while ( i2<t && b[i2]<b[i1] ){ a[ia] = b[i2]; ia++; i2++; } a[ia] = b[i1]; ia++; } while ( i2<t ){ a[ia] = b[i2]; ia++; i2++; } } 再帰呼び出しの様子 • マージソートの再帰呼び出しの様子は、2分木になる merge_sort (int *a, int *b, int s, int t){ int i1, i2, ia; if ( s = = t ) return; merge_sort (b, a, s, (s+t)/2 ); merge_sort (b, a, (s+t)/2, t); for ( i1=s,i2=(s+t)/2,ia=s ; i1<(s+t)/2 ; i1++){ … 分割は、「同じ大きさ」が正解? • 先ほどのマージソートは配列をだいたい同じ大きさに分割して いたが、もっといい分割はあるか? 分割のしかたを変えると、速さは変わるのか? • まず、2つ目の疑問は、Yes • マージソートの各反復の計算時間は、ソートする配列の大きさ に線形 O(n) • もし、毎回 「1個と残り」という分割をすると、配列の長さは毎 回1ずつ小さくなる 計算時間は 1+・・・+n に比例、つまり O(n2) 「同じ大きさ」が正解 • 毎回ちょうど半分に分割すると、配列の長さは毎回半分+1 以下になる O(log n) 回分割すると長さが1になる 各レベルで計算時間を合計すると、 O(n) になるので、合計 はO(nlog n) • 分割の仕方で計算量のオーダーが変わった • これは最適なんだろうか? ソートの計算時間の下界と一致するので、最適 分割すればいい、と言うものではない • 分割ができるからといって、分割統治法を使えば速くなる、と いうものでもない a[1],…,a[n] の中から最大のものを見つける問題は、分割し ても解けるが、速くならない - a[1],…,a[k] と a[k+1],…,a[n] に分割してそれぞれの最大 を再帰的に求める - 両最大値のうち、大きいほうを全体の最大値として返す • 計算木の各頂点で O(1) しか時間 をかけないので、どう分割しても O(n) 時間になる クイックソート • マージソートは、分割前に何もせず、分割後にがんばる - まず、問題を分割できるように整形する - 問題を分割する(できた問題を子問題という) - 各子問題を解く(再帰呼び出し) - 各子問題の解を合わせて、もとの問題の解を作る • 逆のアイディアでソートができないか? • クイックソートと言う方法がある 分割の前にがんばる • まず、数値をある数 k より大きなものと小さなものに分割する k • それぞれを小さい順に並べる • 終わると、全体が小さい順になっている k との大小で分割 • 数値を k との大小比較で分割するにはどうすればいいか? - 右からスキャンして k より小さいものを見つけて止まる - 左からスキャンして k より大きいものを見つけて止まる • 両者を入れ替える • 繰り返す。スキャンしている場所がすれ違ったら終わる 分割のバランスがとれない • k の値によって、分割してできる子問題の大きさは変わる • しかし、真ん中の値(中央値)を見つけるのは、簡単ではない • しょうがないので、適当なものを選ぼう 最悪の場合、1つと残り、という分割を繰り返すので 計算時間は O(n2) になる • しかし、例えば、分割が 1:9 以上になる確率は 2/10 で、 こういうことは、起こりにくい。そんな分割がたくさん起こること は、なおない。平均的には、 O(nlog n) になる 合計が最も大きい区間を求める • a[0],…,a[n-1] の区間 a[h],…,a[k] 中で、合計が最も大きいも のを求める (負の値があると、全体=最大とはかぎらない) • 実験の時系列データの解析など? 各区間の取り方は O(n2) なので、素朴に計算すると O(n3) 時間になる 各区間の合計の計算の仕方を工夫する(左端を固定して、 右側をひとつずつずらし、新しく入ってくる要素を今までの合 計に足し込む)と O(n2) になる • 分割統治法を使うと、速く解ける 分割して計算 ① a[0],…,a[n-1] を a[0],…,a[k-1] と a[k],…,a[n-1]に分割 ② 右端が a[k-1] となる区間の中で最大を求める ③ 左端が a[k] となる区間の中で最大を求める a[k] と a[k] を両方含む区間の中で最大なものは、②と③ を 合わせると求まる ④ 再帰呼び出しで、a[0],…,a[k-1] の中の最大と、a[k],…,a[n-1] の中の最大を求める • ①と②と③ はO(n) 時間 問題を半分に分割するようにすれば、マージソートと同じ計算 量になり、全体の計算時間は O(nlog n) ~余談~: k番目を線形時間で見つける • クイックソートは、問題を分割するときに、だいたい真ん中にな る数が知りたい • こういう、数の中から k 番目にあるものを見つける問題を k best 問題という • 簡単にやろうとしたら、数字をソートすればよい (本末転倒だが) • だが、凝った方法で、ソートせずに線形時間の方法がある ~余談~: k番目を線形時間で見つける 2 • 入力した数を a[0],…,a[n-1] とする • 基本的な戦略は、数 x を選び、 a[0],…,a[n-1] を x をより大き なものと小さなものに分ける • x より小さいものが k 個以上あったら、k 番目はx より小さい x より小さいものの中で k 番目を見つける • x より大きいものが k 個以上あったら、k 番目はx より大きい xより大きいものの中で n-1-k 番目を見つける • という具合に小さくしていく。効率は x の選び方しだい ~余談~: k番目を線形時間で見つける 3 • x を選ぶのには、「いいかげんな」 中央値を使う (クイックソートにも、いいかげんな中央値で十分だが) • a[0],…,a[n-1] 中を7個ずつに分け、それぞれの7個の中で中 央値を見つける(定数時間が n/7 回で O(n) 時間) • 見つけた中央値の中でさらに中央値を見つける (1/7 の大きさの問題を再帰的に解く) • これは、真ん中 1/4 以上はじっこに行くことはない (3/4 の大きさの問題を再帰的に解く、上と合わせて 25/28 ) ~余談~: k番目を線形時間で見つける 4 • それぞれの7個の中で中央値より、半分のものは大きい 大 小 • 半分のもののさらに半分は、●より大きい • 半分のもののさらに半分は、●より小さい • よって、真ん中 1/4 以上はじっこに行くことはない ~余談~: k番目を線形時間で見つける 5 • 問題の大きさの合計が、毎回 25/28 になる • 問題を分けるのにかかる時間は O(n) • 全体の計算時間は O(n + (25/28)n + (25/28)2n + (25/28)3n …) = O(n) 行列積の計算量 行列のかけ算 • 行列の積を求める演算は、行列の操作の中でも基本的なもの だが、計算に比較的時間がかかる (次元の3乗のオーダー) • うまいことやって、もっと速くできないだろうか? × = 分割統治法は使えるか? • ソートでは、問題を分割して高速化した。行列も問題を分割し たらうまく解けるかも • うまいことやって、もっと速くできないだろうか? • 分割するといっても、どのように分割する?どのように解く? • まずはどのように分割するか、分割した問題をどのように解く かを考えないと × = 簡単な分割 • 左の行列は横長の行列2つに、右の行列は縦長の行列2つに 分けてみよう • 横長と縦長の行列の組合せ1つの積を求めると、答えの1ブロ ックが埋まる 4回積を求めると、もとの行列の積が求まる • でも、「正方行列の積」が「正方でない行列の積」になるので、 気持ちが悪いなあ × = もう少し分割 • 両行列ともに4つに分割する • 1/4 ブロックの積を2つ求め、足すと、1ブロック埋まる 正方行列の積を8回求めると、もとの行列の積が求まる 同じ問題を解いているので、再帰呼び出しできる • さて、これは速くなっているのでしょうか??? 解析してみないと、よくわからない。。。 A B E F × C D = G H AE +BG CE +DG AF +BH CF +DH 計算時間の再帰式 • 計算が再帰的なので、計算時間の再帰式を作ろう • この方法で n 次元行列のかけ算をする時間を T(n) とする • 問題を分割したり、積の足し算をして解を求める時間は O(n2)で あるので、定数を使って、cn2 時間であるとする • 再帰は、大きさ n/2 の問題について 8回行われるので、 T(n) = cn2 + 8T(n/2) となる A B E F × C D = G H AE +BG CE +DG AF +BH CF +DH 計算時間の再帰式 • T(n) = cn2 + 8T(n/2) を満たす関数 T(n) とは、どのような関数で あろうか? • 図示して考えてみよう (簡単のため、n = 2k であるとする) • 大きさ n の問題を受け取ったら、cn2 の計算時間をかけ、 8 個の 大きさ n/2 の問題に関して再帰呼び出しが起こる • (8個の)大きさ n/2 の問題は、それぞれ c(n/2)2 の計算時間をか け、それぞれ 8 個の、大きさ n/4 の問題を作る • (64個の)大きさ n/4 の問題は... (以下省略) • (???個の)大きさ 1 の問題が呼び出される 計算時間の再帰式 • 大きさ n の問題を受け取ったら、cn2 の計算時間をかけ、 8 個の 大きさ n/2 の問題に関して再帰呼び出しが起こる... • 1レベル下がると大きさは 1/2 になり、計算時間は 2倍に増える 全体の計算時間は一番下のレベルの2倍を超えない • k = log2n レベル下がると、問題の大きさが 1 になり、再帰呼び 出しは起こらない そのレベルでの計算時間の合計は cn2 ×2k = cn3 • 計算時間は、O(cn3) • 苦労したのに何もかわらないや。。。 再帰式、一般的には • T(n) = cn2 + 8T(n/2) の n2 の部分や、8 の部分、n/2 の 2 の部分 が変化すると、関数 T(n) のオーダーも変化する • どのように変わるか、少し調べてみよう • n2 の部分、8 の部分が変わると、1レベル下がったときの計算時 間の増加量が変わる - 8 の部分が 2 になると、1レベル下がると計算時間が半分に 全体の時間はトップレベルの2倍を超えず、O(n2) - 8 の部分が 4 になると、各レベルでの計算時間は等しくなる 全体の計算時間はトップレベルの高さ倍となり、O(n2 log n) 再帰式、一般的には - n2 が n になると 1レベル下がるごとに計算時間は4倍になる 一番下のレベルの計算時間の合計はトップレベルの n2 倍 になる。しかし、トップレベルの計算時間が O(n) であるので、掛 け合わせると O(n3) - n2 が n4 になると、1レベル下の計算時間は半分になる 全体の計算時間はトップレベルの2倍を超えず、 O(n4) - n2 が n3 になると、各レベル下の計算時間は等しくなる 全体の計算時間は O(n3 log n) 再帰式、一般的には - n/2 が n/4 になると、1レベル下がるごとに計算時間は1/2に なり、さらにレベルの高さが半分に 計算時間の合計はトップレベルの2倍以下。O(n2) - n/2 が n/4 になり、n2 が n になると、1レベル下がるごとに計 算時間は2倍になる。レベルの高さが半分になる 一番下のレベルでは、トップレベルの n1/2 倍。O(n3/2) 再帰式、一般的には • 一般に、T(n) = cna + bT(n/c) という再帰式が成り立つとき、 - a > logc b ならば、トップレベルが全体の計算時間を支配し、 計算量は O(na) となる - a < logc b ならば、一番下のレベルが全体の計算時間を支配 し、計算量は O(nlogc b) となる - a = logc b ならば、各レベルの計算時間は等しくなり、計算量 は高さのファクターが入り、O(nalog n) となる 行列積の高速化 • 先の問題の分割では、答えの行列をブロックごとに求めていた その結果、8回のかけ算を必要としている 行列積は a < logc b (2 < log2 8) の場合であるので、 再帰呼び出しの回数 8 を減らすか、子問題の大きさ n/2 を小さく しなければならない • 工夫して、再帰の数、つまりかけ算の数を減らした人がいる A B E F × C D = G H AE +BG CE +DG AF +BH CF +DH 分配則の利用 • 行列を足してからかけると、行列積の和が得られる 例) (A + B)E = AE + BE B(G – E) = BG – BE • これらをうまく組合わせて足し合わせると、目的である行列積が 出てくる 例) (AE + BE) + (BG – BE) = AE+BG A B E F × C D = G H AE +BG CE +DG AF +BH CF +DH 再帰式の変化 • うまい和積取り方と組合せ方を考えて、7回の行列積で、4つのブ ロックの解が得られるようにした • 計算時間の再帰式は、T(n) = cn2 + 8T(n/2) から T(n) = cn2 + 7T(n/2) になった • log2 7 = 2.80… なので、およそ O(cn2.80) 時間になる • この方法で一番すごいやつは、100個以上の行列に分解して同様 のことを行い、O(cn2.31…) 時間を達成 A B E F × C D = G H AE +BG CE +DG AF +BH CF +DH 疎な行列のかけ算 疎な行列のかけ算はどうなる? • 疎な行列は、疎な持ち方をすると効率が良い • 疎な持ち方にあったかけ算の仕方をしたとき、計算時間はオ ーダーの意味で改善されているのかな? 最悪の場合、全部のセルに値が入るので、改善されない • しかし、疎な行列に興味があるので、行列の疎性に応じた速 さの評価がしたい • 行列の大きさの他に、非ゼロのセルの数を使って、行列積の 計算時間を評価してみよう 行ごとに格納 • 各要素へのアクセスを良くするため、少々構造をつけよう • まずは、1 である要素を、行ごとに分類し、記憶 配列を n 個用意し、そこに各行にある 1 である要素の場 所(列番号のみ) を記録 • 配列が n 個必要になったので、n個メモリが必要だが、行番 号を記録しなくてよくなったので、その分節約できる • これで、使用するメモリは、「1の数」+行の数×2(ほんとは1 にできる)になった。 • アクセスも良くできる。各行の位置を小さい順に並べれば、2 分探索できる (1行の要素数が小さければスキャンで十分) ベクトルの内積 • 疎なベクトルの内積計算をする • 各行(ベクトル)を、添え字の小さい順に並びかえておく • 内積を取るベクトル2つを、小さいほうから同時にスキャンする 同時に、の意味は、同じ値、あるいは両者を混ぜ合わせて小さ い順に並べたときに隣り合う要素を常に見ている、ということ • 両方とも非ゼロであるところが見つかったら、その要素の積を 答えに加える 1 5 5 1 7 3 1 1 3 3 5 4 疎行列積の計算時間 • 左側の行列の各行について、右側の各列との内積を取る 各 i 行につき、① その行が n 回スキャンされ、左側行列 の各列が1回ずつスキャンされる (i 行の大きさ)×n + (右行列の大きさ) • これを各行について行い、時間の合計を取ると (左行列の大きさ)×n + (右行列の大きさ)×n • 行列の大きさ(非ゼロのセル数)が n2 よりもオーダーの意味 で小さければ、計算量の改善がある バケツを使った方法 • バケツを使った行列転置の計算方法と同じアイディアを使うと、 もう少し速くできる • 左行列の第 i 行と右行列の各列の内積を取る問題を考える (i 行と j 列の内積を p(i,j) と表記する) • 第 i 行の非ゼロ要素が、l1,…,lk 番目にあるとする • p(i,j) を各 j について計算するには、各 l=l1,…,lk について、 第 i 行の l 番目の要素と、第 j 列の l 番目の要素の積を求 め、その総和を計算すればよい バケツを使った方法 • バケツを使った行列転置の計算方法と同じアイディアを使うと、 もう少し速くできる • 左行列の第 i 行と右行列の各列の内積を取る問題を考える (i 行と j 列の内積を p(i,j) と表記する) • 第 i 行の非ゼロ要素が、l1,…,lk 番目にあるとする • p(i,j) を各 j について計算するには、各 l=l1,…,lk について、 第 i 行の l 番目の要素と、第 j 列の l 番目の要素の積を求 め、その総和を計算すればよい 計算する部分を図示 • 第 i 行の非ゼロ要素のある位置 l1,…,lk に対応する行を見る • 各列について、要素の積を計算する 第i行 右側の行列 計算の順番を変える • 各列について、緑の部分にある非ゼロ要素を見つけるのは多 少手間がかかる (ゼロである要素を何度もチェックする) • 各l 行について積を計算するのならば楽 • つまり、第 i 行のl 番目の要素 に対して、各列のl 番目の要素 との積を計算するならば楽 • 計算した積は、各列の下にある 箱に足し込めばよい 全ての行についてスキャンすると 箱の中身はその行の内積の値になる 第i行 右側の行列 行のスキャンにも疎性を • i 行のl 番目の要素と、各列のl 番目の要素との積を計算 各列のl 番目が非ゼロであるところのみ、計算すればよい (そうでないところは、積は 0 になるから) • 右側の各行について 非ゼロである要素の位置を 覚える方式で行列を記録すると、 スキャンするのが楽 第i行 右側の行列 計算時間 • 左側の行列の各 i 行の非ゼロ要素がある位置、l について、 右側の行列のl 番目の行をスキャン • 左側の行列の各 i 行についてこの操作を行うと、行列の積の 計算ができる • そのとき、右側の行列の第 j 行は、左側の行列の第 j 列にあ る非ゼロ要素の数だけ、スキャンされる • 右側の行列の第 j 行の 大きさを S(j)、左側の行列の 第 j 列の大きさを T(j) とすると 計算時間は S(j)×T(j) 時間 T(j) S(j) 計算時間 • そのとき、右側の行列の第 j 行は、左側の行列の第 j 列にあ る非ゼロ要素の数だけ、スキャンされる • 右第 j 行の大きさを S(j)、左第 j 列の大きさを T(j) とすると - 第 j 行 に関する計算時間は S(j)×T(j) 時間 • 全体では Σi S(j)×T(j) 時間 - 各行・列の大きさが 定数なら O(n) 時間 - 行・列の大きさがべき乗則 に従うなら、(次数が2以上の とき) O(n) 時間 T(j) S(j) 逆行列の計算(行列の三角化) 逆行列 • 行列の A の逆行列 A-1 の定義は A-1 が AA-1 = I (単位行列)を満たすこと (逆行列 A-1 は A が非退化/フルランク/正規/non-singlarであ るときのみ存在) • A から A-1 を求めるのが、逆行列を求める問題 (黄色の行列の要素が何であるか求める問題(方程式を解くような もの)) 1 × = 1 1 ・・・ 1 1 連立方程式へ • 求める黄色の行列の i 行 j 列の要素を xij とおくと、A と黄色行列 の掛け算をすると、解の各要素には線型方程式が入る 例) 解の 1行1列 a11xij + a21x12 + ・・・ + an1x1n • 解の行列が、単位行列になるように xij を決めると、 A-1 が求まる 例) 解の 1行1列 a11xij + a21x12 + ・・・ + an1x1n が 1 になるように xij を定める 方程式を解いているのと同じ x11 x12 ・・・ x1n x21 x22 ・・・ x2n × 1 = xn1 xn2 ・・・ xnn 1 1 ・・・ 1 1 連立方程式を解く • 解の i 行 j 列 a1jxi1 + a2jxi2 + ・・・ + anjxin = 1 (あるいは0) こういう方程式が n2 本ある • 一方、変数の数は n2 個 退化していなければ解ける • 線型方程式なので、代入していけば解ける それを機械的にやるようにすれば、アルゴリズムができる x11 x12 ・・・ x1n x21 x22 ・・・ x2n × 1 = xn1 xn2 ・・・ xnn 1 1 ・・・ 1 1 1つずつ代入 • こういう方程式を解くときは、ある変数について方程式を解き、そ れを残りに代入する 例) a1jxi1 + a2jxi2 + ・・・ + anjxin = 1 xi1 = (1 - (a2jxi2 + ・・・ + anjxin)) / a1j • こういう操作をやって、すべての変数を消せばよい • 1行1列から順に代入を使って変数を消すアルゴリズムができる x11 x12 ・・・ x1n x21 x22 ・・・ x2n × 1 = xn1 xn2 ・・・ xnn 1 1 ・・・ 1 1 計算時間 • 1つの変数を消すには、代入の式を作り(O(n2)時間)、それを他の 全ての式 (O(n2)個) に代入 (O(n2)時間) 変数は n2 個あるので、O(n6) 時間 逆行列はO(n6) 時間で求められる • 次に、もう少し速い方法を紹介しましょう x11 x12 ・・・ x1n x21 x22 ・・・ x2n × 1 = xn1 xn2 ・・・ xnn 1 1 ・・・ 1 1 行列の基本変形と三角化 • 行列のある行の○○倍を他の行に足すのが行列の基本変形 左から、その列と行の要素を○○にした単位行列をかけるこ とと同じ • 対角成分の上(下)部分にしか非ゼロの数が入っていないとき、 その行列は上三角行列(下三角行列)という 1 1 1 ・・・ 1 1 LU分解 • 行列の下の行に上の行(の○○倍)を足して、上三角化行列を作 る(行列の基本変形をする) • 行列の基本変形を表す行列をかけると、非ゼロ部分を重ね合わ せた行列になる、ので、 • 全ての基本変形を掛け合わせると、下三角行列ができる ((このように上・下三角行列に分解することを、LU分解という) 1 1 1 1 × ・・・ 1 1 1 1 1 = ・・・ 1 1 1 1 ・・・ 1 1 LU分解で逆行列を • 三角化した行列下の逆行列は簡単に求まる 方程式を作ると、解行列の一番下の行では、変数が1つしか ないので、簡単に解ける。その結果を代入すると、下から2番目 の行が解ける、、、という具合になる。 • 左から三角行列の逆行列をかけて、逆行列を求める方程式を簡 単な方程式に変形してしまおう 1 1 1 1 × ・・・ 1 1 = 1 1 ・・・ 1 1 逆行列を • 三角化した行列で表した逆行列の方程式に、左から基本変形行 列の逆行列をかける 左側の三角行列がキャンセルして消える • 解きやすい形になりました 三角化できていれば、行列積(O(n3))と代入(O(n3))の時間 (O(n3))で逆行列が求まる A-1 A -1 × -1 × × = 1 × ・・・1 1 行列の基本変形と三角化 • 上の行の定数倍を下の行から引く 下の行の特定の要素が消せる - 1列目を使って、それより下の行の1列目を消す - 2列目を使って、それより下の行の2列目を消す … とすれば、上三角行列になる(基本変形の行列は下三角) A = × 三角化の計算時間 - 1列目を使って、それより下の i 行の1列目を消す 1列目の要素が同じ大きさになるように、 1行目に a1j /a11 を掛け、それをi 行から引く • 各行について、O(n2) 時間でできる。全部の行について計算 すると O(n3) 時間 逆行列はO(n3) 時間で求められる A = × 疎な行列だと? • 「1行目に a1j /a11 を掛け、それを i 行から引く」 • この操作で、 i 行のゼロ要素が一部非ゼロになる( fill in という) 何回も行うと、多くのゼロだった要素が非ゼロになり、密になる • 現実的には、 fill in がなるべくおこらないように、行・列を入れ替 えておけばいい。例えば、上の行、左の列が疎になるようにとか A = × グラフ探索 探索問題 • 自分のいるところから到達可能なところを全て調べる問題を 考える(これを探索とよぶ) • 例えば、グラフ・ネットワークで、現在地から到達できるところ を全部調べる 行けるとは、枝をたどって移動すること • グラフの中で、ある頂点から到達可能な頂点を全て調べる問 題・調べることをグラフ探索という どういう戦略をとるか • 有向グラフ G=(V,A) の頂点 v から到達可能な頂点を全て出 力する • 素朴にするなら、全ての頂点に対して、v から到達可能かどう か調べる v から到達可能でないことを確認すること自体が、ある種、 v から到達可能な頂点を全て調べることに対応 • 迷路を調べるように、たどっていけるところを少しずつ広げて いくほうがいいだろう 迷路を解くには? • 迷路を解く(迷路でいけるところを全部調べる)にはどうする か? - 出発点から始め、道(枝)をたどる。分かれ道は適当に選ぶ - 行き止まりに来たら、前の分かれ道まで戻って、行ってない 方に行く - 行ってないほうがなくなったら、さらに戻る - 一回行ったことのあるところに出たら、引き返す • 右手法は、立体交差があるとだめ、入り口と出口が外につい ていないとだめ、なので、少々不完全 グラフバージョン • 迷路を解く方法を、グラフに焼きなおそう - 出発頂点から始め、枝をたどる。各頂点で、次に進む枝は 適当に選ぶ - 探索してない枝がない頂点に来たら、前の頂点まで戻って、 行ってない枝に行く - 行ってない枝がなくなったら、さらに戻る - 一回行ったことのある頂点に来たら、引き返す • 「行ったことのある頂点、たどったことのある枝」を覚えておく 必要がありますね • あと、各頂点に対して、「どの頂点からきたか」も 来たことあるか、を記憶 • 「行ったことのある頂点、たどったことのある枝」を覚えておく • 行ったことのある頂点は、各頂点にフラグを用意しておく フラグ=0 行っていない フラグ=1 行ったことがある • さらに、「どの頂点から来たか」も一緒に記憶できる フラグ=-1 or NULL など 行っていない フラグ=それ以外 直前にいた頂点 • 枝も同じようにして覚えられる、がそうしなくても、もっといい方 法がある 順番に枝をたどる • 各頂点に隣接する枝は、番号順か何かで、1つずつたどれる としよう(隣接リスト、隣接配列などで) • 最後にたどったのは何番目の枝かを覚えておき、次に枝をた どるときは、その次をたどる • 頂点の数だけのメモリしかいらない プログラムを書く • グラフの探索のための、データはそろった • さて、アルゴリズム(プログラム)はどうしようか • 各頂点で何をするか考える 接続する枝をたどる 訪問済み(行ったことある)だったら、戻る 訪問してなかったら、先を探索する 行った先の頂点でも、同じように枝をたどる 行った先から帰ってきたら、次の枝を調べる 同じか。じゃあ、再帰呼び出しだ 再帰呼び出しで書く • 再帰呼び出しの各サブルーチンでは何をするか • 頂点を与えると、 - 接続する各枝に対して、その先を探索するよう、再帰呼び 出しをする - ただし、訪問済みならば再帰呼び出ししない - 全部の枝を調べたら、サブルーチン終了 自動的に、もと 来た頂点に戻る • どこから来たか、を覚えなくてよくなり、仕組みが簡単になりま した 再帰呼び出しによるコード • deg[v] は vの次数、edge[v] はv に隣接する頂点の配列 • flag[v] は v の訪問済みマーク。最初は全部0にしておく int flag[n], edge[n][]; DFS (int v){ int j; flag[v] = 1; // v に訪問済みマークをつける for ( j=0 ; j<deg[v] ; j++){ if (flag[edge[v][j]] = = 0 ) DFS( edge[v][j]); // 訪問済みでなければ 再帰呼び出し } } 再帰呼び出しを使わないコード • deg[v] は vの次数、edge[v] はv に隣接する頂点の配列 • flag[v] は v の訪問済みマーク。最初は全部 -1 にしておく int flag[n], k[n], edge[n][]; DFS_ (int v){ int j, vv=v; for ( j=0 ; j<n ; j++){ flag[j] = -1; k[j] = 0; } flag[v] = v; // 最初の頂点だけ、自分から来たことにする。特別 while (1){ if (k[v] = = deg[v] ){ if (flag[v] = = vv ) return; // 最初の頂点の枝を全部たどったら終了 v = flag[v]; // そうでなければ、もときたところに戻る } else { if (flag[edge[v][k[v]]] < 0 ) {flag[edge[v][k[v]] = v; v = edge[v][k[v]]); } // 枝をたどる } } } 深さ優先 • 先ほどの探索方法は、先へ先へと探索を進めていく いわば、深いほうをどんどんたどる • つまり、最後に見つけた頂点の隣を調べている スタックと考え方が同じ • じゃあ、スタックを使ってアルゴリズムをつくろう 新しく訪れた頂点に隣接する頂点で、訪れたことがないもの をスタックに入れる そして、スタックから頂点を取り出す ある頂点から先の探索が終わると、次に探索するべき頂点 が、スタックの最後にある スタックを使ったコード DFS_ (int v){ int j; STACK S; for ( j=0 ; j<n ; j++) flag[j] = -1; STACK_init ( &S, n); // スタックの初期化 STACK_push ( &S, v); // スタックに最初の頂点を挿入 flag[v] = 1; while ( S->t > 0){ // スタックが空になるまで繰り返す STACK_pop (S, &v); // スタックから頂点を取り出す for ( j=0 ; j<deg[v] ; j++ ){ // 接続する枝を順番に調べる if (flag[edge[v][j] < 0 ){ flag[edge[v][j] = v; // もと来た頂点を v に設定 STACK_push (&S, edge[v][j]) } // 訪問していなければ、スタックに入れる } } } STACK_end ( &S ); // スタックの解放(終了操作) } 探索の順番 • 先ほどの探索方法は、いわばスタック的に新しい頂点を見つ ける • それが、迷路をたどる操作とちょうど等価になっている • しかし、コンピュータは物理的に「たどる」必要がないので、探 索していない頂点なら、実はスタックに入っている頂点、どれ を選んでもいい ランダムに選んでもいい 探索済みの領域をじわじわと広げていくイメージ • スタックの代わりにキューを使ってもいい 探索の順番 • キューは、最初に入れたものが最初に出てくる 探索に使うと、出発点の近くが早くにキューに入る つまり、優先されて探索される • こういった、次に探索する頂点をキューに入れて探索する探 索法を幅優先探索という • 対して、先ほどのスタックを使うものは、深さ優先探索とよば れる 幅優先探索の性質 • 幅優先探索を実行すると、 • まず出発点に隣接する頂点(距離1の頂点)がキューに入る • 次に、距離1の頂点に隣接する頂点(距離2の頂点)がキュー に入っていく これら距離2の頂点は、距離1の頂点が全部なくなってから、 キューから出てくる そのとき、距離2の頂点は全部キューに入っている 距離2の頂点が全部キューに入ってから、距離2の頂点が処 理される 以後、距離kの頂点が全部キューに入ってから、距離k+1の頂 点が処理される 距離の計算 • つまり、幅優先探索を使うと、出発点からの距離がわかる (出発点から頂点までの、移動に使う枝の最小) • 各頂点をキューに入れるときに、その頂点の距離を、今の頂 点の距離+1にする • これより短く行くことはできないので、これが正しい距離になる 出てくる キューを使ったコード BFS (int v){ int j; QUEUE Q; for ( j=0 ; j<n ; j++) flag[j] = -1; QUEUE_init ( &Q, n ); // キューの初期化 QUEUE_ins ( &Q, v ); // キューに最初の点を挿入 flag[v] = 0; while ( Q->s != Q->t){ // キューが空になるまで繰り返す QUEUE_ext ( Q, &v ); // キューから頂点を取り出す for ( j=0 ; j<deg[v] ; j++ ){ if (flag[edge[v][j] < 0 ){ flag[edge[v][j] = flag[v]+1; // 距離を v までの距離 +1 に設定 QUEUE_ins (&Q, edge[v][j]) } // 訪問していなければ、キューに入れる } } } QUEUE_end ( &Q ); // キューの解放(終了操作) } 列挙アルゴリズム 列挙問題 • 列挙問題とは、与えられた問題の解を全て出力する問題 さっきから出てきている例は、列挙問題 - 入力した数の組合せで、合計がある範囲にあるものを全て 出力する - 入力グラフに含まれるパスを全て見つける - 入力グラフに含まれるクリークを全て見つける - 大きさ k までの2分木を全て出力する - 入力した文字列の部分文字列で、複数回現れるものを全て 見つける - 入力した数列の中から部分降順列を全て見つける • 列挙問題を解くアルゴリズムを列挙アルゴリズムという 列挙アルゴリズムの評価 • 列挙問題は自然に、指数個の解を持つ 出力にかかる時間だけで指数時間なので、多項式時間の 議論ができない • そこで、「出力の大きさ」もパラメータとして考え、計算時間が 入力と出力の大きさの多項式であるとき、出力多項式時間と いい、列挙アルゴリズムの効率性の指標にする • ただ、実際は出力が非常に大きいことが多いので、解1つあ たりの計算時間(入力の多項式時間)で評価することも多い 遅延 • 列挙問題の解の数は、計算してみないとわからない 出力多かったり、時間がかかったりする • 時間がかかる場合、途中でやめる、という選択肢があるが、 それならば、早めになるべく多くの解が手に入るほうがいい • そこで、解が1つ出てから次の解が得られるまでの計算時間 の最大値を遅延(あるいは遅延時間)と定義し、それで早め に解が出てくるかどうかを評価する 出力多かったり、時間がかかったりする • 遅延が入力の多項式なら多項式遅延、今まで見つけた解の 数、と入力の多項式なら逐次多項式時間
© Copyright 2025 ExpyDoc