【第7回】計算量

計算量を見積もって、事前に TLE を回避しましょう!

Homeブログ一覧【第7回】計算量

その解法、 TLE しない?

以下の問題の解法を考えてみましょう。

整数 NN が与えられるので、 11 から NN までの総和を求めてください。

多くの人は主に以下の 2 つの解法が思い浮かぶかと思います。

  1. for 文を使って 11 から NN まで足していく
  2. 等差数列の和の公式を使って、 N(N+1)2\dfrac{N (N + 1)}{2} を計算する

これらの計算回数を考えてみましょう。 1 番の解法では、 NN 回の足し算を行います。これに対して、2 番の解法では、足し算・掛け算・割り算の 3 回の計算で済みます。

このように大まかな計算回数を見積もる指標の 1 つに、 計算量 というものがあります。 計算量を見積もることで、コードを提出する前に TLE を回避することができます。 AtCoder では TLE とジャッジされるとペナルティが課せられるため、処理対象のデータが非常に大きくなった時の処理時間を大雑把に評価することが大切です! (1 ナノ秒かかる処理を 10 兆回ループさせると 1 ns ×1013=104 s1 \text{ ns } \times 10^{13} = 10^4 \text{ s} 、つまり 3 時間もかかってしまいます......)

計算量の記法と性質

オーダー記法

先程の問題の解法 1 と解法 2 の計算量を見積もってみましょう。

解法 1 は for ループを使うため、 NN に比例した計算回数 がかかります。 これを計算量を表す記号を用いて O(N)\Omicron(N) と表記します。

これに対して、解法 2 は NN の値は計算時間に影響を及ぼしません。つまり、計算回数は 一定 です。これを O(1)\Omicron(1) と表記します。

計算量の表記には、このような オーダー記法 を使います。 代表的なオーダーを処理時間が短い順 (性能が良い順) に並べると、以下のようになります。

O\Omicron 記法名前使用例
O(1)\Omicron(1)定数時間vector でのランダムアクセス、 queue.push
O(logN)\Omicron(\log N)対数時間ソート済み配列の二分探索
O(N)\Omicron(N)線形時間ソートされていない vector での値の探索
O(NlogN)\Omicron(N \log N)準線形・線形対数時間sortstable_sort
O(N2)\Omicron(N^2)二乗時間二重ループ
O(Na)\Omicron(N^a)多項式時間 (a1a \geq 1)行列計算 (N3N^3)
O(aN)\Omicron(a^N)指数時間 (a1a \geq 1)愚直な素数探索と因数分解 (良い方法ではない)
O(N!)\Omicron(N!)階乗時間順列全探索、 TSP (巡回セールスマン問題)

下に行けば行くほど計算量が大きく、処理時間がかかります。

より厳密には...

プログラムは必要な計算をするために、メモリを使います。入力に対してどのくらい計算時間やメモリの使用量が変化するかを示す指標が計算量です。

  • 時間計算量: 必要な計算 (四則演算や数値の比較) の回数
  • 空間計算量: プログラムが実行される際に必要なメモリの量

単に「計算量」といった場合、競技プログラミングでは時間計算量を指すことが多いです。 しかし、厳密な計算回数やメモリ使用量(KB とか)を求めることは難しいです(実装方法やプログラミング言語によって異なるため)。そのため計算量を見積もるときには、大雑把に評価できる オーダー記法 がよく使われます。

O\Omicron, Ω\Omega, Θ\Theta の記法がありますが、競技プログラミングでは O\Omicron 記法がよく使われます ( この場合だと、厳密には Θ\Theta の意味で使われることが多いようです ) 。

O\Omicron は アルファベットの OO ではなくて、ギリシャ文字の「オミクロン」です。アルファベットで代替されることも多いようです。

nn が十分大きいとき、それぞれの記号は以下のように使われます。

表記読み方意味
f(n)=O(g(n))f(n) = \Omicron(g(n))f(n)f(n) のオーダーが g(n)g(n) より小さいf(n)f(n)g(n)g(n) と同じくらいかまたは遅いスピードで増加する
f(n)=Ω(g(n))f(n) = \Omega(g(n))f(n)f(n) のオーダーが g(n)g(n) より大きいf(n)f(n)g(n)g(n) と同じくらいかまたは速いスピードで増加する
f(n)=Θ(g(n))f(n) = \Theta(g(n))f(n)f(n) のオーダーが g(n)g(n) と等しいf(n)f(n)g(n)g(n) と同じくらいの速度で増加する

厳密な定義はここでは紹介しませんが、気になる方は調べてみてください。情報工学科の「離散数学」で用いられている教科書、守屋 悦朗さんの『やさしい離散数学』にも載っていると思います。

オーダー記法の性質

