情報科学(5) - lecture.ecc.u-tokyo.ac.jpshagiya/slide5.pdf例: 銀行 オンラインシステム...

52
情報科学(5) アルゴリズムと計算量

Upload: others

Post on 29-Jan-2021

1 views

Category:

Documents


0 download

TRANSCRIPT

  • 情報科学(5)

    アルゴリズムと計算量

  • 例: 銀行オンラインシステム

    復習: 問題解決とアルゴリズム(cf. 「情報」6.1.1)

    • コンピュータによる問題解決ではアルゴリズムが重要になる

    – いきなりプログラムを書くことはまれ※問題:

    – コンピュータにやらせたいこと全般– 計算問題から,長期間のサービスまで

    ※モデル: 問題を抽象化したもの

    問題 モデル アルゴリズム プログラム

  • アルゴリズムとは

    • 9 世紀の数学者 al-Khwarizmi の名に因む• 問題を解くための手順

    – 有限の手順で答えを出すもの– 曖昧さが(あまり)ない– 特定のプログラミング言語に依らない– コンピュータに実行させるとは限らない

    • 問題の例– 「整数 x が素数かどうかを判定する」– 「整数 x と y の最大公約数を求める」

    答えが出るかどうか分からないものは

    アルゴリズムとは言わないcf. コラッツ予想

  • アルゴリズムのない問題は?

    1. 整数を素因数分解する2. 代数方程式の整数解を求める3. 将棋の必勝手を求める4. 数独パズルを解く5. 路線図の最短経路を求める

  • アルゴリズムの役割

    • 時間やメモリに関する性質を考えることができる– 良し悪しが分かる → 計算量 (後述)

    • 具体的な問題と独立して考えることができる– 問題における細かな点を無視して,解法の重要な点だけを考える

    – 異なる問題も同じアルゴリズムで解ける場合がある

    • 具体的なプログラムと独立して考えることができる– プログラムにおける細かな点を無視して考えられる

    – 特定のプログラミング言語に依らない

  • 1つの問題に複数のアルゴリズムがあり得る

    • アルゴリズムが違えば計算時間も変わる

    • プログラミング・テクニックによる速度の違いよりも大きいことが多い

    • 例: 高速フーリエ変換 (Cooley と Tukey が 1965 年に再発明)– 周期関数を周波数成分に分解する

    アルゴリズム

    • 単純な方法では成分数 N の2乗に比例する時間がかかるところを,このアルゴリズムでは N×log(N) に比例する時間で済ませられる

    – 類似アルゴリズムである離散コサイン変換は,音声や映像の圧縮・信号処理などに幅広く使われている

    © NDTnet - [email protected]

  • アルゴリズムによる実行時間の違い

    • アルゴリズムによる計算時間の違いを実際のプログラミングによって調べる

    • 問題とアルゴリズム:– 平方根の計算: 数え上げ/二分法/ニュートン法

    – フィボナッチ数: 定義通り/数え上げ/行列– 最大公約数: 単純なもの/ユークリッドの互除法

    – 整列: 単純ソート/併合ソート

  • フィボナッチ数の計算

    • 問題: フィボナッチ数列の第 k 要素の数を求める– フィボナッチ数列: 1,1,2,3,5,8,13,21,34,…– 第 k 要素を fk と書くと fk =fk−1 + fk−2 となる数列ただし f0 = f1 = 1

    • アルゴリズム– 定義通りに計算する方法– 第0要素から順に数え上げる方法– 行列を用いる方法

  • 100番目のフィボナッチ数は?

    1. 偶数2. 奇数

    ただし,最初のフィボナッチ数を1番目と数える

  • 定義通りに計算するフィボナッチ数

    • 定義: – 第 k 要素を fk と書くと

    fk =fk−1 + fk−2ただし f0 = f1 = 1

    • fk を関数 fibr(k)にすればよい

    • 計算時間は?

    def fibr(k)if k==0 || k==11

    else fibr(k-1)+fibr(k-2)

    endend

    もっと要領のいい書き方は?

  • 第0要素から順に数え上げるフィボナッチ数の計算

  • 第0要素から順に数え上げるフィボナッチ数の計算

    • 計算時間は?

    def fibl(k)f=1p1=1for i in 2..kp2 = p1 # fib(i-2)p1 = f # fib(i-1)f = p1 + p2 # fib(i)

    endf # fib(k)

    end

  • スピード競争

    • 共通資料から,bench.rb と fib.rb をダウンロード

    irb(main):004:0> load("./bench.rb")

    irb(main):005:0> load ("./fib.rb")

    irb(main):006:0> run("fibr", 10)

    irb(main):007:0> run ("fibl", 10)

    irb(main):009:0> for k in 10..24

    irb(main):010:1> run("fibr", k)

    irb(main):011:1> run("fibl", k)

    irb(main):012:1> end

  • irb(main):026:0> command("set logscale y")

    irb(main):027:0> for k in 10..32

    irb(main):028:1> run("fibr", k)

    irb(main):029:1> end

  • def fibl6(k)f=1p1=1for i in 2..kp2 = p1p1 = ff = (p1 + p2) % 1000000

    endf

    endirb(main):030:0> reset()

    irb(main):031:0> command("unset logscale")

    irb(main):032:0> for m in 1..10

    irb(main):033:1> run("fibl6", 100000*m)

    irb(main):034:1> end

    下 6 桁

  • 行列を用いるフィボナッチ数の計算法 (1)

    • (fk+1, fk)をベクトルだと思うと

    という関係がある

    =

    =

    =

    +

    11

    0111

    0111

    0111

    0

    1

    1

    1

    kk

    k

    k

    k

    k

    ff

    ff

    ff

  • 行列を用いるフィボナッチ数の計算法 (2)

    • Qk を高速に計算する方法はあるか?• Q2k=(Qk)2 という関係を利用する

    – 例: Q16=(Q8)2=((Q4)2)2=((Q2)2)2)2 ―― 2乗を4回

    =

    =

    +11

    11

    01111 k

    k

    k

    k Qf

    f

    ==

    − )odd is ()()even is ()(

    )0(

    1

    22/

    kQQkQkE

    Qk

    kk

  • Q の 8 乗は?

    答えを

    [email protected]にメール

    行列の書き方は自由

    答えられなかった場合でもメールを送りなさい(出席調査のため)

  • 020406080100

    フィボナッチ数の計算:アルゴリズムの違い

    • 定義通り

    • 数え上げ

    • 行列を使う

    fib(10)fib(9)

    fib(8)

    fib(8)

    fib(7)

    fib(7)

    fib(6)

    fib(7)

    fib(6)

    fib(6)

    fib(5)

    fib(6)

    fib(5)

    fib(5)

    fib(4)

    (0,1,1) (1,2,1) (2,3,2) (3,5,3) (4,8,5) (5,13,8)

    Q100 Q50

    Q25

    Q24

    Q12

    Q6

    Q3

    Q2

    Q1

    Q0

  • 整列 (sorting)• 問題: n 個のデータを大小順に並べる

    (単純化): n 個の整数を小さい順に並べる– 色々な場面で使われる– データの「下ごしらえ」としても使われる

    例: 検索を高速化するために整列しておく

    • データ数は,時として非常に大きくなる– 「全学生の氏名を学生証番号順に表示」– 「『今日の料理』という言葉を持つページを信頼度順に表示」

  • 国語辞典で先に来るのは?

    1. しょうほう2. しようほう3. じょうほ4. じょうほう5. ショー

    最初だけではなく,すべてを順序付けてみよう

  • 整列アルゴリズム

    とても沢山ある

    • 単純整列法: 1番小さいものを見つけ,2番目に小さいものを見つけ,...

    • 併合整列法:

    • ビン整列法• 基数整列法• クイック整列法

  • 単純整列法

    •方法:– i=0,1,2,…について列の中で i 番目に小さい数を見つけ,それを第 i 要素へ移動

    – 「i 番目に小さい数」とは列の第 i 要素以降の最小値

    第0要素から最小値を探す

    第0要素と交換

    第1要素と交換

    第1要素から最小値を探す

    第2要素と交換

    第2要素から最小値を探す

  • def min_index(a, i)min = i for j in (i+1)..(a.length()-1)if a[j] < a[min]min = j

    endendmin

    end

    def simplesort(a) for i in 0..(a.length()-1)k = min_index(a, i)tmp = a[i]a[i] = a[k]a[k] = tmp

    enda

    end

    単純ソート

    配列 a の第 i 要素から終わりまでの間での最小要素の添え字を返す

    第0要素から順にそれ以降の最小要素を入れていく

    simplesort.rb

  • ソートプログラムの動きを自分で可視化して理解しよう

    • 数値配列を背の高さの違う人形の並びとして表現– 人形同士には身長を比較する不等号演算が定義されている

    > >=

  • 人形の並びの生成(すべてのプログラムに共通)

    def index_to_x(i)i * 30 + 60

    end

    def person16()a = Array.new(16)for i in 0..15a[i] = Person.new()a[i].x = index_to_x(i)a[i].y = 400a[i].height = rand(60) + 40

    enda

    end

    第 i 要素の人形の x 座標y座標の初期値は400固定

    480

    640

    (0,0)

    アニメ画面

    • Person.new()で人形を生成• その時点で左上に表示される• 人形の x 座標と y 座標を各々 .x .y と記述(文字列の長さを .length() と書くのと同様),代入も可能

    • 人形の身長は .height と記述

    0以上60未満の乱数

    デフォールトの画面には16個程度が最適

    x

    y

    ()があったりなかったりするが,オブジェクト指向の回に詳しく説明

  • 人形の入れ替え

    tmp = a[i]a[i] = a[k]a[k].move(index_to_x(i), 400)a[k] = tmptmp.move(index_to_x(k), 400)

    a[k]にあった人形を第 i 要素の位置へ動かす

    tmpに一時的に保存してあった人形を第 k 要素の位置へ動かす

    本来のプログラムとうまく対応していることに注意

    .moveに注意

    横に動くだけならyは初期値のまま

  • 人形のフラッシュ

    • 人形はnewされたときに画面上に黒地に白で表示される• これをふわっと一瞬色をつけるには,これまでに使ってきた

    RGB配列を使って人形.flash(秒,RGB配列)

    と書く (秒は実数)

    • フラッシュしたあとは元の色に戻る• 色のデフォールト(色を指定しなかったときの標準色)は [1, 0,

    0] つまり赤色, たとえばa[k].flash(0.5)

    • 自分でいろいろいじって見るとよい

  • 人形の挙手,表示・非表示,色

    • 人形の手を挙げ下げするには人形.raise_hand()人形.lower_hand()

    • 人形を非表示にするには人形.hide()

    – 再び表示するには 人形.show()– なお,人形はnewで生成されると自動的に表示される

    • 人形に色をつけるには– 人形.color = RGB配列– 人形をnewで作ると最初は[1, 1, 1],つまり白

  • include(Komaba::MiniStar)

    def index_to_x(i)i * 30 + 60

    end

    def person16()a = Array.new(16)for i in 0..15a[i] = Person.new()a[i].x = index_to_x(i)a[i].y = 400a[i].height = rand(60) + 40

    enda

    end

    animated_simplesort.rb

    すべてのアニメーションプログラムに共通

    (以下のアニメプログラムでは省略)

    ここにsimplesort.rbのmin_indexの定義を入れる

    def simplesort(a)for i in 0..(a.length()-1)k = min_index(a, i)a[k].flash(0.5)tmp = a[i]a[i] = a[k]a[k].move(index_to_x(i), 400)a[k] = tmptmp.move(index_to_x(k), 400)

    enda

    end

  • アニメーションプログラムの実行

    • isrbでアニメーション入りのRubyプログラムをロード• 以下のように人形配列をランダムに生成して実行

    a=person16()simplesort(a)

    • もう一度やりたい場合は,表示されている人形を以下のように消して上を繰り返せばよい

    for i in 0 .. (a.length()-1)a[i].hide()

    endこれをhide_personsという関数にしてファイルに入れておくと便利(自分で行いなさい)

    人形の表示を消す.show()で再び表示

  • def min_index(a, i)min = i for j in (i+1)..(a.length()-1)a[j].raise_hand() # raise hands of compared personsa[min].raise_hand() # a[j].lower_hand() # and soon lower thema[min].lower_hand() # (It is enough to show the comparison.)if a[j] < a[min]min = j

    endendmin

    end

    simplesortなど他の関数の定義は同じ

    more_animated_simplesort.rb

    比較される人形が手を挙げる

    比較回数が見えるので単純ソートの遅さが実感できる

    さらにいろいろ見えるようにしよう(手を挙げさせる)

  • 併合整列法(merge sort)

    • 前提:– 整列済みの列が沢山ある

    • 方法: – 2つの列を順序よくくっつけて1つにする(併合)

    – 列が1つになるまで繰り返す

    9

    3

    1

    4

    6

    4

    3

    1

    3

    9

    4

    8

    7

    3

    3

    1

    39

    14

    46

    13

    39

    48

    37

    13

    1349

    1346

    3489

    1337

    11334469

    13334789

    111333334446789

  • 併合整列法: 整列済の列の併合• 方法 (a, b を併合した c を作る):

    – c を作る (長さは(a の長さ)+(b の長さ))– 2つの列の先頭を比べ,小さい方をコピーする– どちらかの最後に至るまで続ける– 残った列をコピーする

    a

    b

    c

    ↑ia

    ↑ib

    ↑ic

  • 併合整列法: 併合を繰り返す•前提:

    – 併合される列は配列の配列from にしまわれている

    • 方法:– from が長さ1の配列になるま

    で繰り返す

    • from の半分の長さの配列 to を作る

    • from の第(2i)要素と第(2i+1) 要素を併合して toの第 i 要素にしまう

    • 現在の to を新たな from にする

    from012345

    ……

    to012

    ……

  • def mergesort(a)n = a.length()from = Array.new(n)for i in 0..(n-1)from[i] = [a[i]]

    endwhile n > 1to = Array.new((n+1)/2)for i in 0..(n/2-1)to[i] = merge(from[i*2], from[i*2+1])

    endif !is_even(n)to[(n+1)/2-1] = from[n-1]

    endfrom = ton = (n+1)/2

    endfrom[0]

    end

    併合整列法

    def is_even(n)n % 2 == 0

    end

    中で定義してもloadしてもよい

    mergesort.rb

    各要素が大きさ1の配列である配列fromを作る

    半分の長さの配列toを作る

    fromの2要素を併合してtoにコピーfromが奇数個のときは,

    最後の要素は併合せずにコピーする

    toを新たなfromとする

    fromの要素が1つになるまで繰り返す

    最後はfromの第0要素に整列済みの配列が入っている

  • def merge(a, b)c = Array.new(a.length() + b.length())ia = 0ib = 0ic = 0while ia < a.length() && ib < b.length()if a[ia] < b[ib]c[ic] = a[ia]ia = ia + 1ic = ic + 1

    elsec[ic] = b[ib]ib = ib + 1ic = ic + 1

    endend

    if ia < a.length()for i in ia..(a.length() - 1)c[ic] = a[i]ic = ic + 1

    endelseif ib < b.length()for i in ib..(b.length() - 1)c[ic] = b[i]ic = ic + 1

    endend

    endc

    end

    アニメ化はこの部分に対して行なう(前のページの部分はそのまま)

    mergsort.rb

    2つの整列済み配列の併合

    a,b,c の現在の先頭を示す添え字

    a, b の先頭から小さいほうをc にコピー

    灰色の if 文は,片方の配列が空になったときの処理 (a または b の残り

    を c にコピー)

  • def merge(a, b)for i in 0..(a.length() - 1)

    a[i].flash(0.5, [1, 0, 0])endfor i in 0..(b.length() - 1)

    b[i].flash(0.5, [0, 0, 1])endc = Array.new(a.length() + b.length())ia = 0ib = 0ic = 0while ia < a.length() && ib < b.length()

    if a[ia] < b[ib]c[ic] = a[ia]move_smaller(c[ic], ic - ia)ia = ia + 1ic = ic + 1

    elsec[ic] = b[ib]move_smaller(c[ic], ic - (a.length() + ib))ib = ib + 1ic = ic + 1

    endend

    if ia < a.length()for i in ia..(a.length() - 1)

    c[ic] = a[i]move_smaller(c[ic], ic - i)ic = ic + 1

    endelse

    if ib < b.length()for i in ib..(b.length() - 1)

    c[ic] = b[i]move_smaller(c[ic], ic - (a.length() + i))ic = ic + 1

    endend

    endc

    end

    マージされる2つの配列をそれぞれ赤と青にフラッシュ

    ちょっと考える必要

    ちょっと考える必要

    animated_mergesort.rb

    mergesort.rbのmergesortは省略

  • def next_y(person)if person.y == 400

    200else

    400end

    end

    def move_smaller(person, dx)person.move(person.x + dx * 30, next_y(person))

    end

    人形の移動の記述を短くするために関数化

    animated_mergesort.rb

    マージをするたびに人形の y 座標を

    上と下で交代させる

  • さらにいろいろ見えるようにしよう(手を挙げさせる)

    def merge(a, b)for i in 0..(a.length() - 1) a[i].color = [1,0,0]

    end for i in 0..(b.length() - 1)

    b[i].color = [0,0,1] endc = Array.new(a.length() + b.length())ia = 0ib = 0ic = 0while ia < a.length() && ib < b.length()a[ia].raise_hand() ; b[ib].raise_hand() if a[ia] < b[ib]c[ic] = a[ia]move_smaller(c[ic], ic - ia) ia = ia + 1ic = ic + 1

    else... (以下はanimated_mergesort.rbのmergeと同じ)

    flashではなく,マージしている間,元の配列を色づけしておく.

    比較の前に両者の手を挙げさせる

    手はmove_smaller関数の中で降ろす

    色もmove_smaller関数の中で白にリセットされる

    more_animated_mergesort.rb

  • def move_smaller(person, dx)person.move(person.x + dx * 30, next_y(person))person.lower_hand() person.color = [1,1,1]

    end手を降ろし,色を白に戻す

    more_animated_mergesort.rb

    ここで言及されていない関数の定義は変更なし

    mergeの中で比較をしないで移動するものがあることがよく見える

    ずっと色がついているのとflashするのとではどちらが見やすいか?

  • 併合整列法の再帰版

    def mergesort(a)submergesort(a,0,a.length())

    end

    def submergesort(a,i,n)if n

  • クイックソート(quick sort)• 配列の途中の適当な要素(ピボット)を選ぶ• 左から順にピボットより大きい要素を探し,右側からピボットより小さい要素を探し,それらを入れ替えていく

    • 両側の探索がぶつかったところの左右をみると,左側のどれもが,右側のどれよりも小さくなっている

    • この左右の部分配列それぞれについて再帰的に同じことをする(配列は小さくなっていく)

    • 配列要素が1個になれば再帰の終端

    以下のプログラムアニメーションでそれを実感しよう

  • while l medr = r - 1

    endif l

  • include(Komaba::MiniStar)

    def quicksort(a)qsort(a, 0, a.length()-1)

    end

    def qsort(a, from, to) if from < topivot = (from + to) / 2med = a[pivot]med.flash(1.5)l = fromr = to

    while l medr = r - 1

    endif l

  • def qsort(a, from, to)if from < tofor i in from .. to a[i].color = [1, 1, 0] end pivot = (from + to) / 2med = a[pivot]med.color = [1,0,0]l = fromr = towhile l meda[r].raise_hand(); a[r].lower_hand()r = r - 1

    endp1 = a[l] ; p2 = a[r] p1.raise_hand(); p2.raise_hand()

    if l

  • 練習問題

    • Quicksortはなかなか理解しにくいアルゴリズムである.共通教材にあるアニメを改造してもっとわかりやすくしてみよう.それができれば君はQuicksortを深く理解したことになる.

    • Personに関するいろいろな操作を使って,ほかの整列アルゴリズムを可視化してみよう

    • 整列以外にPersonを使って,面白いアニメを作ってみよう– オブジェクト指向の講義のときに,Personの操作の種類をもっと増やす予定

  • 課題: アルゴリズムによる速度差の実測

    • 平方根・整列の複数のアルゴリズムに対応したプログラムを作り,速度の違いを調べる

    – 平方根の場合: x/δの大きさによって時間がどう変化するかをグラフを描いてみよ,1秒間に何回計算できるかで比べよ

    – 整列の場合: ランダムな列を作り,それを整列させてみよ.列の長さで時間がどう変化するかをグラフを描いてみよ

    • グラフから,実行時間を予測する式を推定せよ

  • load("./randoms.rb ") # randoms(id,size,max)load("./bench.rb") # run(function_name, x, v)load("./simplesort.rb") # simplesort(a)load("./mergesort.rb") # mergesort(a)

    def compare_sort(max, step)for i in 1..(max/step)x=i*stepa=randoms(i,x,1)run("simplesort", x, a)a=randoms(i,x,1)run("mergesort", x, a)

    endend

  • 計算量

    • これまで見てきたように,アルゴリズムによってプログラムの実行時間は大きく変わり得る

    • プログラムを作らずに,良いアルゴリズムを選ぶにはどうすればよいか?

    →計算量– アルゴリズムが答えを出すのに必要とする資源の見積り

    – 資源 = 計算回数や記憶容量

    – 通常はおおまかな式(オーダー, O)で見積り比較する←それでも充分

  • 計算量の求め方

    1. 各演算,関数呼び出し,判断,分岐をそれぞれ「1回の計算」とする

    2. 入力の引数 x に対する計算回数の式を求める

    3. 定数を消し,主要な項だけを残す (計算量のオーダー)

    ※3を前提にすると1, 2は厳密でなくともよくなる

    情報科学(5)復習: 問題解決とアルゴリズム �(cf. 「情報」6.1.1)アルゴリズムとはアルゴリズムのない問題は?アルゴリズムの役割1つの問題に�複数のアルゴリズムがあり得るアルゴリズムによる�実行時間の違いフィボナッチ数の計算100番目のフィボナッチ数は?定義通りに計算する�フィボナッチ数第0要素から順に数え上げる�フィボナッチ数の計算第0要素から順に数え上げる�フィボナッチ数の計算スピード競争スライド番号 14下 6 桁行列を用いるフィボナッチ数の計算法 (1)行列を用いるフィボナッチ数の計算法 (2)Q の 8 乗は?スライド番号 21フィボナッチ数の計算:�アルゴリズムの違い整列 (sorting)国語辞典で先に来るのは?整列アルゴリズム単純整列法スライド番号 27ソートプログラムの動きを�自分で可視化して理解しよう人形の並びの生成�(すべてのプログラムに共通)人形の入れ替え人形のフラッシュ人形の挙手,表示・非表示,色スライド番号 33アニメーションプログラムの実行スライド番号 35併合整列法(merge sort)併合整列法: 整列済の列の併合併合整列法: 併合を繰り返すスライド番号 39スライド番号 40スライド番号 41スライド番号 42さらにいろいろ見えるようにしよう(手を挙げさせる)スライド番号 44併合整列法の再帰版クイックソート(quick sort)スライド番号 47スライド番号 48スライド番号 49練習問題課題: アルゴリズムによる速度差の実測スライド番号 52計算量計算量の求め方