基礎的なアルゴリズム
Download
1 / 97

基礎的なアルゴリズム - PowerPoint PPT Presentation


  • 58 Views
  • Uploaded on

基礎的なアルゴリズム. ・ 再帰呼び出し ・ 分割統治とバランス化 ・ グラフ探索 ・ 列挙法 ・ 動的計画. 再帰呼び出し. サブルーチンを作る. ・ サブルーチンとは、いわばプログラムの部品 ・ プログラム実行中、そこに行き一定の処理をして、またもとの場所に帰ってこれる ・ 通常プログラムを作る際には、作業を階層的に「部品化」し、設計をやりやすくする - 作業単位がわかる - 全体の見通しがよくなる ・ サブルーチンを実行することを、 呼び出す という. 再帰呼び出し. ・ 通常の部品の概念では、部品の中に同じ部品が入ることはない

loader
I am the owner, or an agent authorized to act on behalf of the owner, of the copyrighted work described.
capcha
Download Presentation

PowerPoint Slideshow about ' 基礎的なアルゴリズム' - abigail-puckett


An Image/Link below is provided (as is) to download presentation

Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author.While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server.


- - - - - - - - - - - - - - - - - - - - - - - - - - E N D - - - - - - - - - - - - - - - - - - - - - - - - - -
Presentation Transcript
基礎的なアルゴリズム

・ 再帰呼び出し

・ 分割統治とバランス化

・ グラフ探索

・ 列挙法

・ 動的計画



サブルーチンを作る

・サブルーチンとは、いわばプログラムの部品

・ プログラム実行中、そこに行き一定の処理をして、またもとの場所に帰ってこれる

・通常プログラムを作る際には、作業を階層的に「部品化」し、設計をやりやすくする

- 作業単位がわかる

- 全体の見通しがよくなる

・サブルーチンを実行することを、呼び出すという


再帰呼び出し

・通常の部品の概念では、部品の中に同じ部品が入ることはない

・ しかし、サブルーチンは、自分自身を呼び出せる

(サブルーチンが自分自身を呼び出すことを再帰呼び出しという)

・自分を呼び出すと、サブルーチンの最初から、呼び出すところまでを繰り返すループになる

 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回求めると、もとの行列の積が求まる

同じ問題を解いているので、再帰呼び出しできる

・さて、これは速くなっているのでしょうか???

解析してみないと、よくわからない。。。

AE

+BG

AF

+BH

A

B

E

F

×

CE

+DG

CF

+DH

C

D

G

H


計算時間の再帰式

・ 計算が再帰的なので、計算時間の再帰式を作ろう

・ この方法でn次元行列のかけ算をする時間を T(n)とする

・ 問題を分割したり、積の足し算をして解を求める時間は O(n2)であるので、定数を使って、cn2時間であるとする

・ 再帰は、大きさ n/2 の問題について 8回行われるので、

T(n) = cn2 + 8T(n/2)

となる

AE

+BG

AF

+BH

A

B

E

F

×

CE

+DG

CF

+DH

C

D

G

H


計算時間の再帰式

・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 > logcbならば、トップレベルが全体の計算時間を支配し、計算量は O(na) となる

 - a < logcbならば、一番下のレベルが全体の計算時間を支配し、計算量は O(nlogc b) となる

 - a = logcbならば、各レベルの計算時間は等しくなり、計算量は高さのファクターが入り、O(nalog n) となる


行列積の高速化

・ 先の問題の分割では、答えの行列をブロックごとに求めていた

その結果、8回のかけ算を必要としている

行列積は a < logcb (2 < log28)の場合であるので、

再帰呼び出しの回数 8を減らすか、子問題の大きさ n/2 を小さくしなければならない

・ 工夫して、再帰の数、つまりかけ算の数を減らした人がいる

AE

+BG

AF

+BH

A

B

E

F

×

CE

+DG

CF

+DH

C

D

G

H


分配則の利用

・ 行列を足してからかけると、行列積の和が得られる

例)(A + B)E = AE + BE B(G – E) = BG – BE

・ これらをうまく組合わせて足し合わせると、目的である行列積が出てくる

例)(AE + BE) + (BG – BE) = AE+BG

AE

+BG

AF

+BH

A

B

E

F

×

CE

+DG

CF

+DH

C

D

G

H


再帰式の変化

・ うまい和積取り方と組合せ方を考えて、7回の行列積で、4つのブロックの解が得られるようにした

・ 計算時間の再帰式は、T(n) = cn2 + 8T(n/2)から

T(n) = cn2 + 7T(n/2)になった

・log27 = 2.80… なので、およそ O(cn2.80) 時間になる

・ この方法で一番すごいやつは、100個以上の行列に分解して同様のことを行い、O(cn2.31…) 時間を達成

AE

+BG

AF

+BH

A

B

E

F

×

CE

+DG

CF

+DH

C

D

G

H



疎な行列のかけ算はどうなる?

・ 疎な行列は、疎な持ち方をすると効率が良い

・ 疎な持ち方にあったかけ算の仕方をしたとき、計算時間はオーダーの意味で改善されているのかな?

 最悪の場合、全部のセルに値が入るので、改善されない

・ しかし、疎な行列に興味があるので、行列の疎性に応じた速さの評価がしたい

・ 行列の大きさの他に、非ゼロのセルの数を使って、行列積の計算時間を評価してみよう


行ごとに格納

・各要素へのアクセスを良くするため、少々構造をつけよう

・ まずは、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

1

x21 x22 ・・・ x2n

1

1

×

・・・

1

xn1 xn2 ・・・xnn

1


連立方程式を解く

・ 解の i 行 j 列 a1jxi1 + a2jxi2 + ・・・ + anjxin = 1 (あるいは0)

こういう方程式が n2本ある

・ 一方、変数の数は n2個  退化していなければ解ける

・ 線型方程式なので、代入していけば解ける

それを機械的にやるようにすれば、アルゴリズムができる

x11 x12 ・・・ x1n

1

x21 x22 ・・・ x2n

1

1

×

・・・

1

xn1 xn2 ・・・xnn

1


1つずつ代入

・ こういう方程式を解くときは、ある変数について方程式を解き、それを残りに代入する

例)a1jxi1 + a2jxi2 + ・・・ + anjxin = 1

 xi1 = (1 - (a2jxi2 + ・・・ + anjxin)) / a1j

・ こういう操作をやって、すべての変数を消せばよい

・ 1行1列から順に代入を使って変数を消すアルゴリズムができる

x11 x12 ・・・ x1n

1

x21 x22 ・・・ x2n

1

1

×

・・・

1

xn1 xn2 ・・・xnn

1


計算時間

・1つの変数を消すには、代入の式を作り(O(n2)時間)、それを他の全ての式 (O(n2)個) に代入 (O(n2)時間)

 変数は n2 個あるので、O(n6) 時間

・ 次に、もう少し速い方法を紹介しましょう

逆行列はO(n6) 時間で求められる

x11 x12 ・・・ x1n

1

x21 x22 ・・・ x2n

1

1

×

・・・

1

xn1 xn2 ・・・xnn

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

A-1

-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つ出てから次の解が得られるまでの計算時間の最大値を遅延(あるいは遅延時間)と定義し、それで早めに解が出てくるかどうかを評価する

出力多かったり、時間がかかったりする

・ 遅延が入力の多項式なら多項式遅延、今まで見つけた解の数、と入力の多項式なら逐次多項式時間


ad