MENU

動的計画法(DP)をやさしく解説!基本から応用まで

プログラミングの勉強を進めていくと、「動的計画法(DP)」という言葉をよく耳にするようになります。競技プログラミングやアルゴリズムの問題では定番の手法ですが、「聞いたことはあるけど、いまいち仕組みや使い方が分からない……」という方も多いのではないでしょうか。この記事では、動的計画法の基本的な考え方から、よくあるパターン、実際の問題への応用方法まで、初心者にも分かりやすく丁寧に解説します。ぜひ、DPのコツをつかんで、アルゴリズム問題のレベルアップを目指しましょう!


目次

動的計画法(DP)とは何か?

動的計画法(Dynamic Programming、略してDP)は、「ある大きな問題を、小さな部分問題に分割して、それらを解いていくことで、最終的な答えを効率的に求める」アルゴリズムの手法です。
一度解いた部分問題の答えを保存(メモ化)しておき、同じ問題を繰り返し解かずに済むことで、無駄な計算を省くことができます。

例えば、フィボナッチ数列を再帰的に求めると、同じ計算を何度も繰り返してしまい、計算量が爆発的に増えます。
そこで、既に計算した結果を保存しておけば、必要なときにすぐに利用できる――これが動的計画法の根本的な考え方です。


DPのポイント:「部分問題」「メモ化」「再利用」

動的計画法を使いこなすには、次の3つのポイントを押さえることが重要です。

  • 部分問題に分解する
     最初に解きたい大きな問題を、小さく分けて考えます。
  • 部分問題の結果を保存(メモ化)する
     同じ部分問題を何度も計算しないように、計算結果を配列や辞書などに保存します。
  • 保存した結果を再利用する
     必要なときに保存した結果を取り出して使い、全体の効率を上げます。

フィボナッチ数列でDPを体感してみよう

まずは有名な例である「フィボナッチ数列」を使って、動的計画法の考え方を実際に見てみましょう。

フィボナッチ数列の問題

フィボナッチ数列は次のように定義されます。

  • F(0) = 0
  • F(1) = 1
  • F(n) = F(n-1) + F(n-2) (n ≥ 2)

これを再帰関数で書くと、とてもシンプルですが、計算量が多くなってしまいます。

再帰のみの場合

def fib(n):
    if n == 0 or n == 1:
        return n
    return fib(n-1) + fib(n-2)

この実装だと、例えばfib(5)を求めるとき、fib(3)やfib(2)など同じ計算を何度も行っています。

DP(メモ化再帰、トップダウン)で解決!

memo = {}

def fib(n):
    if n == 0 or n == 1:
        return n
    if n not in memo:
        memo[n] = fib(n-1) + fib(n-2)
    return memo[n]

このように、計算した値をmemoという辞書に保存し、必要なときにすぐ取り出します。

DP(配列を使ったボトムアップ)

def fib(n):
    dp = [0] * (n+1)
    if n == 0:
        return 0
    dp[1] = 1
    for i in range(2, n+1):
        dp[i] = dp[i-1] + dp[i-2]
    return dp[n]

このように、「小さい問題から順に計算していく」のがボトムアップ型のDPです。


DPの種類:トップダウン(メモ化再帰)とボトムアップ(配列表)

動的計画法には、大きく分けて「トップダウン」と「ボトムアップ」の2種類の実装方法があります。

手法特徴メリットデメリット
トップダウン再帰 + メモ化で、必要なものだけ計算実装が直感的で書きやすいスタックオーバーフローに注意
ボトムアップ配列を使い、小さい順からすべて計算メモリ使用量が分かりやすい全て計算する必要がある場合もある

DPが活躍する代表的な問題パターン

動的計画法が役立つのは、「部分問題の答えを使い回せる」ような問題です。よくあるパターンは以下の通りです。

1. ナップサック問題(部分和問題)

重さと価値が決まった複数の品物から、制限された重さの中で最大の価値を得るには、どの品物を選べばよいか?

このような「選び方の組み合わせを効率よく探索したい」ときにDPが使われます。

2. 最長増加部分列(LIS)

数列から、昇順に並んだ部分列で、最も長いものの長さを求めよ。

部分問題として「ある位置までのLISの長さ」を保存して再利用します。

