コンテンツにスキップ

動的計画法:部分問題を一度だけ解いて指数時間を多項式時間に変える

対象読者: 本テーマ第 1〜5 章(O 記法、データ構造、ソート、探索、グラフアルゴリズム)を学んだ人。再帰と漸化式、有向非巡回グラフと位相順の考え方を仮定し、動的計画法は仮定しません。

前提となる記事:グラフアルゴリズム:BFS・DFS・ダイクストラ法を正しさから理解する

生 Markdown

フィボナッチ数のように、前の答えを足して次の答えを作る計算を、定義どおりに書くと極端に遅くなることがあります。原因は、同じ計算を何度もやり直しているからです。この記事では「一度出した答えを書き留めて使い回す」という考え方を整理し、なぜそれで速くなるのか、どんな問題に使えるのかを調べ、荷物の詰め込み問題(ナップサック問題)で実際に使ってみます。

  • 動的計画法は「問題を部分問題に分ける」技法ではありません。分割統治もそれをします。本質は、分けた部分問題が重複して現れるときに、答えを表に記録して一度しか解かないことです。
  • 部分問題の依存関係が有向非巡回グラフ(DAG)(閉路のない有向グラフ)になっていれば、位相順(依存される側が必ず先に来る並び順)に解けます。全体の計算時間は「部分問題の個数 × 1 つあたりの遷移コスト」で見積もれます(定理 3.2)。この 1 本の定理で、以下のすべての計算量が出ます。
  • フィボナッチ数を定義どおりの再帰で計算すると、関数呼び出しの総数はちょうど 2Fn+1−12F_{n+1} - 1 回、すなわち Θ(φn)\Theta(\varphi^n) 回(φn\varphi^n に比例して指数的に増える回数)です。表を使えば O(n)O(n) 回の加算で済みます。
  • 動的計画法が使えるかどうかは最適部分構造が成り立つかで決まります。成り立たない例(最長単純道)を見れば、どこで壊れるのかがはっきりします。
  • 0-1 ナップサック問題は O(nW)O(nW) の表計算で最適値と最適解の両方が求まります。ただしこれは入力のビット長に対する多項式時間アルゴリズムではありません(擬多項式時間。§6.3 で説明します)。この差は本質的で、ナップサック問題自体は NP 困難(効率のよい解法が知られていない難問の仲間)です。

1. 動機:同じ部分問題を何度も解いていないか

Section titled “1. 動機:同じ部分問題を何度も解いていないか”

再帰は、問題を小さな同種の問題に分解する強力な道具です。マージソートは長さ nn の列を長さ ⌊n/2⌋\lfloor n/2 \rfloor と ⌈n/2⌉\lceil n/2 \rceil の 2 つに分け、それぞれを整列してから併合しました(ソートアルゴリズム、定理 4.2[ソートアルゴリズム])。ここで見落としやすい前提があります。分割された 2 つの部分列は互いに交わらない、つまり左半分を整列する仕事と右半分を整列する仕事はまったく別物だ、ということです。

ところが、同じ形で書いた再帰が破滅的に遅くなることがあります。フィボナッチ数 F0=0F_0 = 0、F1=1F_1 = 1、Fn=Fn−1+Fn−2F_n = F_{n-1} + F_{n-2} (n≥2)(n \ge 2) の定義をそのまま Python に写してみます。

def fib_naive(n):
if n <= 1:
return n
return fib_naive(n - 1) + fib_naive(n - 2)

このコードは正しく動きます。しかし fib_naive(40) は普通の計算機で数秒かかり、fib_naive(50) は分単位になります。nn を 1 増やすごとに時間がおよそ 1.6 倍、10 増やすと約 123 倍です。理由は、呼び出しの木を描けばすぐに見えます。

fib_naive(5) の呼び出し木。F(3) は 2 回、F(2) は 3 回、F(1) は 5 回、それぞれ独立に計算されている

F(3)F(3) を計算する部分木が、まるごと 2 回現れています。F(2)F(2) は 3 回です。nn を大きくすればこの重複は指数的に膨れ上がります。一方で、私たちが本当に知る必要のある値は F0,F1,…,FnF_0, F_1, \ldots, F_n の n+1n+1 個しかありません。一度計算した答えを書き留めて使い回す——これが動的計画法のすべてです。

アイデア自体は素朴です。しかしそれだけでは、次の 3 つの問いに答えられません。

  1. 使い回してよいのはなぜか。部分問題の答えは、それを呼び出した文脈に依存しないのか。
  2. どんな問題で使えるのか。使えない問題との境目はどこか。
  3. どれだけ速くなるのか。速さは何で決まるのか。

この記事はこの 3 つに順に答えます。なお「動的計画法(dynamic programming)」という名前は 1950 年代に Richard Bellman が付けたものです。彼は自伝のなかで、当時の国防長官が「研究」という語を嫌ったため、数学的な色を消しつつ印象のよい語を選んだ、という趣旨のことを書いています。名前から意味を読み取ろうとしても徒労で、ここでの programming は「表を作る計画」ほどの意味であり、プログラミング言語とは関係ありません。

計算量は単位費用 RAM モデル(定義 2.1[計算量と O 記法])で数えます。加減算・比較・配列の添字アクセスを 1 ステップとします。OO 記法については 計算量と O 記法 を、配列が添字アクセスを O(1)O(1) でできることについては 基本的なデータ構造(命題 3.2[基本的なデータ構造])を前提とします。有向非巡回グラフ(DAG)と位相ソートは グラフアルゴリズム(DAG であることの特徴づけは 定理 4.2[グラフアルゴリズム])の内容を使います。

記号を決めておきます。

  • [n]={1,2,…,n}[n] = \{1, 2, \ldots, n\} とし、[0]=∅[0] = \emptyset とします。
  • Z≥0\mathbb{Z}_{\ge 0} は非負整数全体、Z≥1\mathbb{Z}_{\ge 1} は正整数全体を表します。
  • φ=1+52=1.6180…\varphi = \dfrac{1 + \sqrt{5}}{2} = 1.6180\ldots を黄金比とします。φ\varphi は t2=t+1t^2 = t + 1 の正の解なので、φ2=φ+1\varphi^2 = \varphi + 1 が成り立ちます。この関係だけを後で使います。
  • 部分集合 S⊆[n]S \subseteq [n] と数列 (ai)(a_i) に対し、a(S)=∑i∈Saia(S) = \sum_{i \in S} a_i と略記します。a(∅)=0a(\emptyset) = 0 です。

まず、動的計画法という手法を一般的な形で書き下します。個々の問題ごとに計算量を数え直さずに済むようにするためです。

次の定義は記号が多いですが、言っていることは「各部分問題の答えは、いくつかの別の部分問題の答えから決まり、その依存関係に循環がない」ということだけです。たとえばフィボナッチ数では、FkF_k が Fk−1F_{k-1} と Fk−2F_{k-2} から決まります(§4.1 で実際に当てはめます)。

定義 3.1(部分問題族と依存グラフ)↗

有限集合 SS と、各 s∈Ss \in S に対して定まる値 V(s)V(s) の組が次の 3 条件を満たすとき、これを部分問題族と呼ぶことにします。

  1. 各 s∈Ss \in S に、SS の元からなる有限列 D(s)=(s1,…,sks)D(s) = (s_1, \ldots, s_{k_s}) が定まっている。これを ss の依存先と呼びます。
  2. 各 s∈Ss \in S に関数 gsg_s が定まっていて、V(s)=gs(V(s1),…,V(sks))V(s) = g_s\bigl(V(s_1), \ldots, V(s_{k_s})\bigr) が成り立つ。
  3. 有向グラフ G=(S,E)G = (S, E)、E={(s′,s):s∈S, s′∈D(s)}E = \bigl\{ (s', s) : s \in S,\ s' \in D(s) \bigr\} が閉路をもたない(DAG である)。

ks=0k_s = 0 である ss、すなわち依存先をもたない部分問題を基底と呼びます。基底では V(s)=gs()V(s) = g_s() は定数として直接与えられます。

条件 3 の辺は「依存される側 →\to 依存する側」の向きに引いてあります。ですから GG の位相順序に沿って計算すれば、必要な値は必ず先に手に入っています。この観察をそのまま定理にしたのが次です。

定理 3.2(動的計画法の計算量)✓

定義 3.1 の意味の部分問題族 (S,D,g)(S, D, g) が与えられ、N=∣S∣N = |S| とする。表への読み書きが 11 ステップででき、各 ss について、依存先の値 V(s1),…,V(sks)V(s_1), \ldots, V(s_{k_s}) がすでに表にあるという条件のもとで gsg_s の評価が c(s)c(s) ステップでできるとする。このとき、すべての s∈Ss \in S に対する V(s)V(s) を

O(N+∑s∈Sc(s))O\Bigl(N + \sum_{s \in S} c(s)\Bigr)

ステップ、O(N)O(N) の作業領域で計算できる。とくに、ある定数 cc が存在してすべての s∈Ss \in S で c(s)≤cc(s) \le c ならば、計算時間は O(Nc)O(Nc) である。

証明(定理 3.2)

G=(S,E)G = (S, E) を 定義 3.1 の条件 3 の DAG とします。

第 1 段:位相順序をとる。 条件 3 より GG は DAG なので、位相順序、すなわち SS の全体の並べ替え s(1),s(2),…,s(N)s^{(1)}, s^{(2)}, \ldots, s^{(N)} で、(s(p),s(q))∈E(s^{(p)}, s^{(q)}) \in E ならば必ず p<qp < q となるものが存在します(グラフアルゴリズム)。その計算には O(N+∣E∣)O(N + |E|) ステップかかります。ここで ∣E∣=∑s∈Sks|E| = \sum_{s \in S} k_s ですが、gsg_s を評価するには少なくとも ksk_s 個の引数を読まなければならないので c(s)≥ksc(s) \ge k_s であり、したがって ∣E∣≤∑sc(s)|E| \le \sum_{s} c(s) です。つまり位相ソートの費用は主張の O(N+∑sc(s))O(N + \sum_s c(s)) に収まります。

第 2 段:位相順に埋める。 大きさ NN の表 TT を用意し(確保と初期化に O(N)O(N))、q=1,2,…,Nq = 1, 2, \ldots, N の順に T[s(q)]←gs(q)(T[s1],…,T[sk])T[s^{(q)}] \leftarrow g_{s^{(q)}}\bigl(T[s_1], \ldots, T[s_{k}]\bigr) と代入します(D(s(q))=(s1,…,sk)D(s^{(q)}) = (s_1, \ldots, s_k))。

この代入が正しく実行できることを確認します。si∈D(s(q))s_i \in D(s^{(q)}) なら (si,s(q))∈E(s_i, s^{(q)}) \in E ですから、位相順序の定義により sis_i は s(q)s^{(q)} より前に並んでいます。よって T[si]T[s_i] にはすでに値が入っています。さらに qq に関する帰納法により、その値は V(si)V(s_i) に等しい:q=1q = 1 のとき s(1)s^{(1)} は依存先をもてない(もてば依存先が s(1)s^{(1)} より前に来て矛盾)ので基底であり、T[s(1)]=gs(1)()=V(s(1))T[s^{(1)}] = g_{s^{(1)}}() = V(s^{(1)})。qq 未満で成立を仮定すれば、T[si]=V(si)T[s_i] = V(s_i) なので条件 2 より T[s(q)]=gs(q)(V(s1),…,V(sk))=V(s(q))T[s^{(q)}] = g_{s^{(q)}}(V(s_1), \ldots, V(s_k)) = V(s^{(q)}) です。

第 3 段:費用を数える。 各 qq での代入は仮定より c(s(q))c(s^{(q)}) ステップです。総和は ∑s∈Sc(s)\sum_{s \in S} c(s)。これに第 1 段と表の初期化の O(N+∑sc(s))O(N + \sum_s c(s)) を足しても主張の範囲です。領域は表の O(N)O(N) に、位相ソートの作業領域 O(N)O(N) を足して O(N)O(N) です。

最後の主張は、c(s)≤cc(s) \le c なら ∑sc(s)≤Nc\sum_s c(s) \le Nc であり、また c≥1c \ge 1 としてよいので N+Nc=O(Nc)N + Nc = O(Nc) となることから従います。

この定理の使い方は決まっています。部分問題を何個作ったか(NN)と、1 つの部分問題を解くのに何ステップかかるか(cc)を数える。掛ければ計算量が出る。 以降のフィボナッチ、ナップサック、編集距離の計算量は、すべてこの一言で片づきます。

3.1. トップダウンとボトムアップ

Section titled “3.1. トップダウンとボトムアップ”

定理 3.2 の証明では位相順序を先に求めましたが、実装では 2 つの流儀があります。

命題 3.3(メモ化再帰の計算量)✓

定義 3.1 の設定のもとで、次の再帰手続き solve(s)\mathrm{solve}(s) を考える。

  • 表 T[s]T[s] が「未計算」でなければ T[s]T[s] を返す。
  • そうでなければ、D(s)=(s1,…,sk)D(s) = (s_1, \ldots, s_k) の各要素に対して solve(si)\mathrm{solve}(s_i) を呼び、その結果に gsg_s を適用した値を T[s]T[s] に書き込んで返す。

このとき、s0∈Ss_0 \in S から GG の辺を逆向きに辿って到達できる部分問題の集合を R⊆SR \subseteq S(s0∈Rs_0 \in R)とすると、solve(s0)\mathrm{solve}(s_0) は停止し、V(s0)V(s_0) を返す。その計算時間は O(∣R∣+∑s∈Rc(s))O\bigl(|R| + \sum_{s \in R} c(s)\bigr) であり、RR に属さない部分問題は一度も評価されない。

証明(命題 3.3)

停止性と正しさ。 GG は DAG なので、ss から辺を逆向きに辿る道の長さの最大値 h(s)h(s)(ss の依存の深さ)が有限に定まります。閉路があればこれは定義できませんから、ここで条件 3 を使っています。h(s)h(s) に関する帰納法で示します。h(s)=0h(s) = 0 なら D(s)D(s) は空、つまり ss は基底なので、solve(s)\mathrm{solve}(s) は再帰せずに gs()=V(s)g_s() = V(s) を返して停止します。h(s)=d>0h(s) = d > 0 とすると、各 si∈D(s)s_i \in D(s) は h(si)≤d−1h(s_i) \le d - 1 を満たすので、帰納法の仮定より solve(si)\mathrm{solve}(s_i) は停止して V(si)V(s_i) を返します。よって solve(s)\mathrm{solve}(s) は停止し、条件 2 より gs(V(s1),…,V(sk))=V(s)g_s(V(s_1), \ldots, V(s_k)) = V(s) を返します。

各部分問題の本体は高々 1 回しか実行されない。 ss の本体(再帰呼び出しと gsg_s の評価)が実行し終わると T[s]T[s] に値が書き込まれ、以後「未計算」ではなくなります。したがって 2 回目以降の solve(s)\mathrm{solve}(s) は最初の分岐で戻ります。本体が実行されるのは最初の 1 回だけです。

呼び出し回数。 本体が実行される ss は RR の元だけです(s0s_0 から到達不能な部分問題は呼ばれません)。本体 1 回につき再帰呼び出しは ∣D(s)∣=ks|D(s)| = k_s 回起きるので、solve\mathrm{solve} の総呼び出し回数は 1+∑s∈Rks1 + \sum_{s \in R} k_s です。定理 3.2 の証明と同じ理由で ks≤c(s)k_s \le c(s) ですから、呼び出しのオーバーヘッド(11 回あたり O(1)O(1))の総和は O(∣R∣+∑s∈Rc(s))O(|R| + \sum_{s \in R} c(s)) に収まります。本体での gsg_s の評価は合計 ∑s∈Rc(s)\sum_{s \in R} c(s) ステップです。

2 つの流儀を比べます。

メモ化再帰(トップダウン)表計算(ボトムアップ)
計算順序再帰が依存関係を自動的に辿る位相順を人が決めてループに書く
実際に解く部分問題必要なものだけ(命題 3.3 の RR)表の全マス
実装素朴な再帰に表を足すだけループの順序を設計する必要がある
弱点再帰の深さ制限、呼び出しのオーバーヘッド使わない部分問題も解いてしまう
領域の削減しにくい「直前の行だけ持つ」等がやりやすい

どちらも 定理 3.2 と同じオーダーの計算量になります。以下では両方を使い分けます。

4. フィボナッチ数列:指数時間から線形時間へ

Section titled “4. フィボナッチ数列:指数時間から線形時間へ”

§1 で観察した重複を、まず定量化します。

命題 4.1(素朴な再帰の呼び出し回数)✓

T(n)T(n) を、fib_naive(n) の実行中に起きる関数呼び出しの総数(最初の 1 回を含む)とする。n≥0n \ge 0 に対して

T(n)=2Fn+1−1T(n) = 2F_{n+1} - 1

が成り立つ。さらに n≥1n \ge 1 に対して 2φn−1−1≤T(n)≤2φn−12\varphi^{n-1} - 1 \le T(n) \le 2\varphi^{n} - 1 であり、したがって T(n)=Θ(φn)T(n) = \Theta(\varphi^n) である。

証明(命題 4.1)

等式。 nn に関する強い帰納法です。n=0n = 0 のとき、fib_naive(0) は再帰せずに戻るので T(0)=1T(0) = 1。一方 2F1−1=2⋅1−1=12F_1 - 1 = 2 \cdot 1 - 1 = 1 で一致します。n=1n = 1 のときも同様に T(1)=1T(1) = 1 で、2F2−1=2⋅1−1=12F_2 - 1 = 2 \cdot 1 - 1 = 1 です。n≥2n \ge 2 とし、nn 未満で成立を仮定します。fib_naive(n) は自分自身の 1 回に加えて fib_naive(n-1) と fib_naive(n-2) を呼ぶので

T(n)=1+T(n−1)+T(n−2)=1+(2Fn−1)+(2Fn−1−1)=2(Fn+Fn−1)−1=2Fn+1−1T(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

となります。最後の等号はフィボナッチ数の漸化式です。

評価。 補助的に、m≥1m \ge 1 に対して φm−2≤Fm≤φm−1\varphi^{m-2} \le F_m \le \varphi^{m-1} を示します。m=1m = 1 では φ−1=0.618…≤1=F1≤φ0=1\varphi^{-1} = 0.618\ldots \le 1 = F_1 \le \varphi^0 = 1、m=2m = 2 では φ0=1≤1=F2≤φ1\varphi^0 = 1 \le 1 = F_2 \le \varphi^1 で成立します。m≥3m \ge 3 とし、mm 未満で成立を仮定すると、§2 で確認した φ2=φ+1\varphi^2 = \varphi + 1 を使って

Fm=Fm−1+Fm−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},Fm=Fm−1+Fm−2≤φm−2+φm−3=φm−3(φ+1)=φm−1F_m = F_{m-1} + F_{m-2} \le \varphi^{m-2} + \varphi^{m-3} = \varphi^{m-3}(\varphi + 1) = \varphi^{m-1}

が得られます。この評価を m=n+1m = n+1 に適用すれば φn−1≤Fn+1≤φn\varphi^{n-1} \le F_{n+1} \le \varphi^{n} であり、等式に代入して主張が出ます。φ>1\varphi > 1 なので T(n)T(n) は指数的に増大します。

この式は具体的な数を入れると効きます。T(50)=2F51−1=40,730,022,147T(50) = 2F_{51} - 1 = 40{,}730{,}022{,}147、およそ 4.1×10104.1 \times 10^{10} 回。1 秒間に 10910^9 回の関数呼び出しをこなす計算機でも 40 秒です。n=100n = 100 ではどうなるかは 演習 8.1 で計算してもらいます。

定義 3.1 の言葉で書き直します。部分問題の添字集合は S={0,1,…,n}S = \{0, 1, \ldots, n\}、値は V(k)=FkV(k) = F_k、依存先は D(0)=D(1)=()D(0) = D(1) = ()(基底)、k≥2k \ge 2 では D(k)=(k−1,k−2)D(k) = (k-1, k-2)、遷移は gk(a,b)=a+bg_k(a, b) = a + b です。辺は kk の小さい方から大きい方へ向かうので閉路はなく、DAG の条件も満たされます。

系 4.2(フィボナッチ数の線形時間計算)✓

上の部分問題族に 定理 3.2 を適用すると、F0,F1,…,FnF_0, F_1, \ldots, F_n のすべてが O(n)O(n) ステップ、O(n)O(n) の領域で計算できる。FnF_n だけが必要なら領域は O(1)O(1) でよい。

証明(系 4.2)

部分問題の個数は N=n+1N = n + 1 です。遷移 gkg_k は表引き 2 回と加算 1 回なので c(k)=O(1)c(k) = O(1)、すなわち定数 cc で上から押さえられます。定理 3.2 より計算時間は O(Nc)=O(n)O(Nc) = O(n)、領域は O(N)=O(n)O(N) = O(n) です。

領域についての後半を示します。位相順序は 0,1,2,…,n0, 1, 2, \ldots, n ととれ、kk の計算に必要なのは k−1k-1 と k−2k-2 の値だけです。よって直前 2 つの値を変数 2 個に保持し、kk を 1 つ進めるごとに更新すれば、表全体を保持する必要はありません。使う記憶は変数 2 個と添字 1 個で O(1)O(1) です。

トップダウン(メモ化再帰)とボトムアップ(反復)の両方を書いておきます。

def fib_memo(n, memo=None):
if memo is None:
memo = {}
if n <= 1:
return n
if n in memo: # すでに計算済みなら表を引くだけ
return memo[n]
memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo)
return memo[n]
def fib_table(n):
if n <= 1:
return n
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))
return b

fib_memo は素朴な再帰に 3 行足しただけです。それでも 命題 3.3 より、実際に本体が実行される部分問題は R={0,1,…,n}R = \{0, 1, \ldots, n\} の n+1n+1 個だけになります。

例 4.3(n = 10 での比較)

n=10n = 10 の表を実際に埋めます。左から順に Fk=Fk−1+Fk−2F_{k} = F_{k-1} + F_{k-2} を計算するだけです。

kk012345678910
FkF_k011235813213455

加算は 9 回、表のマスは 11 個です。一方、素朴な再帰の呼び出し回数は 命題 4.1 より T(10)=2F11−1=2⋅89−1=177T(10) = 2F_{11} - 1 = 2 \cdot 89 - 1 = 177 回でした。n=10n = 10 ですでに 16 倍の差がついています。n=30n = 30 では T(30)=2F31−1=2,692,537T(30) = 2F_{31} - 1 = 2{,}692{,}537 回に対し、表は 31 マスです。

注意 4.4(単位費用モデルの限界)

系 4.2 の O(n)O(n) は「1 回の加算が O(1)O(1)」という単位費用モデルの下での値です。しかし 命題 4.1 の証明で示した Fm≥φm−2F_m \ge \varphi^{m-2} より、FnF_n の 2 進表記は Θ(n)\Theta(n) ビットあります。多倍長整数として扱えば 1 回の加算は Θ(n)\Theta(n) ビット演算なので、ビット演算で数えた計算量は Θ(n2)\Theta(n^2) です。それでも指数から多項式への改善は本物です。

なお、FnF_n だけが欲しいなら、行列の等式

(Fn+1FnFnFn−1)=(1110)n\begin{pmatrix} F_{n+1} & F_n \\ F_n & F_{n-1} \end{pmatrix} = \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix}^{n}

を繰り返し二乗法で計算して、乗算 O(log⁡n)O(\log n) 回に減らせます。これは動的計画法ではなく、問題固有の代数構造を使う別の技法です。動的計画法は「構造が見つからないときにも使える汎用の道具」だと考えてください。

5. 最適部分構造:使える条件と、壊れる例

Section titled “5. 最適部分構造:使える条件と、壊れる例”

フィボナッチ数は「値を計算する」問題でした。動的計画法が本領を発揮するのは最適化問題ですが、そこには追加の条件が要ります。

定義 5.1(最適部分構造)↗

最適化問題の族 {Ps}s∈S\{P_s\}_{s \in S} があり、PsP_s の最適値を V(s)V(s) とします。各 s∈Ss \in S に対して依存先 D(s)=(s1,…,sk)D(s) = (s_1, \ldots, s_k) と関数 gsg_s が存在して

V(s)=gs(V(s1),…,V(sk))V(s) = g_s\bigl(V(s_1), \ldots, V(s_k)\bigr)

が成り立ち、かつ依存グラフが DAG になるとき、この問題族は最適部分構造をもつといいます。

言い換えると、PsP_s の最適値が部分問題の最適値だけから復元できる、ということです。ここが要点で、「部分問題の最適解を貼り合わせたものが実行可能解になる」ことと「実行可能解を分解すると部分問題の実行可能解になる」ことの両方が要ります。この確認に使う標準的な議論が切り貼り論法です。PsP_s の最適解 xx を分解して得た部分解 xix_i が PsiP_{s_i} の最適解でないとすると、xix_i をより良い解に取り替えて(切って貼って)xx より良い PsP_s の解が作れてしまい、xx の最適性に反する、という背理法です。定理 6.2 の証明では、この論法を全単射の形で厳密に実行します。

最適部分構造は自明に成り立つ性質ではありません。壊れる例を見ておくと、何が効いているのかがはっきりします。次の例で使うグラフを先に図で示します。

uvwx
次の例のグラフ。太線は u から v への単純道 u→x→w→v(3 辺)。この道に辺 vw を足すと w を 2 度通ってしまう

例 5.2(最長単純道では最適部分構造が壊れる)

無向グラフ GG を、頂点集合 {u,v,w,x}\{u, v, w, x\}、辺集合 {uv, vw, wx, xu, uw}\{uv,\ vw,\ wx,\ xu,\ uw\} で定めます。単純道とは同じ頂点を 2 度通らない道のことで、L(a,b)L(a, b) を aa から bb への単純道の辺数の最大値とします。

