問題
$N$ 頂点の根付き木(根は頂点1)が与えられる。各頂点 $v$ には値 $a_v$ が設定されている。 以下の $Q$ 個のクエリを処理せよ:
1 v x:頂点 $v$ の値を $x$ に更新する2 v k:頂点 $v$ の祖先(深さが $\text{depth}(v) - k \cdot B$ 以上、$B = \lfloor\sqrt{N}\rfloor$)の XOR を求める
制約
$1 \le N, Q \le 2 \times 10^5$
$1 \le a_v \le 10^9$
$1 \le x \le 10^9$
$1 \le k \le \lfloor\sqrt{N}\rfloor$
時間制限: 2秒
入出力例
入力例 1
7 4
1 2 4 8 16 32 64
1 1 2 2 3 3
2 6 1
1 3 5
2 7 1
2 4 2
出力例 1
6
7
3
概念図: ブロック分割による祖先XOR
ヒント(段階的開示)
ヒント1: 方向性
木全体を「ブロック」に分割する考え方があります。$B = \lfloor\sqrt{N}\rfloor$ 個の祖先ごとに集約値を保持するとクエリを高速化できます。
ヒント2: アプローチ
- 各頂点 $v$ に対して「$B$ 個上の祖先」への参照
anc_B[v]を事前計算 - 各頂点のブロック内XOR累積値
prefix_xor[v]を保持 - クエリ: ブロック境界までの端数をナイーブに辿り、残りはブロック単位で累積XOR
ヒント3: 実装骨格
B = isqrt(N)
# 各頂点のB個上の祖先を事前計算 O(N*B) = O(N√N)
for v in BFS_order:
u = v
for _ in range(B):
u = parent[u]; if u==0: break
anc_B[v] = u
prefix_xor[v] = XOR(a[v], a[parent[v]], ..., a[child_of_anc_B])
# クエリ処理
def query(v, k): # k*B個上の祖先までのXOR
result, cur = 0, v
for _ in range(k):
result ^= prefix_xor[cur]
cur = anc_B[cur]
return result
模範解答 (Python)
import sys
from collections import deque
from math import isqrt
def main():
input = sys.stdin.readline
N, Q = map(int, input().split())
a = [0] + list(map(int, input().split())) # 1-indexed
parent = [0] * (N + 1)
children = [[] for _ in range(N + 1)]
if N > 1:
p = list(map(int, input().split()))
for i in range(2, N + 1):
parent[i] = p[i - 2]
children[p[i-2]].append(i)
else:
input()
B = max(1, isqrt(N))
depth = [0] * (N + 1)
anc_B = [0] * (N + 1)
prefix_xor = [0] * (N + 1)
order = []
q = deque([1])
while q:
v = q.popleft()
order.append(v)
for c in children[v]:
depth[c] = depth[v] + 1
q.append(c)
for v in order:
u = v
for _ in range(B):
u = parent[u]
if u == 0:
break
anc_B[v] = u
xr = 0
u = v
for _ in range(B):
if u == 0:
break
xr ^= a[u]
u = parent[u]
prefix_xor[v] = xr
def query_xor(v, k):
result = 0
cur = v
steps = k * B
while steps >= B and anc_B[cur] != 0:
result ^= prefix_xor[cur]
cur = anc_B[cur]
steps -= B
for _ in range(steps):
if cur == 0:
break
result ^= a[cur]
cur = parent[cur]
return result
def update(v, x):
a[v] = x
bfs_q = deque([(v, 0)])
while bfs_q:
u, d = bfs_q.popleft()
xr = 0
w = u
for _ in range(B):
if w == 0:
break
xr ^= a[w]
w = parent[w]
prefix_xor[u] = xr
if d < B:
for c in children[u]:
bfs_q.append((c, d + 1))
results = []
for _ in range(Q):
line = list(map(int, input().split()))
if line[0] == 1:
update(line[1], line[2])
else:
results.append(query_xor(line[1], line[2]))
print('\n'.join(map(str, results)))
main()
Step-by-Step 解説
1平方根分解の設計
$B = \lfloor\sqrt{N}\rfloor$ を単位として祖先チェーンをブロック化する。各頂点は「$B$ 個上の祖先」ポインタとブロック内XOR集計値を持つ。
$B = \lfloor\sqrt{N}\rfloor$ を単位として祖先チェーンをブロック化する。各頂点は「$B$ 個上の祖先」ポインタとブロック内XOR集計値を持つ。
2前処理 O(N√N)
BFSで浅い順に全頂点を処理。各頂点について $B$ ステップ上の祖先を辿りながら XOR を集積する。
BFSで浅い順に全頂点を処理。各頂点について $B$ ステップ上の祖先を辿りながら XOR を集積する。
3クエリ処理 O(√N)
ブロック単位で `anc_B` ポインタをたどりXORを累積 → 残りの端数をナイーブに辿る。
ブロック単位で `anc_B` ポインタをたどりXORを累積 → 残りの端数をナイーブに辿る。
4更新処理
更新頂点 $v$ の子孫で「ブロック内に $v$ を含む」頂点の `prefix_xor` を再計算。BFSで深さ $B$ までの子孫を再計算する。
更新頂点 $v$ の子孫で「ブロック内に $v$ を含む」頂点の `prefix_xor` を再計算。BFSで深さ $B$ までの子孫を再計算する。
5計算量
前処理: $O(N\sqrt{N})$、クエリ: $O(\sqrt{N})$、更新: $O(N)$ 最悪(線形木の場合)、平均的に $O(\sqrt{N})$。
前処理: $O(N\sqrt{N})$、クエリ: $O(\sqrt{N})$、更新: $O(N)$ 最悪(線形木の場合)、平均的に $O(\sqrt{N})$。
計算量
前処理: $O(N\sqrt{N})$
クエリ: $O(\sqrt{N})$ per query
更新: $O(N)$ 最悪、$O(B \cdot \text{branch\_factor})$ 平均
空間: $O(N)$
クエリ: $O(\sqrt{N})$ per query
更新: $O(N)$ 最悪、$O(B \cdot \text{branch\_factor})$ 平均
空間: $O(N)$
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
anc_B[v] が 0 を考慮しない | 根より上は存在しない | if anc_B[cur] == 0: break |
| prefix_xor の更新範囲が不足 | 子孫のブロックを見逃す | BFSで深さBまでの子孫全てを更新 |
| 1-indexed/0-indexed の混在 | 配列添字バグ | 入力時に1-indexedに統一 |
| XOR 初期値を 0 にしない | 誤った値から累積が始まる | result = 0 で初期化 |
次のステップ
- 発展問題: 木の平方根分解で「部分木内のXOR最大値クエリ」を $O(\sqrt{N})$ で処理する
- 類題: 重心分解を使った木のパスクエリ
- 応用: Heavy-Light Decompositionとの比較・使い分け