D - 工場の稼働計画 / Factory Operation Plan Editorial by admin
Claude Opus 5 (High)概要
\(N\) 日間の各日について「稼働」「休止」を選び、利益の合計を最大化する問題です。休止は 2 日以上連続できず、連続稼働 2 日目以降はコスト \(C\) がかかります。典型的な 区間 DP ではなく「直前の日の状態」を持つ DP(bit DP 的な状態 DP) で解けます。
考察
重要な気づき
ある日の利益は、その日の選択(稼働/休止)と 前日の選択 だけで決まります。
- \(i\) 日目に休止 → 利益 \(0\)。ただし前日も休止だったらそもそも禁止。
- \(i\) 日目に稼働 → 前日が休止(または \(i=1\))なら \(A_i\)、前日も稼働なら \(A_i - C\)。
つまり「前日が稼働だったか休止だったか」という情報 1 ビットだけを覚えておけば、\(i\) 日目以降の意思決定に必要な情報はすべて揃います。過去の詳しい履歴(何日連続で動かしたか等)は不要です。ここがポイントで、問題文の「連続して稼働している日数にかかわらず \(C\)」という条件がこれを保証しています。
素朴なアプローチの問題点
全ての稼働/休止の組み合わせを試すと \(2^N\) 通りで、\(N \le 10^6\) ではまったく間に合いません。
また「\(A_i > 0\) の日は稼働、\(A_i \le 0\) の日は休止」といった貪欲は誤りです。例えば
N=3, C=0, A = [5, -1, 5]
の場合、\(2\) 日目を休止したいところですが、\(C=0\) なので \(2\) 日目も稼働して \(5 + (-1-0) + (5-0) = 9\) とするのが最適です(\(2\) 日目を休止すると \(5+0+5=10\) で、こちらの方が良い例もあり)、逆に
N=3, C=100, A = [5, 5, 5]
では毎日稼働すると \(5 + (5-100) + (5-100) = -185\) ですが、\(2\) 日目を休止して \(5 + 0 + 5 = 10\) が最適です。このように \(C\) と \(A_i\) のバランスで最適解が変わるため、局所的な判断では決められません。
解決策
「前日が稼働 / 休止」の 2 状態を持つ DP を行えば、各日 \(O(1)\) で遷移でき、全体 \(O(N)\) で解けます。
アルゴリズム
DP の定義:
- \(\mathrm{work}[i]\) … \(i\) 日目まで見て、\(i\) 日目に 稼働 させたときの利益合計の最大値
- \(\mathrm{rest}[i]\) … \(i\) 日目まで見て、\(i\) 日目に 休止 させたときの利益合計の最大値
初期値(\(i=1\)): $\(\mathrm{work}[1] = A_1,\qquad \mathrm{rest}[1] = 0\)$
(\(1\) 日目に稼働する場合、前日が存在しないので \(C\) は引かれません。)
遷移(\(i \ge 2\)):
- \(i\) 日目に休止する場合、前日は必ず稼働していなければならない(2 日連続休止は禁止)ので $\(\mathrm{rest}[i] = \mathrm{work}[i-1]\)$
- \(i\) 日目に稼働する場合、前日は稼働でも休止でもよく $\(\mathrm{work}[i] = \max\bigl(\mathrm{rest}[i-1] + A_i,\ \ \mathrm{work}[i-1] + A_i - C\bigr)\)$
答え: $\(\max(\mathrm{work}[N],\ \mathrm{rest}[N])\)$
期間外の状態は考慮しないので、最終日はどちらの状態でも構いません。
具体例で確認
\(N=3, C=100, A=[5,5,5]\) の場合:
| \(i\) | \(\mathrm{work}\) | \(\mathrm{rest}\) |
|---|---|---|
| 1 | \(5\) | \(0\) |
| 2 | \(\max(0+5,\ 5+5-100) = 5\) | \(5\) |
| 3 | \(\max(5+5,\ 5+5-100) = 10\) | \(5\) |
答えは \(\max(10,5)=10\) となり、「稼働・休止・稼働」が最適だと分かります。
計算量
- 時間計算量: \(O(N)\)
- 空間計算量: \(O(1)\)(入力配列を除く。DP は直前の 2 値のみ保持)
実装のポイント
配列を持たず変数 2 つで回す:\(\mathrm{work}[i]\) の計算に \(\mathrm{rest}[i-1]\) が必要なので、更新順に注意します。新しい値を一時変数
nrest,nworkに入れてから代入すると安全です(rest = workを先に実行してしまうと古いrestが壊れます)。\(N=1\) の扱い:ループが回らず、\(\max(A_1, 0)\) が答えになります。上のコードは初期値のまま \(\max(\mathrm{work},\mathrm{rest}) = \max(A_1,0)\) を出力するので、特別扱い不要です。
オーバーフロー:\(|A_i| \le 10^9\)、\(N \le 10^6\) なので合計は最大 \(10^{15}\) 程度。C++ 等では
long longを使います(Python は多倍長整数なので問題なし)。入出力の高速化:\(N \le 10^6\) なので、Python では
input()を繰り返し呼ぶのではなくsys.stdin.buffer.read().split()で一括読み込みします。さらに、リストを作らず必要な要素だけint()変換しながらループすると定数倍が軽くなります。休止が「必ず利益 0」であることに注意:\(\mathrm{rest}[i] = \mathrm{work}[i-1] + 0\) で、\(A_i\) は加算しません。
ソースコード
import sys
def main():
data = sys.stdin.buffer.read().split()
n = int(data[0])
c = int(data[1])
NEG = float('-inf')
a1 = int(data[2])
rest = 0
work = a1
for i in range(1, n):
ai = int(data[2 + i])
nrest = work
nwork = rest + ai
w2 = work + ai - c
if w2 > nwork:
nwork = w2
rest = nrest
work = nwork
ans = rest if rest > work else work
sys.stdout.write(str(ans) + "\n")
main()
この解説は claude5opus-high によって生成されました。