フィボナッチと同じ発想で「最後の辺で場合分け」すれば、L(u,w)=max⁡{L(u,t)+1:t は w の隣接点}L(u, w) = \max\{L(u, t) + 1 : t \text{ は } w \text{ の隣接点}\} という漸化式を書きたくなります。しかしこれは成り立ちません。実際に両辺を計算します。

まず左辺です。uu から ww への単純道を列挙します。u→wu \to w(1 辺)、u→v→wu \to v \to w(2 辺)、u→x→wu \to x \to w(2 辺)。3 辺の単純道はありません:u→v→?→wu \to v \to ? \to w の形にするには vv の隣接点が u,wu, w しかないので不可能、u→x→?→wu \to x \to ? \to w も xx の隣接点が u,wu, w だけなので不可能です。よって L(u,w)=2L(u, w) = 2 です。

次に右辺の一項を計算します。ww の隣接点 vv について、uu から vv への単純道は u→vu \to v(1 辺)、u→w→vu \to w \to v(2 辺)、u→x→w→vu \to x \to w \to v(3 辺)で、L(u,v)=3L(u, v) = 3 です。したがって右辺は L(u,v)+1=4L(u, v) + 1 = 4 以上になり、左辺の 22 と一致しません。

破綻の理由は具体的です。L(u,v)L(u,v) を達成する道 u→x→w→vu \to x \to w \to v に辺 vwvw を足すと u→x→w→v→wu \to x \to w \to v \to w となり、ww を 2 度通るので単純道になりません。部分問題の最適解が、上位の問題では実行可能ですらないのです。原因は、部分問題「uu から vv への最長単純道」が「これまでどの頂点を使ったか」という情報を捨ててしまっていることにあります。部分問題が状態を十分に表現していないわけです。

なお、最長単純道問題は NP 困難(定義 6.1[P≠NP 予想とは何か])であることが知られています(P≠NP予想とは何か)。安直な動的計画法が効かないのには理由がある、ということです。

定義 6.1(0-1 ナップサック問題)↗

n∈Z≥1n \in \mathbb{Z}_{\ge 1}、重さ w1,…,wn∈Z≥1w_1, \ldots, w_n \in \mathbb{Z}_{\ge 1}、価値 v1,…,vn∈R≥0v_1, \ldots, v_n \in \mathbb{R}_{\ge 0}、容量 W∈Z≥0W \in \mathbb{Z}_{\ge 0} が与えられる。部分集合 S⊆[n]S \subseteq [n] が実行可能であるとは w(S)=∑i∈Swi≤Ww(S) = \sum_{i \in S} w_i \le W が成り立つことをいう。実行可能な SS のうち v(S)=∑i∈Sviv(S) = \sum_{i \in S} v_i を最大にするものを求めよ、というのが 0-1 ナップサック問題である。

「0-1」は各品物を入れるか入れないか(0 個か 1 個か)の二択である、という意味です。素朴には 2n2^n 通りの部分集合を全部試せば解けますが、n=60n = 60 で 260≈1.15×10182^{60} \approx 1.15 \times 10^{18} 通りとなり現実的ではありません。

部分問題の設計が勝負です。「品物 1,…,i1, \ldots, i までしか使えず、容量が jj しかない」という制限つきの問題を考えます。

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)

この最大値は必ず存在します。集合 F(i,j)={S⊆[i]:w(S)≤j}\mathcal{F}(i,j) = \{S \subseteq [i] : w(S) \le j\} は ∅\emptyset を含むので空でなく、2[i]2^{[i]} の部分集合なので有限だからです。求めたい答えは K(n,W)K(n, W) です。

次の定理のねらいは、K(i,j)K(i,j) を「品物 ii を入れない場合」と「入れる場合」の 2 つに分け、どちらも 1 つ前の行 i−1i-1 の値で書けることを示すことです。証明では、入れる場合の部分集合と、1 つ前の行の部分集合が 1 対 1 に対応することを使います。

定理 6.2(ナップサック問題の漸化式)✓

定義 6.1 の設定のもとで、上の K(i,j)K(i,j) について次が成り立つ。

  1. すべての 0≤j≤W0 \le j \le W に対し K(0,j)=0K(0, j) = 0。
  2. 1≤i≤n1 \le i \le n かつ 0≤j<wi0 \le j < w_i のとき K(i,j)=K(i−1,j)K(i, j) = K(i-1, j)。
  3. 1≤i≤n1 \le i \le n かつ wi≤j≤Ww_i \le j \le W のとき
K(i,j)=max⁡{ K(i−1,j),  vi+K(i−1, j−wi) }.K(i, j) = \max\bigl\{\, K(i-1, j),\ \ v_i + K(i-1,\ j - w_i) \,\bigr\}.
証明(定理 6.2)

1 の証明。 S⊆[0]=∅S \subseteq [0] = \emptyset なる SS は S=∅S = \emptyset のみで、w(∅)=0≤jw(\emptyset) = 0 \le j より実行可能、v(∅)=0v(\emptyset) = 0 です。したがって最大値は 00 です。

2 の証明。 j<wij < w_i とします。S∈F(i,j)S \in \mathcal{F}(i,j) が i∈Si \in S を満たすと仮定すると、重さはすべて正(定義 6.1 で wk∈Z≥1w_k \in \mathbb{Z}_{\ge 1} としました)なので w(S)≥wi>jw(S) \ge w_i > j となり、w(S)≤jw(S) \le j に矛盾します。よって F(i,j)\mathcal{F}(i,j) の元はすべて ii を含まず、S⊆[i]∖{i}=[i−1]S \subseteq [i] \setminus \{i\} = [i-1] です。逆に F(i−1,j)\mathcal{F}(i-1,j) の元は F(i,j)\mathcal{F}(i,j) の元でもあります。つまり F(i,j)=F(i−1,j)\mathcal{F}(i,j) = \mathcal{F}(i-1,j) であり、同じ集合上で同じ関数 vv を最大化するので K(i,j)=K(i−1,j)K(i,j) = K(i-1,j) です。

3 の証明。 wi≤jw_i \le j とします。F(i,j)\mathcal{F}(i,j) を、ii を含まないもの全体 A\mathcal{A} と、ii を含むもの全体 B\mathcal{B} に分割します。F(i,j)=A⊔B\mathcal{F}(i,j) = \mathcal{A} \sqcup \mathcal{B} です。

A\mathcal{A} について。2 の証明と同じ議論で A=F(i−1,j)\mathcal{A} = \mathcal{F}(i-1, j) です。よって max⁡S∈Av(S)=K(i−1,j)\max_{S \in \mathcal{A}} v(S) = K(i-1, j)。

B\mathcal{B} について。写像 Φ:B→F(i−1,j−wi)\Phi : \mathcal{B} \to \mathcal{F}(i-1, j-w_i)、Φ(S)=S∖{i}\Phi(S) = S \setminus \{i\} が全単射であることを示します。

  • 行き先が正しい: S∈BS \in \mathcal{B} なら S∖{i}⊆[i−1]S \setminus \{i\} \subseteq [i-1] であり、w(S∖{i})=w(S)−wi≤j−wiw(S \setminus \{i\}) = w(S) - w_i \le j - w_i です(i∈Si \in S を使いました)。よって Φ(S)∈F(i−1,j−wi)\Phi(S) \in \mathcal{F}(i-1, j-w_i)。
  • 逆写像がある: Ψ(T)=T∪{i}\Psi(T) = T \cup \{i\} とおきます。T∈F(i−1,j−wi)T \in \mathcal{F}(i-1, j-w_i) なら T⊆[i−1]T \subseteq [i-1] より i∉Ti \notin T なので w(T∪{i})=w(T)+wi≤(j−wi)+wi=jw(T \cup \{i\}) = w(T) + w_i \le (j - w_i) + w_i = j であり、T∪{i}⊆[i]T \cup \{i\} \subseteq [i] かつ ii を含むので Ψ(T)∈B\Psi(T) \in \mathcal{B} です。i∉Ti \notin T から Φ(Ψ(T))=T\Phi(\Psi(T)) = T、i∈Si \in S から Ψ(Φ(S))=S\Psi(\Phi(S)) = S が従い、Φ\Phi は全単射です。

さらに、i∈Si \in S より v(S)=vi+v(S∖{i})=vi+v(Φ(S))v(S) = v_i + v(S \setminus \{i\}) = v_i + v(\Phi(S)) です。Φ\Phi が全単射なので

max⁡S∈Bv(S)=max⁡T∈F(i−1, j−wi)(vi+v(T))=vi+K(i−1, j−wi)\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)

が成り立ちます。ここで F(i−1,j−wi)\mathcal{F}(i-1, j-w_i) は ∅\emptyset を含むので空でなく(j−wi≥0j - w_i \ge 0 を使いました)、右辺の最大値は存在します。また {i}∈B\{i\} \in \mathcal{B} なので B≠∅\mathcal{B} \ne \emptyset、∅∈A\emptyset \in \mathcal{A} なので A≠∅\mathcal{A} \ne \emptyset です。

