Day 090-Q1 — Segment Tree Merging(小到大マージ・部分木の最頻色集計)

2026-07-13 赤色 Master / Phase 8+ ★★★★★★★★★ 動的セグ木・マージ・DSU on Tree系

問題

$N$ 頂点の根付き木(根は頂点1)が与えられる。頂点 $i$ には色 $c_i$($1 \le c_i \le N$)が塗られている。各頂点 $v$ について、$v$ を根とする部分木に含まれる頂点の色の中で最も出現回数が多い色をすべて合計した値を求め、頂点 $1$ から $N$ まで順に出力せよ(最多出現回数の色が複数あればそれらの色番号をすべて足す)。

制約

パラメータ範囲備考
$N$$1 \le N \le 2\times10^5$頂点数
$c_i$$1 \le c_i \le N$頂点の色
入力の辺木を成すことが保証される

入出力例

入力例1

4
1 2 1 3
1 2
1 3
1 4

出力例1

1 2 1 3

入力例2

5
1 1 2 2 3
1 2
2 3
2 4
1 5

出力例2

3 2 2 2 3

例2: 頂点1の部分木の色は $[1,1,2,2,3]$。色1・色2がともに2回でタイのため答えは $1+2=3$。

概念図: 子の値域セグ木を親へマージ

merge(root[v], root[child]) を子から親へ繰り返す v=1 c=1 c=2 c=3 値域セグ木(座標=色, 値=出現回数) leaf(1): cnt=1 leaf(2): cnt=1 leaf(3): cnt=1 merge後の v=1: cnt(1)=2, cnt(2)=1, cnt(3)=1 → max_cnt=2, color_sum=1(答え)

ヒント

ヒント1(方向性)

各頂点で愚直に部分木を再集計すると最悪 $O(N^2)$。小さい部分木の情報を大きい方へ「マージ」しながら答えを確定させる手法(DSU on Tree/セグメント木マージ)が必要になる。

ヒント2(アプローチ)

色ごとの出現回数を持つ値域セグメント木(座標=色番号)を頂点ごとに動的に確保する。子のセグ木を親へ再帰的にマージすれば、各頂点の部分木の色集計がそのまま手に入る。各ノードに「区間内の最大出現回数」と「その回数を達成する色の合計」を持たせて結合する。

ヒント3(ほぼ答え)
def merge(a, b, lo, hi):
    if a == 0: return b
    if b == 0: return a
    if lo == hi:
        cnt[a] += cnt[b]
        max_cnt[a] = cnt[a]; color_sum[a] = lo
        return a
    mid = (lo + hi) // 2
    left[a] = merge(left[a], left[b], lo, mid)
    right[a] = merge(right[a], right[b], mid+1, hi)
    # a の集約値を左右の子から pull() で再計算
    return a

模範解答

import sys

def main():
    input_data = sys.stdin.buffer.read().split()
    idx = 0
    n = int(input_data[idx]); idx += 1
    c = [int(input_data[idx + i]) for i in range(n)]; idx += n
    graph = [[] for _ in range(n + 1)]
    for _ in range(n - 1):
        u = int(input_data[idx]); idx += 1
        v = int(input_data[idx]); idx += 1
        graph[u].append(v)
        graph[v].append(u)

    limit = n * 40 + 10
    left = [0] * limit
    right = [0] * limit
    cnt = [0] * limit
    max_cnt = [0] * limit
    color_sum = [0] * limit
    node_count = 0

    def new_node():
        nonlocal node_count
        node_count += 1
        return node_count

    def pull(node):
        l, r = left[node], right[node]
        lc, rc = max_cnt[l], max_cnt[r]
        if lc > rc:
            max_cnt[node] = lc; color_sum[node] = color_sum[l]
        elif rc > lc:
            max_cnt[node] = rc; color_sum[node] = color_sum[r]
        else:
            max_cnt[node] = lc; color_sum[node] = color_sum[l] + color_sum[r]
        cnt[node] = cnt[l] + cnt[r]

    def update(node, lo, hi, pos):
        if node == 0:
            node = new_node()
        if lo == hi:
            cnt[node] += 1
            max_cnt[node] = cnt[node]
            color_sum[node] = lo
            return node
        mid = (lo + hi) // 2
        if pos <= mid:
            left[node] = update(left[node], lo, mid, pos)
        else:
            right[node] = update(right[node], mid + 1, hi, pos)
        pull(node)
        return node

    def merge(a, b, lo, hi):
        if a == 0: return b
        if b == 0: return a
        if lo == hi:
            cnt[a] += cnt[b]
            max_cnt[a] = cnt[a]; color_sum[a] = lo
            return a
        mid = (lo + hi) // 2
        left[a] = merge(left[a], left[b], lo, mid)
        right[a] = merge(right[a], right[b], mid + 1, hi)
        pull(a)
        return a

    order = []
    parent = [0] * (n + 1)
    visited = [False] * (n + 1)
    stack = [1]
    visited[1] = True
    while stack:
        v = stack.pop()
        order.append(v)
        for u in graph[v]:
            if not visited[u]:
                visited[u] = True
                parent[u] = v
                stack.append(u)

    root = [0] * (n + 1)
    ans = [0] * (n + 1)
    for v in reversed(order):
        root[v] = update(root[v], 1, n, c[v - 1])
        for u in graph[v]:
            if u != parent[v]:
                root[v] = merge(root[v], root[u], 1, n)
        ans[v] = color_sum[root[v]]

    sys.stdout.write(' '.join(map(str, ans[1:])) + '\n')

main()

計算量: ならし $O(N \log N)$(各点更新が $O(\log N)$ ノードを生成し、マージは片方が空なら即終了するため総破棄数が生成数を超えない)。

Step-by-Step 解説

Step 1: 値域セグメント木を頂点ごとに動的生成

配列を毎回 $N$ サイズ確保すると $O(N^2)$ 空間になるため、必要なノードだけ動的に確保するポインタベースの実装にする。

Step 2: 反復DFSで処理順序を作る

再帰DFSはパス状の木で最悪 $O(N)$ 深度になりスタックオーバーフローの危険がある。スタックを使った反復DFSなら、頂点 $v$ の部分木全体が `order` 上で $v$ の直後に連続して現れるため、`reversed(order)` が正しい葉→根の処理順序になる。

Step 3: 自分の色を1点更新してから子をマージ

操作意味
update(root[v], 1, n, c[v])自分の色を1回カウント
root[v] = merge(root[v], root[u], 1, n)子 $u$ の情報を統合
color_sum[root[v]]頂点 $v$ の答え

Step 4: マージのならし計算量

`merge` は毎回片方の入力ノードを吸収して破棄する形になる。全体で生成されるノード数は $O(N\log N)$ であり、マージで消費されるノード数もこれを超えない。

よくあるミス

ミス原因正しい書き方
動的セグ木のノード上限を $N$ 個しか確保しない1回の更新で $O(\log N)$ 個生成されることを見落とす上限を $N\log N$ オーダー(余裕を持たせて)確保する
再帰DFSで RecursionErrorパス状の木で深さが $N$ に達する明示的なスタックによる反復DFSを使う
mergeの戻り値を使わないポインタを書き換えず新しい根を返す設計を忘れるroot[v] = merge(...) と必ず代入する

次のステップ

  • 発展問題: 部分木内で $k$ 番目に大きい出現回数の色を求めるクエリに拡張する(セグ木上二分探索)
  • 発展問題: DSU on Tree(軽重分解)で同じ問題を解き実装量を比較する

自己評価

理解度: / /

自分の回答:

気づき・メモ: