回 2-9 道 2 データ構造とアルゴリズム

動的計画法は、大きな問題を小さな部分問題に分け、一度解いた部分問題の答えを表に残して使い回す解き方です。フィボナッチ数を素直な再帰で求めると、fib(25) で関数が 242785 回呼ばれますが、答えを残すと本体が動くのは 26 回、表を下から埋めると足し算は 24 回で済みます。前の答えから次の答えを作る式 (漸化式) が立ち、同じ部分問題が何度も出てくる問題で効きます。

同じ計算を何度もしている所を呼ばれた回数で見つけ、答えを表に残して 1 回ずつに減らせるようになります。

同じ計算を、何万回もしていないか

動的計画法 (どうてきけいかくほう) は、同じ部分問題を何度も解き直す計算を、答えを表に残して 1 回ずつに減らす解き方です。英語の Dynamic Programming から DP とも呼ばれます。

前の回 2-5 で、フィボナッチ数の素直な再帰は呼ぶ回数が急に増えると触れました。フィボナッチ数は、0、1 から始め、前の 2 つを足して次を作る数の並びです。0、1、1、2、3、5、8、13 と続きます。

これをそのまま再帰で書くと、答えが 75025 の fib(25) で、関数が 24 万回以上呼ばれます。この回では、関数の入口で 1 を足して呼ばれた回数を数え、答えを覚える書き方と表を埋める書き方で、回数がどれだけ減るかを測ります。

動的計画法はどう速くするのか

動的計画法は、部分問題の答えを表に書き、同じ問いが来たら計算せずに表から読んで速くします。書き方は、再帰で上から解きながら覚えるメモ化と、小さい方から表を埋めるやり方の 2 つです。

素直な再帰は、同じ問いを何度も解き直す

素直な再帰が遅いのは、同じ小さな計算を何千回もくり返しているからです。

fib(5) は fib(4) と fib(3) を呼び、fib(4) もまた fib(3) を呼ぶので、fib(3) は 2 回解かれます。この重なりは下の段ほど膨らみます。

下の図で n を動かすと、fib(k) ごとに呼ばれた回数の棒が伸びます。「一度出た答えを覚えておく」を入れると、どの fib(k) も計算は 1 回ずつになります。

fib の呼び出し回数

メモ化は、上から解きながら答えを覚える

メモ化 (memoization) は、関数が返した答えを引数ごとに覚え、同じ引数で呼ばれたら本体を動かさずに返す書き方です。素直な再帰の前に 1 行足すだけで書けます。

Python では標準の functools.cache を使います。公式文書によると lru_cache(maxsize=None) と同じ物で、答えは引数を鍵にした辞書に入るので、引数は数や文字列のようなハッシュできる値に限られます。

cache を付けた関数には cache_info() があり、表から読んだ回数 (hits) と、表に無くて本体を動かした回数 (misses) を返します。読んだ回数と動かした回数を足すと、呼ばれた回数になります。

下から埋めるやり方は、小さい方から表を作る

下から埋めるやり方は、いちばん小さな部分問題の答えを表の先頭に置き、漸化式を使って小さい方から順にマスを埋めていく書き方です。再帰を使わず、ループ 1 本で書けます。

フィボナッチ数なら、表 dp の dp[0] に 0、dp[1] に 1 を置き、k を 2 から n まで進めながら dp[k] = dp[k − 1] + dp[k − 2] で埋めます。dp[k] を埋めるときには、要る 2 つのマスはもう埋まっています。

下の図で n を上げると、埋まるマスが増えます。「変数 2 本に圧縮」を入れると、直前の 2 つしか読まないので表の全部は要らないことが分かります。

DP配列でフィボナッチ

2 つの書き方を並べると、次の通りです。

メモ化 (上から)表 (下から)
書き方再帰に覚える仕組みを足すループで小さい方から埋める
解く部分問題呼ばれた物だけ表のマスを全部
呼び出しの深さn に比例して深くなる深くならない
覚える量の節約しにくい直前の分だけ残せる

部分問題と漸化式を立てる

動的計画法が使えるのは、大きな問題の答えが小さな問題の答えから作れて、その小さな問題が何度も出てくるときです。前の性質を最適部分構造、後の性質を部分問題の重なりと呼びます。

部分問題 (ぶぶんもんだい) は、元の問題を小さくした同じ形の問題です。漸化式 (ぜんかしき) は、前の答えから次の答えを作る式です。