空でない 2 つの有限集合の合併上の最大値は、それぞれの最大値の大きい方に等しいので、

K(i,j)=max⁡{max⁡S∈Av(S), max⁡S∈Bv(S)}=max⁡{K(i−1,j), vi+K(i−1,j−wi)}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\}

となり、3 が示されました。

注意 6.3(どの仮定をどこで使ったか)

証明を振り返ると、wi≥1w_i \ge 1(正であること)は 2 の場合分けだけで使い、wi,Ww_i, W が整数であることは実は上の証明では使っていません。整数性は、次に見るように表の添字を j=0,1,…,Wj = 0, 1, \ldots, W と有限個に限るために必要になります。wiw_i が実数だと、あり得る残り容量が連続無限個になって表が作れません。

また、価値 vi≥0v_i \ge 0 という仮定も証明のどこでも使っていません。viv_i が負の実数でも 定理 6.2 はそのまま成り立ちます(負の価値の品物は最適解に入らないだけです)。仮定は少ない方が定理は強いので、これは書き留めておく価値があります。

行 i-1行 iK(i-1, j-wi)K(i-1, j)K(i, j)品物 i を入れない品物 i を入れる(価値 +vi)
ナップサック表の遷移。セル (i, j) は 1 行上の 2 つのセルだけから決まる

系 6.4(ナップサック問題の計算量)✓

定義 6.1 の 0-1 ナップサック問題の最適値 K(n,W)K(n, W) は O(nW)O(nW) ステップ、O(nW)O(nW) の領域で計算できる。

証明(系 6.4)

部分問題の添字集合を S={(i,j):0≤i≤n, 0≤j≤W}S = \{(i,j) : 0 \le i \le n,\ 0 \le j \le W\}、値を V(i,j)=K(i,j)V(i,j) = K(i,j) とします。定理 6.2 より、依存先は D(0,j)=()D(0,j) = ()、j<wij < w_i のとき D(i,j)=((i−1,j))D(i,j) = ((i-1, j))、j≥wij \ge w_i のとき D(i,j)=((i−1,j), (i−1,j−wi))D(i,j) = ((i-1,j),\ (i-1, j-w_i)) ととれます。ここで wiw_i と WW が整数(定義 6.1)なので j−wij - w_i も整数であり、0≤j−wi≤W0 \le j - w_i \le W より確かに SS の元です。

依存グラフが DAG であることを確認します。辺は必ず第 1 成分が i−1i-1 の点から ii の点へ向かうので、辺を辿るたびに第 1 成分が 1 ずつ増えます。閉路があれば第 1 成分が元に戻らねばならず、矛盾です。よって 定義 3.1 の条件がすべて満たされます。

部分問題の個数は N=(n+1)(W+1)N = (n+1)(W+1) です。遷移は表引き高々 2 回、加算 1 回、比較 1 回なので c(i,j)=O(1)c(i,j) = O(1) です。定理 3.2 より計算時間は 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) です。n≥1n \ge 1 なので W≤nWW \le nW かつ 1≤n1 \le n であり、これは O(nW+n)O(nW + n) と書けます。W=0W = 0 の場合を別に扱えば(このとき答えは 00 です)、以後 W≥1W \ge 1 として O(nW)O(nW) と書けます。

位相順序としては、ii を 00 から nn へ増やし、各 ii の中では jj を 00 から WW へ動かす順序がとれます。実際、辺 (i−1,⋅)→(i,⋅)(i-1, \cdot) \to (i, \cdot) は必ず ii が増える向きなので、この順序で並べれば依存先は必ず先に来ます。

def knapsack(w, v, W):
"""w[i], v[i] は 0-indexed。最適値と、選んだ品物の 1-indexed 番号を返す。"""
n = len(w)
K = [[0] * (W + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(W + 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]])
chosen, j = [], W # 解の復元
for i in range(n, 0, -1):
if K[i][j] != K[i - 1][j]:
chosen.append(i)
j -= w[i - 1]
chosen.reverse()
return K[n][W], chosen

解の復元が正しい理由。 最適「値」だけでなく最適「解」が欲しいことがほとんどです。上のコードは表を右下から左上へ辿って選んだ品物を復元しています。その正当性を確かめます。

まず 定理 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) \ne K(i-1,j) ならば K(i,j)>K(i−1,j)K(i,j) > K(i-1,j) であり、3 の max⁡\max は第 2 項で達成されています。つまり j≥wij \ge w_i かつ K(i,j)=vi+K(i−1,j−wi)K(i,j) = v_i + K(i-1, j-w_i) です。このとき、(i−1,j−wi)(i-1, j-w_i) の最適解 TT をとると、証明中の写像 Ψ\Psi により T∪{i}T \cup \{i\} は (i,j)(i,j) の実行可能解で、その価値は vi+v(T)=vi+K(i−1,j−wi)=K(i,j)v_i + v(T) = v_i + K(i-1,j-w_i) = K(i,j)、すなわち最適解です。よって「品物 ii を選び、(i−1,j−wi)(i-1, j-w_i) へ移る」という手続きは最適性を保ちます。

逆に K(i,j)=K(i−1,j)K(i,j) = K(i-1,j) のときは、(i−1,j)(i-1,j) の最適解がそのまま (i,j)(i,j) の最適解になります(A⊆F(i,j)\mathcal{A} \subseteq \mathcal{F}(i,j) なので実行可能で、価値が K(i,j)K(i,j) に等しいからです)。よって「品物 ii を選ばず (i−1,j)(i-1, j) へ移る」も最適性を保ちます。ii が nn から 00 まで減るので手続きは必ず停止し、停止時に集めた集合は (n,W)(n,W) の最適解です。復元にかかる時間は O(n)O(n) です。

例 6.5(表を最後まで埋める)

品物 4 個、容量 W=7W = 7 の例です。

品物 ii1234
重さ wiw_i1345
価値 viv_i1457

行 00 は 定理 6.2 の 1 より全部 00 です。行 11(w1=1w_1 = 1, v1=1v_1 = 1)は、j=0j = 0 では j<w1j < w_1 なので K(1,0)=K(0,0)=0K(1,0) = K(0,0) = 0、j≥1j \ge 1 では max⁡{0, 1+K(0,j−1)}=max⁡{0,1}=1\max\{0,\ 1 + K(0, j-1)\} = \max\{0, 1\} = 1 です。

行 22(w2=3w_2 = 3, v2=4v_2 = 4)を丁寧に計算します。j=0,1,2j = 0,1,2 では j<3j < 3 なので上の行をそのまま写して 0,1,10, 1, 1。j=3j = 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。j=4j=4 では max⁡{1, 4+K(1,1)}=max⁡{1,5}=5\max\{1,\ 4 + K(1,1)\} = \max\{1, 5\} = 5。j=5,6,7j = 5, 6, 7 では 4+K(1,j−3)=4+1=54 + K(1, j-3) = 4 + 1 = 5 なので、いずれも 55 です。

同様に行 33(w3=4w_3 = 4, v3=5v_3 = 5)と行 44(w4=5w_4 = 5, v4=7v_4 = 7)を埋めると、表全体は次のようになります。

i\ji \backslash j01234567
000000000
101111111
201145555
301145669
401145789

たとえば K(3,7)=max⁡{K(2,7), 5+K(2,3)}=max⁡{5, 5+4}=9K(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}=9K(4,7) = \max\{K(3,7),\ 7 + K(3,2)\} = \max\{9,\ 7+1\} = 9 です。

復元。 (i,j)=(4,7)(i,j) = (4,7) から始めます。K(4,7)=9=K(3,7)K(4,7) = 9 = K(3,7) なので品物 4 は選ばず (3,7)(3,7) へ。K(3,7)=9≠5=K(2,7)K(3,7) = 9 \ne 5 = K(2,7) なので品物 3 を選び、jj を 7−w3=37 - w_3 = 3 にして (2,3)(2,3) へ。K(2,3)=4≠1=K(1,3)K(2,3) = 4 \ne 1 = K(1,3) なので品物 2 を選び、jj を 3−w2=03 - w_2 = 0 にして (1,0)(1,0) へ。K(1,0)=0=K(0,0)K(1,0) = 0 = K(0,0) なので品物 1 は選びません。

得られた解は S={2,3}S = \{2, 3\} で、重さ 3+4=7≤73 + 4 = 7 \le 7、価値 4+5=94 + 5 = 9 です。全 16 通りを手で確かめても、価値 99 を超える実行可能解はありません(品物 2 と 4 は重さ 8>78 > 7、品物 3 と 4 は 9>79 > 7 で入りません)。

