Day 059-Q2 — SegTree Walk + 遅延伝播

2026-06-12 赤色 Master / Phase 8+ ★★★★★★★★★ SegTree Walk / 遅延伝播 / 最左位置二分探索

問題

長さ $N$ の数列 $A$ に対して $Q$ 個のクエリを処理せよ。

  • 1 l r x: $A[l..r]$ の全要素に $x$ を加算する。
  • 2 l r: $A[l..r]$ の中で、値が初めて $0$ 以下になる最左位置(1-indexed)を出力せよ。存在しなければ $-1$ を出力。

制約

パラメータ範囲
$N, Q$$1 \le N, Q \le 2 \times 10^5$
$A_i$$-10^9 \le A_i \le 10^9$
$x$$-10^9 \le x \le 10^9$

入出力例

入力例 1

6 4
3 5 -1 2 4 -3
2 1 6
1 2 3 -6
2 1 6
2 4 6

出力例 1

3
2
6

クエリ1: $A[1..6]$ で最左 $\le 0$ は位置3($A_3=-1$)。
クエリ2: $A[2..3]$ に $-6$ 加算 → $A = [3,-1,-7,2,4,-3]$。
クエリ3: 最左 $\le 0$ は位置2($A_2=-1$)。
クエリ4: $A[4..6] = [2,4,-3]$ → 最左 $\le 0$ は位置6。

概念図: SegTree Walk

SegTree Walk — 最左 ≤0 位置をO(log N)で探索 min=-3 min=-1 min=-3 min=3 min=-1 min=2 min=-3 3 5 -1 2 4 -3 Walk ロジック: 根から降下: 左子の min ≤ 0 かつ範囲 [l,r] 内 → 左へ。そうでなければ右へ。葉に到達 = 答え。 push_down を各ノードで実行し遅延を伝播してから分岐判断。計算量 O(log N)。

ヒント(段階的開示)

ヒント1: 方向性
遅延伝播セグメント木(区間加算 + 区間最小値)を構築し、クエリ2に対して SegTree Walk(木上の二分探索)を使う。Walk では「左子の最小値 $\le 0$ かつ範囲内」なら左へ降り、そうでなければ右へ降りる。
ヒント2: アプローチ
  • 遅延セグ木のノード: tree[node] = 区間最小値, lazy[node] = 未伝播の加算値
  • push_down: tree[child] += lazy[node]; lazy[child] += lazy[node]
  • walk 関数: 区間外 or 最小値 > 0 → -1。葉 → その位置。内部 → 左子再帰、失敗なら右子
ヒント3: コード骨格
def walk(node, node_l, node_r, ql, qr):
    if node_r < ql or qr < node_l or tree[node] > 0:
        return -1
    if node_l == node_r:
        return node_l + 1  # 1-indexed
    push_down(node)
    mid = (node_l + node_r) // 2
    res = walk(2*node, node_l, mid, ql, qr)
    if res != -1: return res
    return walk(2*node+1, mid+1, node_r, ql, qr)

模範解答 (Python)

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

def main():
    N, Q = map(int, input().split())
    A = list(map(int, input().split()))
    SIZE = 1
    while SIZE < N: SIZE <<= 1

    tree = [inf] * (2 * SIZE)
    lazy = [0] * (2 * SIZE)

    for i in range(N):
        tree[SIZE + i] = A[i]
    for i in range(SIZE - 1, 0, -1):
        tree[i] = min(tree[2*i], tree[2*i+1])

    def push_down(node):
        if lazy[node]:
            for child in (2*node, 2*node+1):
                tree[child] += lazy[node]
                lazy[child] += lazy[node]
            lazy[node] = 0

    def update(node, node_l, node_r, ql, qr, val):
        if qr < node_l or node_r < ql: return
        if ql <= node_l and node_r <= qr:
            tree[node] += val
            lazy[node] += val
            return
        push_down(node)
        mid = (node_l + node_r) // 2
        update(2*node, node_l, mid, ql, qr, val)
        update(2*node+1, mid+1, node_r, ql, qr, val)
        tree[node] = min(tree[2*node], tree[2*node+1])

    def walk(node, node_l, node_r, ql, qr):
        if node_r < ql or qr < node_l or tree[node] > 0:
            return -1
        if node_l == node_r:
            return node_l + 1  # 1-indexed
        push_down(node)
        mid = (node_l + node_r) // 2
        res = walk(2*node, node_l, mid, ql, qr)
        if res != -1: return res
        return walk(2*node+1, mid+1, node_r, ql, qr)

    for _ in range(Q):
        q = list(map(int, input().split()))
        if q[0] == 1:
            l, r, x = q[1]-1, q[2]-1, q[3]
            update(1, 0, SIZE-1, l, r, x)
        else:
            l, r = q[1]-1, q[2]-1
            print(walk(1, 0, SIZE-1, l, r))

main()

Step-by-Step 解説

Step 1: 遅延伝播セグメント木の設計

各ノードに区間最小値 tree[node] と遅延加算値 lazy[node] を持たせる。

Step 2: push_down の実装

内部ノードの遅延を子ノードに伝播: tree[child] += lazy[node]; lazy[child] += lazy[node]。その後 lazy[node] = 0

Step 3: SegTree Walk の核心

tree[node] > 0」(この部分木に $\le 0$ の要素なし)なら即座に $-1$ を返す。これにより探索が $O(\log N)$ に収まる。

Step 4: walk での push_down の必要性

子ノードへの分岐前に push_down が必須。遅延が伝播されていないと子の最小値が不正確になり正しい答えを返せない。

Step 5: 計算量

update: $O(\log N)$。walk: $O(\log N)$。全体 $O((N+Q)\log N)$。

よくあるミス

ミス原因正しい書き方
walk 内で push_down を忘れる子の最小値が遅延込みで正しくない内部ノード分岐直前に push_down(node)
範囲外を inf で初期化しない配列外アクセスで誤答tree = [inf] * (2 * SIZE)
葉での返り値が 0-indexed のまま問題は 1-indexed を要求return node_l + 1

次のステップ

発展問題: 区間加算・区間最大値・最右 $\ge X$ 位置クエリを組み合わせた問題を実装せよ。

自己評価

解いた後に記入してください。