階段の上り方で考えます。1 回に 1 段か 2 段上れるとき、n 段目に着く上り方は何通りかという問題です。最後の 1 歩は、n − 1 段目からの 1 段か、n − 2 段目からの 2 段なので、上り方はその 2 つの和になります。

階段の上り方の漸化式

ways(n) = ways(n - 1)
        + ways(n - 2)
ways(0) = 1, ways(1) = 1

この式はフィボナッチ数と同じ形です。部分問題は「k 段目への上り方」で、表の 1 マスが 1 つの答えです。

動かして確かめる

1 本目は、素直な再帰の関数の入口で 1 を足し、呼ばれた回数を数えます。出力の 2 行目からは、左から n、答え、呼ばれた回数、前の行の何倍か、です。最後に、fib(20) の中で各 fib(k) が呼ばれた回数を出します。

動かす 素直な再帰の呼ばれた回数

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

fib(6) は 25 回
5 5 15 -
10 55 177 11.8
15 610 1973 11.1
20 6765 21891 11.1
25 75025 242785 11.1
fib(20) の中で
 fib 19 1
 fib 10 89
 fib 2 4181
 fib 1 6765

n が 5 増えるたびに、呼ばれた回数は 11 倍あまりになり、fib(25) では 242785 回です。fib(20) の中では、fib(19) は 1 回しか呼ばれないのに、fib(10) は 89 回、fib(2) は 4181 回も呼ばれています。同じ問いを何千回も解き直していることが、遅さの正体です。

2 本目は、メモ化と表のやり方で fib(25) を求めます。メモ化は本体の頭で 1 を足して「本体が動いた回数」を、表のやり方は足し算のたびに 1 を足して「足した回数」を数えます。

動かす メモ化と表で回数を数える

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

上から 75025 26
 表から読んだ 23
 本体を動かした 26
 呼ばれた 49
下から 75025 24
0 1 1 2 3 5 8 13 21 34 55

メモ化では、本体が動いたのは fib(0) から fib(25) の 26 回だけです。呼ばれた 49 回のうち 23 回は表から読んで返し、素直な再帰の 242785 回から 49 回まで減りました。表のやり方は、dp[2] から dp[25] の 24 マスを 1 回ずつ足して埋めました。

3 本目は、階段の上り方を表で求めます。dp[k] を「k 段目への上り方」とし、0 段目は「まだ上らない」の 1 通りとします。出力は左から段の数、上り方です。

動かす 階段の上り方を表で求める

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

1 1 通り
2 2 通り
3 3 通り
4 5 通り
5 8 通り
6 13 通り
7 21 通り
8 34 通り
9 55 通り
10 89 通り
45 段 1836311903

1 段から 5 段の上り方は 1、2、3、5、8 通りで、フィボナッチ数を 1 つずらした並びです。45 段では 1836311903 通りですが、表なら 1 段に 1 回の足し算で埋まります。

動的計画法はどこで崩れるのか

動的計画法は、部分問題の答えを組み合わせると全体の答えになること、表に入る大きさであること、の 2 つが前提です。前提が崩れると、答えを間違えるか、表が手に負えない大きさになります。

大きい硬貨から取ると、枚数が最小にならない

硬貨の両替は、金額ちょうどを作る最小の枚数を求める問題です。大きい硬貨から取れるだけ取るやり方 (貪欲法、どんよくほう) は、硬貨の組み合わせによっては最小になりません。

動的計画法では、dp[a] を「a 円を作る最小の枚数」とします。a 円を作る最後の 1 枚が c 円なら、残りは a − c 円なので、dp[a] は dp[a − c] + 1 のうち最も小さい物です。次のコードは、硬貨が 1、3、4 円のときに 1 円から 20 円まで、日本の 6 種類の硬貨で 1 円から 1000 円まで、2 つのやり方を比べます。

動かす 貪欲法と表で枚数を比べる

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

6 円 貪欲 3
6 円 表 2
外れる金額 [6, 10, 14, 18]
日本の硬貨で外れる 0

6 円を貪欲法で作ると、4 円、1 円、1 円の 3 枚です。表で求めると、3 円と 3 円の 2 枚です。1 円から 20 円の中では、6、10、14、18 円で貪欲法が外れました。日本の硬貨では、1 円から 1000 円まで 1 つも外れません。

