問題
$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 テクニック
ヒント(段階的開示)
ヒント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]$。
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\}$ の個数。
$prev\_pos[i]$ = 位置 $i$ と同色の、$i$ 未満の最大位置(なければ $-1$)。区間 $[L,R]$ の異なる色数 = $\{i \in [L,R] : prev\_pos[i] < L\}$ の個数。
3Merge Sort Tree の構築
SegTree の各ノードに $prev\_pos$ のソート済みリストを保持。クエリは各ノードで
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)$。
色変更 $v: old\_c \to new\_c$ は最大3箇所の $prev\_pos$ に影響: $pos$ 自身、$old\_c$ の次要素、$new\_c$ の次要素。各修正は $O(\log^2 N)$。
5SortedList の活用
Python の
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)$
クエリ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)$ に改善する手法
- 応用: 「区間内の最頻値」クエリへの拡張