Day 050-Q1 — Euler Tour + 遅延SegTree カラーマージ(動的部分木異なる色数クエリ)

2026-06-03 赤色 Master / Phase 8+ ★★★★★★★★★ Euler Tour / Merge Sort Tree / distinct count

問題

$N$ 頂点の根付き木(根は頂点 $1$)が与えられる。各頂点には初期色 $C_i \in [1, K]$ が付いている。次の $Q$ 個のクエリを処理せよ。

  • クエリ1: 1 v c — 頂点 $v$ の色を $c$ に変更する。
  • クエリ2: 2 v — 頂点 $v$ の部分木に含まれる 異なる色の数 を出力する。

制約

$1 \le N, Q \le 2 \times 10^5$
$1 \le K \le 2 \times 10^5$
$1 \le C_i \le K$
時間制限: 3秒

入出力例

入力例 1

7 4 5
1 2 3 1 2 3 4
1 2
1 3
2 4
3 5
3 6
3 7
2 1
1 4 4
2 1
1 2 3
2 3

出力例 1

4
4
3

クエリ 2 3: 頂点3の部分木 = {3,5,6,7}、変更後の色 {3,2,3,4} = {2,3,4} → 3種類

概念図: Euler Tour と distinct count テクニック

根付き木 (N=7) 1 色:1 2 色:2 3 色:3 4 色:1 5 色:2 6 色:3 7 色:4 Euler Tour: [1,2,4,3,5,6,7] → 部分木v3=[3,5,6,7] = 区間[3,6]
distinct count テクニック: prev_pos[i] < L の個数 = 異なる色数 pos=0 色:1 prev=-1 pos=1 色:2 prev=-1 pos=2 色:1 prev=0 pos=3 色:3 prev=-1 pos=4 色:2 prev=1 pos=5 色:3 prev=3 pos=6 色:4 prev=-1 クエリ v3: L=3, R=6 → pos 3,4,5,6 で prev_pos < 3 の個数 = {-1,-1 以外 1→NG, 3→NG} → 3個 (色3,2,4)

ヒント(段階的開示)

ヒント1: 方向性
木のクエリを列クエリに変換できないか考えよ。Euler Tour(DFS 順付け)で部分木が連続区間になる性質を使う。
ヒント2: アプローチ
  • Euler Tour で $[in_v, out_v]$ に部分木をマッピング
  • distinct count テクニック: $prev\_pos[i]$ = 位置 $i$ と同色の直前の出現位置(なければ $-1$)
  • クエリ $[L,R]$ の答え = $\{i \in [L,R] : prev\_pos[i] < L\}$ の個数
  • これは Merge Sort Tree(各SegTreeノードに $prev\_pos$ のソート済みリスト)で $O(\log^2 N)$
  • 更新は色変更で影響する最大3箇所の $prev\_pos$ を修正
ヒント3: SortedList を使う実装骨格
from sortedcontainers import SortedList

# 色ごとの Euler Tour 位置管理
color_pos = defaultdict(SortedList)  # color -> sorted positions

# Merge Sort Tree
seg = [SortedList() for _ in range(4 * N)]

# クエリ: [L,R]内でprev_pos[i] < L の個数
def seg_query(node, l, r, ql, qr, threshold):
    if qr < l or r < ql: return 0
    if ql <= l and r <= qr:
        return seg[node].bisect_left(threshold)
    mid = (l + r) // 2
    return (seg_query(2*node, l, mid, ql, qr, threshold) +
            seg_query(2*node+1, mid+1, r, ql, qr, threshold))

模範解答 (Python)

import sys
from sortedcontainers import SortedList
from collections import defaultdict
input = sys.stdin.readline

