Official

C - 隣接ペナルティ付き選択 / Selection with Adjacent Penalty Editorial by admin

GPT 5.2 High

概要

仕事をいくつか選んで報酬の合計を最大化したいが、連番の仕事を両方選ぶたびに \(K\) 円のペナルティがかかる。これを最大利益になるように選ぶ問題です。

考察

重要なのは、ペナルティが「隣り合う番号同士(\(i\)\(i+1\))」の関係にしか依存しない点です。
つまり、仕事 \(i\) を選ぶかどうかの影響は、基本的に直前の仕事 \(i-1\) を選んだかどうかだけで決まります(それより前の選び方は、直前の状態に要約できる)。

素朴に「全ての部分集合を試す」やり方だと \(2^N\) 通りあり、\(N \le 2 \times 10^5\) では到底間に合いません。
そこで、「今見ている位置まででの最適値」を動的計画法(DP)で更新していきます。

ペナルティの発生条件を整理すると:

  • 仕事 \(i\) を選び、かつ仕事 \(i-1\) も選んでいるときに限り、追加で \(K\) 引かれる
  • それ以外(どちらか片方でも選ばない)なら、ペナルティは発生しない

この「直前も選んだか?」だけ覚えておけば十分、というのがDPの肝です。

アルゴリズム

\(dp0, dp1\) の2状態でDPします(添字 \(i\) は「現在位置」だと思ってください)。

  • \(dp0\):ここまで見てきた仕事の中で、最後の仕事(直近の仕事)を選んでいないときの最大利益
  • \(dp1\):ここまで見てきた仕事の中で、最後の仕事(直近の仕事)を選んでいるときの最大利益

仕事 \(i\)(報酬 \(A_i\))を処理するときの遷移は以下です。

1) 新しい \(dp0\)(仕事 \(i\) を選ばない)

仕事 \(i\) を選ばないなら、直前が選ばれていたかどうかは関係なく、最大を取ればよい: - \(new0 = \max(dp0, dp1)\)

2) 新しい \(dp1\)(仕事 \(i\) を選ぶ)

仕事 \(i\) を選ぶ場合、直前の状態で場合分けします。

  • 直前を選んでいない(\(dp0\))から選ぶ:ペナルティなし
    利益は \(dp0 + A_i\)
  • 直前も選んでいる(\(dp1\))から続けて選ぶ:隣接ペアが1つ増えるのでペナルティ \(K\)
    利益は \(dp1 + A_i - K\)

したがって、 - \(new1 = \max(dp0 + A_i,\; dp1 + A_i - K)\)

これを \(i=1\) から順に更新していき、最後に \(\max(dp0, dp1)\) が答えです。

小さな例

\(A=[5,4,3],\; K=2\) のとき:

  • 1番(5)を選ぶ:利益 5
  • 2番(4)も選ぶと隣接なので \(-2\):利益 \(5+4-2=7\)
  • 3番(3)も選ぶとさらに隣接なので \(-2\):利益 \(7+3-2=8\)

DPはこのように「連続で選んだら \(K\) 引く」を逐次的に処理できます。

計算量

  • 時間計算量: \(O(N)\)(各仕事につき定数回の更新)
  • 空間計算量: \(O(1)\)\(dp0, dp1\) の2つだけ)

実装のポイント

  • DP配列を持たず、\(dp0, dp1\) のみを更新することでメモリを節約できます。

  • 初期化は

    • \(dp0=0\)(まだ何も選んでいない)
    • \(dp1=A_1\)(1番目を選ぶ) としています。
  • 問題では「1つ以上選ぶ」必要がありますが、今回は \(A_i \ge 1\) なので最終的に \(\max(dp0,dp1)\) は必ず正となり、空集合(0)が最適になることはありません(そのため特別な処理なしで通ります)。

    ソースコード

import sys

def main():
    it = iter(map(int, sys.stdin.buffer.read().split()))
    N = next(it)
    K = next(it)
    A = [next(it) for _ in range(N)]

    dp0 = 0          # i not selected
    dp1 = A[0]       # i selected

    for i in range(1, N):
        ai = A[i]
        new0 = dp0 if dp0 > dp1 else dp1
        new1 = dp0 + ai
        cand = dp1 + ai - K
        if cand > new1:
            new1 = cand
        dp0, dp1 = new0, new1

    ans = dp0 if dp0 > dp1 else dp1
    print(ans)

if __name__ == "__main__":
    main()

この解説は gpt-5.2-high によって生成されました。

posted:
last update: