Day 179-Q3 — 2026-10-15 | 傾きが単調でない木の上の DP を、巻き戻し付き Li Chao 木で(挿入で書き換えた節点を記録し、DFS の帰りに戻す。Day178-Q3 の二分探索 CHT との速さと正しさの比較つき)

2026-10-15 赤色 Master (Phase 8+) ★★★★★★★★★ DP 高速化(焦点帯 M1)。Day178-Q3(CEOI 2009 Harbingers と同じ形と記憶)では、直線の傾きが根からの道に沿って単調だったので、凸包を配列で持って二分探索で 1 か所だけ上書きすれば済んだ。傾きが道に沿って単調でないと凸包の端に足せないので、その方法は使えない。Li Chao 木は傾きの順序を問わないうえ、1 回の挿入で書き換える節点が $O(\log N)$ 個なので、書き換えを記録して戻せば木の上でも使える

問題

頂点 $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:DFS が頂点 5 にいるとき、Li Chao 木に入っているのは根からの道 1→2→4 の直線だけ 4 2 3 5 6 1 C=5 D=0 2 C=1 D=4 6 C=3 D=6 3 C=9 D=6 4 C=2 D=7 5 C=7 D=12 0 4 6 7 12 x = D ℓ1: 5x + 0 ℓ2: 1x + 19 ℓ4: 2x + 16 ℓ3: 9x − 27(道の外。入っていてはいけない) x = D_5 = 12 での最小は ℓ2:19 + 12 = 31 dp_5 = 31 + S_5 = 32 巻き戻さないと頂点 5 の ℓ5 = 7x − 52 が残り、 後で訪れる頂点 3(x = 6)を −10 + 2 = −8 と誤る 傾き 5, 1, 2 は道に沿って単調でない → 凸包の端に足す CHT は使えない

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

ヒント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 MB0.36 s・207 MB
一本道 $N=2\times10^5$1.18 s・398 MB0.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 版の実装と比較

自己評価

自分の回答

気づき・メモ