貪欲法は、その場で最も得に見える物を取り、後で取り直しません。動的計画法は、最後の 1 枚の全部の場合を比べるので、どの硬貨の組み合わせでも最小が出ます。下の図で、硬貨を 1、3、4 に替えて 6 円を見ると、貪欲法の段が 1 枚多くなります。

コイン両替最小枚数

上からの書き方は呼び出しの深さで止まり、表は大きさで止まる

メモ化は答えを覚えても、再帰なので、呼び出しのスタックに n 段ぶんのフレームを積みます。前の回 2-5 で見た通り、Python は呼び出しの深さに上限を持ち、超えると RecursionError で止まります。表のやり方は深さが増えない代わりに、マスの数だけ覚える場所を使います。

動かす 深さと表の大きさ

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

上限 1000
3000 は止まった
下から 3000 の桁数 627
3500 行どうし 12257001 マス
品物 20 個の選び方 1048576 通り
上限 10 億の表 21000000021 マス

手元の CPython 3.14 では上限は 1000 で、メモ化した fib(3000) は止まりました (ブラウザの Python では上限が違うことがあります)。下からのループは、直前の 2 つだけを残して fib(3000) まで進み、627 桁の答えを出しました。

表は部分問題の数だけマスが要り、3500 行のファイルどうしを比べる表 (関わる実例で扱います) は 12257001 マスです。品物が 20 個で重さの上限が 10 億のナップサック問題では、表は 21000000021 マスになり、20 個の選び方を全部試す 1048576 通りのほうがずっと少なく済みます。動的計画法がいつも速いわけではありません。

動的計画法は仕事でどう確かめるのか

仕事では、まず素直な全探索を書き、同じ引数で何度も呼ばれていることを数で確かめてから、表に残す形に直し、小さな入力で全探索と答えを照らします。

  1. 部分問題を 1 文で決める。「dp[a] は a 円を作る最小の枚数」のように、表の 1 マスが何の答えかを書く
  2. 部分問題に持たせる値は、その先の計算に効く物だけにする。引数が増えるほど表のマスが増える
  3. 漸化式と、いちばん小さな答え (dp[0] など) を書く
  4. 埋める順を決める。dp[k] を埋めるとき、使うマスがもう埋まっている順にする
  5. 小さな入力で全探索と答えが一致するかを試験で確かめる。上の硬貨のように全部比べると外れる入力が見つかる
  6. Python なら cache_info() で、表から読んだ回数が 0 でないかを見る。0 なら同じ部分問題が出ておらず、表に残しても速くならない

関わる実例

差分を出す道具 diff は最長共通部分列を解いている (1976年7月)

2 つのファイルの違いを出す道具 diff は、最長共通部分列 (2 つの並びの両方に、順番を保って出てくる要素の並びのうち最も長い物) を解いて、変わらなかった行を見つけています。1976年7月の Bell 研究所の報告 (Computing Science Technical Report 41) で、J. W. Hunt と M. D. McIlroy がそう書いています。

報告は、先頭 i 行と j 行の最長共通部分列の長さを P[i][j] とし、行が同じなら左上に 1 を足し、違えば上と左の大きい方を取る漸化式を、素直な動的計画法として示しています。そして、このやり方は時間も覚える場所も行数の積に比例し、場所のほうがもっと悪いと書いています。

崩れた前提は「表を全部持てば済む」です。diff は、最長の長さが変わる大事な一致の所だけを追う形に直しました。報告によると、当時の計算機 PDP-11/45 で 3500 行のファイルどうしを比べると、diff は 0.25〜0.75 分の CPU 時間、動的計画法を速めた別のやり方は約 5 分でした。

今の GNU diff の文書は、基本の手順として Myers の 1986 年の論文を挙げています。次のコードは、報告の最初の例のファイル (1 行を 1 字で書いた物) で表を埋め、後ろからたどって残る行を出します。

動かす 最長共通部分列で残る行を出す

python実行可能
出力

▶ 実行ボタンを押すと結果がここに表示されます

実行すると、次の通りに出ます

残る行 ['a', 'b', 'e']
マスの数 64
0 0 0 0 0 0 0 0
0 0 1 1 1 1 1 1
0 0 1 2 2 2 2 2
0 0 1 2 2 2 2 2
0 0 1 2 2 2 2 2
0 0 1 2 2 2 2 3
0 0 1 2 2 2 2 3
0 0 1 2 2 2 2 3

