この記事はこの 3 つに順に答えます。なお「動的計画法(dynamic programming)」という名前は 1950 年代に Richard Bellman が付けたものです。彼は自伝のなかで、当時の国防長官が「研究」という語を嫌ったため、数学的な色を消しつつ印象のよい語を選んだ、という趣旨のことを書いています。名前から意味を読み取ろうとしても徒労で、ここでの programming は「表を作る計画」ほどの意味であり、プログラミング言語とは関係ありません。
計算量は単位費用 RAM モデル(Definition 2.1[Complexity and Big-O Notation] )で数えます。加減算・比較・配列の添字アクセスを 1 ステップとします。O O O 記法については 計算量と O 記法 を、配列が添字アクセスを O ( 1 ) O(1) O ( 1 ) でできることについては 基本的なデータ構造 (Proposition 3.2[Fundamental Data Structures] )を前提とします。有向非巡回グラフ(DAG)と位相ソートは グラフアルゴリズム (DAG であることの特徴づけは Theorem 4.2[グラフアルゴリズム] )の内容を使います。
記号を決めておきます。
[ n ] = { 1 , 2 , … , n } [n] = \{1, 2, \ldots, n\} [ n ] = { 1 , 2 , … , n } とし、[ 0 ] = ∅ [0] = \emptyset [ 0 ] = ∅ とします。
Z ≥ 0 \mathbb{Z}_{\ge 0} Z ≥ 0 は非負整数全体、Z ≥ 1 \mathbb{Z}_{\ge 1} Z ≥ 1 は正整数全体を表します。
φ = 1 + 5 2 = 1.6180 … \varphi = \dfrac{1 + \sqrt{5}}{2} = 1.6180\ldots φ = 2 1 + 5 = 1.6180 … を黄金比とします。φ \varphi φ は t 2 = t + 1 t^2 = t + 1 t 2 = t + 1 の正の解なので、φ 2 = φ + 1 \varphi^2 = \varphi + 1 φ 2 = φ + 1 が成り立ちます。この関係だけを後で使います。
部分集合 S ⊆ [ n ] S \subseteq [n] S ⊆ [ n ] と数列 ( a i ) (a_i) ( a i ) に対し、a ( S ) = ∑ i ∈ S a i a(S) = \sum_{i \in S} a_i a ( S ) = ∑ i ∈ S a i と略記します。a ( ∅ ) = 0 a(\emptyset) = 0 a ( ∅ ) = 0 です。
まず、動的計画法という手法を一般的な形で書き下します。個々の問題ごとに計算量を数え直さずに済むようにするためです。
条件 3 の辺は「依存される側 → \to → 依存する側」の向きに引いてあります。ですから G G G の位相順序に沿って計算すれば、必要な値は必ず先に手に入っています。この観察をそのまま定理にしたのが次です。
Theorem 3.2 (動的計画法の計算量 )
Definition 3.1 の意味の部分問題族 ( S , D , g ) (S, D, g) ( S , D , g ) が与えられ、N = ∣ S ∣ N = |S| N = ∣ S ∣ とする。表への読み書きが 1 1 1 ステップででき、各 s s s について、依存先の値 V ( s 1 ) , … , V ( s k s ) V(s_1), \ldots, V(s_{k_s}) V ( s 1 ) , … , V ( s k s ) がすでに表にあるという条件のもとで g s g_s g s の評価が c ( s ) c(s) c ( s ) ステップでできるとする。このとき、すべての s ∈ S s \in S s ∈ S に対する V ( s ) V(s) V ( s ) を
O ( N + ∑ s ∈ S c ( s ) ) O\Bigl(N + \sum_{s \in S} c(s)\Bigr) O ( N + s ∈ S ∑ c ( s ) ) ステップ、O ( N ) O(N) O ( N ) の作業領域で計算できる。とくに、ある定数 c c c が存在してすべての s ∈ S s \in S s ∈ S で c ( s ) ≤ c c(s) \le c c ( s ) ≤ c ならば、計算時間は O ( N c ) O(Nc) O ( N c ) である。
Proof(Theorem 3.2) G = ( S , E ) G = (S, E) G = ( S , E ) を Definition 3.1 の条件 3 の DAG とします。
第 1 段:位相順序をとる。 条件 3 より G G G は DAG なので、位相順序、すなわち S S S の全体の並べ替え s ( 1 ) , s ( 2 ) , … , s ( N ) s^{(1)}, s^{(2)}, \ldots, s^{(N)} s ( 1 ) , s ( 2 ) , … , s ( N ) で、( s ( p ) , s ( q ) ) ∈ E (s^{(p)}, s^{(q)}) \in E ( s ( p ) , s ( q ) ) ∈ E ならば必ず p < q p < q p < q となるものが存在します(グラフアルゴリズム )。その計算には O ( N + ∣ E ∣ ) O(N + |E|) O ( N + ∣ E ∣ ) ステップかかります。ここで ∣ E ∣ = ∑ s ∈ S k s |E| = \sum_{s \in S} k_s ∣ E ∣ = ∑ s ∈ S k s ですが、g s g_s g s を評価するには少なくとも k s k_s k s 個の引数を読まなければならないので c ( s ) ≥ k s c(s) \ge k_s c ( s ) ≥ k s であり、したがって ∣ E ∣ ≤ ∑ s c ( s ) |E| \le \sum_{s} c(s) ∣ E ∣ ≤ ∑ s c ( s ) です。つまり位相ソートの費用は主張の O ( N + ∑ s c ( s ) ) O(N + \sum_s c(s)) O ( N + ∑ s c ( s )) に収まります。
第 2 段:位相順に埋める。 大きさ N N N の表 T T T を用意し(確保と初期化に O ( N ) O(N) O ( N ) )、q = 1 , 2 , … , N q = 1, 2, \ldots, N q = 1 , 2 , … , N の順に T [ s ( q ) ] ← g s ( q ) ( T [ s 1 ] , … , T [ s k ] ) T[s^{(q)}] \leftarrow g_{s^{(q)}}\bigl(T[s_1], \ldots, T[s_{k}]\bigr) T [ s ( q ) ] ← g s ( q ) ( T [ s 1 ] , … , T [ s k ] ) と代入します(D ( s ( q ) ) = ( s 1 , … , s k ) D(s^{(q)}) = (s_1, \ldots, s_k) D ( s ( q ) ) = ( s 1 , … , s k ) )。
この代入が正しく実行できることを確認します。s i ∈ D ( s ( q ) ) s_i \in D(s^{(q)}) s i ∈ D ( s ( q ) ) なら ( s i , s ( q ) ) ∈ E (s_i, s^{(q)}) \in E ( s i , s ( q ) ) ∈ E ですから、位相順序の定義により s i s_i s i は s ( q ) s^{(q)} s ( q ) より前に並んでいます。よって T [ s i ] T[s_i] T [ s i ] にはすでに値が入っています。さらに q q q に関する帰納法により、その値は V ( s i ) V(s_i) V ( s i ) に等しい:q = 1 q = 1 q = 1 のとき s ( 1 ) s^{(1)} s ( 1 ) は依存先をもてない(もてば依存先が s ( 1 ) s^{(1)} s ( 1 ) より前に来て矛盾)ので基底であり、T [ s ( 1 ) ] = g s ( 1 ) ( ) = V ( s ( 1 ) ) T[s^{(1)}] = g_{s^{(1)}}() = V(s^{(1)}) T [ s ( 1 ) ] = g s ( 1 ) ( ) = V ( s ( 1 ) ) 。q q q 未満で成立を仮定すれば、T [ s i ] = V ( s i ) T[s_i] = V(s_i) T [ s i ] = V ( s i ) なので条件 2 より T [ s ( q ) ] = g s ( q ) ( V ( s 1 ) , … , V ( s k ) ) = V ( s ( q ) ) T[s^{(q)}] = g_{s^{(q)}}(V(s_1), \ldots, V(s_k)) = V(s^{(q)}) T [ s ( q ) ] = g s ( q ) ( V ( s 1 ) , … , V ( s k )) = V ( s ( q ) ) です。
第 3 段:費用を数える。 各 q q q での代入は仮定より c ( s ( q ) ) c(s^{(q)}) c ( s ( q ) ) ステップです。総和は ∑ s ∈ S c ( s ) \sum_{s \in S} c(s) ∑ s ∈ S c ( s ) 。これに第 1 段と表の初期化の O ( N + ∑ s c ( s ) ) O(N + \sum_s c(s)) O ( N + ∑ s c ( s )) を足しても主張の範囲です。領域は表の O ( N ) O(N) O ( N ) に、位相ソートの作業領域 O ( N ) O(N) O ( N ) を足して O ( N ) O(N) O ( N ) です。
最後の主張は、c ( s ) ≤ c c(s) \le c c ( s ) ≤ c なら ∑ s c ( s ) ≤ N c \sum_s c(s) \le Nc ∑ s c ( s ) ≤ N c であり、また c ≥ 1 c \ge 1 c ≥ 1 としてよいので N + N c = O ( N c ) N + Nc = O(Nc) N + N c = O ( N c ) となることから従います。
∎ この定理の使い方は決まっています。部分問題を何個作ったか(N N N )と、1 つの部分問題を解くのに何ステップかかるか(c c c )を数える。掛ければ計算量が出る。 以降のフィボナッチ、ナップサック、編集距離の計算量は、すべてこの一言で片づきます。
Theorem 3.2 の証明では位相順序を先に求めましたが、実装では 2 つの流儀があります。
Proposition 3.3 (メモ化再帰の計算量 )
Definition 3.1 の設定のもとで、次の再帰手続き s o l v e ( s ) \mathrm{solve}(s) solve ( s ) を考える。
表 T [ s ] T[s] T [ s ] が「未計算」でなければ T [ s ] T[s] T [ s ] を返す。
そうでなければ、D ( s ) = ( s 1 , … , s k ) D(s) = (s_1, \ldots, s_k) D ( s ) = ( s 1 , … , s k ) の各要素に対して s o l v e ( s i ) \mathrm{solve}(s_i) solve ( s i ) を呼び、その結果に g s g_s g s を適用した値を T [ s ] T[s] T [ s ] に書き込んで返す。
このとき、s 0 ∈ S s_0 \in S s 0 ∈ S から G G G の辺を逆向きに辿って到達できる部分問題の集合を R ⊆ S R \subseteq S R ⊆ S (s 0 ∈ R s_0 \in R s 0 ∈ R )とすると、s o l v e ( s 0 ) \mathrm{solve}(s_0) solve ( s 0 ) は停止し、V ( s 0 ) V(s_0) V ( s 0 ) を返す。その計算時間は O ( ∣ R ∣ + ∑ s ∈ R c ( s ) ) O\bigl(|R| + \sum_{s \in R} c(s)\bigr) O ( ∣ R ∣ + ∑ s ∈ R c ( s ) ) であり、R R R に属さない部分問題は一度も評価されない。
Proof(Proposition 3.3) 停止性と正しさ。 G G G は DAG なので、s s s から辺を逆向きに辿る道の長さの最大値 h ( s ) h(s) h ( s ) (s s s の依存の深さ)が有限に定まります。閉路があればこれは定義できませんから、ここで条件 3 を使っています。h ( s ) h(s) h ( s ) に関する帰納法で示します。h ( s ) = 0 h(s) = 0 h ( s ) = 0 なら D ( s ) D(s) D ( s ) は空、つまり s s s は基底なので、s o l v e ( s ) \mathrm{solve}(s) solve ( s ) は再帰せずに g s ( ) = V ( s ) g_s() = V(s) g s ( ) = V ( s ) を返して停止します。h ( s ) = d > 0 h(s) = d > 0 h ( s ) = d > 0 とすると、各 s i ∈ D ( s ) s_i \in D(s) s i ∈ D ( s ) は h ( s i ) ≤ d − 1 h(s_i) \le d - 1 h ( s i ) ≤ d − 1 を満たすので、帰納法の仮定より s o l v e ( s i ) \mathrm{solve}(s_i) solve ( s i ) は停止して V ( s i ) V(s_i) V ( s i ) を返します。よって s o l v e ( s ) \mathrm{solve}(s) solve ( s ) は停止し、条件 2 より g s ( V ( s 1 ) , … , V ( s k ) ) = V ( s ) g_s(V(s_1), \ldots, V(s_k)) = V(s) g s ( V ( s 1 ) , … , V ( s k )) = V ( s ) を返します。
各部分問題の本体は高々 1 回しか実行されない。 s s s の本体(再帰呼び出しと g s g_s g s の評価)が実行し終わると T [ s ] T[s] T [ s ] に値が書き込まれ、以後「未計算」ではなくなります。したがって 2 回目以降の s o l v e ( s ) \mathrm{solve}(s) solve ( s ) は最初の分岐で戻ります。本体が実行されるのは最初の 1 回だけです。
呼び出し回数。 本体が実行される s s s は R R R の元だけです(s 0 s_0 s 0 から到達不能な部分問題は呼ばれません)。本体 1 回につき再帰呼び出しは ∣ D ( s ) ∣ = k s |D(s)| = k_s ∣ D ( s ) ∣ = k s 回起きるので、s o l v e \mathrm{solve} solve の総呼び出し回数は 1 + ∑ s ∈ R k s 1 + \sum_{s \in R} k_s 1 + ∑ s ∈ R k s です。Theorem 3.2 の証明と同じ理由で k s ≤ c ( s ) k_s \le c(s) k s ≤ c ( s ) ですから、呼び出しのオーバーヘッド(1 1 1 回あたり O ( 1 ) O(1) O ( 1 ) )の総和は O ( ∣ R ∣ + ∑ s ∈ R c ( s ) ) O(|R| + \sum_{s \in R} c(s)) O ( ∣ R ∣ + ∑ s ∈ R c ( s )) に収まります。本体での g s g_s g s の評価は合計 ∑ s ∈ R c ( s ) \sum_{s \in R} c(s) ∑ s ∈ R c ( s ) ステップです。
∎ 2 つの流儀を比べます。
メモ化再帰(トップダウン) 表計算(ボトムアップ) 計算順序 再帰が依存関係を自動的に辿る 位相順を人が決めてループに書く 実際に解く部分問題 必要なものだけ(Proposition 3.3 の R R R ) 表の全マス 実装 素朴な再帰に表を足すだけ ループの順序を設計する必要がある 弱点 再帰の深さ制限、呼び出しのオーバーヘッド 使わない部分問題も解いてしまう 領域の削減 しにくい 「直前の行だけ持つ」等がやりやすい
どちらも Theorem 3.2 と同じオーダーの計算量になります。以下では両方を使い分けます。
§1 で観察した重複を、まず定量化します。
Proposition 4.1 (素朴な再帰の呼び出し回数 )
T ( n ) T(n) T ( n ) を、fib_naive(n) の実行中に起きる関数呼び出しの総数(最初の 1 回を含む)とする。n ≥ 0 n \ge 0 n ≥ 0 に対して
T ( n ) = 2 F n + 1 − 1 T(n) = 2F_{n+1} - 1 T ( n ) = 2 F n + 1 − 1 が成り立つ。さらに n ≥ 1 n \ge 1 n ≥ 1 に対して 2 φ n − 1 − 1 ≤ T ( n ) ≤ 2 φ n − 1 2\varphi^{n-1} - 1 \le T(n) \le 2\varphi^{n} - 1 2 φ n − 1 − 1 ≤ T ( n ) ≤ 2 φ n − 1 であり、したがって T ( n ) = Θ ( φ n ) T(n) = \Theta(\varphi^n) T ( n ) = Θ ( φ n ) である。
Proof(Proposition 4.1) 等式。 n n n に関する強い帰納法です。n = 0 n = 0 n = 0 のとき、fib_naive(0) は再帰せずに戻るので T ( 0 ) = 1 T(0) = 1 T ( 0 ) = 1 。一方 2 F 1 − 1 = 2 ⋅ 1 − 1 = 1 2F_1 - 1 = 2 \cdot 1 - 1 = 1 2 F 1 − 1 = 2 ⋅ 1 − 1 = 1 で一致します。n = 1 n = 1 n = 1 のときも同様に T ( 1 ) = 1 T(1) = 1 T ( 1 ) = 1 で、2 F 2 − 1 = 2 ⋅ 1 − 1 = 1 2F_2 - 1 = 2 \cdot 1 - 1 = 1 2 F 2 − 1 = 2 ⋅ 1 − 1 = 1 です。n ≥ 2 n \ge 2 n ≥ 2 とし、n n n 未満で成立を仮定します。fib_naive(n) は自分自身の 1 回に加えて fib_naive(n-1) と fib_naive(n-2) を呼ぶので
T ( n ) = 1 + T ( n − 1 ) + T ( n − 2 ) = 1 + ( 2 F n − 1 ) + ( 2 F n − 1 − 1 ) = 2 ( F n + F n − 1 ) − 1 = 2 F n + 1 − 1 T(n) = 1 + T(n-1) + T(n-2) = 1 + (2F_n - 1) + (2F_{n-1} - 1) = 2(F_n + F_{n-1}) - 1 = 2F_{n+1} - 1 T ( n ) = 1 + T ( n − 1 ) + T ( n − 2 ) = 1 + ( 2 F n − 1 ) + ( 2 F n − 1 − 1 ) = 2 ( F n + F n − 1 ) − 1 = 2 F n + 1 − 1 となります。最後の等号はフィボナッチ数の漸化式です。
評価。 補助的に、m ≥ 1 m \ge 1 m ≥ 1 に対して φ m − 2 ≤ F m ≤ φ m − 1 \varphi^{m-2} \le F_m \le \varphi^{m-1} φ m − 2 ≤ F m ≤ φ m − 1 を示します。m = 1 m = 1 m = 1 では φ − 1 = 0.618 … ≤ 1 = F 1 ≤ φ 0 = 1 \varphi^{-1} = 0.618\ldots \le 1 = F_1 \le \varphi^0 = 1 φ − 1 = 0.618 … ≤ 1 = F 1 ≤ φ 0 = 1 、m = 2 m = 2 m = 2 では φ 0 = 1 ≤ 1 = F 2 ≤ φ 1 \varphi^0 = 1 \le 1 = F_2 \le \varphi^1 φ 0 = 1 ≤ 1 = F 2 ≤ φ 1 で成立します。m ≥ 3 m \ge 3 m ≥ 3 とし、m m m 未満で成立を仮定すると、§2 で確認した φ 2 = φ + 1 \varphi^2 = \varphi + 1 φ 2 = φ + 1 を使って
F m = F m − 1 + F m − 2 ≥ φ m − 3 + φ m − 4 = φ m − 4 ( φ + 1 ) = φ m − 4 φ 2 = φ m − 2 , F_m = F_{m-1} + F_{m-2} \ge \varphi^{m-3} + \varphi^{m-4} = \varphi^{m-4}(\varphi + 1) = \varphi^{m-4} \varphi^{2} = \varphi^{m-2}, F m = F m − 1 + F m − 2 ≥ φ m − 3 + φ m − 4 = φ m − 4 ( φ + 1 ) = φ m − 4 φ 2 = φ m − 2 , F m = F m − 1 + F m − 2 ≤ φ m − 2 + φ m − 3 = φ m − 3 ( φ + 1 ) = φ m − 1 F_m = F_{m-1} + F_{m-2} \le \varphi^{m-2} + \varphi^{m-3} = \varphi^{m-3}(\varphi + 1) = \varphi^{m-1} F m = F m − 1 + F m − 2 ≤ φ m − 2 + φ m − 3 = φ m − 3 ( φ + 1 ) = φ m − 1 が得られます。この評価を m = n + 1 m = n+1 m = n + 1 に適用すれば φ n − 1 ≤ F n + 1 ≤ φ n \varphi^{n-1} \le F_{n+1} \le \varphi^{n} φ n − 1 ≤ F n + 1 ≤ φ n であり、等式に代入して主張が出ます。φ > 1 \varphi > 1 φ > 1 なので T ( n ) T(n) T ( n ) は指数的に増大します。
∎ この式は具体的な数を入れると効きます。T ( 50 ) = 2 F 51 − 1 = 40,730,022,147 T(50) = 2F_{51} - 1 = 40{,}730{,}022{,}147 T ( 50 ) = 2 F 51 − 1 = 40 , 730 , 022 , 147 、およそ 4.1 × 10 10 4.1 \times 10^{10} 4.1 × 1 0 10 回。1 秒間に 10 9 10^9 1 0 9 回の関数呼び出しをこなす計算機でも 40 秒です。n = 100 n = 100 n = 100 ではどうなるかは Exercise 8.1 で計算してもらいます。
Definition 3.1 の言葉で書き直します。部分問題の添字集合は S = { 0 , 1 , … , n } S = \{0, 1, \ldots, n\} S = { 0 , 1 , … , n } 、値は V ( k ) = F k V(k) = F_k V ( k ) = F k 、依存先は D ( 0 ) = D ( 1 ) = ( ) D(0) = D(1) = () D ( 0 ) = D ( 1 ) = ( ) (基底)、k ≥ 2 k \ge 2 k ≥ 2 では D ( k ) = ( k − 1 , k − 2 ) D(k) = (k-1, k-2) D ( k ) = ( k − 1 , k − 2 ) 、遷移は g k ( a , b ) = a + b g_k(a, b) = a + b g k ( a , b ) = a + b です。辺は k k k の小さい方から大きい方へ向かうので閉路はなく、DAG の条件も満たされます。
Corollary 4.2 (フィボナッチ数の線形時間計算 )
上の部分問題族に Theorem 3.2 を適用すると、F 0 , F 1 , … , F n F_0, F_1, \ldots, F_n F 0 , F 1 , … , F n のすべてが O ( n ) O(n) O ( n ) ステップ、O ( n ) O(n) O ( n ) の領域で計算できる。F n F_n F n だけが必要なら領域は O ( 1 ) O(1) O ( 1 ) でよい。
Proof(Corollary 4.2) 部分問題の個数は N = n + 1 N = n + 1 N = n + 1 です。遷移 g k g_k g k は表引き 2 回と加算 1 回なので c ( k ) = O ( 1 ) c(k) = O(1) c ( k ) = O ( 1 ) 、すなわち定数 c c c で上から押さえられます。Theorem 3.2 より計算時間は O ( N c ) = O ( n ) O(Nc) = O(n) O ( N c ) = O ( n ) 、領域は O ( N ) = O ( n ) O(N) = O(n) O ( N ) = O ( n ) です。
領域についての後半を示します。位相順序は 0 , 1 , 2 , … , n 0, 1, 2, \ldots, n 0 , 1 , 2 , … , n ととれ、k k k の計算に必要なのは k − 1 k-1 k − 1 と k − 2 k-2 k − 2 の値だけです。よって直前 2 つの値を変数 2 個に保持し、k k k を 1 つ進めるごとに更新すれば、表全体を保持する必要はありません。使う記憶は変数 2 個と添字 1 個で O ( 1 ) O(1) O ( 1 ) です。
∎ トップダウン(メモ化再帰)とボトムアップ(反復)の両方を書いておきます。
def fib_memo ( n , memo= None ) :
if n in memo: # すでに計算済みなら表を引くだけ
memo[n] = fib_memo ( n - 1 , memo ) + fib_memo ( n - 2 , memo )
a, b = 0 , 1 # a = F(0), b = F(1)
for _ in range ( 2 , n + 1 ):
a, b = b, a + b # (F(k-2), F(k-1)) -> (F(k-1), F(k))
fib_memo は素朴な再帰に 3 行足しただけです。それでも Proposition 3.3 より、実際に本体が実行される部分問題は R = { 0 , 1 , … , n } R = \{0, 1, \ldots, n\} R = { 0 , 1 , … , n } の n + 1 n+1 n + 1 個だけになります。
Example 4.3 (n = 10 での比較 )
n = 10 n = 10 n = 10 の表を実際に埋めます。左から順に F k = F k − 1 + F k − 2 F_{k} = F_{k-1} + F_{k-2} F k = F k − 1 + F k − 2 を計算するだけです。
k k k 0 1 2 3 4 5 6 7 8 9 10 F k F_k F k 0 1 1 2 3 5 8 13 21 34 55
加算は 9 回、表のマスは 11 個です。一方、素朴な再帰の呼び出し回数は Proposition 4.1 より T ( 10 ) = 2 F 11 − 1 = 2 ⋅ 89 − 1 = 177 T(10) = 2F_{11} - 1 = 2 \cdot 89 - 1 = 177 T ( 10 ) = 2 F 11 − 1 = 2 ⋅ 89 − 1 = 177 回でした。n = 10 n = 10 n = 10 ですでに 16 倍の差がついています。n = 30 n = 30 n = 30 では T ( 30 ) = 2 F 31 − 1 = 2,692,537 T(30) = 2F_{31} - 1 = 2{,}692{,}537 T ( 30 ) = 2 F 31 − 1 = 2 , 692 , 537 回に対し、表は 31 マスです。
フィボナッチ数は「値を計算する」問題でした。動的計画法が本領を発揮するのは最適化問題ですが、そこには追加の条件が要ります。
Definition 5.1 (最適部分構造 )
最適化問題の族 { P s } s ∈ S \{P_s\}_{s \in S} { P s } s ∈ S があり、P s P_s P s の最適値を V ( s ) V(s) V ( s ) とします。各 s ∈ S s \in S s ∈ S に対して依存先 D ( s ) = ( s 1 , … , s k ) D(s) = (s_1, \ldots, s_k) D ( s ) = ( s 1 , … , s k ) と関数 g s g_s g s が存在して
V ( s ) = g s ( V ( s 1 ) , … , V ( s k ) ) V(s) = g_s\bigl(V(s_1), \ldots, V(s_k)\bigr) V ( s ) = g s ( V ( s 1 ) , … , V ( s k ) ) が成り立ち、かつ依存グラフが DAG になるとき、この問題族は最適部分構造 をもつといいます。
言い換えると、P s P_s P s の最適値が部分問題の最適値だけ から復元できる、ということです。ここが要点で、「部分問題の最適解を貼り合わせたものが実行可能解になる」ことと「実行可能解を分解すると部分問題の実行可能解になる」ことの両方が要ります。この確認に使う標準的な議論が切り貼り論法 です。P s P_s P s の最適解 x x x を分解して得た部分解 x i x_i x i が P s i P_{s_i} P s i の最適解でないとすると、x i x_i x i をより良い解に取り替えて(切って貼って)x x x より良い P s P_s P s の解が作れてしまい、x x x の最適性に反する、という背理法です。Theorem 6.2 の証明では、この論法を全単射の形で厳密に実行します。
最適部分構造は自明に成り立つ性質ではありません。壊れる例を見ておくと、何が効いているのかがはっきりします。
Example 5.2 (最長単純道では最適部分構造が壊れる )
無向グラフ G G G を、頂点集合 { u , v , w , x } \{u, v, w, x\} { u , v , w , x } 、辺集合 { u v , v w , w x , x u , u w } \{uv,\ vw,\ wx,\ xu,\ uw\} { uv , v w , w x , xu , u w } で定めます。単純道とは同じ頂点を 2 度通らない道のことで、L ( a , b ) L(a, b) L ( a , b ) を a a a から b b b への単純道の辺数の最大値とします。
フィボナッチと同じ発想で「最後の辺で場合分け」すれば、L ( u , w ) = max { L ( u , t ) + 1 : t は w の隣接点 } L(u, w) = \max\{L(u, t) + 1 : t \text{ は } w \text{ の隣接点}\} L ( u , w ) = max { L ( u , t ) + 1 : t は w の隣接点 } という漸化式を書きたくなります。しかしこれは成り立ちません。実際に両辺を計算します。
まず左辺です。u u u から w w w への単純道を列挙します。u → w u \to w u → w (1 辺)、u → v → w u \to v \to w u → v → w (2 辺)、u → x → w u \to x \to w u → x → w (2 辺)。3 辺の単純道はありません:u → v → ? → w u \to v \to ? \to w u → v → ? → w の形にするには v v v の隣接点が u , w u, w u , w しかないので不可能、u → x → ? → w u \to x \to ? \to w u → x → ? → w も x x x の隣接点が u , w u, w u , w だけなので不可能です。よって L ( u , w ) = 2 L(u, w) = 2 L ( u , w ) = 2 です。
次に右辺の一項を計算します。w w w の隣接点 v v v について、u u u から v v v への単純道は u → v u \to v u → v (1 辺)、u → w → v u \to w \to v u → w → v (2 辺)、u → x → w → v u \to x \to w \to v u → x → w → v (3 辺)で、L ( u , v ) = 3 L(u, v) = 3 L ( u , v ) = 3 です。したがって右辺は L ( u , v ) + 1 = 4 L(u, v) + 1 = 4 L ( u , v ) + 1 = 4 以上になり、左辺の 2 2 2 と一致しません。
破綻の理由は具体的です。L ( u , v ) L(u,v) L ( u , v ) を達成する道 u → x → w → v u \to x \to w \to v u → x → w → v に辺 v w vw v w を足すと u → x → w → v → w u \to x \to w \to v \to w u → x → w → v → w となり、w w w を 2 度通るので単純道になりません。部分問題の最適解が、上位の問題では実行可能ですらない のです。原因は、部分問題「u u u から v v v への最長単純道」が「これまでどの頂点を使ったか」という情報を捨ててしまっていることにあります。部分問題が状態を十分に表現していないわけです。
なお、最長単純道問題は NP 困難(Definition 6.1[P≠NP 予想とは何か] )であることが知られています(P≠NP予想とは何か )。安直な動的計画法が効かないのには理由がある、ということです。
Definition 6.1 (0-1 ナップサック問題 )
n ∈ Z ≥ 1 n \in \mathbb{Z}_{\ge 1} n ∈ Z ≥ 1 、重さ w 1 , … , w n ∈ Z ≥ 1 w_1, \ldots, w_n \in \mathbb{Z}_{\ge 1} w 1 , … , w n ∈ Z ≥ 1 、価値 v 1 , … , v n ∈ R ≥ 0 v_1, \ldots, v_n \in \mathbb{R}_{\ge 0} v 1 , … , v n ∈ R ≥ 0 、容量 W ∈ Z ≥ 0 W \in \mathbb{Z}_{\ge 0} W ∈ Z ≥ 0 が与えられる。部分集合 S ⊆ [ n ] S \subseteq [n] S ⊆ [ n ] が実行可能 であるとは w ( S ) = ∑ i ∈ S w i ≤ W w(S) = \sum_{i \in S} w_i \le W w ( S ) = ∑ i ∈ S w i ≤ W が成り立つことをいう。実行可能な S S S のうち v ( S ) = ∑ i ∈ S v i v(S) = \sum_{i \in S} v_i v ( S ) = ∑ i ∈ S v i を最大にするものを求めよ、というのが 0-1 ナップサック問題である。
「0-1」は各品物を入れるか入れないか(0 個か 1 個か)の二択である、という意味です。素朴には 2 n 2^n 2 n 通りの部分集合を全部試せば解けますが、n = 60 n = 60 n = 60 で 2 60 ≈ 1.15 × 10 18 2^{60} \approx 1.15 \times 10^{18} 2 60 ≈ 1.15 × 1 0 18 通りとなり現実的ではありません。
部分問題の設計が勝負です。「品物 1 , … , i 1, \ldots, i 1 , … , i までしか使えず、容量が j j j しかない」という制限つきの問題を考えます。
K ( i , j ) = max { v ( S ) : S ⊆ [ i ] , w ( S ) ≤ j } ( 0 ≤ i ≤ n , 0 ≤ j ≤ W ) K(i, j) = \max\bigl\{\, v(S) \ :\ S \subseteq [i],\ w(S) \le j \,\bigr\} \qquad (0 \le i \le n,\ 0 \le j \le W) K ( i , j ) = max { v ( S ) : S ⊆ [ i ] , w ( S ) ≤ j } ( 0 ≤ i ≤ n , 0 ≤ j ≤ W ) この最大値は必ず存在します。集合 F ( i , j ) = { S ⊆ [ i ] : w ( S ) ≤ j } \mathcal{F}(i,j) = \{S \subseteq [i] : w(S) \le j\} F ( i , j ) = { S ⊆ [ i ] : w ( S ) ≤ j } は ∅ \emptyset ∅ を含むので空でなく、2 [ i ] 2^{[i]} 2 [ i ] の部分集合なので有限だからです。求めたい答えは K ( n , W ) K(n, W) K ( n , W ) です。
Theorem 6.2 (ナップサック問題の漸化式 )
Definition 6.1 の設定のもとで、上の K ( i , j ) K(i,j) K ( i , j ) について次が成り立つ。
すべての 0 ≤ j ≤ W 0 \le j \le W 0 ≤ j ≤ W に対し K ( 0 , j ) = 0 K(0, j) = 0 K ( 0 , j ) = 0 。
1 ≤ i ≤ n 1 \le i \le n 1 ≤ i ≤ n かつ 0 ≤ j < w i 0 \le j < w_i 0 ≤ j < w i のとき K ( i , j ) = K ( i − 1 , j ) K(i, j) = K(i-1, j) K ( i , j ) = K ( i − 1 , j ) 。
1 ≤ i ≤ n 1 \le i \le n 1 ≤ i ≤ n かつ w i ≤ j ≤ W w_i \le j \le W w i ≤ j ≤ W のとき
K ( i , j ) = max { K ( i − 1 , j ) , v i + K ( i − 1 , j − w i ) } . K(i, j) = \max\bigl\{\, K(i-1, j),\ \ v_i + K(i-1,\ j - w_i) \,\bigr\}. K ( i , j ) = max { K ( i − 1 , j ) , v i + K ( i − 1 , j − w i ) } . Proof(Theorem 6.2) 1 の証明。 S ⊆ [ 0 ] = ∅ S \subseteq [0] = \emptyset S ⊆ [ 0 ] = ∅ なる S S S は S = ∅ S = \emptyset S = ∅ のみで、w ( ∅ ) = 0 ≤ j w(\emptyset) = 0 \le j w ( ∅ ) = 0 ≤ j より実行可能、v ( ∅ ) = 0 v(\emptyset) = 0 v ( ∅ ) = 0 です。したがって最大値は 0 0 0 です。
2 の証明。 j < w i j < w_i j < w i とします。S ∈ F ( i , j ) S \in \mathcal{F}(i,j) S ∈ F ( i , j ) が i ∈ S i \in S i ∈ S を満たすと仮定すると、重さはすべて正(Definition 6.1 で w k ∈ Z ≥ 1 w_k \in \mathbb{Z}_{\ge 1} w k ∈ Z ≥ 1 としました)なので w ( S ) ≥ w i > j w(S) \ge w_i > j w ( S ) ≥ w i > j となり、w ( S ) ≤ j w(S) \le j w ( S ) ≤ j に矛盾します。よって F ( i , j ) \mathcal{F}(i,j) F ( i , j ) の元はすべて i i i を含まず、S ⊆ [ i ] ∖ { i } = [ i − 1 ] S \subseteq [i] \setminus \{i\} = [i-1] S ⊆ [ i ] ∖ { i } = [ i − 1 ] です。逆に F ( i − 1 , j ) \mathcal{F}(i-1,j) F ( i − 1 , j ) の元は F ( i , j ) \mathcal{F}(i,j) F ( i , j ) の元でもあります。つまり F ( i , j ) = F ( i − 1 , j ) \mathcal{F}(i,j) = \mathcal{F}(i-1,j) F ( i , j ) = F ( i − 1 , j ) であり、同じ集合上で同じ関数 v v v を最大化するので K ( i , j ) = K ( i − 1 , j ) K(i,j) = K(i-1,j) K ( i , j ) = K ( i − 1 , j ) です。
3 の証明。 w i ≤ j w_i \le j w i ≤ j とします。F ( i , j ) \mathcal{F}(i,j) F ( i , j ) を、i i i を含まないもの全体 A \mathcal{A} A と、i i i を含むもの全体 B \mathcal{B} B に分割します。F ( i , j ) = A ⊔ B \mathcal{F}(i,j) = \mathcal{A} \sqcup \mathcal{B} F ( i , j ) = A ⊔ B です。
A \mathcal{A} A について。2 の証明と同じ議論で A = F ( i − 1 , j ) \mathcal{A} = \mathcal{F}(i-1, j) A = F ( i − 1 , j ) です。よって max S ∈ A v ( S ) = K ( i − 1 , j ) \max_{S \in \mathcal{A}} v(S) = K(i-1, j) max S ∈ A v ( S ) = K ( i − 1 , j ) 。
B \mathcal{B} B について。写像 Φ : B → F ( i − 1 , j − w i ) \Phi : \mathcal{B} \to \mathcal{F}(i-1, j-w_i) Φ : B → F ( i − 1 , j − w i ) 、Φ ( S ) = S ∖ { i } \Phi(S) = S \setminus \{i\} Φ ( S ) = S ∖ { i } が全単射であることを示します。
行き先が正しい: S ∈ B S \in \mathcal{B} S ∈ B なら S ∖ { i } ⊆ [ i − 1 ] S \setminus \{i\} \subseteq [i-1] S ∖ { i } ⊆ [ i − 1 ] であり、w ( S ∖ { i } ) = w ( S ) − w i ≤ j − w i w(S \setminus \{i\}) = w(S) - w_i \le j - w_i w ( S ∖ { i }) = w ( S ) − w i ≤ j − w i です(i ∈ S i \in S i ∈ S を使いました)。よって Φ ( S ) ∈ F ( i − 1 , j − w i ) \Phi(S) \in \mathcal{F}(i-1, j-w_i) Φ ( S ) ∈ F ( i − 1 , j − w i ) 。
逆写像がある: Ψ ( T ) = T ∪ { i } \Psi(T) = T \cup \{i\} Ψ ( T ) = T ∪ { i } とおきます。T ∈ F ( i − 1 , j − w i ) T \in \mathcal{F}(i-1, j-w_i) T ∈ F ( i − 1 , j − w i ) なら T ⊆ [ i − 1 ] T \subseteq [i-1] T ⊆ [ i − 1 ] より i ∉ T i \notin T i ∈ / T なので w ( T ∪ { i } ) = w ( T ) + w i ≤ ( j − w i ) + w i = j w(T \cup \{i\}) = w(T) + w_i \le (j - w_i) + w_i = j w ( T ∪ { i }) = w ( T ) + w i ≤ ( j − w i ) + w i = j であり、T ∪ { i } ⊆ [ i ] T \cup \{i\} \subseteq [i] T ∪ { i } ⊆ [ i ] かつ i i i を含むので Ψ ( T ) ∈ B \Psi(T) \in \mathcal{B} Ψ ( T ) ∈ B です。i ∉ T i \notin T i ∈ / T から Φ ( Ψ ( T ) ) = T \Phi(\Psi(T)) = T Φ ( Ψ ( T )) = T 、i ∈ S i \in S i ∈ S から Ψ ( Φ ( S ) ) = S \Psi(\Phi(S)) = S Ψ ( Φ ( S )) = S が従い、Φ \Phi Φ は全単射です。
さらに、i ∈ S i \in S i ∈ S より v ( S ) = v i + v ( S ∖ { i } ) = v i + v ( Φ ( S ) ) v(S) = v_i + v(S \setminus \{i\}) = v_i + v(\Phi(S)) v ( S ) = v i + v ( S ∖ { i }) = v i + v ( Φ ( S )) です。Φ \Phi Φ が全単射なので
max S ∈ B v ( S ) = max T ∈ F ( i − 1 , j − w i ) ( v i + v ( T ) ) = v i + K ( i − 1 , j − w i ) \max_{S \in \mathcal{B}} v(S) = \max_{T \in \mathcal{F}(i-1,\, j-w_i)} \bigl(v_i + v(T)\bigr) = v_i + K(i-1,\ j-w_i) S ∈ B max v ( S ) = T ∈ F ( i − 1 , j − w i ) max ( v i + v ( T ) ) = v i + K ( i − 1 , j − w i ) が成り立ちます。ここで F ( i − 1 , j − w i ) \mathcal{F}(i-1, j-w_i) F ( i − 1 , j − w i ) は ∅ \emptyset ∅ を含むので空でなく(j − w i ≥ 0 j - w_i \ge 0 j − w i ≥ 0 を使いました)、右辺の最大値は存在します。また { i } ∈ B \{i\} \in \mathcal{B} { i } ∈ B なので B ≠ ∅ \mathcal{B} \ne \emptyset B = ∅ 、∅ ∈ A \emptyset \in \mathcal{A} ∅ ∈ A なので A ≠ ∅ \mathcal{A} \ne \emptyset A = ∅ です。
空でない 2 つの有限集合の合併上の最大値は、それぞれの最大値の大きい方に等しいので、
K ( i , j ) = max { max S ∈ A v ( S ) , max S ∈ B v ( S ) } = max { K ( i − 1 , j ) , v i + K ( i − 1 , j − w i ) } K(i,j) = \max\Bigl\{ \max_{S \in \mathcal{A}} v(S),\ \max_{S \in \mathcal{B}} v(S) \Bigr\} = \max\bigl\{ K(i-1,j),\ v_i + K(i-1, j-w_i) \bigr\} K ( i , j ) = max { S ∈ A max v ( S ) , S ∈ B max v ( S ) } = max { K ( i − 1 , j ) , v i + K ( i − 1 , j − w i ) } となり、3 が示されました。
∎ 行 i-1 行 i K(i-1, j-wi ) K(i-1, j) K(i, j) 品物 i を入れない 品物 i を入れる(価値 +vi ) ナップサック表の遷移。セル (i, j) は 1 行上の 2 つのセルだけから決まる Proof(Corollary 6.4) 部分問題の添字集合を S = { ( i , j ) : 0 ≤ i ≤ n , 0 ≤ j ≤ W } S = \{(i,j) : 0 \le i \le n,\ 0 \le j \le W\} S = {( i , j ) : 0 ≤ i ≤ n , 0 ≤ j ≤ W } 、値を V ( i , j ) = K ( i , j ) V(i,j) = K(i,j) V ( i , j ) = K ( i , j ) とします。Theorem 6.2 より、依存先は D ( 0 , j ) = ( ) D(0,j) = () D ( 0 , j ) = ( ) 、j < w i j < w_i j < w i のとき D ( i , j ) = ( ( i − 1 , j ) ) D(i,j) = ((i-1, j)) D ( i , j ) = (( i − 1 , j )) 、j ≥ w i j \ge w_i j ≥ w i のとき D ( i , j ) = ( ( i − 1 , j ) , ( i − 1 , j − w i ) ) D(i,j) = ((i-1,j),\ (i-1, j-w_i)) D ( i , j ) = (( i − 1 , j ) , ( i − 1 , j − w i )) ととれます。ここで w i w_i w i と W W W が整数(Definition 6.1 )なので j − w i j - w_i j − w i も整数であり、0 ≤ j − w i ≤ W 0 \le j - w_i \le W 0 ≤ j − w i ≤ W より確かに S S S の元です。
依存グラフが DAG であることを確認します。辺は必ず第 1 成分が i − 1 i-1 i − 1 の点から i i i の点へ向かうので、辺を辿るたびに第 1 成分が 1 ずつ増えます。閉路があれば第 1 成分が元に戻らねばならず、矛盾です。よって Definition 3.1 の条件がすべて満たされます。
部分問題の個数は N = ( n + 1 ) ( W + 1 ) N = (n+1)(W+1) N = ( n + 1 ) ( W + 1 ) です。遷移は表引き高々 2 回、加算 1 回、比較 1 回なので c ( i , j ) = O ( 1 ) c(i,j) = O(1) c ( i , j ) = O ( 1 ) です。Theorem 3.2 より計算時間は O ( N ) = O ( ( n + 1 ) ( W + 1 ) ) = O ( n W + n + W + 1 ) O(N) = O((n+1)(W+1)) = O(nW + n + W + 1) O ( N ) = O (( n + 1 ) ( W + 1 )) = O ( nW + n + W + 1 ) 、領域も O ( N ) O(N) O ( N ) です。n ≥ 1 n \ge 1 n ≥ 1 なので、これは O ( n W + W ) O(nW + W) O ( nW + W ) と書けます。W = 0 W = 0 W = 0 の場合を別に扱えば(このとき答えは 0 0 0 です)、以後 W ≥ 1 W \ge 1 W ≥ 1 として O ( n W ) O(nW) O ( nW ) と書けます。
位相順序としては、i i i を 0 0 0 から n n n へ増やし、各 i i i の中では j j j を 0 0 0 から W W W へ動かす順序がとれます。実際、辺 ( i − 1 , ⋅ ) → ( i , ⋅ ) (i-1, \cdot) \to (i, \cdot) ( i − 1 , ⋅ ) → ( i , ⋅ ) は必ず i i i が増える向きなので、この順序で並べれば依存先は必ず先に来ます。
∎ """ w[i], v[i] は 0-indexed。最適値と、選んだ品物の 1-indexed 番号を返す。 """
K = [[ 0 ] * (W + 1 ) for _ in range ( n + 1 ) ]
for i in range ( 1 , n + 1 ):
K[i] [ j ] = K[i - 1 ] [ j ] # 品物 i を入れない
if j >= w[i - 1 ]: # 入れられるなら比較する
K[i] [ j ] = max ( K [ i ] [ j ], v [ i - 1 ] + K [ i - 1 ] [ j - w [ i - 1 ] ] )
for i in range ( n , 0 , - 1 ):
if K[i] [ j ] != K[i - 1 ] [ j ] :
解の復元が正しい理由。 最適「値」だけでなく最適「解」が欲しいことがほとんどです。上のコードは表を右下から左上へ辿って選んだ品物を復元しています。その正当性を確かめます。
まず Theorem 6.2 の 2 と 3 のどちらでも K ( i , j ) ≥ K ( i − 1 , j ) K(i,j) \ge K(i-1,j) K ( i , j ) ≥ K ( i − 1 , j ) です。したがって K ( i , j ) ≠ K ( i − 1 , j ) K(i,j) \ne K(i-1,j) K ( i , j ) = K ( i − 1 , j ) ならば K ( i , j ) > K ( i − 1 , j ) K(i,j) > K(i-1,j) K ( i , j ) > K ( i − 1 , j ) であり、3 の max \max max は第 2 項で達成されています。つまり j ≥ w i j \ge w_i j ≥ w i かつ K ( i , j ) = v i + K ( i − 1 , j − w i ) K(i,j) = v_i + K(i-1, j-w_i) K ( i , j ) = v i + K ( i − 1 , j − w i ) です。このとき、( i − 1 , j − w i ) (i-1, j-w_i) ( i − 1 , j − w i ) の最適解 T T T をとると、証明中の写像 Ψ \Psi Ψ により T ∪ { i } T \cup \{i\} T ∪ { i } は ( i , j ) (i,j) ( i , j ) の実行可能解で、その価値は v i + v ( T ) = v i + K ( i − 1 , j − w i ) = K ( i , j ) v_i + v(T) = v_i + K(i-1,j-w_i) = K(i,j) v i + v ( T ) = v i + K ( i − 1 , j − w i ) = K ( i , j ) 、すなわち最適解です。よって「品物 i i i を選び、( i − 1 , j − w i ) (i-1, j-w_i) ( i − 1 , j − w i ) へ移る」という手続きは最適性を保ちます。
逆に K ( i , j ) = K ( i − 1 , j ) K(i,j) = K(i-1,j) K ( i , j ) = K ( i − 1 , j ) のときは、( i − 1 , j ) (i-1,j) ( i − 1 , j ) の最適解がそのまま ( i , j ) (i,j) ( i , j ) の最適解になります(A ⊆ F ( i , j ) \mathcal{A} \subseteq \mathcal{F}(i,j) A ⊆ F ( i , j ) なので実行可能で、価値が K ( i , j ) K(i,j) K ( i , j ) に等しいからです)。よって「品物 i i i を選ばず ( i − 1 , j ) (i-1, j) ( i − 1 , j ) へ移る」も最適性を保ちます。i i i が n n n から 0 0 0 まで減るので手続きは必ず停止し、停止時に集めた集合は ( n , W ) (n,W) ( n , W ) の最適解です。復元にかかる時間は O ( n ) O(n) O ( n ) です。
Example 6.5 (表を最後まで埋める )
品物 4 個、容量 W = 7 W = 7 W = 7 の例です。
品物 i i i 1 2 3 4 重さ w i w_i w i 1 3 4 5 価値 v i v_i v i 1 4 5 7
行 0 0 0 は Theorem 6.2 の 1 より全部 0 0 0 です。行 1 1 1 (w 1 = 1 w_1 = 1 w 1 = 1 , v 1 = 1 v_1 = 1 v 1 = 1 )は、j = 0 j = 0 j = 0 では j < w 1 j < w_1 j < w 1 なので K ( 1 , 0 ) = K ( 0 , 0 ) = 0 K(1,0) = K(0,0) = 0 K ( 1 , 0 ) = K ( 0 , 0 ) = 0 、j ≥ 1 j \ge 1 j ≥ 1 では max { 0 , 1 + K ( 0 , j − 1 ) } = max { 0 , 1 } = 1 \max\{0,\ 1 + K(0, j-1)\} = \max\{0, 1\} = 1 max { 0 , 1 + K ( 0 , j − 1 )} = max { 0 , 1 } = 1 です。
行 2 2 2 (w 2 = 3 w_2 = 3 w 2 = 3 , v 2 = 4 v_2 = 4 v 2 = 4 )を丁寧に計算します。j = 0 , 1 , 2 j = 0,1,2 j = 0 , 1 , 2 では j < 3 j < 3 j < 3 なので上の行をそのまま写して 0 , 1 , 1 0, 1, 1 0 , 1 , 1 。j = 3 j = 3 j = 3 では max { K ( 1 , 3 ) , 4 + K ( 1 , 0 ) } = max { 1 , 4 + 0 } = 4 \max\{K(1,3),\ 4 + K(1,0)\} = \max\{1, 4+0\} = 4 max { K ( 1 , 3 ) , 4 + K ( 1 , 0 )} = max { 1 , 4 + 0 } = 4 。j = 4 j=4 j = 4 では max { 1 , 4 + K ( 1 , 1 ) } = max { 1 , 5 } = 5 \max\{1,\ 4 + K(1,1)\} = \max\{1, 5\} = 5 max { 1 , 4 + K ( 1 , 1 )} = max { 1 , 5 } = 5 。j = 5 , 6 , 7 j = 5, 6, 7 j = 5 , 6 , 7 では 4 + K ( 1 , j − 3 ) = 4 + 1 = 5 4 + K(1, j-3) = 4 + 1 = 5 4 + K ( 1 , j − 3 ) = 4 + 1 = 5 なので、いずれも 5 5 5 です。
同様に行 3 3 3 (w 3 = 4 w_3 = 4 w 3 = 4 , v 3 = 5 v_3 = 5 v 3 = 5 )と行 4 4 4 (w 4 = 5 w_4 = 5 w 4 = 5 , v 4 = 7 v_4 = 7 v 4 = 7 )を埋めると、表全体は次のようになります。
i \ j i \backslash j i \ j 0 1 2 3 4 5 6 7 0 0 0 0 0 0 0 0 0 1 0 1 1 1 1 1 1 1 2 0 1 1 4 5 5 5 5 3 0 1 1 4 5 6 6 9 4 0 1 1 4 5 7 8 9
たとえば K ( 3 , 7 ) = max { K ( 2 , 7 ) , 5 + K ( 2 , 3 ) } = max { 5 , 5 + 4 } = 9 K(3,7) = \max\{K(2,7),\ 5 + K(2,3)\} = \max\{5,\ 5+4\} = 9 K ( 3 , 7 ) = max { K ( 2 , 7 ) , 5 + K ( 2 , 3 )} = max { 5 , 5 + 4 } = 9 、K ( 4 , 7 ) = max { K ( 3 , 7 ) , 7 + K ( 3 , 2 ) } = max { 9 , 7 + 1 } = 9 K(4,7) = \max\{K(3,7),\ 7 + K(3,2)\} = \max\{9,\ 7+1\} = 9 K ( 4 , 7 ) = max { K ( 3 , 7 ) , 7 + K ( 3 , 2 )} = max { 9 , 7 + 1 } = 9 です。
復元。 ( i , j ) = ( 4 , 7 ) (i,j) = (4,7) ( i , j ) = ( 4 , 7 ) から始めます。K ( 4 , 7 ) = 9 = K ( 3 , 7 ) K(4,7) = 9 = K(3,7) K ( 4 , 7 ) = 9 = K ( 3 , 7 ) なので品物 4 は選ばず ( 3 , 7 ) (3,7) ( 3 , 7 ) へ。K ( 3 , 7 ) = 9 ≠ 5 = K ( 2 , 7 ) K(3,7) = 9 \ne 5 = K(2,7) K ( 3 , 7 ) = 9 = 5 = K ( 2 , 7 ) なので品物 3 を選び、j j j を 7 − w 3 = 3 7 - w_3 = 3 7 − w 3 = 3 にして ( 2 , 3 ) (2,3) ( 2 , 3 ) へ。K ( 2 , 3 ) = 4 ≠ 1 = K ( 1 , 3 ) K(2,3) = 4 \ne 1 = K(1,3) K ( 2 , 3 ) = 4 = 1 = K ( 1 , 3 ) なので品物 2 を選び、j j j を 3 − w 2 = 0 3 - w_2 = 0 3 − w 2 = 0 にして ( 1 , 0 ) (1,0) ( 1 , 0 ) へ。K ( 1 , 0 ) = 0 = K ( 0 , 0 ) K(1,0) = 0 = K(0,0) K ( 1 , 0 ) = 0 = K ( 0 , 0 ) なので品物 1 は選びません。
得られた解は S = { 2 , 3 } S = \{2, 3\} S = { 2 , 3 } で、重さ 3 + 4 = 7 ≤ 7 3 + 4 = 7 \le 7 3 + 4 = 7 ≤ 7 、価値 4 + 5 = 9 4 + 5 = 9 4 + 5 = 9 です。全 16 通りを手で確かめても、価値 9 9 9 を超える実行可能解はありません(品物 2 と 4 は重さ 8 > 7 8 > 7 8 > 7 、品物 3 と 4 は 9 > 7 9 > 7 9 > 7 で入りません)。
表は ( n + 1 ) ( W + 1 ) (n+1)(W+1) ( n + 1 ) ( W + 1 ) マスありますが、Theorem 6.2 の右辺に現れるのは行 i − 1 i-1 i − 1 だけです。したがって 1 行分の配列を使い回せます。ただし更新の順序に注意が要ります。
Proposition 6.6 (1 次元配列による計算 )
長さ W + 1 W+1 W + 1 の配列 κ \kappa κ を κ [ j ] = 0 \kappa[j] = 0 κ [ j ] = 0 (0 ≤ j ≤ W 0 \le j \le W 0 ≤ j ≤ W )で初期化し、i = 1 , 2 , … , n i = 1, 2, \ldots, n i = 1 , 2 , … , n の順に、各 i i i について j j j を W W W から w i w_i w i まで降順 に動かして
κ [ j ] ← max { κ [ j ] , v i + κ [ j − w i ] } \kappa[j] \leftarrow \max\bigl\{ \kappa[j],\ v_i + \kappa[j - w_i] \bigr\} κ [ j ] ← max { κ [ j ] , v i + κ [ j − w i ] } と更新する。このとき、i i i 回目のループを終えた時点で、すべての 0 ≤ j ≤ W 0 \le j \le W 0 ≤ j ≤ W について κ [ j ] = K ( i , j ) \kappa[j] = K(i, j) κ [ j ] = K ( i , j ) が成り立つ。とくに全体を終えると κ [ W ] = K ( n , W ) \kappa[W] = K(n, W) κ [ W ] = K ( n , W ) である。
Proof(Proposition 6.6) i i i に関する帰納法です。i = 0 i = 0 i = 0 (ループ開始前)では κ [ j ] = 0 = K ( 0 , j ) \kappa[j] = 0 = K(0,j) κ [ j ] = 0 = K ( 0 , j ) が Theorem 6.2 の 1 より成り立ちます。
i − 1 i-1 i − 1 回目を終えた時点で κ [ j ] = K ( i − 1 , j ) \kappa[j] = K(i-1, j) κ [ j ] = K ( i − 1 , j ) (すべての j j j )が成り立つとします。i i i 回目のループで、添字 j j j を書き換える瞬間の κ [ j ] \kappa[j] κ [ j ] と κ [ j − w i ] \kappa[j - w_i] κ [ j − w i ] の値を調べます。
w i ≥ 1 w_i \ge 1 w i ≥ 1 なので j − w i < j j - w_i < j j − w i < j です。このループは j j j を降順に動かすので、添字 j − w i j - w_i j − w i の書き換えは添字 j j j の書き換えより後 に起こります。また添字 j j j 自身はこのループでまだ書き換えられていません。したがって、右辺を評価する時点で κ [ j ] \kappa[j] κ [ j ] と κ [ j − w i ] \kappa[j-w_i] κ [ j − w i ] はどちらも i − 1 i-1 i − 1 回目終了時の値、すなわち帰納法の仮定より K ( i − 1 , j ) K(i-1, j) K ( i − 1 , j ) と K ( i − 1 , j − w i ) K(i-1, j-w_i) K ( i − 1 , j − w i ) です。よって代入後の値は
max { K ( i − 1 , j ) , v i + K ( i − 1 , j − w i ) } = K ( i , j ) \max\bigl\{ K(i-1,j),\ v_i + K(i-1, j-w_i) \bigr\} = K(i,j) max { K ( i − 1 , j ) , v i + K ( i − 1 , j − w i ) } = K ( i , j ) であり、最後の等号は Theorem 6.2 の 3 です(j ≥ w i j \ge w_i j ≥ w i の範囲を動かしているので 3 が適用できます)。j < w i j < w_i j < w i の範囲は書き換えられず K ( i − 1 , j ) K(i-1,j) K ( i − 1 , j ) のままですが、これは Theorem 6.2 の 2 より K ( i , j ) K(i,j) K ( i , j ) に等しい値です。以上ですべての j j j で κ [ j ] = K ( i , j ) \kappa[j] = K(i,j) κ [ j ] = K ( i , j ) となりました。
∎ def knapsack_value ( w , v , W ) :
for j in range ( W , wi - 1 , - 1 ): # 降順が本質的
kappa[j] = max ( kappa [ j ] , vi + kappa [ j - wi ])
Caution
range(W, wi - 1, -1) を range(wi, W + 1) に書き換える、つまり昇順にすると答えが変わります。昇順だと κ [ j − w i ] \kappa[j - w_i] κ [ j − w i ] がすでに i i i 回目の更新を受けており、品物 i i i を 2 回以上使った値が混ざるからです。これは 0-1 ナップサックとしては誤りですが、各品物を何個でも使ってよい問題の正解になります(Exercise 8.3 )。1 文字の違いが解く問題を変える、動的計画法らしい落とし穴です。
Corollary 6.4 は嬉しい結果ですが、注意が要ります。計算量は入力のサイズ の関数として測るのが約束でした(計算量と O 記法 、Definition 2.3[Complexity and Big-O Notation] )。ナップサック問題の入力を 2 進表記で書き下すと、その長さは Θ ( ∑ i ( log w i + log v i ) + log W ) \Theta\bigl(\sum_i (\log w_i + \log v_i) + \log W\bigr) Θ ( ∑ i ( log w i + log v i ) + log W ) 程度です。容量 W W W は log W \log W log W ビットで書けてしまいます。
つまり、入力長を L L L とすると W W W は 2 L 2^{L} 2 L 程度まで大きくなり得ます。n = 100 n = 100 n = 100 、W = 2 60 W = 2^{60} W = 2 60 という入力は 2 進表記で数百ビットしかありませんが、O ( n W ) O(nW) O ( nW ) の表は 10 20 10^{20} 1 0 20 マスを超えます。このように「数値そのもの」に対して多項式で、入力のビット長 に対しては指数的な計算量を擬多項式時間 といいます。
0-1 ナップサック問題は NP 困難です。ですから、P ≠ N P \mathrm{P} \ne \mathrm{NP} P = NP ならば真の多項式時間アルゴリズム(Definition 7.1[Complexity and Big-O Notation] )は存在しません(P≠NP予想とは何か )。動的計画法は万能の魔法ではなく、「数値が小さいときに限って指数の壁を回避できる」道具なのだ、と理解してください。
ここまでの例から、動的計画法の設計手順を取り出せます。
部分問題を決める。 何を添字にするか。Example 5.2 が示すように、添字は「上位の問題を解くのに必要な情報をすべて含む」必要があります。
漸化式を立てる。 最適解の「最後の一手」で場合分けするのが定石です。ナップサックでは「品物 i i i を入れたか」でした。
基底を決める。 添字が最小のところで、直接値が定まるようにします。
順序と復元を決める。 依存グラフの位相順序でループを書き、必要なら解を復元する情報を残します。
この手順を、文字列の比較に使う編集距離に当てはめます。
Definition 7.1 (編集距離(レーベンシュタイン距離) )
有限アルファベット上の文字列 x x x 、y y y に対し、次の 3 種の操作をそれぞれコスト 1 1 1 として、x x x を y y y に変換する操作列のうちコストの総和が最小のものの値を編集距離 d ( x , y ) d(x,y) d ( x , y ) と呼ぶ。
1 文字の削除
1 文字の挿入
1 文字を別の 1 文字に置換
Theorem 7.3 (編集距離の漸化式 )
x = x 1 x 2 ⋯ x m x = x_1 x_2 \cdots x_m x = x 1 x 2 ⋯ x m 、y = y 1 y 2 ⋯ y n y = y_1 y_2 \cdots y_n y = y 1 y 2 ⋯ y n を有限アルファベット上の文字列とし、0 ≤ i ≤ m 0 \le i \le m 0 ≤ i ≤ m 、0 ≤ j ≤ n 0 \le j \le n 0 ≤ j ≤ n に対して D ( i , j ) = d ( x 1 ⋯ x i , y 1 ⋯ y j ) D(i,j) = d(x_1 \cdots x_i,\ y_1 \cdots y_j) D ( i , j ) = d ( x 1 ⋯ x i , y 1 ⋯ y j ) とおく(i = 0 i = 0 i = 0 や j = 0 j = 0 j = 0 のときは空文字列とする)。このとき
D ( i , 0 ) = i ( 0 ≤ i ≤ m ) , D ( 0 , j ) = j ( 0 ≤ j ≤ n ) D(i, 0) = i \quad (0 \le i \le m), \qquad D(0, j) = j \quad (0 \le j \le n) D ( i , 0 ) = i ( 0 ≤ i ≤ m ) , D ( 0 , j ) = j ( 0 ≤ j ≤ n ) であり、1 ≤ i ≤ m 1 \le i \le m 1 ≤ i ≤ m 、1 ≤ j ≤ n 1 \le j \le n 1 ≤ j ≤ n に対して
D ( i , j ) = min { D ( i − 1 , j ) + 1 , D ( i , j − 1 ) + 1 , D ( i − 1 , j − 1 ) + [ x i ≠ y j ] } D(i,j) = \min\bigl\{\, D(i-1, j) + 1,\ \ D(i, j-1) + 1,\ \ D(i-1,j-1) + [\, x_i \ne y_j \,] \,\bigr\} D ( i , j ) = min { D ( i − 1 , j ) + 1 , D ( i , j − 1 ) + 1 , D ( i − 1 , j − 1 ) + [ x i = y j ] } が成り立つ。ここで [ x i ≠ y j ] [\,x_i \ne y_j\,] [ x i = y j ] は x i ≠ y j x_i \ne y_j x i = y j のとき 1 1 1 、x i = y j x_i = y_j x i = y j のとき 0 0 0 を表す。
Proof(Theorem 7.3) Remark 7.2 により、D ( i , j ) D(i,j) D ( i , j ) を x 1 ⋯ x i x_1\cdots x_i x 1 ⋯ x i と y 1 ⋯ y j y_1 \cdots y_j y 1 ⋯ y j のアライメントの最小コストとして扱います。
基底。 x 1 ⋯ x i x_1 \cdots x_i x 1 ⋯ x i と空文字列のアライメントは、b t b_t b t がすべてギャップでなければならないので、列は ( x 1 , − ) , … , ( x i , − ) (x_1, -), \ldots, (x_i, -) ( x 1 , − ) , … , ( x i , − ) の i i i 個に限られます。各列のコストは 1 1 1 なので合計 i i i 、これが唯一のアライメントなので D ( i , 0 ) = i D(i,0) = i D ( i , 0 ) = i です。D ( 0 , j ) = j D(0,j) = j D ( 0 , j ) = j も同様です。
≤ \le ≤ の向き。 1 ≤ i ≤ m 1 \le i \le m 1 ≤ i ≤ m 、1 ≤ j ≤ n 1 \le j \le n 1 ≤ j ≤ n とします。x 1 ⋯ x i − 1 x_1\cdots x_{i-1} x 1 ⋯ x i − 1 と y 1 ⋯ y j y_1 \cdots y_j y 1 ⋯ y j の最小コストのアライメントの右端に、列 ( x i , − ) (x_i, -) ( x i , − ) を付け足すと、x 1 ⋯ x i x_1 \cdots x_i x 1 ⋯ x i と y 1 ⋯ y j y_1 \cdots y_j y 1 ⋯ y j のアライメントが得られ、そのコストは D ( i − 1 , j ) + 1 D(i-1,j) + 1 D ( i − 1 , j ) + 1 です。D ( i , j ) D(i,j) D ( i , j ) は最小値ですから D ( i , j ) ≤ D ( i − 1 , j ) + 1 D(i,j) \le D(i-1,j) + 1 D ( i , j ) ≤ D ( i − 1 , j ) + 1 。同様に右端に ( − , y j ) (-, y_j) ( − , y j ) を付ければ D ( i , j ) ≤ D ( i , j − 1 ) + 1 D(i,j) \le D(i,j-1)+1 D ( i , j ) ≤ D ( i , j − 1 ) + 1 、( x i , y j ) (x_i, y_j) ( x i , y j ) を付ければ D ( i , j ) ≤ D ( i − 1 , j − 1 ) + [ x i ≠ y j ] D(i,j) \le D(i-1,j-1) + [x_i \ne y_j] D ( i , j ) ≤ D ( i − 1 , j − 1 ) + [ x i = y j ] が従います。3 つすべてが成り立つので、D ( i , j ) D(i,j) D ( i , j ) は右辺の min \min min 以下です。
≥ \ge ≥ の向き。 x 1 ⋯ x i x_1 \cdots x_i x 1 ⋯ x i と y 1 ⋯ y j y_1\cdots y_j y 1 ⋯ y j の最小コストのアライメント A A A を 1 つとり、その最後の列 ( a ℓ , b ℓ ) (a_\ell, b_\ell) ( a ℓ , b ℓ ) を見ます。i , j ≥ 1 i, j \ge 1 i , j ≥ 1 より A A A は空でなく、また両方ギャップの列は許されないので、可能性は次の 3 つだけです。
( x i , − ) (x_i, -) ( x i , − ) の場合:最後の列を除いた A ′ A' A ′ は x 1 ⋯ x i − 1 x_1\cdots x_{i-1} x 1 ⋯ x i − 1 と y 1 ⋯ y j y_1 \cdots y_j y 1 ⋯ y j のアライメントであり、コストは c o s t ( A ) − 1 \mathrm{cost}(A) - 1 cost ( A ) − 1 です。D ( i − 1 , j ) D(i-1,j) D ( i − 1 , j ) は最小値なので D ( i − 1 , j ) ≤ c o s t ( A ) − 1 = D ( i , j ) − 1 D(i-1,j) \le \mathrm{cost}(A) - 1 = D(i,j) - 1 D ( i − 1 , j ) ≤ cost ( A ) − 1 = D ( i , j ) − 1 、すなわち D ( i , j ) ≥ D ( i − 1 , j ) + 1 D(i,j) \ge D(i-1,j) + 1 D ( i , j ) ≥ D ( i − 1 , j ) + 1 。
( − , y j ) (-, y_j) ( − , y j ) の場合:同様に D ( i , j ) ≥ D ( i , j − 1 ) + 1 D(i,j) \ge D(i,j-1) + 1 D ( i , j ) ≥ D ( i , j − 1 ) + 1 。
( x i , y j ) (x_i, y_j) ( x i , y j ) の場合:最後の列を除いた A ′ A' A ′ は x 1 ⋯ x i − 1 x_1 \cdots x_{i-1} x 1 ⋯ x i − 1 と y 1 ⋯ y j − 1 y_1 \cdots y_{j-1} y 1 ⋯ y j − 1 のアライメントで、コストは c o s t ( A ) − [ x i ≠ y j ] \mathrm{cost}(A) - [x_i \ne y_j] cost ( A ) − [ x i = y j ] です。よって D ( i , j ) ≥ D ( i − 1 , j − 1 ) + [ x i ≠ y j ] D(i,j) \ge D(i-1,j-1) + [x_i \ne y_j] D ( i , j ) ≥ D ( i − 1 , j − 1 ) + [ x i = y j ] 。
いずれの場合も D ( i , j ) D(i,j) D ( i , j ) は右辺の 3 つの量のうち少なくとも 1 つ以上なので、D ( i , j ) ≥ min D(i,j) \ge \min D ( i , j ) ≥ min (3 つの量の最小値)が成り立ちます。両向きを合わせて等号が示されました。
∎ 依存グラフの辺は ( i − 1 , j ) (i-1,j) ( i − 1 , j ) 、( i , j − 1 ) (i,j-1) ( i , j − 1 ) 、( i − 1 , j − 1 ) (i-1,j-1) ( i − 1 , j − 1 ) から ( i , j ) (i,j) ( i , j ) へ向かい、辺を辿るたびに i + j i + j i + j が必ず増えるので閉路はありません。部分問題は ( m + 1 ) ( n + 1 ) (m+1)(n+1) ( m + 1 ) ( n + 1 ) 個、遷移は O ( 1 ) O(1) O ( 1 ) なので、Theorem 3.2 より計算時間は O ( m n ) O(mn) O ( mn ) 、領域は O ( m n ) O(mn) O ( mn ) です(1 行ずつ捨てれば O ( min { m , n } ) O(\min\{m,n\}) O ( min { m , n }) にできます)。
Example 7.4 (kitten と sitting の編集距離 )
x = kitten x = \texttt{kitten} x = kitten (m = 6 m=6 m = 6 )、y = sitting y = \texttt{sitting} y = sitting (n = 7 n=7 n = 7 )の表を埋めます。第 0 行と第 0 列は基底の D ( 0 , j ) = j D(0,j) = j D ( 0 , j ) = j 、D ( i , 0 ) = i D(i,0) = i D ( i , 0 ) = i です。
ε \varepsilon ε s i t t i n g ε \varepsilon ε 0 1 2 3 4 5 6 7 k 1 1 2 3 4 5 6 7 i 2 2 1 2 3 4 5 6 t 3 3 2 1 2 3 4 5 t 4 4 3 2 1 2 3 4 e 5 5 4 3 2 2 3 4 n 6 6 5 4 3 3 2 3
たとえば D ( 1 , 1 ) D(1,1) D ( 1 , 1 ) は x 1 = k x_1 = \texttt{k} x 1 = k 、y 1 = s y_1 = \texttt{s} y 1 = s で異なるので min { D ( 0 , 1 ) + 1 , D ( 1 , 0 ) + 1 , D ( 0 , 0 ) + 1 } = min { 2 , 2 , 1 } = 1 \min\{D(0,1)+1,\ D(1,0)+1,\ D(0,0)+1\} = \min\{2, 2, 1\} = 1 min { D ( 0 , 1 ) + 1 , D ( 1 , 0 ) + 1 , D ( 0 , 0 ) + 1 } = min { 2 , 2 , 1 } = 1 です。D ( 2 , 2 ) D(2,2) D ( 2 , 2 ) は x 2 = y 2 = i x_2 = y_2 = \texttt{i} x 2 = y 2 = i なので min { D ( 1 , 2 ) + 1 , D ( 2 , 1 ) + 1 , D ( 1 , 1 ) + 0 } = min { 3 , 3 , 1 } = 1 \min\{D(1,2)+1,\ D(2,1)+1,\ D(1,1)+0\} = \min\{3, 3, 1\} = 1 min { D ( 1 , 2 ) + 1 , D ( 2 , 1 ) + 1 , D ( 1 , 1 ) + 0 } = min { 3 , 3 , 1 } = 1 です。
答えは D ( 6 , 7 ) = 3 D(6,7) = 3 D ( 6 , 7 ) = 3 。実際、kitten → sitten \texttt{kitten} \to \texttt{sitten} kitten → sitten (k を s に置換)→ sittin \to \texttt{sittin} → sittin (e を i に置換)→ sitting \to \texttt{sitting} → sitting (g を挿入)で 3 手です。表が 3 3 3 を返し、かつ 3 手の変換が実在するので、これが最小です。
Exercise 8.1 易
Proposition 4.1 を使って、fib_naive(100) の関数呼び出し回数を求め、1 秒間に 10 9 10^9 1 0 9 回の呼び出しを処理できる計算機での実行時間を年単位で見積もってください。F 101 = 573,147,844,013,817,084,101 F_{101} = 573{,}147{,}844{,}013{,}817{,}084{,}101 F 101 = 573 , 147 , 844 , 013 , 817 , 084 , 101 を用いてよいものとします。
Solution Proposition 4.1 より呼び出し回数は
T ( 100 ) = 2 F 101 − 1 = 1,146,295,688,027,634,168,201 ≈ 1.15 × 10 21 T(100) = 2F_{101} - 1 = 1{,}146{,}295{,}688{,}027{,}634{,}168{,}201 \approx 1.15 \times 10^{21} T ( 100 ) = 2 F 101 − 1 = 1 , 146 , 295 , 688 , 027 , 634 , 168 , 201 ≈ 1.15 × 1 0 21 回です。10 9 10^9 1 0 9 回毎秒で割ると 1.15 × 10 12 1.15 \times 10^{12} 1.15 × 1 0 12 秒。1 年は約 3.156 × 10 7 3.156 \times 10^{7} 3.156 × 1 0 7 秒なので
1.15 × 10 12 3.156 × 10 7 ≈ 3.6 × 10 4 \frac{1.15 \times 10^{12}}{3.156 \times 10^{7}} \approx 3.6 \times 10^{4} 3.156 × 1 0 7 1.15 × 1 0 12 ≈ 3.6 × 1 0 4 年、およそ 3 万 6 千年です。一方 Corollary 4.2 の方法なら加算 99 回で終わります。同じ再帰式から出発しても、計算の組み立て方でこれだけ差がつきます。
Exercise 8.2 標準
硬貨の額面 c 1 , … , c k ∈ Z ≥ 1 c_1, \ldots, c_k \in \mathbb{Z}_{\ge 1} c 1 , … , c k ∈ Z ≥ 1 が与えられ、各額面は何枚でも使えるとします。金額 A ∈ Z ≥ 0 A \in \mathbb{Z}_{\ge 0} A ∈ Z ≥ 0 をちょうど作るのに必要な硬貨の最小枚数 M ( A ) M(A) M ( A ) を求める漸化式を立て、計算量を Theorem 3.2 で評価してください。また、額面が { 1 , 4 , 5 } \{1, 4, 5\} { 1 , 4 , 5 } 、A = 8 A = 8 A = 8 のとき、「大きい額面から貪欲に取る」方法が最適でないことを確かめてください。
Solution 漸化式。 部分問題を「金額 a a a をちょうど作る最小枚数 M ( a ) M(a) M ( a ) 」(0 ≤ a ≤ A 0 \le a \le A 0 ≤ a ≤ A )とします。作れないときは M ( a ) = + ∞ M(a) = +\infty M ( a ) = + ∞ と約束します。最後に使った硬貨の額面 c t c_t c t で場合分けすると
M ( 0 ) = 0 , M ( a ) = min { M ( a − c t ) + 1 : 1 ≤ t ≤ k , c t ≤ a } ( a ≥ 1 ) M(0) = 0, \qquad M(a) = \min\bigl\{\, M(a - c_t) + 1 \ :\ 1 \le t \le k,\ c_t \le a \,\bigr\} \quad (a \ge 1) M ( 0 ) = 0 , M ( a ) = min { M ( a − c t ) + 1 : 1 ≤ t ≤ k , c t ≤ a } ( a ≥ 1 ) です(c t ≤ a c_t \le a c t ≤ a を満たす t t t が 1 つもなければ M ( a ) = + ∞ M(a) = +\infty M ( a ) = + ∞ )。正しさは、金額 a a a を作る最小の硬貨の多重集合から硬貨 1 枚(額面 c t c_t c t )を取り除くと金額 a − c t a - c_t a − c t を作る多重集合になり、逆に a − c t a - c_t a − c t を作る多重集合に c t c_t c t を 1 枚足すと a a a を作る多重集合になる、という対応から従います(Theorem 6.2 と同じ形の議論です)。
計算量。 部分問題は A + 1 A+1 A + 1 個、各遷移は最大 k k k 回の比較なので c ( a ) ≤ c ⋅ k c(a) \le c \cdot k c ( a ) ≤ c ⋅ k 。c t ≥ 1 c_t \ge 1 c t ≥ 1 より a − c t < a a - c_t < a a − c t < a なので依存グラフは DAG です。Theorem 3.2 より O ( k A ) O(kA) O ( k A ) 時間、O ( A ) O(A) O ( A ) 領域です。
貪欲法の反例。 { 1 , 4 , 5 } \{1,4,5\} { 1 , 4 , 5 } 、A = 8 A = 8 A = 8 で貪欲に取ると、5 → 5 \to 5 → 残り 3 → 1 , 1 , 1 3 \to 1,1,1 3 → 1 , 1 , 1 で 4 枚です。しかし漸化式で表を埋めると
a a a 0 1 2 3 4 5 6 7 8 M ( a ) M(a) M ( a ) 0 1 2 3 1 1 2 3 2
となり、M ( 8 ) = 2 M(8) = 2 M ( 8 ) = 2 です。実際 8 = 4 + 4 8 = 4 + 4 8 = 4 + 4 で 2 枚です。M ( 8 ) = min { M ( 7 ) + 1 , M ( 4 ) + 1 , M ( 3 ) + 1 } = min { 4 , 2 , 4 } = 2 M(8) = \min\{M(7)+1,\ M(4)+1,\ M(3)+1\} = \min\{4, 2, 4\} = 2 M ( 8 ) = min { M ( 7 ) + 1 , M ( 4 ) + 1 , M ( 3 ) + 1 } = min { 4 , 2 , 4 } = 2 と計算されています。貪欲法は「最初の 1 手を確定させる」ため、後の選択肢を壊すことがあります。動的計画法は全ての選択肢を残したまま比較する点が違います。
Exercise 8.3 標準
Definition 6.1 と同じ入力に対し、各品物を何個でも使ってよいとした問題(無限ナップサック問題)を考えます。K ′ ( i , j ) K'(i,j) K ′ ( i , j ) を「品物 1 , … , i 1, \ldots, i 1 , … , i を何個でも使えて容量 j j j のときの最大価値」として漸化式を立て、証明してください。また Proposition 6.6 の 1 次元アルゴリズムで j j j を昇順 に回すとこの問題の答えが得られる理由を説明してください。
Solution 漸化式。 1 ≤ i ≤ n 1 \le i \le n 1 ≤ i ≤ n 、w i ≤ j ≤ W w_i \le j \le W w i ≤ j ≤ W のとき
K ′ ( i , j ) = max { K ′ ( i − 1 , j ) , v i + K ′ ( i , j − w i ) } K'(i,j) = \max\bigl\{\, K'(i-1, j),\ \ v_i + K'(i,\ j - w_i) \,\bigr\} K ′ ( i , j ) = max { K ′ ( i − 1 , j ) , v i + K ′ ( i , j − w i ) } であり、j < w i j < w_i j < w i のときは K ′ ( i , j ) = K ′ ( i − 1 , j ) K'(i,j) = K'(i-1,j) K ′ ( i , j ) = K ′ ( i − 1 , j ) 、K ′ ( 0 , j ) = 0 K'(0,j) = 0 K ′ ( 0 , j ) = 0 です。第 2 項の第 1 添字が i − 1 i-1 i − 1 ではなく i i i である点が Theorem 6.2 との違いです。
証明は同じ形です。品物 i i i の使用個数が 0 0 0 個である多重集合の全体を A \mathcal{A} A 、1 1 1 個以上である全体を B \mathcal{B} B とすると、A \mathcal{A} A 側の最大値は K ′ ( i − 1 , j ) K'(i-1,j) K ′ ( i − 1 , j ) です。B \mathcal{B} B 側については、品物 i i i を 1 個取り除く写像が B \mathcal{B} B から「品物 1 , … , i 1,\ldots,i 1 , … , i を使い容量 j − w i j - w_i j − w i 以下の多重集合の全体」への全単射になり(逆写像は品物 i i i を 1 個足す操作)、価値は v i v_i v i だけ減るので、最大値は v i + K ′ ( i , j − w i ) v_i + K'(i, j-w_i) v i + K ′ ( i , j − w i ) です。取り除いた後も品物 i i i を使ってよいので添字が i i i のまま残る、というのが違いのすべてです。依存グラフは、辺を辿るたびに ( i , j ) (i,j) ( i , j ) の i + j i + j i + j が真に増えるので DAG です。
昇順でよい理由。 1 次元配列 κ \kappa κ で j j j を昇順に回すと、κ [ j ] \kappa[j] κ [ j ] を更新する時点で κ [ j − w i ] \kappa[j - w_i] κ [ j − w i ] は j − w i < j j - w_i < j j − w i < j よりすでに i i i 回目の更新を受け終わっています。Proposition 6.6 と同じ帰納法をたどれば、その値は K ′ ( i , j − w i ) K'(i, j-w_i) K ′ ( i , j − w i ) です。また κ [ j ] \kappa[j] κ [ j ] 自身はまだ i i i 回目の更新前なので K ′ ( i − 1 , j ) K'(i-1,j) K ′ ( i − 1 , j ) です。よって代入結果は max { K ′ ( i − 1 , j ) , v i + K ′ ( i , j − w i ) } = K ′ ( i , j ) \max\{K'(i-1,j),\ v_i + K'(i, j-w_i)\} = K'(i,j) max { K ′ ( i − 1 , j ) , v i + K ′ ( i , j − w i )} = K ′ ( i , j ) となり、上の漸化式そのものになります。降順が 0-1 版、昇順が無限版、という対応です。
Exercise 8.4 難
実数列 a 1 , a 2 , … , a n a_1, a_2, \ldots, a_n a 1 , a 2 , … , a n に対し、添字が増加し値も狭義単調増加する部分列の最大長を求める問題(最長増加部分列)を考えます。L ( i ) L(i) L ( i ) を「a i a_i a i で終わる増加部分列の最大長」として漸化式を立て、O ( n 2 ) O(n^2) O ( n 2 ) で解けることを示してください。さらに a = ( 3 , 1 , 4 , 1 , 5 , 9 , 2 , 6 ) a = (3,1,4,1,5,9,2,6) a = ( 3 , 1 , 4 , 1 , 5 , 9 , 2 , 6 ) で計算してください。
Solution 漸化式。 L ( i ) = 1 + max ( { L ( j ) : 1 ≤ j < i , a j < a i } ∪ { 0 } ) L(i) = 1 + \max\bigl(\{L(j) : 1 \le j < i,\ a_j < a_i\} \cup \{0\}\bigr) L ( i ) = 1 + max ( { L ( j ) : 1 ≤ j < i , a j < a i } ∪ { 0 } ) とします。空集合との合併は「条件を満たす j j j がないときは max \max max を 0 0 0 とする」ための約束で、そのとき L ( i ) = 1 L(i) = 1 L ( i ) = 1 です。
正しさを見ます。a i a_i a i で終わる最長の増加部分列を a j 1 < a j 2 < ⋯ < a j r = a i a_{j_1} < a_{j_2} < \cdots < a_{j_r} = a_i a j 1 < a j 2 < ⋯ < a j r = a i (j r = i j_r = i j r = i )とします。r = 1 r = 1 r = 1 なら L ( i ) = 1 L(i) = 1 L ( i ) = 1 で式は正しい。r ≥ 2 r \ge 2 r ≥ 2 なら、末尾を除いた a j 1 , … , a j r − 1 a_{j_1}, \ldots, a_{j_{r-1}} a j 1 , … , a j r − 1 は a j r − 1 a_{j_{r-1}} a j r − 1 で終わる増加部分列であり、j r − 1 < i j_{r-1} < i j r − 1 < i かつ a j r − 1 < a i a_{j_{r-1}} < a_i a j r − 1 < a i なので r − 1 ≤ L ( j r − 1 ) r - 1 \le L(j_{r-1}) r − 1 ≤ L ( j r − 1 ) 、よって L ( i ) ≤ 1 + max { L ( j ) : ⋯ } L(i) \le 1 + \max\{L(j) : \cdots\} L ( i ) ≤ 1 + max { L ( j ) : ⋯ } 。逆に、条件を満たす任意の j j j について、a j a_j a j で終わる最長増加部分列の後ろに a i a_i a i を付ければ長さ L ( j ) + 1 L(j) + 1 L ( j ) + 1 の増加部分列(a j < a i a_j < a_i a j < a i なので単調性は保たれます)が作れるので L ( i ) ≥ L ( j ) + 1 L(i) \ge L(j) + 1 L ( i ) ≥ L ( j ) + 1 。両向きで等号です。
答えは max 1 ≤ i ≤ n L ( i ) \max_{1 \le i \le n} L(i) max 1 ≤ i ≤ n L ( i ) です。部分問題は n n n 個、L ( i ) L(i) L ( i ) の計算に j = 1 , … , i − 1 j = 1, \ldots, i-1 j = 1 , … , i − 1 の走査で O ( n ) O(n) O ( n ) かかるので、Theorem 3.2 より ∑ i c ( i ) = O ( n 2 ) \sum_i c(i) = O(n^2) ∑ i c ( i ) = O ( n 2 ) です。依存グラフは添字が真に増える向きの辺だけなので DAG です。
計算。 a = ( 3 , 1 , 4 , 1 , 5 , 9 , 2 , 6 ) a = (3,1,4,1,5,9,2,6) a = ( 3 , 1 , 4 , 1 , 5 , 9 , 2 , 6 ) で左から求めます。L ( 1 ) = 1 L(1) = 1 L ( 1 ) = 1 (a 1 = 3 a_1=3 a 1 = 3 、左に何もない)。L ( 2 ) = 1 L(2) = 1 L ( 2 ) = 1 (a 2 = 1 a_2 = 1 a 2 = 1 より小さい値が左にない)。L ( 3 ) L(3) L ( 3 ) :a 3 = 4 a_3 = 4 a 3 = 4 より小さいのは a 1 = 3 , a 2 = 1 a_1 = 3, a_2 = 1 a 1 = 3 , a 2 = 1 で max { 1 , 1 } = 1 \max\{1,1\} = 1 max { 1 , 1 } = 1 、よって 2 2 2 。L ( 4 ) = 1 L(4) = 1 L ( 4 ) = 1 (a 4 = 1 a_4 = 1 a 4 = 1 )。L ( 5 ) L(5) L ( 5 ) :a 5 = 5 a_5 = 5 a 5 = 5 より小さいのは a 1 , a 2 , a 3 , a 4 a_1,a_2,a_3,a_4 a 1 , a 2 , a 3 , a 4 で max { 1 , 1 , 2 , 1 } = 2 \max\{1,1,2,1\} = 2 max { 1 , 1 , 2 , 1 } = 2 、よって 3 3 3 。L ( 6 ) L(6) L ( 6 ) :a 6 = 9 a_6 = 9 a 6 = 9 より小さいのは全部で max { 1 , 1 , 2 , 1 , 3 } = 3 \max\{1,1,2,1,3\} = 3 max { 1 , 1 , 2 , 1 , 3 } = 3 、よって 4 4 4 。L ( 7 ) L(7) L ( 7 ) :a 7 = 2 a_7 = 2 a 7 = 2 より小さいのは a 2 = 1 , a 4 = 1 a_2 = 1, a_4 = 1 a 2 = 1 , a 4 = 1 で max { 1 , 1 } = 1 \max\{1,1\}=1 max { 1 , 1 } = 1 、よって 2 2 2 。L ( 8 ) L(8) L ( 8 ) :a 8 = 6 a_8 = 6 a 8 = 6 より小さいのは a 1 , a 2 , a 3 , a 4 , a 5 , a 7 a_1,a_2,a_3,a_4,a_5,a_7 a 1 , a 2 , a 3 , a 4 , a 5 , a 7 で max { 1 , 1 , 2 , 1 , 3 , 2 } = 3 \max\{1,1,2,1,3,2\} = 3 max { 1 , 1 , 2 , 1 , 3 , 2 } = 3 、よって 4 4 4 。
( L ( i ) ) = ( 1 , 1 , 2 , 1 , 3 , 4 , 2 , 4 ) (L(i)) = (1,1,2,1,3,4,2,4) ( L ( i )) = ( 1 , 1 , 2 , 1 , 3 , 4 , 2 , 4 ) で、答えは 4 4 4 です。L ( 6 ) = 4 L(6) = 4 L ( 6 ) = 4 を達成するのは 1 , 4 , 5 , 9 1, 4, 5, 9 1 , 4 , 5 , 9 (a 2 , a 3 , a 5 , a 6 a_2, a_3, a_5, a_6 a 2 , a 3 , a 5 , a 6 )などです。
なお、「長さごとの末尾の最小値」を配列で管理すると、各 i i i での探索を二分探索に置き換えて O ( n log n ) O(n \log n) O ( n log n ) にできます(探索アルゴリズム 、Theorem 3.3[探索アルゴリズム] )。動的計画法の計算量は、遷移の計算をデータ構造で加速することでさらに下げられることがあります。
T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein, Introduction to Algorithms , 4th ed., MIT Press, 2022 — 第 14 章「Dynamic Programming」(第 3 版では第 15 章)。ロッド切断問題から始めて最適部分構造と部分問題の重複を丁寧に扱っています。
J. Kleinberg, É. Tardos, Algorithm Design , Addison-Wesley, 2005 — 第 6 章「Dynamic Programming」。§6.4 でナップサック問題、§11.8 でその近似解法(FPTAS)を扱っています。
R. Bellman, Dynamic Programming , Princeton University Press, 1957 — 動的計画法の原典。最適性原理の定式化があります。
R. Bellman, Eye of the Hurricane: An Autobiography , World Scientific, 1984 — 「dynamic programming」という名称の由来についての本人の回想。
R. A. Wagner, M. J. Fischer, “The string-to-string correction problem”, Journal of the ACM 21 (1974), 168–173 — 編集距離の O ( m n ) O(mn) O ( mn ) アルゴリズムと、その正しさの証明。
D. Gusfield, Algorithms on Strings, Trees, and Sequences , Cambridge University Press, 1997 — 第 11 章。編集距離、アライメント、動的計画法の関係を体系的に扱っています。
価値を添字にする。 Definition 6.1 のナップサック問題で、価値 v i v_i v i がすべて非負整数だとします。Corollary 6.4 の O ( n W ) O(nW) O ( nW ) は W W W が大きいと使えませんが、添字の役割を入れ替えると別の計算量が得られます。
M ( i , p ) = min { w ( S ) : S ⊆ [ i ] , v ( S ) = p } M(i, p) = \min\bigl\{\, w(S) \ :\ S \subseteq [i],\ v(S) = p \,\bigr\} M ( i , p ) = min { w ( S ) : S ⊆ [ i ] , v ( S ) = p } を「品物 1 , … , i 1,\ldots,i 1 , … , i から選んで価値がちょうど p p p になる部分集合の最小の重さ」と定めます(そのような S S S がなければ + ∞ +\infty + ∞ )。基底は M ( 0 , 0 ) = 0 M(0,0) = 0 M ( 0 , 0 ) = 0 と M ( 0 , p ) = + ∞ M(0,p) = +\infty M ( 0 , p ) = + ∞ (p ≥ 1 p \ge 1 p ≥ 1 )です。Theorem 6.2 とまったく同じ全単射の議論により、p ≥ v i p \ge v_i p ≥ v i のとき
M ( i , p ) = min { M ( i − 1 , p ) , w i + M ( i − 1 , p − v i ) } M(i,p) = \min\bigl\{\, M(i-1, p),\ \ w_i + M(i-1,\ p - v_i) \,\bigr\} M ( i , p ) = min { M ( i − 1 , p ) , w i + M ( i − 1 , p − v i ) } が、p < v i p < v_i p < v i のとき M ( i , p ) = M ( i − 1 , p ) M(i,p) = M(i-1,p) M ( i , p ) = M ( i − 1 , p ) が成り立ちます。求める最適値は max { p : M ( n , p ) ≤ W } \max\{p : M(n,p) \le W\} max { p : M ( n , p ) ≤ W } です。V = ∑ i v i V = \sum_{i} v_i V = ∑ i v i とおくと部分問題は ( n + 1 ) ( V + 1 ) (n+1)(V+1) ( n + 1 ) ( V + 1 ) 個なので、Theorem 3.2 より O ( n V ) O(nV) O ( nV ) 時間です。
Example 6.5 の例では V = 1 + 4 + 5 + 7 = 17 V = 1 + 4 + 5 + 7 = 17 V = 1 + 4 + 5 + 7 = 17 なので O ( 4 ⋅ 18 ) O(4 \cdot 18) O ( 4 ⋅ 18 ) 、この場合は O ( n W ) O(nW) O ( nW ) 版と大差ありませんが、W W W が巨大で価値が小さい入力ではこちらが圧倒的に速くなります。逆に価値が巨大なら O ( n W ) O(nW) O ( nW ) 版を使います。同じ問題に対して複数の部分問題の切り方があり、入力の性質で使い分ける というのは、動的計画法では珍しくありません。
近似への入り口。 O ( n V ) O(nV) O ( nV ) という形は、価値を適当な単位で丸めて V V V を小さくすれば計算が速くなることを示唆します。丸めによる誤差を制御すると、任意の ε > 0 \varepsilon > 0 ε > 0 に対して最適値の ( 1 − ε ) (1-\varepsilon) ( 1 − ε ) 倍以上の解を多項式時間で返すアルゴリズム(FPTAS)が構成できます。詳しくは Kleinberg–Tardos の §11.8 を参照してください。NP 困難性が「厳密解の多項式時間計算」を阻んでも、近似ならこの壁を越えられることがあります。