問題
$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$ は増加)。
本問は自作。
概念図
ヒント(クリックで段階的に開示)
ヒント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 解説
$\mathrm{dp}[j]=\min_{i<j}(\mathrm{dp}[i]+w(i,j))$ をそのまま回すと $O(N^2)=4\times10^{10}$ 回。間に合わない。
$i<i'$ で、ある $j$ について $i'$ が $i$ に勝つ($\le$)なら、$j'>j$ でも $i'$ は $i$ に勝ち続ける。Monge の不等式を $\mathrm{dp}[i]+w(i,j')\ge\dots$ の形に並べ替えると出てくる。だから $x[j]$(最良の区切り、同点なら大きい方でも小さい方でもよい)は $j$ について単調。
rec(l, r) を呼ぶ時点で
- $\mathrm{dp}[l]$ は確定している
- $x[r]$ は区切り $0..l$ の中での最良(
check済み)
rec(0, N) の前に check(0, N) するのは、この条件を最初に満たすため。
$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]$ が確定している。
rec(m, r) の条件は「$x[r]$ は $0..m$ の中での最良」。今は $0..l$ までしか見ていないので、$l+1..m$ を check(i, r) で足す。これで右半分に進める。
各段で、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、例 218、例 3280 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$、原典の確認