a、b、e の 3 行が残り、ほかは足した、消した、替えた行です。報告の例も、w を足し、c と d を x、y、z に替え、f と g を消す差分でした。

出典は An Algorithm for Differential File Comparison (Hunt と McIlroy、Bell 研究所の報告 41) と Overview (GNU diffutils 公式文書) です。

よくある問い

メモ化と表を下から埋めるやり方は、どちらを使えばよいですか

まずメモ化で書くと、素直な再帰に 1 行足すだけで正しさを保ったまま速くなります。n が大きく呼び出しの深さの上限に届くときや、覚える量を直前の分だけに減らしたいときは、表を下から埋める形に直します。どちらも、解く部分問題と漸化式は同じです。

ナップサック問題とは何ですか

重さと価値の決まった品物から、重さの合計が上限を超えないように選び、価値の合計を最大にする問題です。「i 番目までの品物で、重さの合計が u 以下のときの最大の価値」を 1 マスにした表で解け、マスの数は品物の数と上限の積になります。上の出力の通り、上限が 10 億のように大きいと表が大きくなりすぎ、品物が少なければ全部の選び方を試すほうが少なく済みます。

動的計画法は誰が考えたのですか

Richard Bellman の名前で知られています。RAND 研究所に残る The Theory of Dynamic Programming は、Bellman が 1954年9月2日にアメリカ数学会の夏の会合で行った講演の原稿です。名前の由来は、一次資料を確かめられなかったので書きません。

確認クイズ

答えだと思う選択肢を押すと、その場で正誤と理由が開きます。3 問すべてに正しく答えると、この回は修了で、石が緑に変わります (この端末のブラウザに残ります)。間違えた問いは、あとから解き直せます。

1 問目

素直な再帰の fib で fib(20) を求めると、関数の入口で数えた呼ばれた回数は何回でしたか。

2 問目

このコードを動かすと何が出ますか。

本体が動いた回数

from functools import cache

body = 0


@cache
def f(n):
    global body
    body += 1
    if n < 2:
        return n
    return f(n - 1) + f(n - 2)


f(10)
print(body)

3 問目

硬貨が 1 円、3 円、4 円の 3 種類のとき、6 円を作る最小の枚数について正しい物はどれですか。

説明課題

フィボナッチ数を素直な再帰で求めると遅い理由と、答えを表に残すと速くなる理由を、「同じ部分問題」と「1 回ずつ」という言葉を使って、3 文以内で説明してください。書いたら、下の模範解答を開いて比べてください。

模範解答

自分の説明を書いてから開いて、比べてください。

この回の先の一歩

同じ計算を何度もしている所を見つけて結果を残す考え方は、表示を速くするキャッシュや、2 つの文の差分を求める処理に、そのままつながります。いまの自分が転職までの道のどの段にいるかは、転職診断で確かめられます。

もっと深く

この回の続きを、ゆめさくのコースの章や道具で深められます。頁の中の確認クイズに答えたあと、手を動かす量を増やしたいときに使ってください。

一次資料

どれも 2026年10月26日に開いて、題と中身を確かめました。

次へ

前の回 2-8ダイクストラ法と最短経路点と線、幅優先、重みのある道
次の回 2-10試験と面接で確かめる基本情報 科目B、道場 10 問、面接 20 問準備中

道 2 データ構造とアルゴリズムの回

  1. 2-0アルゴリズムとは順次、分岐、反復の 3 つで書く手順頁で読む
  2. 2-1線形探索と二分探索100 万件でも 20 回で見つかる頁で読む
  3. 2-2バブルソートと挿入・選択ソート遅いソート 3 つ、2 倍で約 4 倍頁で読む
  4. 2-3クイックソートとマージソート速いソート 2 つ、平均と最悪頁で読む
  5. 2-4スタックとキュー積む順と並ぶ順頁で読む
  6. 2-5再帰自分を呼んで小さくする頁で読む
  7. 2-6木と二分探索木たどる順、片寄り頁で読む
  8. 2-7ハッシュテーブル鍵から一気に引く、衝突頁で読む
  9. 2-8ダイクストラ法と最短経路点と線、幅優先、重みのある道頁で読む
  10. 2-9動的計画法同じ計算を 2 度しないこの回
  11. 2-10試験と面接で確かめる基本情報 科目B、道場 10 問、面接 20 問頁で読む

道 2 の頁へ戻るほかの道を見る