情報工学概論 (アルゴリズムとデータ構造) アルゴリズムの概念 ~ オーダーと計算量 ~ ・ コンピュータの基礎 ・ アルゴリズムの概念 ・ オーダーの定義と計算量 ・ NP完全問題 自己紹介 名前: 定兼 邦彦 所属: 国立情報学研究所 & 総合研究大学院大学 経歴: 1991年理1入学,2000年理学系研究科情報科学専攻終了 2000年 東北大学情報科学研究科助手 2003年 九州大学システム情報科学研究院助教授 2009年 国立情報学研究所准教授 研究分野: データ圧縮,データ構造,情報検索 最近の研究: • 簡潔データ構造 • 文字列検索のデータ構造 • Webグラフ構造の解析,探索 目的・成績評価・参考書 ・ アルゴリズム理論による設計は、大規模なデータを高速で処理 する際に特に有効 ・ 部品 (データ構造) を組み合わせてプログラムを作るだけでなく, 部品の中身を理解する ・ 成績評価はレポート ・ 参考書:アルゴリズムイントロダクション(近代科学社)など ・ メール: [email protected] ホームページ: http://researchmap.jp/sada/ 授業の内容 ・ コンピュータの仕組み、プログラムの仕組み CPU、メモリ、OS、メモリ管理、命令、変数 ・ データ構造(データの記憶方法) スタック、キュー、ヒープ、2分木、ハッシュ ・ 基本的なアルゴリズム 再帰呼び出し、分割統治、列挙、動的計画法、ソート、文字列マ ッチング ・ 応用アルゴリズム 計算幾何,行列演算,索引構造 コンピュータの基礎 コンピュータの基礎 ・ コンピュータはいくつかの部品で成り立っているが、メインの 部分はCPUとよばれる演算装置 ・ 車でいえばエンジンのようなもので、これがあるかないか が、コンピュータであるかどうかの境目のようなもの ・ CPUの他にメモリという、数値を複数記録できる装置がある CPU メモリ プログラム ・ コンピュータができることは、 - メモリから数値を読むこと、および書くこと - 足す引く掛けるなどの算術演算をすること - 数の大小の判定をして、行う処理を変えること ・ これらの操作のことを命令という ・ 命令をどのように行うかは、やはりメモリに記録する。これを プログラム と言う ・ プログラムに書かれたとおりに処理を行わせることを、プロ グラムを 実行する という CPU メモリ プログラムの実行 ・ プログラムは、メモリに書いてある命令を順次実行する ・ 命令は数値になっていて、それぞれの数値に機能が割当て られている ・ 処理中に、次から行う処理の場所を変更することができる ・ 条件によって処理を変える場合には、この機能を使って、条 件を満たす場合のみ次から実行する命令を変える(条件 分岐という) CPU メモリ プログラミング言語 ・ CPUの命令は数値。しかも、1つ1つの処理は非常に単純(これ をマシン語、あるいは機械語という) ・ これを組合せてプログラムを作るのは、かなり大変 ・ そこで、通常はもう少し抽象化して、人間に見やすくした「高級 言語」とよばれるものを使う (C,JAVA,Perl, Ruby, Basic, シェルスクリプトなど) ・ 高級言語で書かれたプログラムを、CPUに実行させるには、プ ログラムをいったんマシン語に翻訳する(この作業をコンパイ ルという)か、プログラムどおりの処理をするプログラム(インタ ープリタという)を使う 概念的な例 1: 場所 15 に 0 を書き込む 2: 場所 16 に 2 を書き込む 3: 場所 15 と 16 の値を足し、それを場所 15 に書き込む 4: 場所 17 の値が 10 以上ならば、8へいく 5: 3へ行く 8: ... ・ 3-5のように局所的に繰り返して実行される部分をループ という CPU メモリ メモリのアクセス ・ メモリは1バイトずつ数値が記憶されている ・ メモリには、記憶場所に0から始まる番号がついていて、好きな場 所のデータに一手でアクセス(書き込み/読み出し)できる ・ このように、好きな場所に一手でアクセスできるメモリをランダムア クセスメモリという ・ ランダムアクセスでない記憶装置は、例えばCDとかビデオテープ 1 0 1 1 0 1 0 1 キャッシュメモリ ・ コンピュータのメモリのアクセス速度は、演算速度よりかなり遅 い(10倍以上) ・ そのため、メモリを読み出すときは、しばらく演算をとめて待つこ とになる。次の命令を読むときもそう。 ・ そこで、メモリを読むときはまとめてたくさん読む ・ 読んだメモリは、CPUに直結した速いメモリにしばらく取っておく (この操作をキャッシュ、キャッシュに使うメモリをキャッシュメモ リという) キャッシュ CPU メモリ メモリ ディスク キャッシュによる高速化 ・ メモリアクセスが、キャッシュに入っている場所ばかりだと計算は 大幅に速くなる キャッシュの効率を高めるような保存法が重要 引き続いて、キャッシュの中を見ることが多い計算をさせると、 高速化できる ・ ディスクのアクセスにも、同じようにキャッシュを使う CPU直結メモリの代わりに、通常のメモリを使う。メモリのほうが ディスクよりアクセス速度が速いので、高速化できる CPU メモリ HDD 変数 ・ 高級言語では、数値を記憶する際に変数というものを使う ・ 変数には、何か値が1つ入る ・ コンパイルして実行するときに、各変数にメモリの場所が割当て られる。以後、変数をアクセスするときには、その場所にアクセ スするようになる 配列 ・ 大量のデータを扱う場合、全てのデータに直接変数を割当てる のは、大変。プログラムを書くのも大変 ・ そこで、配列を使う ・ 配列を使うと、変数+添え字、という形でデータにアクセスでき る。添え字のところは変数を入れられるので、例えば、100個の 変数を0にする、といった作業も、ループを使って楽にできる A[0] = 1 A[1] = 0 A[2] = 1 … 1 0 1 1 0 1 0 1 OS ・ コンピュータは、機種によって、入出力の方法が違う ・ ディスプレイに文字を書くためには、文字の形になるよう、デー タを書き込まなければいけない ・ こういった、ハードウェアによる違いを吸収する、低レベルの処 理を行う、実行するプログラムの管理、などをするプログラム がある ・ これをOSという (Windows、UNIXなど) ・ 固有の接続機器を、標準的な入出力方法を用いて扱うプログラ ムをデバイスドライバという ・ メモリの管理もOSが行う メモリの確保 ・ 普通、コンピュータでは、複数のプログラムが同時に実行されて いる(含デバイスドライバ) ・ そのため、メモリの適当な場所に適当に数値を書き込むと、他の プログラムの実行を阻害する(下手をすると動きが止まる) ⇒まともなOSはそのような動作を禁止している ・ そのため、メモリを使いたいときには、OSにお願いして、必要な 分だけ使える場所をあてがってもらう(これをメモリを確保すると いう) 数値の表現(バイト) ・ メモリは数値を覚えられるが、実は 0 と 1 しか覚えられない (ビットという) ・ しかし、大量に 01 の数値を記憶できる ・ 実際は、01しか記憶できないのでは用をなさないので、01 の数値を8個セットにして、それを2進数とみなす。(0-255が 表現できる) - これを 1バイトという ・ 足す引く掛けるの算術演算の結果、 2進数9桁目に繰り上 がったとき、あるいはマイナスになったときは、その部分は 無視して計算する 1 0 1 1 0 1 0 1 128+32+16+4+1 = 181 データの記憶 ・ メモリには、記録されている数値が整数か、文字か、といった、 データの種類を覚えておく機能はないので(数値として付加的 に記録することはできるが)、自分(プログラム)が何を書いた か管理する ・ つまり、「何のデータはどこに書いた」ということを決めておく。プ ログラムでは、データの計算をする際には、場所を決めて書く ・ 常に0か1の値が書いてあるので、「書き込まれていない」という ことも検出できない 点数 点数 20 点数 20 20 1 0 1 1 0 1 0 1 整数小数文字 - 整数: 01の数値をいくつかセットにして、それを2進数とみなす。 通常、32ビット = 4バイト。 (0-40億くらい) - 実数: 整数と小数点の位置をセットで記憶。小数点は2進数の 位置で記憶。通常、整数が56ビット、小数点が8ビット(256桁分) 演算誤差が生じる (1/3 * 3 = 0.999999…) - 文字: 文字と整数値を対応させたコード表があり、それを使っ て整数値として記憶する 1 0 1 1 0 1 0 1 + 128+32+16+4+1 = 181 0 1 0 1 (4+1)/16 = 0.3125 負の数の表現(バイト) - 負の数: 最上位のビットが1になった数は、256を引いて、 負の数とみなす。通常の数と思って計算したときと、負の 数と思って計算したときの結果が同じなので、結果を変換 する必要がない 例: 255 は-1 になる。1を足すと、 1111111 + 1 = 100000000。9桁目は無視するので、0 これは、実際の -1 +1 の答えと一致する 例: -2 は 254、-5 は 251。両者を足すと、505。256の剰余を 求めると249。これは-7に対応。両者を掛けると 63754。剰 余を求めると 10。 C言語では • char: 符号ありバイト -128 から 127 を表す • unsigned char: 符号なしバイト 0 から 255を表す 桁あふれ (overflow) に注意 符号なし: 30 * 5 = 150 符号あり: 30 * 5 = -106 間違い アルゴリズム • アルゴリズムとは – 入力 (input): ある値(の集合) – 出力(output): ある値(の集合) – 明確に定義された計算手続き • 明確に定義された計算問題を解くための道具 • 問題の記述とは – 望むべき入出力関係を指定すること ソーティング問題の形式的定 義 • 入力: n 個の数の列 〈a1, a2, ..., an〉 • 出力: a’1 a’2 … a’n であるような入力 列の置換 〈a’1, a’2, ..., a’n〉 • 入力例 (具体例, instance) – 入力 〈31, 41, 59, 26, 41,58〉 – 出力 〈26, 31, 41, 41, 58, 59〉 ソーティング • 計算機科学における基本的な操作 • 多くのアルゴリズムが開発されている • 入力例によって優劣が異なる – ソートすべきデータの数 – どの程度までデータがすでにソートされているか – 用いる記憶装置の種類 (主記憶, ディスク, テープ) アルゴリズムの正しさ • アルゴリズムが正しい (correct) ⇒全ての具体例に対して正しい出力とともに停止する – 与えられた計算問題を解く (solve) という. • 正しくないアルゴリズム – ある具体例に対して望ましい答えを出力せずに停止 – ある具体例に対して全く停止しない • 確率的アルゴリズムも存在 – 高い確率で正しい答えを出す or 早く停止する 挿入ソート • 入力: 長さ n の配列 A[0..n-1] • 出力: ソートされた配列 A[0..n-1] void INSERTION-SORT(data *A, int n) { data key; int i, j; for (j=1; j < n; j++) { key = A[j]; // A[j] をソート列 A[0..j-1] に挿入する i = j – 1; while (i >= 0 && A[i] > key) { A[i+1] = A[i]; i = i -1; } A[i+1] = key; } } 5 2 4 6 1 3 2 5 4 6 1 3 2 4 5 6 1 3 2 4 5 6 1 3 1 2 4 5 6 3 1 2 3 4 5 6 入力 出力 C言語の場合 #include <stdio.h> #include <stdlib.h> typedef int data; void INSERTION_SORT(data *A, int n) { data key; int i, j; for (j=1; j < n; j++) { key = A[j]; // A[j] をソート列 A[0..j-1] に挿入する i = j - 1; while (i >= 0 && A[i] > key) { A[i+1] = A[i]; i = i - 1; } A[i+1] = key; } } int main(int argc, char *argv[]) { data A[14] = {27,17,3,16,13,10,1,5,7,12,4,8,9,0}; int i,n; n = 14; INSERTION_SORT(A,n); for (i=0;i<n;i++) printf("%d ",A[i]); printf("\n"); } Rubyの場合 def insertion_sort(a) for j in 1..(a.length-1) do key = a[j] i = j-1 while i >= 0 && a[i] > key do a[i+1] = a[i] i = i-1 end a[i+1] = key end end a = [27,17,3,16,13,10,1,5,7,12,4,8,9,0] insertion_sort(a) pa Fortran 90の場合 program main do j = s+1, t key = a(j) i = j-1 do if (.not. (i >= s .and. a(i) > key)) exit a(i+1) = a(i) i=i-1 end do a(i+1) = key end do end subroutine insertionsort integer :: a(14) = (/ 27,17,3, 16,13,10,1,5,7,12,4,8,9,0 /) print *, "a = ", a print *, "insertion sort" call insertionsort(a) print *, "a = ", a contains subroutine insertionsort(a) integer a(:) integer i, j, key integer s, t end program main s = lbound(a,1); t = ubound(a,1) アルゴリズムの解析 • アルゴリズムの実行に必要な資源を予測したい – メモリ量 – 通信バンド幅, 論理ゲート – 計算時間 • 解析を行うにはモデルを仮定する必要がある • RAM (random access machine) モデル – 命令は1つずつ実行 – どのメモリ番地も一定の時間で読み書き可 挿入ソートの解析 • INSERTION-SORTの手続きに要する時間は入 力によって変わる. • 一般に, プログラムの実行時間は入力のサイズ の関数で表す. • 入力サイズの定義 – ソーティング, 離散フーリエ変換など: データ数 – 整数の積の計算など: 入力のビット数 – グラフの問題: グラフの頂点と辺の数 実行時間の定義 • 実行される基本的な演算の回数 • プログラムの第 i 行の実行に ci 時間かかるとす る (ci は定数) • 注: サブルーチン呼び出しは定数時間ではない void INSERTION-SORT(data *A, int n) { data key; int i, j; for (j=2; j <= n; j++) { key = A[j]; // A[j] をソート列 A[1..j-1] に挿入する i = j – 1; コスト 回数 c1 c2 n n-1 c4 n-1 n while (i > 0 && A[i] > key) { c5 t j 2 n A[i+1] = A[i]; c6 (t j 1) (t j 1) j 2 n i = i -1; c7 j 2 } A[i+1] = key; c8 j n-1 } } tj : while ループが j の値に対して実行される回数 INSERTION-SORTの実行時間 T (n) c1n c2 (n 1) c4 (n 1) n n n j 2 j 2 j 2 c5 t j c6 (t j 1) c7 (t j 1) c8 (n 1) tj の値は入力によって変化する. 最良の場合 = 配列が全てソートされている場合 tj = 1 T (n) c1n c2 (n 1) c4 (n 1) c5 (n 1) c8 (n 1) (c1 c2 c4 c5 c8 )n (c2 c4 c5 c8 ) an b n の線形関数 (linear function) 最悪の場合 • 配列が逆順にソートされている場合 – tj = j n(n 1) T (n) c1n c2 (n 1) c4 (n 1) c5 1 2 n(n 1) n(n 1) c6 1 c7 1 c8 (n 1) 2 2 an bn c 2 n の2次関数 (quadratic function) 時間計算量 (Time Complexity) • アルゴリズムの実行時間は入力に依存する • アルゴリズムの解析では通常最悪時の実行時 間を考える – 最悪時の実行時間は任意の入力に対する実行時 間の上界 – 最悪時の実行時間を保証する – 「最悪時」は頻繁に起きる (データベース検索など) – 平均時も最悪時と同じくらい悪い (挿入ソートで数 をランダムに挿入) 増加のオーダ • 実行時間の解析を容易にするための抽象化 – – – – – 各行の実行時間 (コスト) を定数 ci とする 最悪の実行時間を an2+bn+c と表す 実行時間の増加率をみるには主要項 an2 で十分 定数係数も無視 (n2) と表す • 挿入ソートは (n2) という最悪実行時間を持つ • あるアルゴリズムが他より効率がよい ⇔最悪実行時間の増加率が低い 関数のオーダ • アルゴリズムの効率を実行時間のオーダ で特徴付け, 相対的な比較を行う • 入力サイズ n が大きいときの挙動を知りた い • アルゴリズムの漸近的 (asymptotic) な効 率を調べる 漸近記号 -記法 • ある関数 g(n) に対し, (g(n)) は次のような集 合と定義する (g(n)) = {f(n): 全ての n n0に対して 0c1 g(n) f(n) c2 g(n) となるような 正の定数c1 , c2 , n0 が存在} ( g (n)) { f (n) : c1 0c2 0n0 0n n0 0 c1 g (n) f (n) c2 g (n)} • f(n) = (g(n)) は f(n) (g(n)) を意味する • 2n2+3n+1 = 2n2 + (n) という表現も用いる 1 2 n 3n (n 2 )を示す 2 1 2 全ての n n0に対して c1n n 3n c2 n 2となればいい 2 1 2 2 2 n で割ると c1n n 3n c2 n 2 2 1 c2 なら n 1に対して右辺の不等号 が成立 2 1 c1 なら n 7に対して左辺の不等号 が成立 14 1 1 よって c1 , c2 , n0 7とすれば成立 14 2 2 6n (n )を背理法で示す 3 2 全ての n n0に対して 6n 3 c2 n 2であるような 定数c2 , n0が存在したとする 任意の大きな nに対して 6n c2となり矛盾 f (n) an 2 bn c (n 2 ) d 任意の d次多項式p(n) ai n iに対し (ad 0) i 0 p ( n ) ( n d ) O-記法 • ある関数 g(n) に対し, O(g(n)) は次のような集 合と定義する O(g(n)) = {f(n): 全ての n n0に対して 0 f(n) c g(n) となるような 正の定数c, n0 が存在} O( g (n)) { f (n) : c 0n0 0n n0 0 f (n) cg (n)} • f(n) = (g(n)) ならば f(n) = O(g(n)) • n = O(n2) も正しい表現 • 挿入ソートの実行時間はO(n2)…正解 • 挿入ソートの実行時間は(n2)…間違い – ソートされた入力に対しては(n) • 実行時間がO(n2)であるという場合, 最悪実行 時間について言っている – 実行時間は入力データに依存するため – 最悪実行時間はデータ数 n のみに依存 -記法 • ある関数 g(n) に対し, (g(n)) は次のような 集合と定義する (g(n)) = {f(n): 全ての n n0に対して 0 c g(n) f(n) となるような 正の定数c, n0 が存在} O( g (n)) { f (n) : c 0n0 0n n0 0 cg (n) f (n)} • f (n), g(n) に対して f(n) = (g(n)) であるため の必要十分条件は f(n) = O(g(n)) かつ f(n) = (g(n)) • -記法は下界を表す • 挿入ソートの最良の実行時間は(n)とは – このアルゴリズムではどのような入力に対して も n に比例した時間が必ず必要という意味 • アルゴリズムの実行時間が(n2)とは – どのような入力に対しても少なくとも n2 に比例 する時間がかかるという意味 o-記法 (リトルオー) • ある関数 g(n) に対し, o(g(n)) は次のような集 合と定義する o(g(n)) = {f(n): 任意の定数 c > 0 に対し ある定数 n0 が存在し, 全ての n n0に対して 0 f(n) c g(n)} o( g (n)) { f (n) : c 0n0 0n n0 0 f (n) cg (n)} f ( n) lim 0が成り立つ n g ( n) • -記法 (g(n)) = {f(n): 任意の定数 c > 0 に対し ある定数 n0 が存在し, 全ての n n0に対して 0 c g(n) f(n)} • n2/2 = (n) • n2/2 (n2) f (n) ( g (n)) g (n) o( f (n)) f ( n) lim が成り立つ n g ( n)
© Copyright 2024 ExpyDoc