オーダー記法には以下の性質があります。

  1. f(n)=O(g(n)),g(n)=O(h(n))f(n)=O(h(n))f(n) = \Omicron(g(n)), g(n) = \Omicron(h(n)) \Rightarrow f(n) = \Omicron(h(n)) (推移律)
  2. O(f(n))+O(g(n))=O(f(n)+g(n))\Omicron(f(n)) + \Omicron(g(n)) = \Omicron(f(n) + g(n))
  3. O(f(n))×O(g(n))=O(f(n)×g(n))\Omicron(f(n)) \times \Omicron(g(n)) = \Omicron(f(n) \times g(n))
  4. O(cf(n))=O(f(n))\Omicron(cf(n)) = \Omicron(f(n)) (定数倍)
  5. O(O(f(n)))=O(f(n))\Omicron(\Omicron(f(n))) = \Omicron(f(n)) (冪等性)

また、 f(n)f(n) が多変数になっても同様に成り立ちます。

表記の問題点

オーダー記法の意味を考えると、 例えば O(x)=O(x2)\Omicron(x) = \Omicron(x^2) という式が成り立ちます。 ですが、これは「 xx を十分に大きくしたときに、 xx (左辺) は x2x^2 (右辺) と同じくらいかまたは遅いスピードで増加する」という意味なので、O(x2)O(x)\Omicron(x^2) \neq \Omicron(x) であることに注意してください。

コンピュータ関連におけるオーダー記法では、できるだけ簡単な形に式を変形して表記することが一般的です。 基本的には以下のルールさえ覚えれば変形することができます。

ルール 1: 計算量が一番大きい項だけを残す

O(N2+N+logN)\Omicron(N^2 + N + \log N)O(N2)\Omicron(N^2) と表記すれば十分です。

ルール 2: 定数倍は無視する

O(3N2)\Omicron(3N^2)O(N2)\Omicron(N^2) と表記します。

また、実は log\log の底は何でもよいです。

O(logcN)\Omicron(\log_c N) を考えてみると、 logcN=1logc×logN\displaystyle \log_c N = \frac{1}{\log c} \times \log N と変形できるため、 logcN\log_c N は自然対数 logN\log N を定数倍したものと見なすことができます。よって、 O(logcN)\Omicron(\log_c N)O(logN)\Omicron(\log N) と表記することが多いです。

計算機科学の分野では、底は 2 とみなされるケースもあります ( 二分探索や 2 進数をよく扱う影響かも ? ) 。

オーダー記法の練習

  1. N+100N + 100
  2. 25N+N3+N225N + N^3 + N^2
  3. 3+53 + 5
  4. 2N+N22^N + N^2
  5. N2+MN^2 + M
  6. log2N\log_{2} N
  7. log10N32\log_{10} N^{32}
答え
  1. O(N)\Omicron(N)
  2. O(N3)\Omicron(N^3)
  3. O(1)\Omicron(1)
  4. O(2N)\Omicron(2^N)
  5. O(N2+M)\Omicron(N^2 + M)
  6. O(logN)\Omicron(\log N)
  7. O(logN)\Omicron(\log N)

計算量と競プロの関係

競技プログラミングでは、計算量は以下のような使い方ができます。

  1. アルゴリズムの最悪の計算量をオーダー記法で求める。
  2. 問題文の「制約」から、最も時間のかかる入力 ( N=105N = 10^5 など ) を探す。
  3. ステップ 1 で求めた計算量に対して、ステップ 2 で見つけた入力を代入する。

ステップ 3 で求めた値によって、

  • 10710^7 以下: 間違いなく間に合う
  • 10810^8: 間に合うことが多い
  • 10910^9: 限界ギリギリ
  • 101010^{10}: ほぼ間違いなく TLE する。アルゴリズムを変える必要あり。

といった感じで、提出前に未然に TLE を回避することができます。

計算量と対数関数

計算量に log\log が出てくるアルゴリズムとしては、二分探索やエラトステネスの篩などが代表的です。

二分探索は、配列から特定の値を探し出すために用いられるアルゴリズムです。そして、このアルゴリズムは非常に高速で、配列の調べる範囲が 1 回の操作をするたびに半分になるという性質があります。このため、 NN 個の要素から特定の値を探す場合、最大でも log2N\log_2 N 回の操作で探し出すことができます。

また、エラトステネスの篩は素数を求めるアルゴリズムです。ある数 NN 以下の素数を全て求めることができ、計算量は O(NloglogN)\Omicron(N \log \log N) となります。

アルゴリズムの詳しい説明はここでは割愛しますが、 計算量が logN\log NNlogNN \log N 程度に収まるアルゴリズムは、 NN が大きい場合にも高速に動作するため、競技プログラミングでは重宝されます。

練習問題