表は (n+1)(W+1)(n+1)(W+1) マスありますが、定理 6.2 の右辺に現れるのは行 i−1i-1 だけです。したがって 1 行分の配列を使い回せます。ただし更新の順序に注意が要ります。配列を上書きすると古い値が消えるので、行 i−1i-1 の値としてまだ読みたい場所を先に壊さないように、jj の大きい方から更新する、というのが降順の理由です。

命題 6.6(1 次元配列による計算)✓

長さ W+1W+1 の配列 κ\kappa を κ[j]=0\kappa[j] = 0(0≤j≤W0 \le j \le W)で初期化し、i=1,2,…,ni = 1, 2, \ldots, n の順に、各 ii について jj を WW から wiw_i まで降順に動かして

κ[j]←max⁡{κ[j], vi+κ[j−wi]}\kappa[j] \leftarrow \max\bigl\{ \kappa[j],\ v_i + \kappa[j - w_i] \bigr\}

と更新する。このとき、ii 回目のループを終えた時点で、すべての 0≤j≤W0 \le j \le W について κ[j]=K(i,j)\kappa[j] = K(i, j) が成り立つ。とくに全体を終えると κ[W]=K(n,W)\kappa[W] = K(n, W) である。

証明(命題 6.6)

ii に関する帰納法です。i=0i = 0(ループ開始前)では κ[j]=0=K(0,j)\kappa[j] = 0 = K(0,j) が 定理 6.2 の 1 より成り立ちます。

i−1i-1 回目を終えた時点で κ[j]=K(i−1,j)\kappa[j] = K(i-1, j)(すべての jj)が成り立つとします。ii 回目のループで、添字 jj を書き換える瞬間の κ[j]\kappa[j] と κ[j−wi]\kappa[j - w_i] の値を調べます。

wi≥1w_i \ge 1 なので j−wi<jj - w_i < j です。このループは jj を降順に動かすので、添字 j−wij - w_i の書き換えは添字 jj の書き換えより後に起こります。また添字 jj 自身はこのループでまだ書き換えられていません。したがって、右辺を評価する時点で κ[j]\kappa[j] と κ[j−wi]\kappa[j-w_i] はどちらも i−1i-1 回目終了時の値、すなわち帰納法の仮定より K(i−1,j)K(i-1, j) と K(i−1,j−wi)K(i-1, j-w_i) です。よって代入後の値は

max⁡{K(i−1,j), vi+K(i−1,j−wi)}=K(i,j)\max\bigl\{ K(i-1,j),\ v_i + K(i-1, j-w_i) \bigr\} = K(i,j)

であり、最後の等号は 定理 6.2 の 3 です(j≥wij \ge w_i の範囲を動かしているので 3 が適用できます)。j<wij < w_i の範囲は書き換えられず K(i−1,j)K(i-1,j) のままですが、これは 定理 6.2 の 2 より K(i,j)K(i,j) に等しい値です。以上ですべての jj で κ[j]=K(i,j)\kappa[j] = K(i,j) となりました。

def knapsack_value(w, v, W):
kappa = [0] * (W + 1)
for wi, vi in zip(w, v):
for j in range(W, wi - 1, -1): # 降順が本質的
kappa[j] = max(kappa[j], vi + kappa[j - wi])
return kappa[W]

6.3. 擬多項式時間という落とし穴

Section titled “6.3. 擬多項式時間という落とし穴”

系 6.4 は嬉しい結果ですが、注意が要ります。計算量は入力のサイズの関数として測るのが約束でした(計算量と O 記法、定義 2.3[計算量と O 記法])。ナップサック問題の入力を 2 進表記で書き下すと、その長さは Θ(∑i(log⁡wi+log⁡vi)+log⁡W)\Theta\bigl(\sum_i (\log w_i + \log v_i) + \log W\bigr) 程度です。容量 WW は log⁡W\log W ビットで書けてしまいます。

つまり、入力長を LL とすると WW は 2L2^{L} 程度まで大きくなり得ます。n=100n = 100、W=260W = 2^{60} という入力は、品物の重さと価値も 2602^{60} 程度までとしても 2 進表記で高々 1 万数千ビットしかありませんが、O(nW)O(nW) の表は 102010^{20} マスを超えます。このように「数値そのもの」に対して多項式で、入力のビット長に対しては指数的な計算量を擬多項式時間といいます。

0-1 ナップサック問題は NP 困難です。ですから、P≠NP\mathrm{P} \ne \mathrm{NP} ならば真の多項式時間アルゴリズム(定義 7.1[計算量と O 記法])は存在しません(P≠NP予想とは何か)。動的計画法は万能の魔法ではなく、「数値が小さいときに限って指数の壁を回避できる」道具なのだ、と理解してください。

7. 設計の手順と、もう一つの例

Section titled “7. 設計の手順と、もう一つの例”

ここまでの例から、動的計画法の設計手順を取り出せます。

  1. 部分問題を決める。 何を添字にするか。例 5.2 が示すように、添字は「上位の問題を解くのに必要な情報をすべて含む」必要があります。
  2. 漸化式を立てる。 最適解の「最後の一手」で場合分けするのが定石です。ナップサックでは「品物 ii を入れたか」でした。
  3. 基底を決める。 添字が最小のところで、直接値が定まるようにします。
  4. 順序と復元を決める。 依存グラフの位相順序でループを書き、必要なら解を復元する情報を残します。

この手順を、文字列の比較に使う編集距離に当てはめます。

定義 7.1(編集距離(レーベンシュタイン距離))↗

有限アルファベット上の文字列 xx、yy に対し、次の 3 種の操作をそれぞれコスト 11 として、xx を yy に変換する操作列のうちコストの総和が最小のものの値を編集距離 d(x,y)d(x,y) と呼ぶ。

  • 1 文字の削除
  • 1 文字の挿入
  • 1 文字を別の 1 文字に置換

注意 7.2(アライメントによる特徴づけ)

証明にはアライメントによる言い換えを使います。xx と yy のアライメントとは、両者にギャップ記号を挿入して長さを揃え、上下 2 段に並べた表のことです。すなわち列 (a1,b1),…,(aℓ,bℓ)(a_1,b_1), \ldots, (a_\ell, b_\ell) であって、ata_t からギャップを除くと xx に、btb_t からギャップを除くと yy になり、(at,bt)(a_t, b_t) が両方ギャップである列は含まないもののことです。列 (at,bt)(a_t,b_t) のコストを at=bta_t = b_t のとき 00、それ以外のとき 11 と定め、アライメントのコストを列ごとのコストの和とします。

このとき、「xx を yy に変換する操作列の最小コスト」と「xx と yy のアライメントの最小コスト」は一致します。証明は Wagner と Fischer の論文、あるいは Gusfield の教科書第 11 章にあります。以下ではこの特徴づけを既知として使います。

次の定理も考え方はナップサックと同じです。2 つの文字列の末尾をどう揃えるか(削除・挿入・置換または一致の 3 通り)で場合分けし、残りを 1 つ小さい部分問題に帰着させます。

定理 7.3(編集距離の漸化式)✓

x=x1x2⋯xmx = x_1 x_2 \cdots x_m、y=y1y2⋯yny = y_1 y_2 \cdots y_n を有限アルファベット上の文字列とし、0≤i≤m0 \le i \le m、0≤j≤n0 \le j \le n に対して D(i,j)=d(x1⋯xi, y1⋯yj)D(i,j) = d(x_1 \cdots x_i,\ y_1 \cdots y_j) とおく(i=0i = 0 や j=0j = 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)

であり、1≤i≤m1 \le i \le m、1≤j≤n1 \le j \le n に対して

D(i,j)=min⁡{ D(i−1,j)+1,  D(i,j−1)+1,  D(i−1,j−1)+[ xi≠yj ] }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\}

が成り立つ。ここで [ xi≠yj ][\,x_i \ne y_j\,] は xi≠yjx_i \ne y_j のとき 11、xi=yjx_i = y_j のとき 00 を表す。

証明(定理 7.3)

注意 7.2 により、D(i,j)D(i,j) を x1⋯xix_1\cdots x_i と y1⋯yjy_1 \cdots y_j のアライメントの最小コストとして扱います。

基底。 x1⋯xix_1 \cdots x_i と空文字列のアライメントは、btb_t がすべてギャップでなければならないので、列は (x1,−),…,(xi,−)(x_1, -), \ldots, (x_i, -) の ii 個に限られます。各列のコストは 11 なので合計 ii、これが唯一のアライメントなので D(i,0)=iD(i,0) = i です。D(0,j)=jD(0,j) = j も同様です。

