問題
$N$ 個の町が $N-1$ 本の道で木になっている。町 1 は首都で、道 $j$ は町 $u_j,v_j$ を長さ $w_j$ で結ぶ。$d_i$ を町 1 から町 $i$ までの距離とする。
町 $i\ (2\le i\le N)$ には伝令が 1 人いて、出発の準備に $S_i$ 分、距離 1 進むのに $V_i$ 分かかる。町 $i$ で事件が起きると、町 $i$ の伝令が首都へ向かって走り出す。途中の町(首都への道の上の町)に着いた伝令は、走り続けるか、その町の伝令に知らせを渡すかを選べる。渡された伝令は準備時間 $S$ のあとで同じように走る。首都の伝令はいない(首都に着いたら終わり)。
各町 $i=2,\dots,N$ について、知らせが首都に届くまでの最短時間 $T_i$ を出力せよ。すなわち
$$T_1=0,\qquad T_i=\min_{j\text{ は }i\text{ の真の祖先}}\bigl(T_j+S_i+V_i\,(d_i-d_j)\bigr)$$
入力形式
N
u_1 v_1 w_1
...
u_{N-1} v_{N-1} w_{N-1}
S_2 V_2
...
S_N V_N
出力形式
$T_2,\dots,T_N$ を空白区切りで 1 行に
制約
| # | 制約 |
|---|---|
| 1 | $2\le N\le2\times10^5$ |
| 2 | $1\le w_j\le10^4$ |
| 3 | $0\le S_i\le10^{12}$、$0\le V_i\le10^6$ |
| 4 | 入力は木 |
入出力例
入力例1
5
1 2 5
2 3 3
2 4 2
4 5 4
10 2
1 5
4 1
3 3出力例1
20 36 11 26(町 4 は $S=4,V=1$ と速いので首都まで自分で走って $4+7=11$。町 5 は町 4 に渡して $3+3\cdot4+11=26$)
入力例2
4
1 2 10
2 3 10
3 4 10
0 1
1000 1
0 100出力例2
10 1020 2010(町 4 の最善は親の町 3 でも首都でもなく、町 2 に渡す $0+100\cdot20+10=2010$。「親か首都か」の 2 択にすると 2020 で誤る。2 例とも祖先を全部試す素朴な DP と一致)
背景と出典
- 同じ漸化式は CEOI 2009 の “Harbingers” と記憶している(原典の問題文は本セッションで未確認。本問の問題文・制約・入力例は自作)
- 列の上の凸包トリック(CHT)は Day018 以降何度か扱った。Day149-Q3 のロールバック付き Union-Find と同じく、「戻せる」ためには 1 回の操作が変える場所を少なくするのが鍵になる
- 「pop 版は木で $O(N^2)$」は本セッションで最悪ケースを作って測った(下の検証ログ)
概念図
ヒント(クリックで段階的に開示)
ヒント1(方向性)
$T_i=S_i+V_id_i+\min_j\bigl(T_j-d_j\cdot V_i\bigr)$。祖先 $j$ ごとに直線 $y=T_j-d_j\,x$ を考え、$x=V_i$ での最小値を取ればよい。DFS で根から下りていくとき、今いる頂点の祖先の直線だけが集合にある状態を保ちたい。
ヒント2(アプローチ)
根からの経路で $d_j$ は狭義に増えるので、直線は傾き $-d_j$ の降順に追加される。最小値の下側包絡線は配列で持てて、
- 問い合わせ: $x$ での値は添字について下に凸 → 二分探索
- 追加: 末尾から「新しい直線のせいで不要になる直線」を消す。不要になるかどうかは添字について単調
ヒント3(誘導)
末尾から 1 本ずつ pop すると、帰りに pop した直線を全部戻す必要がある。代わりに「最初に不要になる位置 pos」を二分探索で求め、(pos, 元の直線, 元の top) を保存して hull[pos] = 新しい直線; top = pos+1 とする。帰りは保存した 3 つを書き戻すだけ。pos より後ろの直線は配列に残っているので消えていない。
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
def main():
data = sys.stdin.buffer.read().split()
n = int(data[0])
adj = [[] for _ in range(n + 1)]
p = 1
for _ in range(n - 1):
u, v, w = int(data[p]), int(data[p + 1]), int(data[p + 2])
p += 3
adj[u].append((v, w))
adj[v].append((u, w))
S = [0, 0]
V = [0, 0]
for _ in range(n - 1):
S.append(int(data[p]))
V.append(int(data[p + 1]))
p += 2
dist = [0] * (n + 1)
dp = [0] * (n + 1)
# 凸包は「祖先の直線 y = dp[j] - dist[j]*x」を傾きの降順に並べた配列。長さ top
HD = [0] * (n + 1) # 傾きの符号を反転した値 dist[j]
HB = [0] * (n + 1) # 切片 dp[j]
HD[0], HB[0] = 0, 0 # 首都の直線
top = 1
# 反復 DFS。戻るときに (上書きした位置, 元の直線, 元の top) を書き戻す
stack = [(1, 0, 0)] # (頂点, 親, 状態 0=入る/1=出る)
saved = {}
while stack:
v, par, state = stack.pop()
if state == 1:
pos, od, ob, otop = saved.pop(v)
HD[pos], HB[pos] = od, ob
top = otop
continue
if v != 1:
d = dist[v]
x = V[v]
# 最小値の直線を二分探索: 値は添字について下に凸
lo, hi = 0, top - 1
while lo < hi:
m = (lo + hi) >> 1
if HB[m] - HD[m] * x >= HB[m + 1] - HD[m + 1] * x:
lo = m + 1
else:
hi = m
val = S[v] + x * d + HB[lo] - HD[lo] * x
dp[v] = val
# 挿入位置を二分探索: 最初に「新しい直線に負ける」位置 k (1<=k<top)
lo, hi = 1, top
while lo < hi:
m = (lo + hi) >> 1
d1, b1 = HD[m - 1], HB[m - 1]
# m 番目が不要 ⇔ (b_new-b1)(d_m-d1) <= (b_m-b1)(d_new-d1)
if (val - b1) * (HD[m] - d1) <= (HB[m] - b1) * (d - d1):
hi = m
else:
lo = m + 1
pos = lo
saved[v] = (pos, HD[pos], HB[pos], top)
HD[pos], HB[pos] = d, val
top = pos + 1
stack.append((v, par, 1))
for u, w in adj[v]:
if u != par:
dist[u] = dist[v] + w
stack.append((u, v, 0))
print(" ".join(map(str, dp[2:])))
main()
計算量: 各頂点で二分探索 2 回、$O(N\log N)$。メモリ $O(N)$。$N=2\times10^5$ で乱数の木 0.37 s、道(深さ $2\times10^5$)0.36 s、pop 版の最悪ケース(ほうき型)でも 0.54 s(Python 3.14.8、Apple M5 Pro、負荷平均 23〜25 での実測、起動込み)。値は最大で $S+V\cdot d\approx10^{12}+10^6\cdot2\times10^9$ 程度、交差判定の積は $10^{30}$ 級になるので Python の整数のまま比べる。
Step-by-Step 解説
1直線に直す
$T_j+S_i+V_i(d_i-d_j)=\underbrace{S_i+V_id_i}_{i\text{ だけ}}+\underbrace{T_j-d_j V_i}_{\text{直線 }j\text{ の }x=V_i\text{ での値}}$。祖先の直線の集合に対する最小値の問い合わせになる。
2列なら普通の CHT
根から葉への 1 本の道なら、$d_j$ が増える順に直線を足し、末尾から不要な直線を pop する。各直線は 1 回しか pop されないので償却 $O(1)$。
3木では償却が壊れる
DFS で子へ下りて pop した直線は、兄弟の子へ行く前に戻さなければならない。長さ $L$ の道の先に $L$ 本の枝があり、各枝の頂点が道の直線をすべて pop するような木(ほうき型)では、同じ $L$ 本が $L$ 回ずつ消えて戻る。$N=3\times10^4$ で pop 回数 $1.0\times10^8$・10.6 s、$N=6\times10^4$ で $4.0\times10^8$・39.8 s と、$N$ が 2 倍で 4 倍になった(実測)。
41 か所だけ上書きする
不要になる直線は末尾側の連続した区間なので、その先頭 pos を二分探索で求める。配列の pos に新しい直線を書き、top を pos+1 にする。pos+1 以降の直線は配列に残ったまま「見えなくなる」だけ。帰りには pos の元の直線と元の top を戻せば、配列は完全に元どおり。1 頂点の変更が 1 か所なので、戻す手間も 1 か所。
5不要判定の式
直線 $k$(傾き $-d_k$、切片 $b_k$)について、$k-1$ と新しい直線 $n$ の交点が、$k-1$ と $k$ の交点より左(以下)なら $k$ は不要。分母を払って
$$(b_n-b_{k-1})(d_k-d_{k-1})\le(b_k-b_{k-1})(d_n-d_{k-1})$$
$d$ は経路上で狭義に増える($w\ge1$)ので分母は正。浮動小数を使わないので誤差が無い。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| 祖先を全部試す | $O(N\cdot\text{深さ})$ | 検算用のみ | 道 $N=2\times10^4$ で 15.08 s(実測)。$2\times10^5$ では 100 倍 |
| 列と同じ pop 版 CHT+pop した直線を戻す | 最悪 $O(N^2)$ | 不採用 | 乱数の木では 0.39 s で速いが、ほうき型 $N=6\times10^4$ で 39.8 s(実測)。$2\times10^5$ では pop が約 $4.4\times10^9$ 回(導出)で届かない |
| Li Chao 木をロールバック付きで | $O(N\log C)$ | 不採用(未実装) | 1 回の追加が変える節点は $O(\log C)$ 個で戻せる。$x=V_i$ の範囲 $10^6$ で 20 段。二分探索版より定数が大きいと見込んで採らなかった(測っていない) |
| 重心分解・HLD で列の CHT に帰着 | $O(N\log^2N)$ | 不採用 | 実装が重く、計算量も悪い |
| 二分探索で挿入+1 か所の上書き(本解) | $O(N\log N)$ | 採用 | 最悪ケースでも 0.54 s |
前提が変わったら
- 辺の長さに 0 があると $d$ が等しい直線が並び、不要判定の分母が 0 になる。同じ $d$ なら切片の小さい方だけ残す処理が要る
- 問い合わせの $x$ が単調(例: 深いほど $V$ が大きい)でも、木では兄弟で $x$ が戻るのでポインタを進める方法は使えない。二分探索のまま
- 祖先でなく任意の頂点から受け取れるなら木の構造が要らず、全頂点の直線集合に対する問題になる(別問題)
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 戻すとき top だけ戻し、上書きした pos を戻さない | 「top を縮めただけ」と思う | pos の元の直線も書き戻す。500 入力中 111 で誤り(実測) |
| 帰りに何も戻さない | 列の CHT をそのまま使う | 兄弟の枝の直線が混ざる。500 入力中 169 で誤り(実測) |
| 親から受け取るだけ | 「渡すなら直前の町」と思う | 祖先すべてが候補。500 入力中 450 で誤り(実測) |
| 親か首都の 2 択 | 中間の祖先を見落とす | 入力例 2。500 入力中 164 で誤り(実測) |
| 交点を浮動小数で比べる | 積が $10^{30}$ 級になり 64 ビット浮動小数では等号が狂う | 分母を払って整数で比べる |
| 再帰 DFS | 深さ $2\times10^5$ で再帰の上限 | 反復 DFS(入る/出るの 2 状態) |
次のステップ
- 発展1: Li Chao 木のロールバック版を実装し、本解と同じ入力で速さを比べる
- 発展2: $T_i$ に「町 $i$ を通る伝令の数」の制限が付いたら(容量付き)、DP が貪欲で崩れるかを全探索で調べる
- 発展3: 道を下る向き(首都から葉へ)の DP に変え、部分木ごとの直線集合をマージする(Li Chao 木のマージ)方法と比べる
検証ログ(本セッションで実行した数値)
- 正しさ(実測): 頂点数 2〜40 の乱数の木と道 500 個(辺長・$S$・$V$ の上限を 0〜$10^{12}$ で振った)で、祖先を全部試す DP・本解・pop 版の 3 つが全件一致
- 誤った方法(実測、同じ 500 個): 上書き位置を戻さない 111、ロールバックなし 169、親だけ 450、親か首都の 2 択 164 個で出力が違う
- 時間(実測、起動込み、負荷平均 23〜25): 本解 $N=2\times10^5$ で乱数 0.37 s(202 MB)、道 0.36 s(240 MB)、ほうき型 0.54 s(171 MB)。ほうき型 $N=3\times10^4$ / $6\times10^4$ で本解 0.08 / 0.15 s、pop 版 10.6 s(pop $1.0\times10^8$ 回)/ 39.8 s($4.0\times10^8$ 回)。pop 版は乱数 $N=2\times10^5$ では 0.39 s(pop 195483 回)。祖先を全部試す DP は道 $N=2\times10^4$ で 15.08 s
- ほうき型の作り方: 深さ $L=N/3$ の道(町 $j$ は $S=j^2,V=1$ で $T_j=j^2+j$、点 $(d,T)$ が下に凸に並ぶ)の先に、子を 1 つずつ持つ枝を約 $N/3$ 本。枝の頂点は $S=0,V=1$ で $T=d$ となり、首都との線分の下にあるので道の直線を全部 pop する
- 扱っていない範囲: 原典の確認、Li Chao 木版の測定、辺長 0