問題
$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
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$
概念図
ヒント(段階的開示)
ヒント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 解説
Li Chao Treeは「点 $x$ における最小値」を管理するのは得意だが、それはすべての直線が常に全域で有効であることが前提になっている。直線ごとに有効範囲 $[l,r]$ があると、単純にLi Chao Treeに入れただけでは範囲外の $x$ でも誤って最小値計算に混ざってしまう。
区間 $[l,r]$ への操作は、セグメント木の標準テクニックで $O(\log N)$ 個の「完全に含まれるノード(代表区間)」に分解できる。今回はこの分解を利用し、直線を代表区間ごとに独立したLi Chao Treeへ挿入する。こうすれば、ある直線が影響するのは、対応する代表区間をルートから辿るときに通過するノードのLi Chao Treeだけに限定される。
点 $x$ のクエリでは、外側セグメント木のルートから葉 $x$ に至る経路上の $O(\log N)$ 個のノードすべてのLi Chao Treeに問い合わせる。区間 $[l,r]$ に対応する直線が挿入されたノードは、必ずこの経路上のどこかに現れる(セグメント木の区間分解の性質による)。
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)$。
各ノードの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分割問題)