Li Chao Tree
Problem Overview
| Attribute | Value |
|---|---|
| Difficulty | Hard |
| Category | Range Queries / DP Optimization |
| Key Technique | Segment Tree with Line Storage |
| Time per Query | O(log N) |
Learning Goals
After studying this topic, you will be able to:
- Understand how Li Chao Tree maintains a set of lines efficiently
- Implement insertion and query operations in O(log N)
- Apply Li Chao Tree for DP optimization problems
- Choose between Li Chao Tree and Convex Hull Trick appropriately
Concept Explanation
Problem: Given a set of lines of the form y = mx + b, efficiently:
- Insert a new line into the set
- Query the minimum (or maximum) y-value at a given x-coordinate
Key Insight: Li Chao Tree is a segment tree where each node stores the “winner” line for its interval’s midpoint. When inserting a new line, we compare it with the existing line at the midpoint and recursively push the “loser” to the appropriate child.
Why Li Chao Tree?
- Arbitrary Query Order: Unlike standard CHT, queries can come in any order
- Simple Implementation: Easier to code than dynamic CHT with binary search
- Persistent Friendly: Can be made persistent for advanced applications
Intuition: Li Chao Tree vs Convex Hull Trick
Pattern Recognition
Key Question: When do we use Li Chao Tree instead of Convex Hull Trick?
The Convex Hull Trick (CHT) requires either:
- Lines inserted in order of increasing/decreasing slope, OR
- Queries in sorted order of x-coordinate
Li Chao Tree removes these restrictions at the cost of slightly higher constant factor.
Comparison Table
| Aspect | Convex Hull Trick | Li Chao Tree |
|---|---|---|
| Insert Order | Must be sorted by slope | Any order |
| Query Order | Must be sorted (for deque) | Any order |
| Time Complexity | O(n) total / O(log n) per query | O(log N) per operation |
| Space | O(n) | O(N) where N = x-range |
| Implementation | Tricky edge cases | Cleaner recursion |
| Use Case | Sorted slopes/queries | Arbitrary order |
Breaking Down the Problem
- What are we storing? Lines in the form y = mx + b
- What’s the query? Find min/max y at a specific x
- Why segment tree? Each node represents an x-interval; we store the “best” line for that interval’s midpoint
Algorithm: How Li Chao Tree Works
Core Idea
Each segment tree node covering interval [lo, hi]:
- Stores the line that is best at the midpoint mid = (lo + hi) / 2
- When inserting a new line, compare at midpoint:
- Winner stays at current node
- Loser is pushed to the child where it might still win
Insert Operation
insert(node, [lo, hi], new_line):
mid = (lo + hi) / 2
if new_line is better at mid than current line:
swap(new_line, current_line)
if lo == hi:
return
if new_line is better at lo:
insert(left_child, [lo, mid], new_line)
else:
insert(right_child, [mid+1, hi], new_line)
Query Operation
query(node, [lo, hi], x):
if node is null:
return infinity (or -infinity for max)
mid = (lo + hi) / 2
current_value = evaluate(node.line, x)
if x <= mid:
return min(current_value, query(left_child, [lo, mid], x))
else:
return min(current_value, query(right_child, [mid+1, hi], x))
Dry Run: Inserting Lines
Let’s trace inserting lines into a Li Chao Tree for minimum queries with x-range [0, 8].
Lines to Insert
- Line A: y = 2x + 1
- Line B: y = -x + 10
- Line C: y = 0.5x + 4
Step-by-Step Execution
Initial State: Empty tree, range [0, 8]
=== Insert Line A: y = 2x + 1 ===
Node [0,8], mid=4:
- No existing line, store A
Tree: [0,8] -> A
=== Insert Line B: y = -x + 10 ===
Node [0,8], mid=4:
- A at mid=4: 2(4)+1 = 9
- B at mid=4: -4+10 = 6
- B wins at mid, swap: node stores B, push A down
- A at lo=0: 2(0)+1 = 1
- B at lo=0: -0+10 = 10
- A better at lo, go LEFT
Node [0,4], mid=2:
- No existing line, store A
Tree:
[0,8] -> B
/
[0,4] -> A
=== Insert Line C: y = 0.5x + 4 ===
Node [0,8], mid=4:
- B at mid=4: 6
- C at mid=4: 0.5(4)+4 = 6
- Tie! Keep B (arbitrary), push C
- C at lo=0: 0.5(0)+4 = 4
- B at lo=0: 10
- C better at lo, go LEFT
Node [0,4], mid=2:
- A at mid=2: 2(2)+1 = 5
- C at mid=2: 0.5(2)+4 = 5
- Tie! Keep A, push C
- C at lo=0: 4
- A at lo=0: 1
- A better at lo, go RIGHT
Node [3,4], mid=3:
- No existing line, store C
Final Tree:
[0,8] -> B (y=-x+10)
/
[0,4] -> A (y=2x+1)
\
[3,4] -> C (y=0.5x+4)
Query Example: Find min at x=1
Query x=1:
Node [0,8]: B gives -1+10=9, go left (1 <= 4)
Node [0,4]: A gives 2(1)+1=3, go left (1 <= 2)
Node [0,2]: null, return inf
Result: min(9, 3, inf) = 3 (from line A)
Solution: Python Implementation
class LiChaoTree:
"""
Li Chao Tree for minimum line queries.
Time: O(log N) per insert/query
Space: O(N) where N = hi - lo + 1
"""
def __init__(self, lo: int, hi: int):
self.lo = lo
self.hi = hi
self.tree = {} # node_id -> (m, b) representing y = mx + b
self.INF = float('inf')
def _eval(self, line, x):
"""Evaluate line y = mx + b at point x."""
if line is None:
return self.INF
m, b = line
return m * x + b
def insert(self, m: int, b: int):
"""Insert line y = mx + b."""
self._insert(1, self.lo, self.hi, (m, b))
def _insert(self, node: int, lo: int, hi: int, new_line):
if lo > hi:
return
mid = (lo + hi) // 2
cur_line = self.tree.get(node)
# Determine winner at midpoint
new_better_at_mid = self._eval(new_line, mid) < self._eval(cur_line, mid)
if new_better_at_mid:
self.tree[node] = new_line
new_line, cur_line = cur_line, new_line
if lo == hi or new_line is None:
return
# Determine which side the loser might still win
new_better_at_lo = self._eval(new_line, lo) < self._eval(cur_line, lo)
if new_better_at_lo:
self._insert(2 * node, lo, mid, new_line)
else:
self._insert(2 * node + 1, mid + 1, hi, new_line)
def query(self, x: int) -> int:
"""Query minimum y-value at x."""
return self._query(1, self.lo, self.hi, x)
def _query(self, node: int, lo: int, hi: int, x: int):
if lo > hi or node not in self.tree:
return self.INF
mid = (lo + hi) // 2
cur_val = self._eval(self.tree.get(node), x)
if lo == hi:
return cur_val
if x <= mid:
return min(cur_val, self._query(2 * node, lo, mid, x))
else:
return min(cur_val, self._query(2 * node + 1, mid + 1, hi, x))
# Example usage
if __name__ == "__main__":
tree = LiChaoTree(0, 1000000)
# Insert lines: y = mx + b
tree.insert(2, 1) # y = 2x + 1
tree.insert(-1, 10) # y = -x + 10
tree.insert(0, 5) # y = 5 (horizontal)
# Query minimum at various x
print(tree.query(0)) # min at x=0
print(tree.query(5)) # min at x=5
print(tree.query(10)) # min at x=10
Common Mistakes
Mistake 1: Using Wrong Infinity Value
Problem: When m and x are both up to 10^6, their product exceeds int range.
Fix: Use long long for all line evaluations and a sufficiently large INF.
Mistake 2: Wrong Recursion Direction
Problem: The loser should go to the child where it has a chance to win. Fix: Use XOR logic: if results differ at lo and mid, go left; else go right.
Mistake 3: Not Handling Empty Nodes
# WRONG: Accessing node without checking
cur_val = self.tree[node].eval(x) # KeyError if node doesn't exist
# CORRECT: Check for existence
cur_line = self.tree.get(node)
if cur_line is None:
return self.INF
Edge Cases
| Case | Input | Handling | Why |
|---|---|---|---|
| Empty tree query | Query before any insert | Return INF | No lines to evaluate |
| Single line | One insert, many queries | Works normally | Base case |
| Parallel lines | Same slope, different intercepts | Keep better one | Loser never wins anywhere |
| Identical lines | Same m and b | Either can stay | No difference in output |
| Negative x queries | x < 0 | Ensure range covers negatives | Adjust lo parameter |
| Large coordinates | x up to 10^9 | Use coordinate compression or implicit tree | Avoid MLE |
| Tie at midpoint | Equal values at mid | Arbitrary winner choice | Consistency matters, not which |
When to Use Li Chao Tree
Use Li Chao Tree When:
- Lines are inserted in arbitrary order (no slope sorting)
- Queries come in arbitrary order (no x sorting)
- You need online queries (no offline processing possible)
- DP optimization where slopes are not monotonic
Use Standard CHT Instead When:
- Lines come in sorted slope order AND queries are sorted
- Memory is very tight (CHT uses O(n), Li Chao uses O(range))
- You need the absolute fastest constant factor
Pattern Recognition Checklist:
- DP recurrence of form:
dp[i] = min(dp[j] + cost(j, i))wherecost(j,i) = a[j] * b[i] + c[j]-> CHT/Li Chao - Need min/max over lines at arbitrary x? -> Li Chao Tree
- Slopes not sorted, queries not sorted? -> Li Chao Tree (not basic CHT)
- Need to delete lines? -> Consider other structures
Comparison: Li Chao vs CHT Implementations
| Feature | Basic CHT (Deque) | Dynamic CHT | Li Chao Tree |
|---|---|---|---|
| Insert Time | O(1) amortized | O(log n) | O(log N) |
| Query Time | O(log n) or O(1)* | O(log n) | O(log N) |
| Slope Order | Required | Not required | Not required |
| Query Order | Required for O(1)* | Not required | Not required |
| Space | O(n) | O(n) | O(N) |
| Code Simplicity | Medium | Complex | Simple |
| Persistence | Hard | Hard | Easy |
*O(1) queries possible with pointer technique when x values are monotonic.
Related Problems
Prerequisites (Do These First)
| Problem | Why It Helps |
|---|---|
| Dynamic Range Sum (CSES) | Segment tree basics |
| Salary Queries (CSES) | Dynamic queries on ranges |
Direct Applications
| Problem | Key Insight |
|---|---|
| Codeforces - Kalila and Dimna | Classic DP + CHT |
| AtCoder - Walk (DP Contest) | Linear function optimization |
Harder (Do These After)
| Problem | New Concept |
|---|---|
| Codeforces - The Bakery | Divide and conquer DP optimization |
| Codeforces - Ann and Books | Li Chao with complex state |
Key Takeaways
- Core Idea: Store “winner” line at midpoint, push “loser” to potential winning region
- Main Advantage: Handles arbitrary insert and query order
- Trade-off: O(N) space for x-range vs O(n) space for CHT
- Use Case: DP optimization when slopes/queries are unsorted
Practice Checklist
Before moving on, make sure you can:
- Implement Li Chao Tree from scratch
- Explain why the loser goes to a specific child
- Convert a DP problem to Li Chao Tree form
- Choose between CHT and Li Chao Tree for a given problem
- Handle both min and max query variants