3. 最小コスト経路問題

2次元グリッドで、左上から右下まで移動する際、通過するマスの合計コストが最小になる経路を求めよ。

この場合も、各マスまでの最小コストをDP配列に保存していきます。


DPでよく使う配列表設計

動的計画法を使いこなすためには、「どんなDP配列を用意するか」を考えるのが重要です。
次のような例を紹介します。

1次元DP配列

  • 例:フィボナッチ数列、LISなど
  • 配列dp[i]に「i番目までの最適解」を保存

2次元DP配列

  • 例:ナップサック問題、グリッド移動
  • 配列dp[i][j]に「i個目までの品物、重さjまでで得られる価値」や「座標(i, j)までの最小コスト」を保存

3次元DP配列

  • 複数の状態を管理したいときに使います
  • 例:スケジューリング問題や状態遷移を管理する問題

DP配列表の設計例:ナップサック問題

ナップサック問題のDP配列表の設計例を見てみましょう。

問題設定

  • 品物の個数:N個
  • 各品物の重さ:w[i]
  • 各品物の価値:v[i]
  • ナップサックの容量:W

DP配列

  • dp[i][j] = 「i個目までの品物で、重さj以下で得られる最大の価値」

遷移式

  • 品物iを選ばない場合:dp[i+1][j] = dp[i][j]
  • 品物iを選ぶ場合(重さ制限内):dp[i+1][j + w[i]] = max(dp[i+1][j + w[i]], dp[i][j] + v[i])

実装例

N, W = map(int, input().split())
w = []
v = []
for _ in range(N):
    wi, vi = map(int, input().split())
    w.append(wi)
    v.append(vi)

dp = [[0] * (W+1) for _ in range(N+1)]

for i in range(N):
    for j in range(W+1):
        # 品物iを選ばない場合
        dp[i+1][j] = max(dp[i+1][j], dp[i][j])
        # 品物iを選ぶ場合
        if j + w[i] <= W:
            dp[i+1][j + w[i]] = max(dp[i+1][j + w[i]], dp[i][j] + v[i])

# 答えは dp[N][W] ではなく、dp[N][j] (j=0~W) の最大値
ans = max(dp[N])
print(ans)

DP設計のための4つのステップ

動的計画法の問題を解くときは、次の4ステップで考えると分かりやすいです。

  1. 状態(DP配列)を定義する
    どんな情報を持った配列・辞書にするか決める。
  2. 初期値を決める
    最初にどんな値が入っているべきか決める。
  3. 遷移(状態の更新)を考える
    どうやって次の状態を計算するか式を考える。
  4. 答えを取り出す
    DP配列のどこが最終的な答えになるか確認する。

DPの実践的なコツ

  • 配列の次元数に注意する
     必要以上に多次元にするとメモリ消費が大きくなります。できるだけ1次元や2次元で工夫しましょう。
  • 状態の圧縮や遷移の工夫
     問題によっては、「今と1つ前だけ持てばいい」など省メモリ化できることもあります。
  • サンプルケースでテストする
     小さい入力例で手計算してみると、DP配列や遷移式が正しいか確かめやすいです。

DPによくあるエラーと解決法

エラー例解決法
配列の初期化ミス0-1float("inf")など、適切な初期値を確認する
インデックスの範囲外アクセスfor文の範囲やif条件を確認し、配列のサイズと一致しているかチェックする
遷移式のミス手計算やデバッグプリントでどこが間違っているか確かめる


DPは「部分問題を保存して賢く使い回す」魔法のテクニック!

動的計画法(DP)は、アルゴリズムの世界でとても強力なテクニックです。
「今まで再帰や全探索で時間がかかっていた問題」が、DPを使うことで一気に高速化できる場合もたくさんあります。
コツをつかむまでは少し難しく感じるかもしれませんが、いくつかのパターンを覚えて、実際に手を動かしながら問題を解いていくことで、自然と身についてきます。

分からないところはこの記事を見返しながら、ぜひ色んなDP問題に挑戦してみてください!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

ブログ運営者。日常の気づきから、言葉の意味、仕組みやトレンドまで「気になったことをわかりやすく」まとめています。調べて納得するのが好き。役立つ情報を、肩の力を抜いて発信中。

コメント

コメントする

目次