AtCoder PR

【AtCoder「足場ジャンプ(Frog 1)」】DP が苦手な人のための“状態遷移の本質”から理解する解法ガイド

記事内に商品プロモーションを含む場合があります

動的計画法(DP)は、競技プログラミングで避けて通れない重要テーマです。
しかし、初〜中級者の多くが共通してつまずくのが、

  • 状態をどう定義するか
  • 遷移式をどう作るか

という「考え方」の部分です。

特に AtCoder の典型問題である「足場ジャンプ(Frog 1)」は、
まさに“DP の基礎となる思考パターン”が詰まっています。

https://atcoder.jp/contests/dp/tasks/dp_a


この記事では、単に解法を示すのではなく、DP の本質を理解し、自分自身で状態遷移を組み立てられるようになることを目標に、丁寧に解説していきます。


導入:なぜ DP がわからなくなるのか?

多くの学習者が DP を苦手に感じる理由は、計算自体よりも「発想」が難しいからです。

  • どの状態をメモすればいいの?
  • 遷移式って何を根拠に作るの?
  • 配列に何を入れればいいのかわからない…

こうした悩みは本当に多いですが、安心してください。
DP の本質は “小さな問題を積み上げる” ただそれだけ です。

まずは今回の問題を通して、
「状態定義 → 遷移式 → 実装」の一連の流れを一緒に辿っていきましょう。


問題の本質:やるべきことは「最小コストの積み上げ」

足場が N 個並んでいて、カエルは 1 から N へジャンプしていきます。

ジャンプできるのは以下の 2 パターン:

  • i → i+1
  • i → i+2

そしてジャンプには以下のコストがかかります:

|h[i] − h[j]|

つまり、
「どの足場を経由して N へ向かうと、総コストが最小になるか」
を求める問題です。

ここで重要なのは、

  • 「未来のジャンプ先」を考える前に、
  • まず“今いる足場までの最小コスト”がわかっていれば、次に進める

という構造があることです。これは DP が使える問題の典型構造です。


DP の考え方:状態を一つに固定すれば道が見える

DP を理解する第一歩は、状態を 1 つだけに固定することです。
今回なら次の 1 文に尽きます。

「足場 i にたどり着くために支払う最小コストを dp[i] とする」

これが 状態定義 です。
この一行が決まれば、解法の 8 割は終わったと言っても過言ではありません。


状態遷移の説明:過去から最小を拾ってくるだけ

足場 i へ到達する方法は、最大で 2 通りしかありません。

  • 足場 i−1 からジャンプ
  • 足場 i−2 からジャンプ

よって、以下のように「過去の最小値にコストを足すだけ」です。

■ 遷移式

dp[i] = min(
    dp[i-1] + |h[i] - h[i-1]|,
    dp[i-2] + |h[i] - h[i-2]|
)


サンプルの分解:実際に値を埋めてみる

入力例:

4
10 30 40 20

足場の高さは次の通り:

足場高さ
110
230
340
420

dp 配列の初期化

dp[1] = 0 (最初からいるのでコスト 0)

dp 2

dp[2] = dp[1] + |10 - 30|
       = 0 + 20
       = 20

dp 3

パターン1:1→2→3
dp[2] + |30 - 40| = 20 + 10 = 30

パターン2:1→3
dp[1] + |10 - 40| = 0 + 30 = 30

よって dp[3] = 30

dp 4

パターン1:2→4
dp[2] + |30 - 20| = 20 + 10 = 30

パターン2:3→4
dp[3] + |40 - 20| = 30 + 20 = 50

よって dp[4] = 30

実装例(Python / TypeScript)

Python

n = int(input())
h = list(map(int, input().split()))

dp = [0] * n
dp[1] = abs(h[1] - h[0])

for i in range(2, n):
    dp[i] = min(
        dp[i-1] + abs(h[i] - h[i-1]),
        dp[i-2] + abs(h[i] - h[i-2])
    )

print(dp[-1])

TypeScript(ケビン作成)


import * as fs from "fs";

function main(input: string): void {
	const trimmedInput = input.trim();
	const [rawN, rawHs] = trimmedInput.split(/\r?\n/);
	const N = Number(rawN)
	const Hs = rawHs.split(" ").map(Number);
	const dp = Array(N).fill(0);
	dp[1] = Math.abs(Hs[1] - Hs[0]);

	for(let i=2; i<N; i++){
		const beforeOne = Hs[i-1];
		const beforeTwo = Hs[i-2];
		const current = Hs[i];

		let cost = 0;
		const costA = dp[i-1] + Math.abs(beforeOne - current);
		const costB = dp[i-2] + Math.abs(beforeTwo - current);
		cost = Math.min(costA, costB)
		dp[i] = cost;
	}

	console.log(dp[N-1]);
}

const inputData: string = fs.readFileSync("/dev/stdin", "utf8")
main(inputData);

計算量:O(N) で高速に解ける

  • 足場ごとに定数回の計算しか行わない → 計算量 O(N)
  • メモリも dp と h の 2 配列で十分 → メモリ O(N)

まとめ:DP の本質は「状態」と「遷移」を考えるだけ

✔ 1. 状態定義が問題を決める

dp[i] = 足場 i にたどり着く最小コスト

✔ 2. 遷移式は「過去の最小値 + コスト」

ジャンプ可能な2パターンを比較するだけで遷移が作れます。

✔ 3. DP の本質は積み上げ

「未来を直接考えない」「今の状態の最小を確定して進む」これが動的計画法の考え方です。


最後に:Frog 1 を理解できれば、DP の世界が一気に開く

Frog 1 は DP の入門問題ですが、
「状態定義」→「遷移式」→「実装」 の流れをつかむには最適です。

この問題が理解できたら、類題として

  • Frog 2(ジャンプ幅が K まで拡大)
  • Frog 3(平方コスト)
  • Typical DP まとめ問題

などにも自然と挑戦できるようになります。

DP が苦手でも、考え方がつかめれば必ず得意分野になります。
私もまだまだAtCoder勉強中ですが、
皆様が、これから DP を武器に競技プログラミングを楽しめるよう、
この記事が少しでも役に立てば嬉しいです!

デイトラでWeb制作を学ぶ ※本リンクはアフィリエイトを含みます。