def solve():
    N, K, Q = map(int, input().split())
    C_raw = list(map(int, input().split()))
    adj = defaultdict(list)
    for _ in range(N - 1):
        u, v = map(int, input().split())
        adj[u].append(v); adj[v].append(u)

    # Euler Tour (反復DFS)
    tin = [0]*(N+1); tout = [0]*(N+1); order = [0]*N; timer = [0]
    stack = [(1, -1, False)]
    while stack:
        v, par, leaving = stack.pop()
        if leaving:
            tout[v] = timer[0]-1; continue
        tin[v] = timer[0]; order[timer[0]] = v; timer[0] += 1
        stack.append((v, par, True))
        for u in adj[v]:
            if u != par: stack.append((u, v, False))

    color_at = [C_raw[order[i]-1] for i in range(N)]
    color_pos = defaultdict(SortedList)
    for i in range(N): color_pos[color_at[i]].add(i)

    prev_pos = [-1]*N
    for i in range(N):
        sl = color_pos[color_at[i]]; idx = sl.index(i)
        if idx > 0: prev_pos[i] = sl[idx-1]

    # Merge Sort Tree
    seg = [SortedList() for _ in range(4*N)]
    def build(node, l, r):
        for i in range(l, r+1): seg[node].add(prev_pos[i])
        if l == r: return
        mid=(l+r)//2; build(2*node,l,mid); build(2*node+1,mid+1,r)
    def seg_upd(node, l, r, pos, ov, nv):
        seg[node].remove(ov); seg[node].add(nv)
        if l==r: return
        mid=(l+r)//2
        if pos<=mid: seg_upd(2*node,l,mid,pos,ov,nv)
        else: seg_upd(2*node+1,mid+1,r,pos,ov,nv)
    def seg_qry(node, l, r, ql, qr, th):
        if qr0 else -1
            prev_pos[pos]=new_pp; seg_upd(1,0,N-1,pos,old_pp,new_pp)
            if idx2+1

Step-by-Step 解説

1Euler Tour による部分木の線形化
DFS で各頂点の入り時刻 $in_v$ と出時刻 $out_v$ を計算。頂点 $v$ の部分木 = Euler Tour 上の区間 $[in_v, out_v]$。
2distinct count テクニック
$prev\_pos[i]$ = 位置 $i$ と同色の、$i$ 未満の最大位置(なければ $-1$)。区間 $[L,R]$ の異なる色数 = $\{i \in [L,R] : prev\_pos[i] < L\}$ の個数。
3Merge Sort Tree の構築
SegTree の各ノードに $prev\_pos$ のソート済みリストを保持。クエリは各ノードで bisect_left(sorted_list, L)。構築 $O(N \log N)$。
4更新時の prev_pos 修正
色変更 $v: old\_c \to new\_c$ は最大3箇所の $prev\_pos$ に影響: $pos$ 自身、$old\_c$ の次要素、$new\_c$ の次要素。各修正は $O(\log^2 N)$。
5SortedList の活用
Python の sortedcontainers.SortedList で色ごとの位置管理を $O(\log N)$ で実現。競技環境で使えるライブラリの典型的な活用例。

計算量

構築: $O(N \log N)$
クエリ2(部分木色数): $O(\log^2 N)$
クエリ1(色更新): $O(\log^2 N)$
全体: $O((N + Q) \log^2 N)$
空間: $O(N \log N)$

よくあるミス

ミス原因正しい書き方
Euler Tour の再帰がスタックオーバーフローN=2×10⁵ で再帰深度超過スタックを明示的に管理する反復 DFS に変更
更新時に隣接要素の prev_pos 更新忘れ挿入・削除は前後2要素に影響旧色の次要素と新色の次要素も必ず更新
Merge Sort Tree の更新で remove ミスSortedList.remove は値ベース正しい old_val を渡しているか確認
クエリの threshold を R+1 にしてしまうprev_pos < L が条件bisect_left(sl, L) が正しい(L 未満の個数)

次のステップ

  • 発展問題: 辺に重みが付いた木で「パス上の異なる値の数」をオンラインクエリ(HLD + Merge Sort Tree)
  • 関連: CDQ 分割統治でオフライン処理し $O(N \log^2 N)$ に改善する手法
  • 応用: 「区間内の最頻値」クエリへの拡張

自己評価