≤\le の向き。 1≤i≤m1 \le i \le m、1≤j≤n1 \le j \le n とします。x1⋯xi−1x_1\cdots x_{i-1} と y1⋯yjy_1 \cdots y_j の最小コストのアライメントの右端に、列 (xi,−)(x_i, -) を付け足すと、x1⋯xix_1 \cdots x_i と y1⋯yjy_1 \cdots y_j のアライメントが得られ、そのコストは D(i−1,j)+1D(i-1,j) + 1 です。D(i,j)D(i,j) は最小値ですから D(i,j)≤D(i−1,j)+1D(i,j) \le D(i-1,j) + 1。同様に右端に (−,yj)(-, y_j) を付ければ D(i,j)≤D(i,j−1)+1D(i,j) \le D(i,j-1)+1、(xi,yj)(x_i, y_j) を付ければ D(i,j)≤D(i−1,j−1)+[xi≠yj]D(i,j) \le D(i-1,j-1) + [x_i \ne y_j] が従います。3 つすべてが成り立つので、D(i,j)D(i,j) は右辺の min⁡\min 以下です。

≥\ge の向き。 x1⋯xix_1 \cdots x_i と y1⋯yjy_1\cdots y_j の最小コストのアライメント AA を 1 つとり、その最後の列 (aℓ,bℓ)(a_\ell, b_\ell) を見ます。i,j≥1i, j \ge 1 より AA は空でなく、また両方ギャップの列は許されないので、可能性は次の 3 つだけです。

  • (xi,−)(x_i, -) の場合:最後の列を除いた A′A' は x1⋯xi−1x_1\cdots x_{i-1} と y1⋯yjy_1 \cdots y_j のアライメントであり、コストは cost(A)−1\mathrm{cost}(A) - 1 です。D(i−1,j)D(i-1,j) は最小値なので D(i−1,j)≤cost(A)−1=D(i,j)−1D(i-1,j) \le \mathrm{cost}(A) - 1 = D(i,j) - 1、すなわち D(i,j)≥D(i−1,j)+1D(i,j) \ge D(i-1,j) + 1。
  • (−,yj)(-, y_j) の場合:同様に D(i,j)≥D(i,j−1)+1D(i,j) \ge D(i,j-1) + 1。
  • (xi,yj)(x_i, y_j) の場合:最後の列を除いた A′A' は x1⋯xi−1x_1 \cdots x_{i-1} と y1⋯yj−1y_1 \cdots y_{j-1} のアライメントで、コストは cost(A)−[xi≠yj]\mathrm{cost}(A) - [x_i \ne y_j] です。よって D(i,j)≥D(i−1,j−1)+[xi≠yj]D(i,j) \ge D(i-1,j-1) + [x_i \ne y_j]。

いずれの場合も D(i,j)D(i,j) は右辺の 3 つの量のうち少なくとも 1 つ以上なので、D(i,j)≥min⁡D(i,j) \ge \min(3 つの量の最小値)が成り立ちます。両向きを合わせて等号が示されました。

依存グラフの辺は (i−1,j)(i-1,j)、(i,j−1)(i,j-1)、(i−1,j−1)(i-1,j-1) から (i,j)(i,j) へ向かい、辺を辿るたびに i+ji + j が必ず増えるので閉路はありません。部分問題は (m+1)(n+1)(m+1)(n+1) 個、遷移は O(1)O(1) なので、定理 3.2 より計算時間は O(mn)O(mn)、領域は O(mn)O(mn) です(1 行ずつ捨てれば O(min⁡{m,n})O(\min\{m,n\}) にできます)。

例 7.4(kitten と sitting の編集距離)

x=kittenx = \texttt{kitten}(m=6m=6)、y=sittingy = \texttt{sitting}(n=7n=7)の表を埋めます。第 0 行と第 0 列は基底の D(0,j)=jD(0,j) = j、D(i,0)=iD(i,0) = i です。

ε\varepsilonsitting
ε\varepsilon01234567
k11234567
i22123456
t33212345
t44321234
e55432234
n66543323

