計算量の評価
$f(n)$ と $g(n)$ が $\mathbb$ 上の関数であるとき、 \[ f(n)= \mathcal(g(n)) \qquad \text \] であるとは、正の実数 $c_1, c_2$ および $n_0$ が存在して、$n_0\leqq n$ なる $n$ に対して \[ c_1\lvert g(n) \rvert \leqq \lvert f(フィボナッチ数列の計算量について フィボナッチ数列の計算量について n) \rvert \leqq c_2\lvert g(n) \rvert \qquad \text \] であるときをいう。 このとき、$f(n)$ は $g(n)$ の オーダー (order)であるといい、big $\mathcal$ を使って、$f(n)= \mathcal(g(n))$ あるいは $f(n) \sim \mathcal(g(n))$ などと書く。 この 記法は $n$ が十分大きくなったときの関数 $f(n)$ の挙動を表していることに注意する。
$f_1(n)= \mathcal(g_1(n))$ および $f_2(n)= \mathcal(g_2(n))$ が正値関数であるとき、定義から次が成立する。 \begin & f_1(n)f_2(n)フィボナッチ数列の計算量について = \mathcal(g_1(n)g_2(n))\\ & f_1(n) フィボナッチ数列の計算量について + f_2(n)= \mathcal(g_1(n) + g_2(フィボナッチ数列の計算量について n)). \end
たとえば、次の多項式 $p(n)$ を考えてみる。 \[ p(n)= フィボナッチ数列の計算量について a_k n^k + a_ n^+\dots +a_2 n^2 + a_1 フィボナッチ数列の計算量について n + a_0 \] $n>1$の時には、自明な関係 \[ 1\leqq n\leqq n^2 \leqq \dots \leqq n^\leqq n^k \] から直ちに得られる関係 \begin a_k n^k \leqq p(n) & \leqq a_k n^k + a_ n^k + \dots + a_2 n^k + a_1 n^k + a_0 n^k\\ & \leqq n^k (a_k + a_ + \dots + a_1 + a_0) \end は、$p(n)=\mathcal(n^k)$ であることを示している。
これより、多項式については $n$ の最大次数に着目して \begin p_0(n) & 5000000000=\mathcal(1) p_1(n) &= 20000 n + 3000000=\mathcal(n)\\ p_2(n) &= 0.000001 n^2 -31412 n - 2600 = フィボナッチ数列の計算量について \mathcal(n^2)\\ p_3(n) &= 300 n^2 + 600 n + 2500 = \mathcal(n^2) \end となり、$p_2(n)$ と $p_3(n)$ は同じ $n^2$ のオーダーで $n$ が十分に大きくなると同じ程度に増加し、$n$のオーダーの $p_1(n)$ を圧倒し、$p_1(n)$ は $p_0(n)$ を圧倒することがわかる(この場合、関数の$\log$-$\log$グラフを書いてみるとこの事実は分かりやすい)。
アルゴリズムの計算量オーダーを低くすることは、大きなデータサイス $n$ に対しては劇的な効果があることになる。 あるいは、指数的オーダーを持つ計算量アルゴリムは大きなデータサイス $n$ に対して 手に負えない (intractable)ことになる。
order | 名前 |
---|---|
$\mathcal(1)$ | 定数 |
$\mathcal(\log n)$ | 対数的 |
$\mathcal(フィボナッチ数列の計算量について フィボナッチ数列の計算量について n)$ | 線形的 |
$\mathcal(n\log n)$ | 対数線形的 |
$\mathcal(n^2)$ | 2乗的 |
$\mathcal(n^3)$ | 3乗的 |
$\mathcal(2^n)$ | 指数的 |
$\mathcal(n!)$ | 階乗的 |
階乗的オーダーが指数的オーダーよりも大きいのは Stirling's の近似公式からわかる。 \[ n! \sim \sqrt <2\pi n>n^n \mathrm^ フィボナッチ数列の計算量について \] 比 $n! /2^n$ の対数をとると \[ \log \left(\frac \right) \sim \left( n+\frac12\right) n -(1+\log 2)n +\text フィボナッチ数列の計算量について \] から、$n\rightarrow \infty$につれて比はいくらでも大きくなるからである。
代表的なアルゴリズムの計算量
ベクトルの内積
$L_1$ノルムの問題サイズはベクトルの長さ $n$ である。
$L_1$ノルム計算の再帰版 one_norm_recur の時間計算量を評価する。 簡単のためにベクトルの長さ $n$ を $n=2^k$ と仮定しても一般性を失わない($k=\log n$)。 10行目の判断で時間 $T_$と15行目の足し算計算に時間 $T_$を費やすのであるが、13行目と14行目で半分の長さの$L_1$ノルム計算をしていることを考慮すると \[ T_(2^k) = T_ + T_ + 2T_(2^) \] の関係、つまり $c= フィボナッチ数列の計算量について フィボナッチ数列の計算量について T_ + T_$ を定数とすると \[ T_(n)= c フィボナッチ数列の計算量について フィボナッチ数列の計算量について + 2T_(n/2) \] が成立する。 このことから、次のように書くことができる。 \begin T_(n) &= c + 2T_(n/2)\\ &= c + 2(c + 2 T_(n/2^2))= c + 2c + 2^2 T_(n/2^2)\\ &= c + 2c + 2^2(c + 2T_(n/2^3))=c + 2c + 2^2c + 2^3T_(n/2^3)\\ & \vdots\\ &= c + 2c + 2^2 c+ 2^3 c+ \dots + 2^c + 2^T_(2)\\ &= c\sum_^ 2^i + n(T_ + T_)\\ &= \fracc + dn \qquad d = T_ + T_\\ &=(c+d)n -c=\mathcal(n) \end
メモリ計算量を考えよう。 $L_1$ノルムを計算するために長さ $n$ フィボナッチ数列の計算量について のベクトルを保持するための入力メモリ量 $M_(n)$ とは区別して、関数内で再帰呼び出しするために必要なメモリを考える必要がある。 サイズ $n$ の計算に再帰呼び出しで必要なメモリ$M_(n)$は \[ M_(n) = c + M_(n/2) \] である。$c$ には left_norm や right_norm (など)に必要な固定領域である。 この漸化式は上の時間計算量と同様に評価でき、$M_(n)=\mathcal(n)$ である。
入力メモリ量 $M_(n)$ は明らかに $=\mathcal(n)$ であることから、関数 one_norm_recur のメモリ計算量 $M_(n)$ は次のようになる。 \[ M_(n) = M_(n) + M_(n) = \mathcal(n) + \mathcal(n) = \mathcal(n). \]
先の関数 one_norm_recur は、リスト分割して再帰呼び出ししている様子の1行の表現が長くなって見づらい。 そこで次のように、リストを左右に分割してそれぞれを フィボナッチ数列の計算量について フィボナッチ数列の計算量について フィボナッチ数列の計算量について left_list と right_list に代入してコードを分かりやすくした関数を one_norm_recur2 としてみた。 しかしながら、以下の計算から明らかになるように、オーダーが上がったメモリ計算量を必要とするようになる。結果、$n$が大きくなると無視できない影響を及ぼしてしまう。
この場合、5行目と6行目のリスト(長さは約半分の $n/2$) left_list と right_list のために必要なメモリが発生するため $M_(n)$ は次のようになる。 \[ M_(n) = c + d n + 2M_(n/2) \]
したがって、 \begin M_(n) &= c + dn+ 2M_(n/2)\\ &= c + dn + 2(c + dn/2 + 2M_(n/2^2))= c + 2c + 2dn + 2^2M_(フィボナッチ数列の計算量について n/2^2)\\ & \vdots\\ &= c + 2c + フィボナッチ数列の計算量について フィボナッチ数列の計算量について 2^2 c+ 2^3 c+ \dots + \fracc + dkn + フィボナッチ数列の計算量について nM_(1)\\ &= c\sum_^ 2^i +d (n\log n) + en\\ &= cn - c +d(n\log n) + en\\ &=\mathcal(n\log n) \end つまり、関数 one_norm_recur2 のメモリ計算量 $M_(n)$ は次のようになる。 \[ M_(n) = M_(n) + フィボナッチ数列の計算量について フィボナッチ数列の計算量について M_(n) = \mathcal(n) + \mathcal(n\log n) = フィボナッチ数列の計算量について \mathcal(n\log n). \] 同じアルゴリズムでありながら、再帰計算の場合にはメモリ計算量のオーダーが $\mathcal(n)$ から $\mathcal(n\log n)$ へと上がってしまうことがわかった。
Fibonacci数の計算
再帰呼び出すしを使ってn番目のFibonacci数を返す杉の関数 fibonacci_recur の計算量評価を考える。
二項係数 $\binom$ は \[ \binom = \frac \] で定義される。 次の二項係数を計算するスクリプトは、明らかに推奨できない。
演習: 上のfor文を使う階乗計算に必要な時間およびメモリ計算量を評価しなさい。 階乗計算にfor文をつかった繰り返し文をつかって二項係数の定義そのままに計算する方法と、上の再帰的方法による計算計算量を比較してみなさい。 どちらの方法でも計算の不安定性は依然として残っているのだが、計算時間には優位な差があることを確認しなさい。
では、次のように二項係数で成立する漸化式 \[ フィボナッチ数列の計算量について \binom = \binom + \binom \] を使って再帰的によって計算するのはどうだろうか。 この場合、$k\approx n/2$ フィボナッチ数列の計算量について 程度で再帰呼び出しの深さが最大になることに注意しよう。
この関係を使うと次の二項係数の再帰的定義を書くことができる。 ただし、n と k の掛け算の位置には配慮が必要である。
演習: 以上で、都合3つの二項係数 $\binom$ の計算方法を考えた(forを使う定義そのものと、2つの再帰的定義)。 これら3つの計算量の評価をまとめ、さらに$n=1,2,3,\dots$ , $k=\approx n/2$ についての計算時間を比較する実験を行いなさい。
線形漸化式と母関数
体 $\mathbb
アルゴリズム
アルゴリズムの実装上の工夫
高速フーリエ変換が使える体 $\mathbb
動的計画法がわかる!ダイクストラ法の実装(Python)や問題への適用手順
今回は,Viterbiアルゴリズムの解説(【技術解説】HMMに基づいたViterbiアルゴリズムによる解推定手法(例題つき))をした際に登場した 動的計画法 について,その解説と,簡単な例を用いたプログラム(Python)での実装例を紹介する.また,問題文から動的計画法を用いて問題を解決する際のプロセス(漸化式の作成方法等)についても触れながら,具体的な応用方法について確認する.まずは,動的計画法とはどういうものなのか,概要を確認しよう.
動的計画法(DP;Dynamic Programming)とは
動的計画法の概要
動的計画法とは そのままでは解けないような大きな問題を複数の小さな問題(部分問題と呼ぶ)に分解し,部分問題を解くことで元の大きな問題を解く手法の総称 である.動的計画法を用いることで多項式時間で解くことができない一部の問題について,類似多項式時間で最適解を求めることができる(後ほど解説).問題のある手法が動的計画法であるかどうかを判断する際には,分割統治法とメモ化の2つを満たしているかどうかが条件となる.はじめに,多項式時間,分割統治法,メモ化について理解しよう.
多項式時間,多項式時間アルゴリズムとは
多項式時間とは多項式で表される 計算時間 を指し,多項式時間アルゴリズムとは 入力サイズ(長さや個数)をnとした時,計算時間(ステップ数)の上界がnの多項式時間で表現できるアルゴリズム フィボナッチ数列の計算量について を指す.例えば,九九を計算するアルゴリズムの計算時間は9×9で計算できる.これをn×nに拡張した場合の計算時間のオーダーは”O”記法を用いてO(n 2 )と表される.これは,この計算時間の上界がn 2 で表現できることを指しているため,n×nの掛け算を計算するアルゴリズムは多項式時間アルゴリズムであるといえる.
しかし,多項式時間で解けない問題も存在する.例えば,今回の解説でも取り上げる最短経路問題は多項式時間で解くことはできない.図のような重み付き経路について,STARTからGOALに最短コストで到着する経路を求める問題を考えてみよう.
最短経路を求めるためには全経路の組み合わせを考慮した上でSTARTからGOALまでのコストを計算し,コストが最小の経路を選択する必要がある.このような問題では入力サイズが増えていく毎に経路パターンが指数的に増加していくため,全経路のコストを計算する手法は現実的ではない.しかし,動的計画法を使用することで,最短経路問題のような多項式時間で解けない問題の最適解を解ける場合がある.その計算時に使用するのが分割統治法とメモ化という2つの手法である.
分割統治法とは
分割統治法とは 対象の問題を部分問題に分割する手法 を指す.では,先ほど挙げた最短経路問題を部分問題に分解してみよう.今回の例では,まずSTARTからENDの全経路を考えるのではなく,ある時点から進むことができる経路のみを考えるというアプローチをとることとする.すると,初めの経路はSTARTからa, b, c, dの4本のみ考えればよいことになる.最短経路問題を解くことを考えると,ここではSTARTから最も低いコストで遷移できるSTART→bを選択しよう.
次は,bから進むことができる経路のみを考え,ここでも最もコストが低い経路を選択する.今回の例ではb→gを選択することになる.
このように全経路を考える問題をある時点から進むことができる経路のみを考える問題(部分問題)に分割するような手法を分割統治法と呼ぶ.
メモ化とは
メモ化とは 計算結果をメモリ上などに保持し,あとから再利用する手法 を指す.メモ化の例として,フィボナッチ数列を計算する問題を考えてみよう.フィボナッチ数列についての説明は割愛する.フィボナッチ数列をPythonで計算する場合,通常は以下のようなコードになる.
CulcFibonacci.py
しかし,このコードではn=10を計算するために,再度n=9~1までの計算をする必要があり,計算時間はO(α n )(α:実数)となるため,nが大きくなるにつれて計算量が指数的に増加していく.
このコードをメモ化によって最適化する際には,複数回計算される点が重要となる.メモ化をするためにメモ化テーブルを作成し,1度計算した値はメモ化テーブルに保持してみよう.
CulcFibonacciMemo.py
メモ化の最大のメリットは計算量が減ることで計算時間を削減できることである.このソースコードでは一度計算したフィボナッチ数をリストに格納しておき,あとで再利用することで計算量を減らす試みがされている.実際に著者のPCで40番目のフィボナッチ数を計算した際の計算時間を比較すると,前者のメモ化なしプログラムでは101.9秒だったのに対して,後者のメモ化ありプログラムでは 0.2秒 と大幅に計算時間が短縮されていた.動的計画法では部分問題を再帰的に計算するため,このメモ化による最適化が大きな意味を持つ.
それでは,動的計画法の概要についてはここまでとし,次は最短経路問題を動的計画法の一種である ダイクストラ法 で解いてみよう.
例題:最短経路問題をダイクストラ法で解く(Python実装)
ダイクストラ法は 最短経路問題を効率よく解く手法 フィボナッチ数列の計算量について である.今回は『分割統治法とは』の項で使用したものより簡単な以下のグラフを用いて計算手順を確認しよう.
ダイクストラ法の計算手順
まずSTARTから伸びるエッジでつながるノード(a, b, c)についてSTARTからの最短経路を求める.今回の例ではSTARTから伸びるエッジでつながるノードのうち,ノードbが最小コスト3で遷移可能であることがわかる.また,STARTから別のノードを経由してbにたどり着いても,負のコストがない限り,他ノード経由の経路はSTART→bの経路よりコストが高くなることがわかるだろう.そのためSTARTからノードbへの最短経路はSTART→bと確定できる.
次に先ほど確定したノードbからたどり着けるノードa, c, GOALについて最小コストを求める.ここでノードbにたどり着くまでのコストは3で確定していることに注意してほしい.また,ここで注目すべきはノードaとノードcには,ノードb以外にSTARTからもエッジが伸びていることである.そのためSTARTから各ノードへの最短経路は フィボナッチ数列の計算量について STARTからの最短距離とノードbからの最短経路のうち,コストが低い経路 となる.これらを踏まえて計算すると,先ほど確定したノードbから各ノード(a, c, GOAL)へ遷移する最小コストはノードaが4,ノードcが5,ノードGOALが8となる.ここから,先ほどと同様に最小コストで遷移できるノードを確定するとノードaが確定される.
手順に従い,次に先ほど確定したノードaについて遷移可能なノードから最小コストのノードを選ぶ.しかし,ノードaから遷移可能なノードSTART, bはどちらもすでに最小コストが確定している.そのため,先ほどノードaの次に小さいコストで遷移可能であったノードcを確定としノードcからたどり着けるノード(GOALのみ)について最小コストを求める.お分かりのとおり,STARTからノードcへの最小コストは5と確定しているため,ノードGOALへ遷移するコストは6となり,ノードGOALが確定する.
ノードGOALが確定した時点で,ダイクストラ法の計算は終了である.最後にノードSTARTからノードGOALへの最短経路(最小コストで遷移可能な経路)を考えよう.これまでの計算でノードGOALにたどり着く経路は「START→b→GOAL(コスト8)」と「START→b→c→GOAL(コスト6)」の2通りがあることがわかっている.もうお分かりのとおり ノードSTARTからノードGOALへの最短経路は「START→b→c→GOAL(コスト6)」 となる.
コメント