Day 124-Q2 — Li Chao Tree on Segment Tree(区間ごとの直線集合を管理する二重データ構造)

2026-08-16 赤色 Master / Phase 8+ ★★★★★★★★★ セグメント木の各ノードにLi Chao Treeを載せる「木の上の木」構造による区間直線挿入・点最小値クエリ

問題

$x$ 座標は $1$ から $N$ の整数で表される。以下の2種類のクエリを $Q$ 個、オンラインで順に処理せよ。

- 1 l r a b: 直線 $y = ax + b$ を追加する。ただしこの直線は $x \in [l, r]$ の範囲でのみ「有効」とする(それ以外の $x$ では存在しないものとして扱う) - 2 x: これまでに追加された直線のうち、$x$ を有効範囲に含むものすべての中で、$y = ax+b$ の最小値を出力する(該当する直線が1本もなければ INF と出力)

入力形式

N Q
query_1
...
query_Q

制約

$1 \le N, Q \le 20000$
$1 \le l \le r \le N$
$|a|, |b| \le 10^9$
$1 \le x \le N$

入出力例

入力例1

5 4
1 1 5 2 3
2 3
1 2 4 -1 10
2 3

出力例1

9
7

1つ目のクエリ後、$x=3$ では直線 $y=2x+3=9$ のみ有効なので出力は $9$。2つ目の直線 $y=-x+10$(有効範囲 $[2,4]$)を追加すると、$x=3$ では $y=2x+3=9$ と $y=-x+10=7$ の2本が有効になり、最小値は $7$

概念図

セグメント木の各ノードに独立したLi Chao Treeを載せる 外側セグメント木(x座標 1〜N) [1,8] root [1,4] [5,8] [1,2] [3,4] 区間[1,4]への直線挿入 → [1,4]ノードのLi Chao Treeに1回だけ挿入 insert O(logN) 点クエリ x=2 → root→[1,4]→[1,2] の 経路上の全Li Chao Treeに問い合わせmin 各ノードのLi Chao Treeは常にドメイン[1,N]で動かす簡略実装。挿入O(log²N)・クエリO(log²N)

ヒント(段階的開示)

ヒント1(方向性)

通常のLi Chao Treeは「常に全域で有効な直線」を管理するデータ構造である。今回は直線ごとに有効な $x$ の範囲が異なる点が難しい。すべての直線をまとめて1つのLi Chao Treeに入れてしまうと、範囲外でも最小値計算に混ざってしまい正しくない。

ヒント2(アプローチ)

「$x$ 座標に関するセグメント木」を外側に用意し、各ノードが担当する区間に完全に含まれるように直線を分解して挿入するという、セグメント木でよく使う「区間をO(log N)個の代表区間に分解する」テクニックを使う。ただし通常のセグメント木のように値をマージするのではなく、各ノードごとに独立したLi Chao Treeインスタンスを持たせる。直線 $[l,r]$ の追加は、外側セグメント木を辿って区間 $[l,r]$ を被覆するO(log N)個のノードそれぞれのLi Chao Treeに、その直線をO(log N)で挿入する(合計O(log²N))。点 $x$ のクエリは、ルートから葉 $x$ までの経路上にあるO(log N)個のノードそれぞれのLi Chao Treeに対して $x$ で問い合わせ、最小値を取ればよい。

ヒント3(誘導)

各ノードのLi Chao Treeは、そのノードが担当する区間ではなく、常に全体の座標範囲 $[1,N]$ をドメインとして動作させて構わない(挿入・クエリのたびに lo=1, hi=N から再帰すればよい、ノードの担当範囲に合わせて縮小する必要はない)。これにより実装が大幅に単純化される。

def seg_update(node, lo, hi, l, r, a, b):
    if r < lo or hi < l: return
    if l <= lo and hi <= r:
        lichao_roots[node] = lc_insert(lichao_roots[node], 1, N, a, b)
        return
    mid = (lo+hi)//2
    seg_update(2*node, lo, mid, l, r, a, b)
    seg_update(2*node+1, mid+1, hi, l, r, a, b)

模範解答 (Python)

import sys

