動的計画法(DP)は、「最適部分構造」 と 「部分問題の重複」 という性質を持つ問題を効率的に解くためのアルゴリズム設計手法です。 具体的には、問題を小さな部分問題に分解し、それらを再利用しながら解を構築する 方法です。
DP を使う際には、以下の 3 つのステップを踏むのが一般的です。
- 状態の定義(
dp[i]を何とするか) - 遷移式(状態の更新方法)を考える
- 初期条件を設定する
- 計算順序を考える(小さい問題から順番に解く)
動的計画法が適用できる問題には、次の 2 つの特徴があります。
「大きな問題の最適解が、小さな部分問題の最適解を使って表現できる」
- 例:
dp[i](部屋iへの最短時間)は、dp[i-1]やdp[i-2]の値を使って求めることができる。
「同じ部分問題を何度も計算する」
- 例:
dp[i]を求めるとき、dp[i-1]やdp[i-2]を参照するが、dp[i-1]もdp[i-2]を参照しているため、計算が重複する。
このような場合、メモ化(配列に値を保存して使い回す) することで、無駄な計算を省略し、高速化できる。
DP にはいくつかの種類があります。よく使われるものを紹介します。
- 例:最短経路、階段の登り方、ナップザック問題
- 特徴:
dp[i]という配列を用意して、ある状態から次の状態へ遷移していく。- 線形 (
O(N)) の計算量で解けることが多い。
例:今回のダンジョン問題
dp[i] = min(dp[i - 1] + A[i], dp[i - 2] + B[i]);- 例:部分和問題、最長共通部分列(LCS)、動的計画法での迷路探索
- 特徴:
dp[i][j]のような表を使い、2 次元的に状態を管理する。- 計算量は
O(N*M)になることが多い。
例:最長共通部分列(LCS)
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1], dp[i-1][j-1] + (文字が一致するか?))- 例:0-1 ナップザック問題
- 特徴:
- ある制約(例:重さ
Wを超えないようにする)を満たしつつ最適解を求める。 dp[i][w]を使うことが多い。
- ある制約(例:重さ
例:ナップザック問題
dp[i][w] = max(dp[i - 1][w], dp[i - 1][w - weight[i]] + value[i]);dp[i] を「部屋 i にたどり着くまでの最短時間」と定義。
部屋 i へは、以下の 2 つの方法で移動できる:
- 部屋
i-1からiへ(時間A[i]) - 部屋
i-2からiへ(時間B[i])
したがって、遷移式は:
dp[i] = min(dp[i - 1] + A[i - 2], dp[i - 2] + B[i - 3]);dp[1] = 0(スタート地点)dp[2] = A[0](部屋2へはA[2]で移動)
function minTimeToReachRoomN(N, A, B) {
let dp = new Array(N + 1).fill(Infinity);
dp[1] = 0;
for (let i = 2; i <= N; i++) {
dp[i] = Math.min(dp[i], dp[i - 1] + A[i - 2]);
if (i > 2) {
dp[i] = Math.min(dp[i], dp[i - 2] + B[i - 3]);
}
}
return dp[N];
}✅ 計算の重複を避けて高速化できる(O(N) や O(N*M) の計算量)
✅ 最適解を求めるのに向いている(最短経路や最大値・最小値を求める問題)
✅ 再帰よりも効率的な場合が多い(特にメモ化なしの再帰は O(2^N) の計算量になることが多い)
- 動的計画法(DP) は、「部分問題の重複」と「最適部分構造」を利用して問題を効率よく解く手法。
- ダンジョン問題 では、
dp[i]を部屋iへの最短時間とし、dp[i-1]とdp[i-2]を使って更新した。 - 計算量
O(N)なので、大きなNに対しても高速に動作する。