D - コンボボーナス / Combo Bonus Editorial by admin
Claude Opus 5 (High)概要
\(N\) 個のステージから任意の部分集合を選び、「選んだステージの \(A_i\) の総和」+「隣接して選んだペアの個数 \(\times C\)」を最大化する問題です。直前のステージを選んだかどうかを状態に持つ簡単な区間 DP(bit DP 的な線形 DP)で解けます。
考察
素朴なアプローチの問題点
「すべてのクリアパターンを試す」と、\(2^N\) 通りの選び方があります。\(N \le 10^6\) なので当然間に合いません。
貪欲は通用するか?
「\(A_i > 0\) のステージだけ選ぶ」という貪欲は誤りです。例えば
N=3, C=10
A = 5, -3, 5
\(A_i>0\) のみ選ぶと \(5+5=10\) ですが、ステージ \(2\) も選ぶと \(5-3+5+2\times 10 = 27\) となり、負のステージを「橋渡し」として選んだ方が得になります。逆に \(C=0\) なら負のステージを選ぶ意味はありません。つまり「その 1 つのステージ単体の得点」だけでは判断できず、前後の選択状況に依存するのがこの問題の本質です。
重要な気づき
スコアの増分に注目します。ステージ \(i\) を選ぶとき、加算されるのは
- \(A_i\)(必ず)
- ステージ \(i-1\) も選んでいれば、さらに \(C\)
だけです。つまり「1 つ前のステージを選んだか否か」さえ分かっていれば、次のステージの得点は確定するわけです。それより前の情報(どのステージを選んだか)は一切必要ありません。
したがって「先頭から順に決めていく」DP で、状態は「直前を選んだ/選んでいない」の 2 通りだけで済みます。
アルゴリズム
\(i\) 番目まで処理した時点での値を次の 2 つで管理します。
- \(dp[i][0]\) : ステージ \(i\) を選ばなかった場合の、ステージ \(1..i\) までで得られる最大スコア
- \(dp[i][1]\) : ステージ \(i\) を選んだ場合の、ステージ \(1..i\) までで得られる最大スコア
遷移は以下の通りです。
- \(dp[i][0] = \max(dp[i-1][0],\; dp[i-1][1])\)
- ステージ \(i\) を選ばないのでスコアは増えず、ボーナスも発生しない。直前はどちらでもよい。
- \(dp[i][1] = \max(dp[i-1][0],\; dp[i-1][1] + C) + A_i\)
- ステージ \(i\) を選ぶので \(A_i\) を加算。さらに直前も選んでいた場合のみボーナス \(C\) が付く。
初期状態は「まだ 0 個処理した状態」として \(dp[0][0]=0\)、\(dp[0][1]=-\infty\)(ステージ 0 は存在しないので「選んだ」状態はあり得ない)とします。
答えは \(\max(dp[N][0], dp[N][1])\) です。
具体例(\(N=3, C=10, A=(5,-3,5)\))
| \(i\) | \(A_i\) | \(dp[i][0]\) | \(dp[i][1]\) |
|---|---|---|---|
| 0 | - | 0 | \(-\infty\) |
| 1 | 5 | 0 | \(\max(0,-\infty)+5=5\) |
| 2 | -3 | \(\max(0,5)=5\) | \(\max(0,5+10)-3=12\) |
| 3 | 5 | \(\max(5,12)=12\) | \(\max(5,12+10)+5=27\) |
答えは \(\max(12,27)=27\) で、先ほどの考察と一致します。
計算量
- 時間計算量: \(O(N)\)
- 空間計算量: \(O(1)\)(DP 配列を持たず、直前の 2 値だけを使い回す。入力を配列に保持する場合は \(O(N)\))
実装のポイント
オーバーフロー対策: \(A_i\) の絶対値が最大 \(10^9\)、\(C\) も最大 \(10^9\)、\(N\) が最大 \(10^6\) なので、スコアは最大でおよそ \(2 \times 10^{15}\) 程度になります。32 bit 整数では溢れるので必ず 64 bit 整数(C++ なら
long long)を使います。\(-\infty\) の扱い: \(dp1\) の初期値に
LLONG_MINをそのまま入れると \(+C\) や \(+A_i\) でオーバーフローします。LLONG_MIN / 4のような十分小さいが余裕のある値を使う、あるいはコードのように「\(dp1\) が \(-\infty\) のときは \(+C\) の候補を作らない」と場合分けするのが安全です。別の方法として、\(dp[1][0]=0,\ dp[1][1]=A_1\) から始めて \(i=2\) 以降をループする実装にすれば \(-\infty\) を使わずに済みます。メモリと入力速度: \(N \le 10^6\) なので \(A\) を配列に持っても問題ありませんが、DP は \(A_i\) を 1 つずつ読みながら進められるため配列は不要です。\(10^6\) 個の整数入力は遅い入力方法だと時間がかかるので、C++ では
scanfや高速読み込み(コードのようにfreadで一括読み込みして自前でパース)、Python ならsys.stdinの利用を推奨します。答えが 0 になるケース: 「1 つもクリアしない」も許されるため、すべて負でボーナスも活かせない場合の答えは \(0\) です。\(dp[0][0]=0\) から始める実装なら自然にこれが含まれます。
ソースコード
#include <bits/stdc++.h>
using namespace std;
static char buf[1 << 25];
int main() {
size_t len = fread(buf, 1, sizeof(buf) - 1, stdin);
buf[len] = '\0';
char *p = buf;
auto readInt = [&]() -> long long {
while (*p && (*p < '0' || *p > '9') && *p != '-') ++p;
bool neg = false;
if (*p == '-') { neg = true; ++p; }
long long x = 0;
while (*p >= '0' && *p <= '9') { x = x * 10 + (*p - '0'); ++p; }
return neg ? -x : x;
};
long long n = readInt();
long long C = readInt();
const long long NEG = LLONG_MIN / 4;
long long dp0 = 0, dp1 = NEG;
for (long long i = 0; i < n; ++i) {
long long a = readInt();
long long n0 = max(dp0, dp1);
long long cand = dp0;
if (dp1 > NEG) cand = max(cand, dp1 + C);
long long n1 = cand + a;
dp0 = n0;
dp1 = n1;
}
printf("%lld\n", max(dp0, dp1));
return 0;
}
この解説は claude5opus-high によって生成されました。