Day 173-Q2 — 2026-10-09 | 個数×重さで課金される便分け(簡易版 LARSCH: 二分探索なしのオンライン Monge DP)

2026-10-09 赤色 Master (Phase 8+) ★★★★★★★★★ DP 高速化(焦点帯 M1)。Day170-Q2 では同じ形の DP を「候補キュー+交点の二分探索」で解いた。今回は交点を一切求めない再帰で、遷移の評価回数を約 $N\log_2 N$ 回に抑える

問題

$N$ 個の荷物が一列に並んでおり、$i$ 番目の重さは $a_i$ である。荷物を並び順のままいくつかの連続した区間に分け、区間ごとに 1 便で運ぶ。

荷物 $l+1,\dots,r$ を 1 便で運ぶ費用は

$$C + \Bigl(\sum_{t=l+1}^{r} a_t\Bigr)\times(r-l)$$

である(固定費 $C$ に、積んだ重さの合計×積んだ個数が加わる)。すべての荷物を運ぶ費用の合計の最小値を求めよ。

入力形式

N C
a_1 a_2 ... a_N

制約

#条件
1$1\le N\le 2\times10^5$
2$0\le C\le 10^{12}$
3$1\le a_i\le 10^6$
4入力はすべて整数

入出力例

入力例1

5 10
3 1 4 1 5

出力例1

53

入力例2

4 0
2 7 1 8

出力例2

18

($C=0$ なら 1 個ずつ運ぶのが最適で、答えは重さの合計)

入力例3

6 100
5 5 5 5 5 5

出力例3

280

(3 例とも $O(N^2)$ の素直な DP と一致)

背景と出典

$\mathrm{dp}[j]=\min_{i<j}\bigl(\mathrm{dp}[i]+w(i,j)\bigr)$ で $w$ が Monge($i<i'$, $j<j'$ で $w(i,j)+w(i',j')\le w(i,j')+w(i',j)$)なら、最良の $i$ は $j$ について単調になる。ただし $\mathrm{dp}[i]$ が $j$ の計算中に決まっていく(オンライン)ので、オフライン用の monotone minima や SMAWK はそのままでは使えない。

オンラインで $O(N)$ にする方法として Larmore–Schieber(1991)の LARSCH 法がある、と私は記憶している。本問で使う再帰はその「簡易版」で、$O(N\log N)$ だが 20 行で書ける。日本の競技プログラミングでは noshi91 氏の解説で知られている、と私は記憶している(原典の論文も解説記事も本セッションでは確認していない。正しさは素直な DP との照合でだけ確かめた)。$O(N)$ の本来の LARSCH は本問では実装していない。

本問の $w(i,j)=C+(S_j-S_i)(j-i)$($S$ は累積和)が Monge であることは、式を展開すると

$$w(i,j)+w(i',j')-w(i,j')-w(i',j)=-(i'-i)(S_{j'}-S_j)-(j'-j)(S_{i'}-S_i)\le0$$

になることから分かる($a_t\ge1$ なので $S$ は増加)。

本問は自作。

概念図

rec(l, r) の 4 つの手順(横軸 = 区切りの位置 0..N) lmr 前提: dp[l] は確定、x[r] は区切り 0..l の中での最良 ① check(x[l]..x[r], m) m の最良は x[l]〜x[r] の間(Monge の単調性) ② rec(l, m) … 戻ると dp[m] が確定 ③ check(l+1..m, r) x[r] を「0..m の中の最良」に更新 ④ rec(m, r) 各段で ① と ③ の check は合わせて O(N) 回、段数 log2 N → 実測 0.92 N log2 N 回(N = 2×10^5)

ヒント(クリックで段階的に開示)

ヒント1(方向性)

$w$ は Monge。だから $\mathrm{dp}[j]$ を与える最良の区切り $x[j]$ は $j$ について単調非減少。ただし「前回の最良から右へ、良くなる間だけ進む」尺取りは誤り($i\mapsto \mathrm{dp}[i]+w(i,j)$ は単峰とは限らない)。

ヒント2(アプローチ)

区間 $[l,r]$ を再帰で処理する。呼ぶ時点で「$\mathrm{dp}[l]$ は確定」「$x[r]$ は区切り $0..l$ の中での最良」を保つ。真ん中 $m$ の最良は $x[l]$ と $x[r]$ の間にあるので、そこだけ調べる。

ヒント3(誘導)
rec(l, r):
    if r - l <= 1: return
    m = (l + r) // 2
    for i in x[l]..x[r]: check(i, m)
    rec(l, m)                 # dp[m] が確定する
    for i in l+1..m: check(i, r)
    rec(m, r)
最初に check(0, N) してから rec(0, N)

模範解答 (Python)

模範解答を表示する(まず自力で解いてから)
import sys


def solve(n, c, s):
    INF = float('inf')
    dp = [INF] * (n + 1)
    x = [0] * (n + 1)          # x[j] = いま分かっている dp[j] の最良の直前の区切り
    dp[0] = 0

    def check(i, j):           # 区切り i から j への遷移を試す(dp[i] は確定済み)
        v = dp[i] + c + (s[j] - s[i]) * (j - i)
        if v < dp[j]:
            dp[j] = v
            x[j] = i

    # 簡易版 LARSCH: rec(l, r) を呼ぶ時点で
    #   dp[l] は確定、x[r] は区切り候補 0..l の中での最良
    def rec(l, r):
        if r - l <= 1:
            return
        m = (l + r) // 2
        for i in range(x[l], x[r] + 1):   # m の最良は x[l]..x[r] の間にある(Monge)
            check(i, m)
        rec(l, m)                          # ここで dp[m] が確定
        for i in range(l + 1, m + 1):      # r に l+1..m からの遷移を足す
            check(i, r)
        rec(m, r)

    check(0, n)
    rec(0, n)
    return dp[n]


def main():
    data = sys.stdin.buffer.read().split()
    n, c = int(data[0]), int(data[1])
    s = [0] * (n + 1)
    for i in range(n):
        s[i + 1] = s[i] + int(data[2 + i])
    print(solve(n, c, s))


if __name__ == "__main__":
    main()

計算量: 時間 $O(N\log N)$、再帰の深さ $O(\log N)$。check の呼び出し回数は実測で $N=10^5$ で 1566679 回($0.94\,N\log_2N$)、$N=2\times10^5$ で 3232903 回($0.92\,N\log_2N$)。$N=2\times10^5$ で 1.08〜2.58 s(Python 3.14.7、M1 MacBook Pro、負荷平均 15〜65 の混んだ時間帯での実測)。

Step-by-Step 解説

1なぜ素直な DP では足りないか

$\mathrm{dp}[j]=\min_{i<j}(\mathrm{dp}[i]+w(i,j))$ をそのまま回すと $O(N^2)=4\times10^{10}$ 回。間に合わない。

2Monge なら最良の区切りは単調

