問題
頂点 $1..N$ の根付き木(根は 1)があり、辺 $(u,v)$ の長さは $w$ である。根からの距離を $D_v$ とする。各頂点 $u$ には集配所があり、$u$ の子孫 $v$ から $u$ まで荷物を運ぶ料金は $C_u\times(D_v-D_u)$ である。さらに頂点 $v$($v\ne1$)から荷物を出すときは、最初に手数料 $S_v$ がかかる。
荷物は $v$ から祖先 $u$ の集配所へ運ばれ、そこから $u$ の荷物として(手数料 $S_u$ は払わずに $u$ からの最安料金で)根まで運ばれる。$\mathrm{dp}_1=0$ として
$$\mathrm{dp}_v=S_v+\min_{u:\ v\text{ の真の祖先}}\Bigl(\mathrm{dp}_u+C_u\,(D_v-D_u)\Bigr)$$
をすべての $v=2..N$ について求めよ。
入力形式
N
S_1 C_1
...
S_N C_N
u_1 v_1 w_1
...
u_{N-1} v_{N-1} w_{N-1}
制約
| # | 制約 |
|---|---|
| 1 | $2\le N\le2\times10^5$ |
| 2 | $1\le w_i\le10^4$ |
| 3 | $0\le S_v,C_v\le10^9$($S_1$ は使わない) |
| 4 | 出力 $\mathrm{dp}_2,\dots,\mathrm{dp}_N$ を 1 行ずつ |
入出力例
入力例1
6
0 5
3 1
2 9
4 2
1 7
5 3
1 2 4
2 3 2
2 4 3
4 5 5
1 6 6出力例1
23
27
30
32
35(頂点 5 は $\min(0+5\cdot12,\ 23+1\cdot8,\ 30+2\cdot5)+1=32$。頂点 2 の安い料金 $C_2=1$ を使うのが得。帰りに巻き戻さない版は、兄弟の部分木の直線が残って頂点 3 を $-8$ と誤る)
入力例2
4
0 10
1 1
1 8
1 1
1 2 3
2 3 3
3 4 3出力例2
31
35
38(一本道で傾きが $10,1,8$ と単調でない。頂点 4 は $\min(90,\ 31+6,\ 35+24)+1=38$。傾きが単調減少と仮定する二分探索 CHT は、傾き 8 の直線を「凸包の端」に置いて傾き 1 の直線を消してしまい 60 と誤る)
背景と出典
- Day178-Q3 は CEOI 2009 Harbingers と同じ形と記憶している(問題文は未確認)。そこでは直線の傾き $-D_u$ が道に沿って単調だった
- Li Chao 木は Li Chao による 2012 年頃の中国語の解説が起源と記憶している(原典は未確認)。「巻き戻し」は書き換えを記録して逆順に戻す一般的な技法で、オフラインの動的連結性(Union-Find の巻き戻し)と同じ発想
- 本問の問題文・制約・入力例は自作
概念図
ヒント(クリックで段階的に開示)
ヒント1(方向性)
$\mathrm{dp}_u+C_u(D_v-D_u)=C_u\cdot D_v+(\mathrm{dp}_u-C_uD_u)$ は、祖先 $u$ ごとの直線 $y=C_ux+(\mathrm{dp}_u-C_uD_u)$ を $x=D_v$ で評価したもの。必要なのは「根から $v$ までの道にある直線の、$x=D_v$ での最小値」。
ヒント2(アプローチ)
DFS で頂点に入るとき直線を足し、出るとき消せばよい。消すのは「最後に足したもの」なので、足したときに変えた場所を記録しておき逆順に戻す。Li Chao 木の挿入は根から葉への 1 本の道しか触らない。
ヒント3(誘導)
- $x$ は $D_v$ の値だけなので、座標圧縮した $D$ の上に Li Chao 木を作る
- 挿入で節点の直線を入れ替えるたびに
(節点, 元の直線)をログに積む。頂点を出るときに、入ったときのログの長さまで pop して戻す - 再帰は深さ $2\times10^5$ になるので、スタックに「入る」「出る(ログの長さ)」の 2 種類を積む反復 DFS にする
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
def main():
data = sys.stdin.buffer.read().split()
n = int(data[0])
S = [0] * (n + 1)
C = [0] * (n + 1)
adj = [[] for _ in range(n + 1)]
pos = 1
for v in range(1, n + 1):
S[v] = int(data[pos]); C[v] = int(data[pos + 1]); pos += 2
for _ in range(n - 1):
u = int(data[pos]); v = int(data[pos + 1]); w = int(data[pos + 2]); pos += 3
adj[u].append((v, w)); adj[v].append((u, w))
# 根からの距離と DFS 順
d = [0] * (n + 1)
par = [0] * (n + 1)
order = []
st = [1]
par[1] = -1
while st:
u = st.pop()
order.append(u)
for v, w in adj[u]:
if v != par[u]:
par[v] = u
d[v] = d[u] + w
st.append(v)
xs = sorted(set(d[1:]))
m = len(xs)
idx = {x: i for i, x in enumerate(xs)}
INF = 1 << 63
size = 1
while size < m:
size <<= 1
LA = [0] * (2 * size) # 傾き
LB = [INF] * (2 * size) # 切片(INF = 直線なし)
log = [] # (節点, 元の傾き, 元の切片)
def insert(a, b):
node, l, r = 1, 0, size - 1
while True:
mid = (l + r) >> 1
ca, cb = LA[node], LB[node]
xm = xs[mid] if mid < m else xs[-1]
if a * xm + b < ca * xm + cb:
log.append((node, ca, cb))
LA[node], LB[node] = a, b
a, b, ca, cb = ca, cb, a, b
if l == r or b >= INF:
return
xl = xs[l] if l < m else xs[-1]
if a * xl + b < ca * xl + cb:
node, r = 2 * node, mid
else:
xr = xs[r] if r < m else xs[-1]
if a * xr + b < ca * xr + cb:
node, l = 2 * node + 1, mid + 1
else:
return
def query(i):
x = xs[i]
node, l, r = 1, 0, size - 1
best = INF
while True:
val = LA[node] * x + LB[node]
if val < best:
best = val
if l == r:
return best
mid = (l + r) >> 1
if i <= mid:
node, r = 2 * node, mid
else:
node, l = 2 * node + 1, mid + 1
dp = [0] * (n + 1)
# 反復 DFS:入るときに問い合わせと挿入、出るときに巻き戻し
stack = [(1, 0)]
while stack:
u, mark = stack.pop()
if mark < 0: # 出る:log を長さ ~mark まで戻す
keep = ~mark
while len(log) > keep:
node, ca, cb = log.pop()
LA[node] = ca; LB[node] = cb
continue
if u != 1:
dp[u] = query(idx[d[u]]) + S[u]
keep = len(log)
insert(C[u], dp[u] - C[u] * d[u])
stack.append((u, ~keep))
for v, w in adj[u]:
if v != par[u]:
stack.append((v, 0))
sys.stdout.write("\n".join(map(str, dp[2:])) + "\n")
main()
計算量: 挿入・問い合わせ・巻き戻しとも 1 回 $O(\log N)$、全体 $O(N\log N)$。$N=2\times10^5$、$C$ が乱数(単調でない)で、乱択木 0.84 s・216 MB、一本道 1.14 s・296 MB、親を直前 3 頂点から選ぶ細長い木 1.08 s・264 MB(Python 3.14.8、Apple M5 Pro、起動込み)。1 回の挿入で積むログは平均 1.96 個(乱択木で 392642 個/$2\times10^5$ 回)。
Step-by-Step 解説
1直線の最小値に直す
祖先 $u$ を直線 $\ell_u(x)=C_ux+(\mathrm{dp}_u-C_uD_u)$ にすると、$\mathrm{dp}_v=S_v+\min_u\ell_u(D_v)$。直線の集合は「根から $v$ の親までの道」で、DFS で入るとき足し出るとき消すスタックになる。
2なぜ二分探索 CHT では駄目か
Day178-Q3 の方法は「傾きが単調な順に足される」ことを使い、新しい直線を凸包の端に置いて、端から見て不要になった直線を二分探索で一気に切り落とす(配列の 1 か所を上書きして top を縮めるだけなので戻すのも $O(1)$)。傾きが単調でないと、新しい直線は凸包の途中に入ることがあり、端に置く前提が崩れる。入力例 2 では傾き $10,1,8$ の順に来て、8 を端に置いた結果 1 の直線が消え、頂点 4 を 60 と誤る。
3Li Chao 木の挿入は 1 本の道
Li Chao 木の各節点は区間 $[l,r]$ を受け持ち、「区間の中点で小さい方の直線」を 1 本持つ。新しい直線は、中点で勝てば入れ替え、負けた方の直線を左右どちらか 1 つの子にだけ送る(2 直線は高々 1 回しか交わらないので、負けた方が勝ちうるのは片側だけ)。だから 1 回の挿入で書き換える節点は根から葉への道の上だけ。
4巻き戻し
書き換えるたびに (節点, 元の傾き, 元の切片) をログに積む。頂点 $u$ に入るときのログの長さ keep を、出るときの印としてスタックに置き、出るときに keep まで pop して書き戻す。子孫の挿入はすべて $u$ の挿入より後にログへ積まれているので、逆順に戻せば $u$ に入る前の状態に正確に戻る。
5大きな数と番兵
$C_uD_v\le10^9\times2\times10^9=2\times10^{18}$、$\mathrm{dp}$ はそれに $S$ の和($2\times10^{14}$)を足した程度。「直線なし」の番兵は切片 $2^{63}$ にした。本セッションで最初 $2^{62}$(約 $4.6\times10^{18}$)にしていたとき、制約外の大きい傾き($10^{12}$)で番兵より大きい値が出て、二分探索 CHT と答えが食い違った。番兵は「あり得る値の最大」より確実に大きくする。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| 祖先を全部見る | $O(N\cdot\text{深さ})$ | 検算用のみ | 一本道で $O(N^2)$ |
| 二分探索 CHT(Day178-Q3) | $O(N\log N)$ | 不採用(誤り) | 傾きが単調でない 400 入力のうち 281 入力で誤る(実測)。傾きが単調な入力では正しく、速い(下) |
| Li Chao 木を巻き戻さない | $O(N\log N)$ | 不採用(誤り) | 400 入力中 156 で誤る(実測)。兄弟の部分木の直線が残る |
| 永続 Li Chao 木(親の版から子の版を作る) | $O(N\log N)$ 時間・空間 | 不採用 | 正しいが、節点の複製で記憶量が $O(N\log N)$ の節点オブジェクトになる。巻き戻しなら追加はログだけ |
| 重心分解・HLD で道を区間に分ける | $O(N\log^2N)$ | 不採用 | 本問は「根からの道」だけなので DFS の巻き戻しで足りる |
| 巻き戻し付き Li Chao 木(本解) | $O(N\log N)$ | 採用 | 0.84〜1.14 s |
傾きが単調な入力での速さ比較($C_u=10^9-D_u/2$、実測):
| 入力 | 本解(Li Chao) | Day178-Q3(二分探索 CHT) |
|---|---|---|
| 乱択木 $N=2\times10^5$ | 0.74 s・211 MB | 0.36 s・207 MB |
| 一本道 $N=2\times10^5$ | 1.18 s・398 MB | 0.77 s・247 MB |
傾きが単調だと分かっているなら二分探索 CHT の方が 1.5〜2 倍速い。一本道で Li Chao が 398 MB になるのは、ログが最大 370588 件たまるため。
前提が変わったら
- 問い合わせが祖先の一部(深さ $D_v-D_u\le R$ の範囲だけ)なら、道の「区間」への問い合わせになり、巻き戻しだけでは足りない。道をセグメント木で区間に分けて各節点に Li Chao 木を持つ($O(N\log^2N)$)
- 直線でなく線分(祖先 $u$ の料金表が $x$ の一部でだけ有効)なら、Li Chao 木の線分挿入($O(\log^2N)$)にして同じく巻き戻せる
- 最大値を求めるなら直線の符号を反転するだけ
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 出るときに巻き戻さない | 一本の道なら不要なので気付かない | 頂点を出るとき、入ったときのログ長まで戻す(入力例 1) |
| 挿入で入れ替えた節点を記録し忘れる | 「新しい直線を置いた節点」だけ記録する | 入れ替えのたびに元の直線を記録 |
| 再帰 DFS | 深さ $2\times10^5$ | 入る・出るの 2 種類を積む反復 DFS |
| 番兵が小さい | $2^{62}$ でも足りると思う | 値の上限 $\approx2\times10^{18}$ より確実に大きく |
| 座標を $0..\max D$ で取る | 距離は $2\times10^9$ まである | 出てくる $D_v$ だけを座標圧縮 |
次のステップ
- 発展1: 「$D_v-D_u\le R_v$ の祖先だけ」から選ぶ版(道の区間問い合わせ)を、HLD+Li Chao で解く
- 発展2: 線分の Li Chao 木の巻き戻し版で、祖先ごとに料金表の有効区間がある版
- 発展3: 傾きが単調なときだけ CHT に切り替える二刀流にし、入力の性質で速さがどう変わるか測る
検証ログ(本セッションで実行した数値)
- 全探索との一致(実測): $N=2..31$、乱択木・一本道・細長い木、$S$ の範囲 4 通り(すべて 0、0〜3、0〜30、0〜$10^9$)の 400 入力で、祖先を全部見る全探索と本解が全件一致
- 誤った方法(実測、同じ 400 入力): 二分探索 CHT 281 入力、巻き戻しなし 156 入力で誤り。$S$ が大きい入力だけの 300 入力では巻き戻しなしの誤りが 0 だった(深い頂点の $\mathrm{dp}$ が大きく、兄弟の直線が勝たない)ので、$S$ を小さくした入力を足した
- 傾きが単調な入力(実測): 200 入力と $N=3000$ の一本道で、本解・CHT・全探索が全件一致。$N=2\times10^5$ の 2 入力で本解と CHT の出力が一致
- 時間(実測、起動込み、$N=2\times10^5$、$w\le10^4$、$C\le10^9$): 乱択木 0.84 s・216 MB、一本道 1.14 s・296 MB、細長い木 1.08 s・264 MB。ログの最大長は乱択木 53、一本道 370588
- 扱っていない範囲: 原典の確認、永続版・HLD 版の実装と比較