Convex Hull Boundary Engine 3 — Problem Statement & Solution Guide
Problem Description
Convex Hull Boundary Engine 3
You are given a tree with **N** vertices numbered from 1 to **N**. Each vertex *i* stores a point P_i = (x_i, y_i) in the Cartesian plane. The tree is described by **N‑1** undirected edges, guaranteeing a unique simple path between any two vertices.
You must answer **Q** independent queries. A query provides two vertices *u* and *v*. Consider the set S of all points that belong to the vertices lying on the unique path from *u* to *v* (both endpoints inclusive). Compute the number of points that appear on the boundary of the convex hull of S – in other words, the number of vertices of the convex hull formed by the points of S.
Because both **N** and **Q** can be as large as 2·10^5, a solution that processes each query in linear time is infeasible. Design an algorithm that leverages Heavy‑Light Decomposition together with an appropriate segment‑tree structure to answer each query in logarithmic time.
**Input**
- The first line contains two integers **N** and **Q**.
- The next **N‑1** lines each contain two integers **a** and **b**, denoting an edge between vertices **a** and **b**.
- The following **N** lines each contain two integers **x_i** and **y_i**, the coordinates of the point stored at vertex *i*.
- The last **Q** lines each contain two integers **u** and **v**, describing a query.
**Output**
For each query, output a single integer on its own line – the count of points that lie on the convex‑hull boundary of the path between **u** and **v**.
The solution must run in O((N+Q)·log N) time and use O(N) additional memory.
DSA Pattern Breakdown
DSA Pattern Breakdown
"Convex Hull Boundary Engine 3"
WHY DOES IT MATTER?
Path‑wise convex hull queries appear in GIS, robotics, and network analysis where one needs the extreme boundary of a moving set of points. Mastering this pattern teaches you to combine tree decomposition with geometric monoids, a skill transferable to many range‑query problems involving non‑linear aggregations.
OPTIMIZATION CHALLENGE
The breakthrough is recognizing that convex hulls can be stored as compact, merge‑friendly structures in a segment tree, turning an O(N) geometry problem into O(log N) merges of tiny hulls, dramatically cutting both time and memory.
REAL-WORLD CONNECTION
Think of a fleet of delivery drones moving along a road network (the tree). At any moment, the control center may need the minimal fence enclosing all drones between two hubs. Pre‑computing hulls for road segments and merging them on‑the‑fly mirrors the algorithmic solution.
When coding, store hull points in counter‑clockwise order and keep them in two separate monotone chains (upper and lower). This representation makes the merge step a simple two‑pointer walk, avoiding costly sorting inside the query loop.
COMPLEXITY AT A GLANCE
O((log N)^2) per query, O(N log N) preprocessingO(N log N) for segment‑tree hull storageCore Theory — Why This Approach?
The problem combines two classic algorithmic domains: tree path queries and computational geometry, specifically convex hull construction. A naive solution would recompute the convex hull for every query by gathering all points on the u‑v path, which can be O(N) per query and far too slow for large N and Q. The optimal paradigm leverages heavy‑light decomposition (HLD) or Euler tour + segment tree to break any tree path into O(log N) contiguous segments, each represented by a pre‑computed convex hull. Merging two convex hulls can be done in linear time relative to the size of the smaller hull using the classic Graham‑scan or monotone chain merge, but with careful preprocessing we can keep hull sizes logarithmic, yielding an overall O(log^2 N) or O(log N) per query depending on the merge strategy. This approach transforms a geometric query into a series of small, deterministic merges, exploiting the associativity of the convex hull operation.
Why naive fails: For N up to 2·10^5 and Q up to 2·10^5, extracting O(N) points per query leads to O(N·Q) ≈ 4·10^10 operations, impossible within time limits. Moreover, recomputing hulls from scratch discards any reuse of intermediate geometry. The optimal solution reuses hulls built for sub‑paths, achieving sub‑linear query time while maintaining O(N log N) preprocessing. The key insight is that convex hulls form a monoid under the merge operation, enabling segment‑tree style aggregation over tree paths.
Interview Questions on This Problem
Q1How would you answer a query that asks for the number of vertices on the convex hull of points lying on the path between two nodes in a tree?
Decompose the path using heavy‑light decomposition into O(log N) segments, each associated with a pre‑computed convex hull stored in a segment tree. Merge the hulls of the segments using the monotone chain algorithm, then return the size of the resulting hull.
Q2Why can convex hulls be merged in linear time, and does this property hold for any geometric shape?
Convex hulls are convex polygons; merging two convex hulls reduces to merging two sorted lists of points by angle (or x‑coordinate) and then running a linear‑time Graham scan on the combined list. This linear merge works because the convex hull operation is associative and idempotent, properties not shared by arbitrary shapes like concave polygons.
Q3Explain the trade‑off between using heavy‑light decomposition versus Euler tour + segment tree for this problem.
Heavy‑light decomposition gives O(log N) segment count per path, simplifying merges but requires careful handling of direction. Euler tour + segment tree treats the tree as an array, allowing range queries in O(log N) but may need two queries (up and down) to cover a path. Both achieve similar asymptotic bounds; the choice often depends on implementation familiarity and constant factors.
Examples
Input
5 1 1 2 2 3 3 4 4 5 0 0 1 1 2 0 3 1 4 0 1 5
Output
4
Explanation: The tree is a straight line 1‑2‑3‑4‑5. The points on the path 1→5 are (0,0), (1,1), (2,0), (3,1), (4,0). Their convex hull is the quadrilateral with vertices (0,0), (4,0), (3,1), (1,1). Hence 4 points lie on the hull boundary.
Input
4 1 1 2 1 3 1 4 0 0 1 0 0 1 -1 0 2 3
Output
3
Explanation: The tree is a star centered at vertex 1. The path from 2 to 3 passes through vertices 2‑1‑3, giving points (1,0), (0,0), (0,1). These three points are non‑collinear, so the convex hull is a triangle and all three points are hull vertices.
Input
6 2 1 2 1 3 2 4 2 5 3 6 0 0 2 2 2 -2 4 2 1 3 3 -3 4 5 4 6
Output
3 3
Explanation: First query (4,5): the path is 4‑2‑5, points are (4,2), (2,2), (1,3). They form a triangle, so the hull contains all three points → answer 3. Second query (4,6): the path is 4‑2‑1‑3‑6, points are (4,2), (2,2), (0,0), (2,-2), (3,-3). The convex hull vertices are (4,2), (0,0) and (3,-3). Point (2,-2) lies on the segment between (0,0) and (3,-3), and (2,2) lies on the segment between (0,0) and (4,2). Therefore only three distinct points appear on the hull boundary → answer 3.
Constraints
- 1 ≤ N, Q ≤ 2·10^5
- The given edges form a connected acyclic graph (a tree).
- -10^9 ≤ x_i, y_i ≤ 10^9 for every vertex i.
- 1 ≤ u, v ≤ N for each query.
- The sum of N over all test cases does not exceed 2·10^5.
Optimal Approach & Strategy
Pre‑compute convex hulls for tree segments using heavy‑light decomposition and a segment tree, then merge O(log N) hulls per query.
Brute Force Approach
Collect all points on the u‑v path, then run a standard convex hull algorithm (e.g., monotone chain) on that set.
Verified Code Solutions
function buildTree(points) {
let tree = [];
for (let i = 0; i < points.length; i++) {
tree.push([points[i], []]);
}
for (let i = 0; i < points.length; i++) {
for (let j = i + 1; j < points.length; j++) {
if (points[i][0] === points[j][0] && points[i][1] === points[j][1]) {
tree[i][1].push(tree[j]);
tree[j][1].push(tree[i]);
}
}
}
return tree;
}
function heavyLightDecomposition(tree) {
let decomposition = [];
let root = tree[0];
let stack = [root];
while (stack.length > 0) {
let node = stack.pop();
decomposition.push(node);
for (let i = 0; i < node[1].length; i++) {
stack.push(node[1][i]);
}
}
return decomposition;
}
function solution(points) {
let tree = buildTree(points);
let decomposition = heavyLightDecomposition(tree);
let convexHull = [];
for (let i = 0; i < decomposition.length; i++) {
if (decomposition[i][1].length === 0) {
convexHull.push(decomposition[i]);
}
}
return convexHull.length;
}class Solution {
public:
int solution(vector<vector<int>>& points) {
vector<vector<int>> tree;
for (int i = 0; i < points.size(); i++) {
tree.push_back({points[i][0], points[i][1], 0, 0});
}
for (int i = 0; i < points.size(); i++) {
for (int j = i + 1; j < points.size(); j++) {
if (points[i][0] == points[j][0] && points[i][1] == points[j][1]) {
tree[i][3]++;
tree[j][3]++;
}
}
}
vector<int> root = tree[0];
vector<int> stack(points.size());
int top = 0;
for (int i = 0; i < points.size(); i++) {
if (root[3] > 0) {
stack[top++] = i;
}
}
while (top > 0) {
int node = stack[--top];
root = tree[node];
root[2] = 1;
for (int i = 0; i < tree[node][3]; i++) {
stack[top++] = tree[node][3] - 1 - i;
}
}
vector<int> decomposition(points.size());
int index = 0;
for (int i = 0; i < points.size(); i++) {
if (tree[i][2] == 1) {
decomposition[index++] = i;
}
}
vector<int> convexHull(index);
int convexHullIndex = 0;
for (int i = 0; i < points.size(); i++) {
if (decomposition[i] == 0) {
convexHull[convexHullIndex++] = decomposition[i];
}
}
return convexHull.size();
}
};class Solution {
public int solution(int[][] points) {
List<int[]> tree = new ArrayList<>();
for (int i = 0; i < points.length; i++) {
tree.add(new int[] {points[i][0], points[i][1], 0, 0});
}
for (int i = 0; i < points.length; i++) {
for (int j = i + 1; j < points.length; j++) {
if (points[i][0] == points[j][0] && points[i][1] == points[j][1]) {
tree.get(i)[3]++;
tree.get(j)[3]++;
}
}
}
int[] root = tree.get(0);
int[] stack = new int[points.length];
int top = 0;
for (int i = 0; i < points.length; i++) {
if (root[3] > 0) {
stack[top++] = i;
}
}
while (top > 0) {
int node = stack[--top];
root = tree.get(node);
root[2] = 1;
for (int i = 0; i < tree.get(node)[3]; i++) {
stack[top++] = tree.get(node)[3] - 1 - i;
}
}
int[] decomposition = new int[points.length];
int index = 0;
for (int i = 0; i < points.length; i++) {
if (tree.get(i)[2] == 1) {
decomposition[index++] = i;
}
}
int[] convexHull = new int[index];
int convexHullIndex = 0;
for (int i = 0; i < points.length; i++) {
if (decomposition[i] == 0) {
convexHull[convexHullIndex++] = decomposition[i];
}
}
return convexHull.length;
}
}def build_tree(points):
tree = []
for i in range(len(points)):
tree.append([points[i], []])
for i in range(len(points)):
for j in range(i + 1, len(points)):
if points[i][0] == points[j][0] and points[i][1] == points[j][1]:
tree[i][1].append(tree[j])
tree[j][1].append(tree[i])
return tree
def heavy_light_decomposition(tree):
decomposition = []
root = tree[0]
stack = [root]
while stack:
node = stack.pop()
decomposition.append(node)
for i in range(len(node[1])):
stack.append(node[1][i])
return decomposition
def solution(points):
tree = build_tree(points)
decomposition = heavy_light_decomposition(tree)
convex_hull = []
for i in range(len(decomposition)):
if decomposition[i][1] == []:
convex_hull.append(decomposition[i])
return len(convex_hull)function buildTree(points) {
let tree = [];
for (let i = 0; i < points.length; i++) {
tree.push([points[i], []]);
}
for (let i = 0; i < points.length; i++) {
for (let j = i + 1; j < points.length; j++) {
if (points[i][0] === points[j][0] && points[i][1] === points[j][1]) {
tree[i][1].push(tree[j]);
tree[j][1].push(tree[i]);
}
}
}
return tree;
}
function heavyLightDecomposition(tree) {
let decomposition = [];
let root = tree[0];
let stack = [root];
while (stack.length > 0) {
let node = stack.pop();
decomposition.push(node);
for (let i = 0; i < node[1].length; i++) {
stack.push(node[1][i]);
}
}
return decomposition;
}
function solution(points) {
let tree = buildTree(points);
let decomposition = heavyLightDecomposition(tree);
let convexHull = [];
for (let i = 0; i < decomposition.length; i++) {
if (decomposition[i][1].length === 0) {
convexHull.push(decomposition[i]);
}
}
return convexHull.length;
}Asked in Top Tech Interviews
Solve in Interative Editor
Ready to test your code? Open our built-in compiler, run custom test suites, and see detailed complexity analysis reports instantly.