1. Formal Invariant — Lower Envelope & Li Chao Midpoint Dominance
Invariant 1: Monotonic Deque CHT Redundancy Condition
Given three lines $L_1, L_2, L_3$ with strictly decreasing slopes $m_1 > m_2 > m_3$ (for minimum queries), the middle line $L_2$ is completely redundant (never strictly minimal at any $x$) if and only if the intersection $x(L_1, L_3)$ lies to the left of or at $x(L_1, L_2)$:
$$\frac{b_3 - b_1}{m_1 - m_3} \le \frac{b_2 - b_1}{m_1 - m_2} \iff (b_3 - b_1)(m_1 - m_2) \le (b_2 - b_1)(m_1 - m_3)$$
Invariant 2: Li Chao Segment Tree Midpoint Property
Each node covering coordinate range $[L, R]$ with midpoint $M = \lfloor (L + R) / 2 \rfloor$ stores a line $\text{seg}$ that is dominant at $M$. When inserting a new line $\text{nw}$, we swap $\text{seg}$ and $\text{nw}$ if $\text{nw}(M) < \text{seg}(M)$. Because two distinct lines intersect at most once, the loser at $M$ can beat the winner on at most one half ($[L, M]$ or $[M+1, R]$), so we recurse into only that single child in $O(\log X)$ time!
Trace dimension-by-dimension SOS DP bit-flips F[mask] += F[mask ^ (1<<i)] without overcounting, Broken Profile staircase wavefront domino tiling, and Digit DP tight-prefix automata.
Invariant & State
2. Li Chao Segment Tree Single-Branch Recursion
seg is now best at M"] Eval -->|"nw(M) >= seg(M)"| Keep["Keep seg at M
nw is loser at M"] Swap --> CheckLeft Keep --> CheckLeft CheckLeft{"Where can loser nw
cross winner seg?"} CheckLeft -->|"nw(L) < seg(L)"| GoLeft["Recurse ONLY Left [L, M]"] CheckLeft -->|"nw(R) < seg(R)"| GoRight["Recurse ONLY Right [M+1, R]"] CheckLeft -->|"Neither"| Done["Terminate Early O(1)"]
3. Choosing the Right Envelope Data Structure
Depending on monotonicity of inserted slopes $m_j$ and query coordinates $x_i$, four regimes arise:
- Slopes $m_j$ monotonic + Queries $x_i$ monotonic: Use a
std::dequewith a moving front pointer and back-hull pruning in $O(1)$ amortized per step ($O(N)$ total). - Slopes $m_j$ monotonic + Queries $x_i$ arbitrary: Maintain the hull in a
std::vectorand binary search the optimal line at $x_i$ in $O(\log N)$ per query. - Slopes $m_j$ arbitrary + Queries $x_i$ arbitrary (Integer/Bounded Domain): Use a Li Chao Segment Tree (or dynamic implicit Li Chao Tree) in $O(N \log(\text{range}))$ time—completely free of division or floating-point intersection bugs!
- Slopes $m_j$ arbitrary + Floating-Point Domain: Use KACTL's
LineContainer(std::multisetordered by slope maintaining right-intersection endpoints) in $O(N \log N)$ time.
4. Hardware & Cache Behavior
Overflow Prevention & Implicit Pointer Allocation
In cross-multiplication $(b_3 - b_1)(m_1 - m_2) \le (b_2 - b_1)(m_1 - m_3)$, if $b_j \sim 10^{15}$ and $m_j \sim 10^9$, the product reaches $10^{24}$, overflowing int64_t! Always cast cross-products to __int128_t (or long double). By contrast, a Li Chao Tree only evaluates $m \cdot x + b$ at coordinate $x \in [L, R]$, which fits cleanly in int64_t whenever $|m \cdot x + b| < 9 \times 10^{18}$. Pre-allocating a flat std::vector<Node> pool for implicit Li Chao nodes eliminates new heap fragmentation.
5. Complexity Matrix
| CHT Variant | Insert Line Time | Query $x_i$ Time | Space Complexity |
|---|---|---|---|
| Monotonic Slopes + Monotonic Queries (Deque) | O(1) amortized | O(1) amortized | O(N) |
| Monotonic Slopes + Arbitrary Queries (Binary Search) | O(1) amortized | O(log N) | O(N) |
| Li Chao Tree (Coordinate Range $[-X, X]$) | O(log X) | O(log X) | O(N log X) |
Dynamic Hull (LineContainer / std::multiset) |
O(log N) | O(log N) | O(N) |
6. Production-Grade C++20 Implementation (Dynamic Implicit Li Chao Tree)
#include <vector>
#include <cstdint>
#include <limits>
#include <algorithm>
class DynamicLiChaoTree {
public:
static constexpr int64_t INF = std::numeric_limits<int64_t>::max() / 4;
struct Line {
int64_t m = 0;
int64_t b = INF;
int64_t eval(int64_t x) const { return m * x + b; }
};
private:
struct Node {
Line line;
int left = -1;
int right = -1;
};
int64_t x_min_, x_max_;
std::vector<Node> pool_;
int new_node() {
pool_.push_back(Node{});
return static_cast<int>(pool_.size()) - 1;
}
void add_line(int u, int64_t l, int64_t r, Line nw) {
int64_t mid = l + (r - l) / 2;
bool left_better = nw.eval(l) < pool_[u].line.eval(l);
bool mid_better = nw.eval(mid) < pool_[u].line.eval(mid);
if (mid_better) {
std::swap(pool_[u].line, nw);
}
if (l == r) return;
if (left_better != mid_better) {
if (pool_[u].left == -1) {
int c = new_node();
pool_[u].left = c;
}
add_line(pool_[u].left, l, mid, nw);
} else {
if (pool_[u].right == -1) {
int c = new_node();
pool_[u].right = c;
}
add_line(pool_[u].right, mid + 1, r, nw);
}
}
int64_t query(int u, int64_t l, int64_t r, int64_t x) const {
if (u == -1) return INF;
int64_t res = pool_[u].line.eval(x);
if (l == r) return res;
int64_t mid = l + (r - l) / 2;
if (x <= mid) {
return std::min(res, query(pool_[u].left, l, mid, x));
} else {
return std::min(res, query(pool_[u].right, mid + 1, r, x));
}
}
public:
DynamicLiChaoTree(int64_t x_min, int64_t x_max, int reserve_nodes = 200000)
: x_min_(x_min), x_max_(x_max) {
pool_.reserve(reserve_nodes);
new_node(); // root is index 0
}
void insert(int64_t m, int64_t b) {
add_line(0, x_min_, x_max_, Line{m, b});
}
int64_t query(int64_t x) const {
return query(0, x_min_, x_max_, x);
}
};
Production Pitfall — Reference Invalidations When Allocating Nodes
In a std::vector<Node> pool, never write pool_[u].left = new_node() as a single expression! In C++, the left-hand side pool_[u].left reference may be evaluated before new_node() triggers a std::vector reallocation, leaving a dangling reference and corrupting memory. Always store int c = new_node(); first and then assign pool_[u].left = c;.