C - スキーコース / Ski Course 解説 by admin
Claude 4.6 Opus (Thinking)概要
\(N \times M\) のグリッドを上から下へ滑り降りる際、「同じ方向(左・左 or 右・右)に2回連続で移動できない」というジグザグ制約のもとで、通過する区画のスコア合計を最大化する動的計画法の問題です。
考察
重要な気づき
各行での位置だけでなく、直前にどの方向に移動したかを記録しないと、ジグザグ制約を正しく判定できません。
例えば、列の変化 \(d_r = c_{r+1} - c_r\) について: - \(d_r > 0\)(右移動)の後に \(d_{r+1} > 0\)(右移動)は 禁止 - \(d_r < 0\)(左移動)の後に \(d_{r+1} < 0\)(左移動)は 禁止 - \(d_r = 0\)(直進)の後はどの方向にも移動可能
素朴なアプローチの問題
単純に dp[r][c] =「第 \(r\) 行の列 \(c\) に到達したときの最大スコア」とすると、次の行への遷移時にジグザグ制約を考慮できません。前回の移動方向が分からないためです。
解決策
DPの状態に直前の移動方向を加えます。方向は「左(\(d < 0\))」「直進(\(d = 0\))」「右(\(d > 0\))」の3種類なので、状態数は \(O(N \times M \times 3)\) となり、制約内に収まります。
アルゴリズム
DPの状態を以下のように定義します:
- \(\text{dp}[c][0]\):現在の行の列 \(c\) に左移動(列が減少)で到達したときの最大スコア
- \(\text{dp}[c][1]\):現在の行の列 \(c\) に直進(列が変化なし)で到達したときの最大スコア
- \(\text{dp}[c][2]\):現在の行の列 \(c\) に右移動(列が増加)で到達したときの最大スコア
遷移
次の行の列 \(c\) に到達する場合を考えます:
左移動で到達(前の行の列 \(c+1\) から来る):
前回の移動方向は直進(1)か右(2)でなければならない(左→左は禁止)
\(\text{ndp}[c][0] = \max(\text{dp}[c+1][1],\ \text{dp}[c+1][2]) + G[r][c]\)直進で到達(前の行の列 \(c\) から来る):
前回の移動方向は何でもよい
\(\text{ndp}[c][1] = \max(\text{dp}[c][0],\ \text{dp}[c][1],\ \text{dp}[c][2]) + G[r][c]\)右移動で到達(前の行の列 \(c-1\) から来る):
前回の移動方向は左(0)か直進(1)でなければならない(右→右は禁止)
\(\text{ndp}[c][2] = \max(\text{dp}[c-1][0],\ \text{dp}[c-1][1]) + G[r][c]\)
初期化
第1行ではまだ移動していないので、全ての列 \(c\) について \(\text{dp}[c][1] = G[1][c]\)(直進扱い、どの方向にも続けられる)とします。
答え
最終行で全ての列・全ての方向の中から最大値を取ります。
計算量
- 時間計算量: \(O(N \times M)\)(各セルで定数回の操作)
- 空間計算量: \(O(M)\)(現在の行と次の行の2行分のDP配列のみ保持)
実装のポイント
初期状態を「直進(方向1)」として初期化することで、第1行からどの方向にも自然に遷移できるようにしています。
NEG_INF = -10^{18}を使い、到達不可能な状態と区別しています。到達不可能な状態からの遷移を防ぐため、val != NEG_INFのチェックを入れています。swap(dp, ndp)でDP配列を使い回すことで、メモリを節約しています。\(N = 1\) の場合は移動が発生しないため、第1行の最大値をそのまま出力する特別処理をしています。
ソースコード
#include <bits/stdc++.h>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int N, M;
cin >> N >> M;
const long long NEG_INF = -1e18;
vector<array<long long, 3>> dp(M, {NEG_INF, NEG_INF, NEG_INF});
vector<array<long long, 3>> ndp(M, {NEG_INF, NEG_INF, NEG_INF});
// Read and process row 0
for (int c = 0; c < M; c++) {
long long g;
cin >> g;
dp[c][1] = g;
}
if (N == 1) {
long long ans = NEG_INF;
for (int c = 0; c < M; c++) ans = max(ans, dp[c][1]);
cout << ans << endl;
return 0;
}
// Process rows 1 to N-1
for (int r = 1; r < N; r++) {
vector<long long> row(M);
for (int c = 0; c < M; c++) cin >> row[c];
for (int c = 0; c < M; c++) {
ndp[c] = {NEG_INF, NEG_INF, NEG_INF};
// ndp[c][0]: arrived by moving left (from c+1 in previous row)
// allowed after straight(1) or right(2)
if (c + 1 < M) {
long long val = max(dp[c+1][1], dp[c+1][2]);
if (val != NEG_INF)
ndp[c][0] = val + row[c];
}
// ndp[c][1]: arrived by moving straight (from c in previous row)
// allowed after any direction
{
long long val = max({dp[c][0], dp[c][1], dp[c][2]});
if (val != NEG_INF)
ndp[c][1] = val + row[c];
}
// ndp[c][2]: arrived by moving right (from c-1 in previous row)
// allowed after left(0) or straight(1)
if (c - 1 >= 0) {
long long val = max(dp[c-1][0], dp[c-1][1]);
if (val != NEG_INF)
ndp[c][2] = val + row[c];
}
}
swap(dp, ndp);
}
long long ans = NEG_INF;
for (int c = 0; c < M; c++) {
ans = max(ans, max({dp[c][0], dp[c][1], dp[c][2]}));
}
cout << ans << endl;
return 0;
}
この解説は claude4.6opus-thinking によって生成されました。