Li Chao tree in Python
A segment tree over x coordinates storing a line per node, supporting line insertion and minimum or maximum queries at a point.
- File
li_chao_tree.py, 100 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/li-chao-tree/. The authors are listed in prov/E3PVVE.jsonl.
from typing import Optional
Line = tuple[int, int] # (a, b) for y = a*x + b
def _val(f: Line, x: int) -> int:
return f[0] * x + f[1]
class LiChaoTree:
"""Minimum of lines y = a*x + b at integer points of [lo, hi).
Nodes are created only when a line is placed, so n insertions make at
most n nodes. insert and query take O(log(hi - lo)) time.
"""
def __init__(self, lo: int, hi: int) -> None:
"""The domain is the integers lo <= x < hi; requires lo < hi."""
if lo >= hi:
raise ValueError("need lo < hi")
self.lo = lo
self.hi = hi
self.line: list[Optional[Line]] = [None] # node 0 is the root
self.left: list[int] = [-1]
self.right: list[int] = [-1]
def _child(self, v: int, go_left: bool) -> int:
kids = self.left if go_left else self.right
if kids[v] == -1:
kids[v] = len(self.line)
self.line.append(None)
self.left.append(-1)
self.right.append(-1)
return kids[v]
def insert(self, a: int, b: int) -> None:
"""Add the line y = a*x + b."""
f: Line = (a, b)
v, lo, hi = 0, self.lo, self.hi # @line L1
while True: # @line L2
g = self.line[v]
if g is None: # @line L3
self.line[v] = f
return
mid = (lo + hi) // 2 # @line L4
if _val(f, mid) < _val(g, mid): # @line L5
self.line[v], f = f, g
g = self.line[v]
assert g is not None
if hi - lo == 1: # @line L6
return
if _val(f, lo) < _val(g, lo): # @line L7
v, hi = self._child(v, True), mid
elif _val(f, hi - 1) < _val(g, hi - 1): # @line L8
v, lo = self._child(v, False), mid
else: # @line L9
return
def query(self, x: int) -> Optional[int]:
"""The least value of the lines at x, or None if there are none."""
if not self.lo <= x < self.hi:
raise IndexError("x outside the domain")
v, lo, hi = 0, self.lo, self.hi # @line L10
best: Optional[int] = None
while v != -1 and self.line[v] is not None: # @line L11
y = _val(self.line[v], x) # type: ignore[arg-type]
best = y if best is None or y < best else best # @line L12
if hi - lo == 1: # @line L13
break
mid = (lo + hi) // 2 # @line L14
if x < mid: # @line L15
v, hi = self.left[v], mid
else:
v, lo = self.right[v], mid
return best # @line L16
def stored(self) -> list[Optional[Line]]:
"""The line of each node, in order of creation."""
return list(self.line)
def check_invariant(self) -> None:
"""Check that no filled node has an empty parent and that every
stored line is lowest at the middle among the lines of its subtree."""
stack = [(0, self.lo, self.hi)]
while stack:
v, lo, hi = stack.pop()
g = self.line[v]
mid = (lo + hi) // 2
for c, a, b in ((self.left[v], lo, mid), (self.right[v], mid, hi)):
if c == -1:
continue
h = self.line[c]
if h is not None:
assert g is not None, "filled node below an empty one"
assert _val(g, mid) <= _val(h, mid), "node line not lowest at mid"
if a < b:
stack.append((c, a, b))
test_li_chao_tree.py 60 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/li-chao-tree/. The authors are listed in prov/E3PVVE.jsonl.
import random
import pytest
from li_chao_tree import LiChaoTree
def test_example() -> None:
t = LiChaoTree(0, 8)
for a, b in [(0, 5), (1, 0), (-1, 7), (-2, 12)]:
t.insert(a, b)
assert t.line[0] == (-1, 7)
assert t.line[t.left[0]] == (1, 0)
assert t.line[t.right[0]] == (-2, 12)
assert len(t.line) == 3
assert t.query(2) == 2 and t.query(5) == 2 and t.query(7) == -2
lines = [(0, 5), (1, 0), (-1, 7), (-2, 12)]
for x in range(8):
assert t.query(x) == min(a * x + b for a, b in lines)
t.check_invariant()
def test_edges() -> None:
with pytest.raises(ValueError):
LiChaoTree(3, 3)
t = LiChaoTree(-5, -4)
assert t.query(-5) is None
t.insert(2, 1)
t.insert(3, 0)
assert t.query(-5) == -15
with pytest.raises(IndexError):
t.query(-4)
big = LiChaoTree(-(10**9), 10**9)
big.insert(10**9, 10**18)
big.insert(-(10**9), 10**18)
assert big.query(0) == 10**18
assert big.query(-(10**9)) == 0
def test_random_against_brute_force() -> None:
rng = random.Random(2013)
for _ in range(500):
lo = rng.randint(-30, 30)
hi = lo + rng.randint(1, 40)
t = LiChaoTree(lo, hi)
lines: list[tuple[int, int]] = []
for _ in range(rng.randint(0, 25)):
if rng.random() < 0.5:
a, b = rng.randint(-6, 6), rng.randint(-50, 50)
t.insert(a, b)
lines.append((a, b))
else:
x = rng.randrange(lo, hi)
want = min((a * x + b for a, b in lines), default=None)
assert t.query(x) == want
assert len(t.line) <= max(1, len(lines))
t.check_invariant()
What the next level needs
- the tests did not run in this build