def solve():
    input_data = sys.stdin.read().split()
    idx = 0
    N = int(input_data[idx]); idx += 1
    Q = int(input_data[idx]); idx += 1
    INF = float('inf')

    # --- Li Chao Tree (pointer-based, domain [1, N] 固定) ---
    class LC:
        __slots__ = ['a', 'b', 'left', 'right']
        def __init__(self, a, b):
            self.a = a
            self.b = b
            self.left = None
            self.right = None

    def lc_insert(node, lo, hi, a, b):
        if node is None:
            return LC(a, b)
        mid = (lo + hi) // 2
        l_new = a * lo + b < node.a * lo + node.b
        m_new = a * mid + b < node.a * mid + node.b
        if m_new:
            node.a, node.b, a, b = a, b, node.a, node.b
        if lo == hi:
            return node
        if l_new != m_new:
            node.left = lc_insert(node.left, lo, mid, a, b)
        else:
            node.right = lc_insert(node.right, mid + 1, hi, a, b)
        return node

    def lc_query(node, lo, hi, x):
        if node is None:
            return INF
        res = node.a * x + node.b
        if lo == hi:
            return res
        mid = (lo + hi) // 2
        if x <= mid:
            return min(res, lc_query(node.left, lo, mid, x))
        else:
            return min(res, lc_query(node.right, mid + 1, hi, x))

    # --- 外側セグメント木(各ノードは Li Chao Tree のルートを保持) ---
    size = 1
    while size < N:
        size *= 2
    lichao_roots = {}

    def seg_update(node, lo, hi, l, r, a, b):
        if r < lo or hi < l:
            return
        if l <= lo and hi <= r:
            lichao_roots[node] = lc_insert(lichao_roots.get(node), 1, N, a, b)
            return
        mid = (lo + hi) // 2
        seg_update(2 * node, lo, mid, l, r, a, b)
        seg_update(2 * node + 1, mid + 1, hi, l, r, a, b)

    def seg_query(node, lo, hi, x):
        res = lc_query(lichao_roots.get(node), 1, N, x)
        if lo == hi:
            return res
        mid = (lo + hi) // 2
        if x <= mid:
            return min(res, seg_query(2 * node, lo, mid, x))
        else:
            return min(res, seg_query(2 * node + 1, mid + 1, hi, x))

    out = []
    sys.setrecursionlimit(300000)
    for _ in range(Q):
        t = input_data[idx]; idx += 1
        if t == '1':
            l = int(input_data[idx]); idx += 1
            r = int(input_data[idx]); idx += 1
            a = int(input_data[idx]); idx += 1
            b = int(input_data[idx]); idx += 1
            seg_update(1, 1, N, l, r, a, b)
        else:
            x = int(input_data[idx]); idx += 1
            res = seg_query(1, 1, N, x)
            out.append(str(res) if res != INF else "INF")

    print('\n'.join(out))

solve()

Step-by-Step 解説

1なぜ単純なLi Chao Treeでは足りないか
Li Chao Treeは「点 $x$ における最小値」を管理するのは得意だが、それはすべての直線が常に全域で有効であることが前提になっている。直線ごとに有効範囲 $[l,r]$ があると、単純にLi Chao Treeに入れただけでは範囲外の $x$ でも誤って最小値計算に混ざってしまう。
2セグメント木による区間分解
区間 $[l,r]$ への操作は、セグメント木の標準テクニックで $O(\log N)$ 個の「完全に含まれるノード(代表区間)」に分解できる。今回はこの分解を利用し、直線を代表区間ごとに独立したLi Chao Treeへ挿入する。こうすれば、ある直線が影響するのは、対応する代表区間をルートから辿るときに通過するノードのLi Chao Treeだけに限定される。
3点クエリはルートから葉までの経路
点 $x$ のクエリでは、外側セグメント木のルートから葉 $x$ に至る経路上の $O(\log N)$ 個のノードすべてのLi Chao Treeに問い合わせる。区間 $[l,r]$ に対応する直線が挿入されたノードは、必ずこの経路上のどこかに現れる(セグメント木の区間分解の性質による)。
4計算量
1回の直線追加は $O(\log N)$ 個のノードそれぞれに $O(\log N)$ かけて挿入するので $O(\log^2 N)$。1回のクエリは $O(\log N)$ 個のノードそれぞれに $O(\log N)$ かけて問い合わせるので同じく $O(\log^2 N)$。全体で $O((N+Q)\log^2 N)$。
5ドメインを固定する簡略化
各ノードのLi Chao Treeは、本来そのノードが担当する $x$ 区間だけをドメインにすれば無駄なノード生成を減らせるが、実装が複雑になる。今回は常にドメイン $[1,N]$ で動かす簡略版を採用した。正しさは保たれる(Li Chao Treeはどんなドメインで動かしても、そのドメイン内のクエリに対して常に正しい最小値を返す)が、メモリ効率はやや劣る。

よくあるミス

ミス原因正しい書き方
直線をそのまま1つの共通Li Chao Treeに入れる有効範囲の概念を無視している外側セグメント木で区間分解してから、各ノード専用のLi Chao Treeに入れる
外側セグメント木のノードごとにLi Chao Treeのドメインを毎回変えようとして実装が破綻する「ドメイン固定でも正しい」ことに気づいていない全ノードで一貫してドメイン $[1,N]$ を使う
クエリ時に経路上の1ノードだけ調べて終わるセグメント木上のどのノードにも直線が挿入されうることを見落とすルートから葉までのすべてのノードのLi Chao Treeを調べて最小を取る
再帰の深さ制限でエラーになるPython のデフォルト再帰上限(1000)を超えるsys.setrecursionlimit を大きめに設定する

次のステップ

  • 発展: 区間ごとに有効な直線を「追加だけでなく削除もできる」ようにするには、区間を時間軸に見立ててオフラインで処理する「セグメント木上のUndo可能なLi Chao Tree」(オフライン動的Li Chao Tree)に一般化できる。
  • 次回予告: Weighted Matroid Union(重み付きマトロイド合併・K-forest分割問題)

自己評価

自分の回答

気づき・メモ