C - スキーコース / Ski Course 解説 by admin
Claude 4.6 Opus (Thinking)概要
\(N \times M\) のグリッドを上から下へ滑り降りるコースを選ぶ際、「同じ方向に2回連続で曲がれない(ジグザグ制約)」という条件のもとで、通過する区画のスコア合計を最大化する問題です。動的計画法(DP)で解きます。
考察
重要な気づき
この問題のポイントは「連続する同じ方向への移動を禁止する」というジグザグ制約をどう扱うかです。
例えば、今いるセルに「右移動(\(d=+1\))で到着した」場合、次の移動で再び右(\(d=+1\))に進むことはできません。一方、直進(\(d=0\))や左(\(d=-1\))には進めます。
つまり、次にどの方向に進めるかは、直前の移動方向に依存します。これは「状態」として持つべき情報です。
素朴なアプローチの問題点
もし全てのコースを列挙すると、各行で最大3通りの選択肢があるため \(O(3^N)\) 通りとなり、到底間に合いません。
解決策
直前の移動方向を状態に加えたDPを行えば、各セル・各状態について \(O(1)\) で遷移を計算でき、全体で \(O(NM)\) で解けます。
アルゴリズム
状態の定義
\(dp[r][c][s]\):第 \(r\) 行の列 \(c\) に、状態 \(s\) で到着したときのスコア合計の最大値
状態 \(s\) は以下の3種類: - 状態0:直進(\(d=0\))で到着した、もしくはスタート地点 - 状態1:右移動(\(d=+1\))で到着した - 状態2:左移動(\(d=-1\))で到着した
遷移
第 \(r\) 行の列 \(c\) への到着を考えます:
直進で到着(\((r-1, c) \to (r, c)\)):
前の行の同じ列から来る。直前の状態は何でもOK。
\(\text{new\_dp0}[c] = \max(dp0[c], dp1[c], dp2[c]) + G[r][c]\)右移動で到着(\((r-1, c-1) \to (r, c)\)):
前の行の列 \(c-1\) から来る。ただし、状態1(前回も右移動)からは来られない。
\(\text{new\_dp1}[c] = \max(dp0[c-1], dp2[c-1]) + G[r][c]\)左移動で到着(\((r-1, c+1) \to (r, c)\)):
前の行の列 \(c+1\) から来る。ただし、状態2(前回も左移動)からは来られない。
\(\text{new\_dp2}[c] = \max(dp0[c+1], dp1[c+1]) + G[r][c]\)
初期値と答え
- 初期値:第1行の全ての列 \(c\) について \(dp0[c] = G[0][c]\)(スタート地点)
- 答え:第 \(N\) 行の全ての列・全ての状態の最大値
具体例
3行3列で、列を左から1,2,3とします。もし行1で列2にいて、行2で列3に移動(右移動)したら状態1です。次の行3では:
- 列2へ(左移動)→ OK ✓
- 列3へ(直進)→ OK ✓
- 列4へ(右移動)→ NG ✗(右が2回連続)
計算量
- 時間計算量: \(O(NM)\) — 各セルについて3状態の遷移を \(O(1)\) で計算
- 空間計算量: \(O(M)\) — 行ごとにDPテーブルを更新するため、前の行の情報のみ保持
実装のポイント
DPの配列を行ごとに更新する(前の行の値を参照して新しい行の値を計算する)ことで、空間を \(O(M)\) に抑えている。
到達不可能な状態を
NEG_INF = -10^18で表し、到達可能な状態のみ遷移に使う。入力が大きくなりうる(\(NM \leq 2 \times 10^6\))ため、
sys.stdin.buffer.read()で高速に読み込んでいる。Python では定数倍が重い場合、この解法がギリギリになる可能性がある(PyPy推奨のケースもある)。
ソースコード
import sys
def main():
data = sys.stdin.buffer.read().split()
idx = 0
N = int(data[idx]); idx += 1
M = int(data[idx]); idx += 1
G = []
for i in range(N):
row = [int(data[idx + j]) for j in range(M)]
idx += M
G.append(row)
NEG_INF = -10**18
# state 0: arrived straight (or start), state 1: arrived via d=+1 (right), state 2: arrived via d=-1 (left)
dp0 = [NEG_INF] * M
dp1 = [NEG_INF] * M
dp2 = [NEG_INF] * M
for c in range(M):
dp0[c] = G[0][c]
for r in range(1, N):
new_dp0 = [NEG_INF] * M
new_dp1 = [NEG_INF] * M
new_dp2 = [NEG_INF] * M
gr = G[r]
for c in range(M):
# Arrive via straight (d=0): from (r-1, c), any previous state allowed
v0 = dp0[c]
v1 = dp1[c]
v2 = dp2[c]
val = v0
if v1 > val: val = v1
if v2 > val: val = v2
if val != NEG_INF:
new_dp0[c] = val + gr[c]
# Arrive via right (d=+1): from (r-1, c-1), exclude state 1 (can't have two rights)
if c > 0:
v0 = dp0[c - 1]
v2 = dp2[c - 1]
val = v0
if v2 > val: val = v2
if val != NEG_INF:
new_dp1[c] = val + gr[c]
# Arrive via left (d=-1): from (r-1, c+1), exclude state 2 (can't have two lefts)
if c < M - 1:
v0 = dp0[c + 1]
v1 = dp1[c + 1]
val = v0
if v1 > val: val = v1
if val != NEG_INF:
new_dp2[c] = val + gr[c]
dp0 = new_dp0
dp1 = new_dp1
dp2 = new_dp2
ans = NEG_INF
for c in range(M):
if dp0[c] > ans: ans = dp0[c]
if dp1[c] > ans: ans = dp1[c]
if dp2[c] > ans: ans = dp2[c]
print(ans)
main()
この解説は claude4.6opus-thinking によって生成されました。