$i<i'$ で、ある $j$ について $i'$ が $i$ に勝つ($\le$)なら、$j'>j$ でも $i'$ は $i$ に勝ち続ける。Monge の不等式を $\mathrm{dp}[i]+w(i,j')\ge\dots$ の形に並べ替えると出てくる。だから $x[j]$(最良の区切り、同点なら大きい方でも小さい方でもよい)は $j$ について単調。

3再帰の不変条件

rec(l, r) を呼ぶ時点で

  • $\mathrm{dp}[l]$ は確定している
  • $x[r]$ は区切り $0..l$ の中での最良(check 済み)

rec(0, N) の前に check(0, N) するのは、この条件を最初に満たすため。

4真ん中 m を求める

$m$ の本当の最良(区切り $0..m-1$ の中)のうち、区切り $0..l$ に限った最良は、単調性から $x[l]$ 以上 $x[r]$ 以下にある。そこだけ check(i, m) すれば、「$x[m]$ は $0..l$ の中での最良」になり、rec(l, m) の条件が満たされる。rec(l, m) の中で $l+1..m-1$ からの遷移が足されて、戻ったときには $\mathrm{dp}[m]$ が確定している。

5右半分の準備

rec(m, r) の条件は「$x[r]$ は $0..m$ の中での最良」。今は $0..l$ までしか見ていないので、$l+1..m$ を check(i, r) で足す。これで右半分に進める。

6なぜ $O(N\log N)$ か

各段で、check(i, r) の総数は区間の長さの和 $=N$。check(i, m) の総数は $x[r]-x[l]+1$ の和で、同じ段の区間どうしで端が重なるだけなので $O(N)$。段数 $\log_2 N$ をかけて $O(N\log N)$。実測の $0.92\,N\log_2 N$ 回はこれと合う。

迷った他案と採らなかった理由

案計算量採否理由
素直な DP$O(N^2)$検算用のみ1000 ケースの照合に使用
前回の最良から右へ、良くなる間だけ進む尺取り$O(N)$不採用(誤り)$i\mapsto\mathrm{dp}[i]+w(i,j)$ が単峰でない。1000 ケース中 118 ケースで誤答。最小の反例は $N=3,C=10,a=(1,1,5)$ で 31(正解 29)
候補キュー+交点の二分探索(Day170-Q2 の方法)$O(N\log N)$比較用正しいが、候補の「勝ち始める位置」を二分探索で求めるコードが要る。$N=2\times10^5$ で 1.54〜3.62 s(実測、本解と同じ負荷の下で 1.4〜2.2 倍遅い)
Convex Hull Trick$O(N)$〜$O(N\log N)$不採用$w(i,j)$ が $j$ の 1 次式の形 $A_i\cdot B_j+\dots$ に分かれない($S_j\cdot j$ の項はよいが、$-S_i\cdot j-S_j\cdot i$ の 2 つの積が残り、直線 1 本で表せない)
本来の LARSCH$O(N)$不採用(未実装)実装が長く、本セッションでは正しく書ける自信がなかった。簡易版の $\log_2 N\approx18$ 倍の差は $N=2\times10^5$ で 1〜3 s に収まっている
簡易版 LARSCH(本解)$O(N\log N)$採用交点の計算が要らず、Monge であれば $w$ の中身を問わない

前提が変わったら:

  • $N=10^6$ 以上なら、Python で $N\log N\approx2\times10^7$ 回の評価は 10 s を超える見込み(実測の比例からの推定)。本来の LARSCH か、C++ にする
  • $w$ が Monge でない(例えば重さが負になりうる)なら、単調性が崩れて本解は誤る。素直な DP か別の構造(凸性など)を探す
  • 便の数がちょうど $K$なら、Alien DP(ペナルティ $\lambda$ を $C$ に足して二分探索)と組み合わせる。本解の $C$ がちょうどその $\lambda$ の役になる

よくあるミス

ミス原因正しい書き方
尺取りで「悪くなったら止まる」最良が単調でも、評価値が単峰とは限らない1000 ケース中 118 ケースで誤答(実測)。$a=(1,1,5)$ で区切り 1 が 0 に負けて止まり、区切り 2 を見ない
rec の前に check(0, N) を忘れる不変条件「$x[r]$ は $0..l$ の最良」が最初から崩れる最初に check(0, n)
右半分の前に check(i, r) を足さない$x[r]$ が古いまま右の区間に渡るfor i in range(l + 1, m + 1): check(i, r)
INF を 10**18 にする$w$ は最大で約 $10^{12}+2\times10^{11}\times2\times10^5=4\times10^{16}$ 程度だが、$C$ と足すと読み違えやすいPython なら float('inf') で比較だけに使う(整数との比較は正確)
再帰の深さを心配して setrecursionlimit を大きくする深さは $\log_2 N\approx18$不要

次のステップ

  • 発展1: 本来の LARSCH を実装し、本解と評価回数を比べる($O(N)$ の定数が $\log_2N\approx18$ より小さいか)
  • 発展2: 便の数をちょうど $K$ にする(Alien DP と組み合わせる)
  • 発展3: Larmore–Schieber(1991)の原典と、noshi91 氏の解説を確認する(本セッションでは未確認)

検証ログ(本セッションで実行した数値)

  • 素直な $O(N^2)$ DP との照合(実測): $N\le40$、$C\in\{0,1,5,50,1000,10^6\}$、$a_i$ の上限 $\{1,3,10,10^6\}$ のランダム 1000 ケースで不一致 0
  • 候補キュー+二分探索(Day170-Q2 の方法): 同じ 1000 ケースで不一致 0、$N=10^5,2\times10^5$ の 4 ケースで本解と一致
  • 誤った方法(実測): 尺取りは 1000 ケース中 118 ケースで誤答。最小の反例($N\le8$ のランダム 20 万ケースから $N$ と重さの和が最小のもの)は $N=3,C=10,a=(1,1,5)$ で 31(正解 29)
  • 入力例(実測): 例 1 53、例 2 18、例 3 280
  • check の回数(実測): $N=10^3$: 11853($1.19N\log_2N$)、$10^4$: 139030(1.05)、$10^5$: 1566679(0.94)、$2\times10^5$: 3232903(0.92)
  • 実行時間(実測、負荷平均 15〜65): $N=10^5$ で本解 0.78〜0.81 s/キュー+二分探索 1.73〜1.75 s、$N=2\times10^5$ で本解 1.08〜2.58 s/キュー+二分探索 1.54〜3.62 s
  • 扱っていない範囲: 本来の $O(N)$ LARSCH、便の数の指定、Monge でない $w$、原典の確認

自己評価

自分の回答

気づき・メモ