Day 157-Q2 — 木上のIsotonic Regression(Slope Trick + ヒープmerge)

2026-09-18 赤色 Master / Phase 8+ ★★★★★★★★★ 子のL-heapのみ小さい方から大きい方へmerge、R-heapは破棄

問題

根付き木があり、頂点 $0,\dots,N-1$ 、頂点 $v$ の親を $\mathrm{par}(v)$(根は親なし)とする。各頂点に目標値 $t_v$ が与えられる。整数値 $a_v$ を各頂点に割り当て、以下を満たすようにしたい。

  • 制約: 根以外の全頂点 $v$ について $a_v \le a_{\mathrm{par}(v)}$(親は子以上)
  • 目的: $\displaystyle\sum_v |a_v - t_v|$ を最小化

この最小値を求めよ(直線上の「非減少列への最小コスト変換」の木への一般化)。

入力形式

N
par_1 ... par_{N-1}
t_0 ... t_{N-1}

制約

$1 \le N \le 2\times10^4$
$0 \le \mathrm{par}(v) < v$
$-10^9 \le t_v \le 10^9$

入出力例

入力例1(鎖)

5
0 1 2 3
5 1 9 2 8

出力例1

14

入力例2(分岐木)

6
0 0 0 1 1
4 9 1 7 2 10

出力例2

6

概念図:子のL-heapのみをmergeし、R-heapは捨てる

頂点 v = 0(子 1,2,3)での処理 child 1 child 2 child 3 L_1 (keep) R_1 (discard) L_2 (keep) R_2 (discard) L_3 (keep) R_3 (discard) v = 0 t_v 挿入 L_v = union(L_1,L_2,L_3) ∪ {t_v} R_v = {t_v}(自分の分だけ新規) while L_v.top() > R_v.top(): pop両方 → cost += 差 入れ替えてpush small-to-large mergeで 全体 O(N log N)

ヒント(段階的開示)

ヒント1(方向性)

$a_v \le a_{\mathrm{par}(v)}$ という制約は、直線上の「単調非減少列」問題の親子版だと考える。Slope Trick(凸区分線形関数を2つのヒープで表す技法)を使う。

ヒント2(アプローチ)

頂点 $v$ を根とする部分木で「$a_v=x$ に固定したときの最小コスト」を凸関数 $F_v(x)$ とする。子 $c$ が親に渡せる情報は $G_c(x)=\min_{y\le x}F_c(y)$。$F_c$ が凸だから、$G_c$ は「左側はそのまま、右側は平ら」になる——これをヒープ表現で表すとどうなるか考える。

ヒント3(誘導)
# 子ごとに Lc を(大きい方へ)マージして Lv に統合、Rc は捨てる
# Lv, Rv に t_v を1個ずつ push
# while Lv.top() > Rv.top():
#     lp = Lv から pop, rp = Rv から pop
#     cost += lp - rp
#     lp を Rv に、rp を Lv に push  (入れ替え)

模範解答 (Python)

import sys, heapq

def solve():
    data = sys.stdin.read().split()
    idx = 0
    n = int(data[idx]); idx += 1
    parent = [-1] * n
    for v in range(1, n):
        parent[v] = int(data[idx]); idx += 1
    t = [int(data[idx + i]) for i in range(n)]; idx += n

    children = [[] for _ in range(n)]
    root = 0
    for v in range(n):
        if parent[v] == -1:
            root = v
        else:
            children[parent[v]].append(v)

    order, stack = [], [root]
    while stack:
        v = stack.pop()
        order.append(v)
        stack.extend(children[v])
    post = order[::-1]

    L = [None] * n
    R = [None] * n
    cost = [0] * n

    for v in post:
        Lv, Rv = [], []
        total = 0
        for c in children[v]:
            lc = L[c]
            total += cost[c]
            if len(lc) > len(Lv):
                Lv, lc = lc, Lv
            for val in lc:
                heapq.heappush(Lv, val)

        heapq.heappush(Lv, -t[v])
        heapq.heappush(Rv, t[v])

        while Lv and Rv and -Lv[0] > Rv[0]:
            lp = -heapq.heappop(Lv)
            rp = heapq.heappop(Rv)
            total += lp - rp
            heapq.heappush(Rv, lp)
            heapq.heappush(Lv, -rp)

        L[v], R[v], cost[v] = Lv, Rv, total

    print(cost[root])

solve()

Step-by-Step 解説

1部分木固定コスト関数 $F_v(x)$ の凸性
$F_v(x)=|x-t_v|+\sum_c G_c(x)$ は凸の和で凸。左ヒープ(最大値以下側の折れ点)・右ヒープ(最小値以上側の折れ点)の2本で完全に復元できる。
2$G_c(x)=\min_{y\le x}F_c(y)$ の導出
$F_c$ は $y<L_{c,\mathrm{top}}$ で単調減少、$[L_{c,\mathrm{top}},R_{c,\mathrm{top}}]$ で最小、$y>R_{c,\mathrm{top}}$ で単調増加。よって $G_c$ は「$L_c$ はそのまま、$R_c$ は丸ごと消えて平ら」になる。
3複数の子のマージと入れ替え補正
複数の子の $L_c$ を合わせ、自分の $t_v$ を両ヒープに push。$L_v$ の最大値が $R_v$ の最小値を超えていれば凸関数として不整合なので、pop→差をコストに加算→入れ替えてpushで回復する(直線版Day103と同じ発想)。
4実測(本ページ作成時にPythonで実行)
テスト内容結果
入力例1(鎖)模範解答 14 / 厳密DP 14 一致
入力例2(分岐木)模範解答 6 / 厳密DP 6 一致
ランダム400件(N≤9)候補値×木DP $O(N^3)$ との不一致 0件
N=20000のランダム木0.020秒

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

案採否理由
leftist heap / pairing heap を自作しO(log N)一発mergeを実現不採用正しさの検証コストが高く、バグ混入リスクの割に本問の規模(N≤2×10^4)では恩恵が薄い
Pythonのheapqによるsmall-to-large union(本ページ採用)採用DSU small-to-largeと同じ償却解析でO(N log N)が保証され、実装・検証が容易
子の $R_c$ も何らかの形で持ち越す不採用$G_c(x)=\min_{y\le x}F_c(y)$ の導出上、$R_c$ は数学的に完全に消える。持ち越すと誤り

よくあるミス

ミス原因正しい書き方
子の $R_c$ もマージしてしまう直線DPの感覚のまま木に持ち込む子の $R_c$ は必ず捨てる
大きい方を小さい方へマージsmall-to-largeの向きを逆に要素数の少ない方を多い方へpush
交換ループでcostに符号を逆にして加算$L$最大ヒープと$R$最小ヒープの向きの取り違え常に lp - rp($lp>rp$)を加える
制約の向き(親≥子/親≤子)を取り違える直線版との類推で符号を逆にしやすい逆向きにしたいなら $t_v\to-t_v$ とする

次のステップ

  • 発展: 最終的な割り当て $a_v$ を復元する(ヒープの最終状態から逆算)
  • 発展2: 制約を「$a_v\le a_{\mathrm{par}(v)}+k$」のように許容誤差付きに一般化
  • 扱わなかった範囲: leftist heapによる真の$O(\log N)$mergeの自前実装
  • 次回予告: mergeable heapの速度比較

自己評価

自分の回答

気づき・メモ