たとえば D(1,1)D(1,1) は x1=kx_1 = \texttt{k}、y1=sy_1 = \texttt{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 です。D(2,2)D(2,2) は x2=y2=ix_2 = y_2 = \texttt{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 です。

答えは D(6,7)=3D(6,7) = 3。実際、kitten→sitten\texttt{kitten} \to \texttt{sitten}(k を s に置換)→sittin\to \texttt{sittin}(e を i に置換)→sitting\to \texttt{sitting}(g を挿入)で 3 手です。表が 33 を返し、かつ 3 手の変換が実在するので、これが最小です。

演習 8.1易

命題 4.1 を使って、fib_naive(100) の関数呼び出し回数を求め、1 秒間に 10910^9 回の呼び出しを処理できる計算機での実行時間を年単位で見積もってください。F101=573,147,844,013,817,084,101F_{101} = 573{,}147{,}844{,}013{,}817{,}084{,}101 を用いてよいものとします。

解答

命題 4.1 より呼び出し回数は

T(100)=2F101−1=1,146,295,688,027,634,168,201≈1.15×1021T(100) = 2F_{101} - 1 = 1{,}146{,}295{,}688{,}027{,}634{,}168{,}201 \approx 1.15 \times 10^{21}

回です。10910^9 回毎秒で割ると 1.15×10121.15 \times 10^{12} 秒。1 年は約 3.156×1073.156 \times 10^{7} 秒なので

1.15×10123.156×107≈3.6×104\frac{1.15 \times 10^{12}}{3.156 \times 10^{7}} \approx 3.6 \times 10^{4}

年、およそ 3 万 6 千年です。一方 系 4.2 の方法なら加算 99 回で終わります。同じ再帰式から出発しても、計算の組み立て方でこれだけ差がつきます。

演習 8.2標準

硬貨の額面 c1,…,ck∈Z≥1c_1, \ldots, c_k \in \mathbb{Z}_{\ge 1} が与えられ、各額面は何枚でも使えるとします。金額 A∈Z≥0A \in \mathbb{Z}_{\ge 0} をちょうど作るのに必要な硬貨の最小枚数 M(A)M(A) を求める漸化式を立て、計算量を 定理 3.2 で評価してください。また、額面が {1,4,5}\{1, 4, 5\}、A=8A = 8 のとき、「大きい額面から貪欲に取る」方法が最適でないことを確かめてください。

解答

漸化式。 部分問題を「金額 aa をちょうど作る最小枚数 M(a)M(a)」(0≤a≤A0 \le a \le A)とします。作れないときは M(a)=+∞M(a) = +\infty と約束します。最後に使った硬貨の額面 ctc_t で場合分けすると

M(0)=0,M(a)=min⁡{ M(a−ct)+1 : 1≤t≤k, ct≤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)

です(ct≤ac_t \le a を満たす tt が 1 つもなければ M(a)=+∞M(a) = +\infty)。正しさは、金額 aa を作る最小の硬貨の多重集合から硬貨 1 枚(額面 ctc_t)を取り除くと金額 a−cta - c_t を作る多重集合になり、逆に a−cta - c_t を作る多重集合に ctc_t を 1 枚足すと aa を作る多重集合になる、という対応から従います(定理 6.2 と同じ形の議論です)。

計算量。 部分問題は A+1A+1 個、各遷移は最大 kk 回の比較なので c(a)≤c⋅kc(a) \le c \cdot k。ct≥1c_t \ge 1 より a−ct<aa - c_t < a なので依存グラフは DAG です。定理 3.2 より O(kA)O(kA) 時間、O(A)O(A) 領域です。

貪欲法の反例。 {1,4,5}\{1,4,5\}、A=8A = 8 で貪欲に取ると、5→5 \to 残り 3→1,1,13 \to 1,1,1 で 4 枚です。しかし漸化式で表を埋めると

aa012345678
M(a)M(a)012311232

となり、M(8)=2M(8) = 2 です。実際 8=4+48 = 4 + 4 で 2 枚です。M(8)=min⁡{M(7)+1, M(4)+1, M(3)+1}=min⁡{4,2,4}=2M(8) = \min\{M(7)+1,\ M(4)+1,\ M(3)+1\} = \min\{4, 2, 4\} = 2 と計算されています。貪欲法は「最初の 1 手を確定させる」ため、後の選択肢を壊すことがあります。動的計画法は全ての選択肢を残したまま比較する点が違います。

演習 8.3標準

定義 6.1 と同じ入力に対し、各品物を何個でも使ってよいとした問題(無限ナップサック問題)を考えます。K′(i,j)K'(i,j) を「品物 1,…,i1, \ldots, i を何個でも使えて容量 jj のときの最大価値」として漸化式を立て、証明してください。また 命題 6.6 の 1 次元アルゴリズムで jj を昇順に回すとこの問題の答えが得られる理由を説明してください。

解答

漸化式。 1≤i≤n1 \le i \le n、wi≤j≤Ww_i \le j \le W のとき

K′(i,j)=max⁡{ K′(i−1,j),  vi+K′(i, j−wi) }K'(i,j) = \max\bigl\{\, K'(i-1, j),\ \ v_i + K'(i,\ j - w_i) \,\bigr\}

であり、j<wij < w_i のときは K′(i,j)=K′(i−1,j)K'(i,j) = K'(i-1,j)、K′(0,j)=0K'(0,j) = 0 です。第 2 項の第 1 添字が i−1i-1 ではなく ii である点が 定理 6.2 との違いです。

証明は同じ形です。品物 ii の使用個数が 00 個である多重集合の全体を A\mathcal{A}、11 個以上である全体を B\mathcal{B} とすると、A\mathcal{A} 側の最大値は K′(i−1,j)K'(i-1,j) です。B\mathcal{B} 側については、品物 ii を 1 個取り除く写像が B\mathcal{B} から「品物 1,…,i1,\ldots,i を使い容量 j−wij - w_i 以下の多重集合の全体」への全単射になり(逆写像は品物 ii を 1 個足す操作)、価値は viv_i だけ減るので、最大値は vi+K′(i,j−wi)v_i + K'(i, j-w_i) です。取り除いた後も品物 ii を使ってよいので添字が ii のまま残る、というのが違いのすべてです。依存グラフは、辺を辿るたびに (i,j)(i,j) の i+ji + j が真に増えるので DAG です。

昇順でよい理由。 1 次元配列 κ\kappa で jj を昇順に回すと、κ[j]\kappa[j] を更新する時点で κ[j−wi]\kappa[j - w_i] は j−wi<jj - w_i < j よりすでに ii 回目の更新を受け終わっています。命題 6.6 と同じ帰納法をたどれば、その値は K′(i,j−wi)K'(i, j-w_i) です。また κ[j]\kappa[j] 自身はまだ ii 回目の更新前なので K′(i−1,j)K'(i-1,j) です。よって代入結果は max⁡{K′(i−1,j), vi+K′(i,j−wi)}=K′(i,j)\max\{K'(i-1,j),\ v_i + K'(i, j-w_i)\} = K'(i,j) となり、上の漸化式そのものになります。降順が 0-1 版、昇順が無限版、という対応です。

演習 8.4難

実数列 a1,a2,…,ana_1, a_2, \ldots, a_n に対し、添字が増加し値も狭義単調増加する部分列の最大長を求める問題(最長増加部分列)を考えます。L(i)L(i) を「aia_i で終わる増加部分列の最大長」として漸化式を立て、O(n2)O(n^2) で解けることを示してください。さらに a=(3,1,4,1,5,9,2,6)a = (3,1,4,1,5,9,2,6) で計算してください。

解答

漸化式。 L(i)=1+max⁡({L(j):1≤j<i, aj<ai}∪{0})L(i) = 1 + \max\bigl(\{L(j) : 1 \le j < i,\ a_j < a_i\} \cup \{0\}\bigr) とします。空集合との合併は「条件を満たす jj がないときは max⁡\max を 00 とする」ための約束で、そのとき L(i)=1L(i) = 1 です。

正しさを見ます。aia_i で終わる最長の増加部分列を aj1<aj2<⋯<ajr=aia_{j_1} < a_{j_2} < \cdots < a_{j_r} = a_i(jr=ij_r = i)とします。r=1r = 1 なら L(i)=1L(i) = 1 で式は正しい。r≥2r \ge 2 なら、末尾を除いた aj1,…,ajr−1a_{j_1}, \ldots, a_{j_{r-1}} は ajr−1a_{j_{r-1}} で終わる増加部分列であり、jr−1<ij_{r-1} < i かつ ajr−1<aia_{j_{r-1}} < a_i なので r−1≤L(jr−1)r - 1 \le L(j_{r-1})、よって L(i)≤1+max⁡{L(j):⋯ }L(i) \le 1 + \max\{L(j) : \cdots\}。逆に、条件を満たす任意の jj について、aja_j で終わる最長増加部分列の後ろに aia_i を付ければ長さ L(j)+1L(j) + 1 の増加部分列(aj<aia_j < a_i なので単調性は保たれます)が作れるので L(i)≥L(j)+1L(i) \ge L(j) + 1。両向きで等号です。

答えは max⁡1≤i≤nL(i)\max_{1 \le i \le n} L(i) です。部分問題は nn 個、L(i)L(i) の計算に j=1,…,i−1j = 1, \ldots, i-1 の走査で O(n)O(n) かかるので、定理 3.2 より ∑ic(i)=O(n2)\sum_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) で左から求めます。L(1)=1L(1) = 1(a1=3a_1=3、左に何もない)。L(2)=1L(2) = 1(a2=1a_2 = 1 より小さい値が左にない)。L(3)L(3):a3=4a_3 = 4 より小さいのは a1=3,a2=1a_1 = 3, a_2 = 1 で max⁡{1,1}=1\max\{1,1\} = 1、よって 22。L(4)=1L(4) = 1(a4=1a_4 = 1)。L(5)L(5):a5=5a_5 = 5 より小さいのは a1,a2,a3,a4a_1,a_2,a_3,a_4 で max⁡{1,1,2,1}=2\max\{1,1,2,1\} = 2、よって 33。L(6)L(6):a6=9a_6 = 9 より小さいのは全部で max⁡{1,1,2,1,3}=3\max\{1,1,2,1,3\} = 3、よって 44。L(7)L(7):a7=2a_7 = 2 より小さいのは a2=1,a4=1a_2 = 1, a_4 = 1 で max⁡{1,1}=1\max\{1,1\}=1、よって 22。L(8)L(8):a8=6a_8 = 6 より小さいのは a1,a2,a3,a4,a5,a7a_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、よって 44。

(L(i))=(1,1,2,1,3,4,2,4)(L(i)) = (1,1,2,1,3,4,2,4) で、答えは 44 です。L(6)=4L(6) = 4 を達成するのは 1,4,5,91, 4, 5, 9(a2,a3,a5,a6a_2, a_3, a_5, a_6)などです。

なお、「長さごとの末尾の最小値」を配列で管理すると、各 ii での探索を二分探索に置き換えて O(nlog⁡n)O(n \log n) にできます(探索アルゴリズム、定理 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(mn)O(mn) アルゴリズムと、その正しさの証明。
  • D. Gusfield, Algorithms on Strings, Trees, and Sequences, Cambridge University Press, 1997 — 第 11 章。編集距離、アライメント、動的計画法の関係を体系的に扱っています。

Appendix: 同じ問題に対する別の動的計画法

Section titled “Appendix: 同じ問題に対する別の動的計画法”

価値を添字にする。 定義 6.1 のナップサック問題で、価値 viv_i がすべて非負整数だとします。系 6.4 の O(nW)O(nW) は WW が大きいと使えませんが、添字の役割を入れ替えると別の計算量が得られます。

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\}

を「品物 1,…,i1,\ldots,i から選んで価値がちょうど pp になる部分集合の最小の重さ」と定めます(そのような SS がなければ +∞+\infty)。基底は M(0,0)=0M(0,0) = 0 と M(0,p)=+∞M(0,p) = +\infty(p≥1p \ge 1)です。定理 6.2 とまったく同じ全単射の議論により、p≥vip \ge v_i のとき

M(i,p)=min⁡{ M(i−1,p),  wi+M(i−1, p−vi) }M(i,p) = \min\bigl\{\, M(i-1, p),\ \ w_i + M(i-1,\ p - v_i) \,\bigr\}

が、p<vip < v_i のとき 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\} です。V=∑iviV = \sum_{i} v_i とおくと部分問題は (n+1)(V+1)(n+1)(V+1) 個なので、定理 3.2 より O(nV)O(nV) 時間です。

例 6.5 の例では V=1+4+5+7=17V = 1 + 4 + 5 + 7 = 17 なので O(4⋅18)O(4 \cdot 18)、この場合は O(nW)O(nW) 版と大差ありませんが、WW が巨大で価値が小さい入力ではこちらが圧倒的に速くなります。逆に価値が巨大なら O(nW)O(nW) 版を使います。同じ問題に対して複数の部分問題の切り方があり、入力の性質で使い分けるというのは、動的計画法では珍しくありません。

近似への入り口。 O(nV)O(nV) という形は、価値を適当な単位で丸めて VV を小さくすれば計算が速くなることを示唆します。丸めによる誤差を制御すると、任意の ε>0\varepsilon > 0 に対して最適値の (1−ε)(1-\varepsilon) 倍以上の解を多項式時間で返すアルゴリズム(FPTAS)が構成できます。詳しくは Kleinberg–Tardos の §11.8 を参照してください。NP 困難性が「厳密解の多項式時間計算」を阻んでも、近似ならこの壁を越えられることがあります。

このテーマの 100 本ノック(50 問)

執筆: Claude Code(Sonnet 5.5)・監査 3 回(Claude Code)・最終 2026-10-12

この記事の誤りを報告する ・運営: 夢現技研合同会社 ・料金 ・利用条件 ・特定商取引法に基づく表記

© 2026 夢現技研合同会社 ・コード例は MIT ライセンスです。