Day 039-Q2 — HLD + BIT による木パス sqrt 和クエリ

2026-05-22 赤色 Master / Phase 8+ ★★★★★★★★★ HLD + BIT / Sqrt Decomp on Tree

問題

$N$ 頂点の根付き木(根 = 1)と $Q$ 個のクエリ。各辺には整数の重みがある。

  • 1 u w: 頂点 $u$ の親との辺の重みを $w$ に変更
  • 2 u v: パス $(u, v)$ 上の辺重みの整数平方根の総和 $\sum_{e \in \text{path}(u,v)} \lfloor \sqrt{w_e} \rfloor$ を求めよ

制約

$2 \le N \le 10^5$
$1 \le Q \le 10^5$
$1 \le w \le 10^9$
時間制限: 3sec / メモリ: 256MB

入出力例

入力例 1

5 4
1 4
1 9
2 16
2 25
2 1 5
1 3 100
2 1 5
2 3 4

出力例 1

12
17
12

初期辺重み: (1,2)=4, (1,3)=9, (2,4)=16, (2,5)=25。クエリ2(1→5): path=辺(1,2),(2,5)→√4+√25=2+5=7... 実際は path=1-2-5。

概念図: HLD による木の平坦化

1 2 3 4 5 w=4 w=9 w=16 w=25 Heavy chain: 1→2→5 Light edge: 1→3, 2→4 HLD 平坦化後の BIT pos[1]=0, pos[2]=1, pos[5]=2 pos[3]=3, pos[4]=4 辺重みは深い側の頂点の位置に格納

ヒント(段階的開示)

ヒント1: 方向性
HLD でパスを $O(\log N)$ 個の連続区間に分解し、各区間の $\lfloor \sqrt{w} \rfloor$ 合計を BIT で管理。辺の重みは深い側の頂点に対応させる。
ヒント2: 辺→頂点の対応
辺 $(parent[v], v)$ の重みを頂点 $v$ に持たせると、辺クエリが点クエリに帰着できる。パスクエリ時は LCA の辺(LCA 自身の位置)を除外することに注意。
ヒント3: 更新と BIT
# 更新クエリ: 辺重みを変更
old_sq = isqrt(edge_w[u])
new_sq = isqrt(w)
edge_w[u] = w
bit.add(pos[u] + 1, new_sq - old_sq)  # 差分更新

# パスクエリ
def path_sum(u, v):
    res = 0
    while head[u] != head[v]:
        if depth[head[u]] < depth[head[v]]:
            u, v = v, u
        res += bit.query(pos[head[u]] + 1, pos[u] + 1)
        u = parent[head[u]]
    if depth[u] > depth[v]:
        u, v = v, u
    if u != v:  # LCA 辺を除外
        res += bit.query(pos[u] + 2, pos[v] + 1)
    return res

模範解答 (Python)

import sys
from math import isqrt
input = sys.stdin.readline

class BIT:
    def __init__(self, n):
        self.n = n
        self.data = [0] * (n + 1)
    def add(self, i, v):
        while i <= self.n:
            self.data[i] += v
            i += i & -i
    def prefix(self, i):
        s = 0
        while i > 0:
            s += self.data[i]
            i -= i & -i
        return s
    def query(self, l, r):
        return self.prefix(r) - self.prefix(l - 1)

def solve():
    N, Q = map(int, input().split())
    parent = [0] * (N + 1)
    edge_w = [0] * (N + 1)
    children = [[] for _ in range(N + 1)]
    for v in range(2, N + 1):
        p, w = map(int, input().split())
        parent[v] = p
        edge_w[v] = w
        children[p].append(v)

    size = [1] * (N + 1)
    depth = [0] * (N + 1)
    heavy = [-1] * (N + 1)
    order = []
    stack = [1]
    visited = [False] * (N + 1)
    while stack:
        u = stack[-1]
        if not visited[u]:
            visited[u] = True
            order.append(u)
            for c in children[u]:
                depth[c] = depth[u] + 1
                stack.append(c)
        else:
            stack.pop()
            p = parent[u]
            size[p] += size[u]
            if heavy[p] == -1 or size[u] > size[heavy[p]]:
                heavy[p] = u

    pos = [0] * (N + 1)
    head = [0] * (N + 1)
    cur_pos = [0]
    stack2 = [(1, 1)]
    while stack2:
        u, h = stack2.pop()
        head[u] = h
        pos[u] = cur_pos[0]
        cur_pos[0] += 1
        for c in children[u]:
            if c != heavy[u]:
                stack2.append((c, c))
        if heavy[u] != -1:
            stack2.append((heavy[u], h))

    bit = BIT(N + 1)
    for v in range(2, N + 1):
        bit.add(pos[v] + 1, isqrt(edge_w[v]))

    def path_sum(u, v):
        res = 0
        while head[u] != head[v]:
            if depth[head[u]] < depth[head[v]]:
                u, v = v, u
            res += bit.query(pos[head[u]] + 1, pos[u] + 1)
            u = parent[head[u]]
        if depth[u] > depth[v]:
            u, v = v, u
        if u != v:
            res += bit.query(pos[u] + 2, pos[v] + 1)
        return res

    out = []
    for _ in range(Q):
        line = list(map(int, input().split()))
        if line[0] == 1:
            _, u, w = line
            bit.add(pos[u] + 1, isqrt(w) - isqrt(edge_w[u]))
            edge_w[u] = w
        else:
            _, u, v = line
            out.append(path_sum(u, v))
    print('\n'.join(map(str, out)))

solve()

Step-by-Step 解説

1辺重みの頂点対応
辺 $(parent[v], v)$ の重みを頂点 $v$ に持たせることで点更新・区間和に帰着。
2HLD 前処理
重い子(サブツリーが最大の子)を優先して DFS し、連続した HLD 位置を割り当てる。
3BIT 初期化
bit.add(pos[v]+1, isqrt(edge_w[v])) で各辺の $\lfloor\sqrt{w}\rfloor$ を登録。
4パスクエリ
チェーンを登りながら BIT 区間和を積算。LCA での辺除外(pos[u]+2 から)に注意。
5差分更新
重み変更時は $\lfloor\sqrt{w_{\text{new}}}\rfloor - \lfloor\sqrt{w_{\text{old}}}\rfloor$ を BIT に add。

計算量

HLD 前処理: $O(N)$
パスクエリ: $O(\log^2 N)$ — チェーン $O(\log N)$ × BIT $O(\log N)$
点更新: $O(\log N)$

よくあるミス

ミス原因正しい書き方
LCA の辺を二重カウント辺クエリで LCA 自身の辺を含めるpos[u]+2 から始めて LCA 辺を除外
浅い側の頂点に辺を持たせる辺と頂点の対応ミス必ず深い側(子)の pos[v] に格納
DFS スタックで重辺を後に処理軽辺の子を後に積むべき軽辺を先に stack2.append してから重辺を積む
更新時に old_sq を再計算忘れBIT が古い値のままisqrt(w) - isqrt(edge_w[u]) を add

次のステップ

  • 発展: 区間辺一括変更 → 遅延伝播セグメント木 + HLD
  • 応用: パス上の最大辺重みクエリ(HLD + セグメント木)

自己評価

自分の回答

気づき・メモ