Coding questions · MathWorks
Compute intersections for each segment
This question evaluates algorithmic problem-solving, interval reasoning, and complexity-analysis skills by requiring an efficient method to count how many other segments intersect each interval while accounting for edge cases like equal endpoints and duplicate segments.
You are given segments on a number line. Segment is the closed interval (inclusive endpoints), with and .
For each segment (with ), determine how many other segments intersect it. Two segments intersect if they share at least one common point on the number line.
Return an integer array counts of length , where counts[i] is the number of other segments that intersect segment .
Design an algorithm that is faster than the brute-force — aim for — analyze its time and space complexity, and implement:
int[] countIntersections(int[] startsAt, int[] endsAt)
Example. startsAt = [1, 3], endsAt = [4, 5] → counts = [1, 1], because segment and segment share the points and .
Hint: Reframe via the complement
Counting intersections directly is awkward because the overlap condition couples both endpoints. It is far easier to count the segments that do not intersect segment and subtract from . Two segments fail to intersect iff one lies entirely to the left of the other.
Hint: Split the non-intersecting set in two
A segment misses segment in exactly one of two disjoint ways: ends strictly before begins (), or begins strictly after ends (). These two groups cannot overlap, so you can count each independently and add them.
Hint: Turn each count into a search on a sorted array
"How many ?" and "how many ?" are order-statistic queries. Sort the array of all start values and the array of all end values once, then answer every query with a binary search (
lower_bound/upper_bound). Segment never counts itself, so you can query over all values and rely on the to remove it.
Constraints & Assumptions
- , so an pairwise scan ( operations) is too slow; target .
- . Endpoint values fit in a 32-bit
int, but intermediate counts (up to ) fit comfortably inintas well; the result array isint[]. - Segments are closed (inclusive) intervals; touching at a single shared endpoint counts as an intersection.
- A segment does not intersect itself;
counts[i]is over the other segments.
Clarifying Questions to Ask
- Are endpoints inclusive, so segments that merely touch at one point (e.g. and ) count as intersecting? (Assume yes.)
- How should duplicate segments be treated — does each identical segment count the others, including identical copies? (Assume yes: identity is by index , not by value, so two segments with the same coordinates each intersect the other.)
- Should the answer for segment include itself? (Assume no — exclude self.)
- Can a segment be degenerate (, a single point)? (Assume yes; it still intersects anything covering that point.)
- Is the array small enough that the output fits in memory and ordinary 32-bit integers, or do we need 64-bit counts? (With the counts fit in 32-bit.)
What a Strong Answer Covers
- Complement insight: recognizing that intersection is hard to count directly but non-intersection decomposes cleanly into two disjoint, easy-to-count groups (segments entirely left, segments entirely right).
- Correct strict vs. non-strict boundaries: using and (strict) so that touching/equal endpoints are correctly not excluded, matching inclusive-interval semantics.
- Order-statistic queries via sorting + binary search: sorting starts and ends once, then binary searches, for time and extra space — with a clear complexity argument.
- Self-exclusion handling: arguing why querying over all values and subtracting one yields exactly the count over the other segments (segment is never in its own left or right group).
- Edge cases: duplicates, degenerate single-point segments, all-identical segments, and a single segment () handled correctly.
- Working implementation with the stated complexity, plus verification on the given example.
Follow-up Questions
- Suppose instead of a per-segment count you only need the total number of intersecting pairs. How does your approach simplify, and what is the relationship between the per-segment counts and that total?
- If segments arrived as a stream and you had to maintain, after each insertion, the number of currently-stored segments intersecting the newest one, what data structure would you use (e.g. a balanced BST / order-statistic tree / Fenwick tree over compressed coordinates)?
- The coordinate range is up to but there are at most segments. How would coordinate compression let you replace binary search with a Fenwick (BIT) / prefix-sum approach, and when is that preferable?
- How would you extend the idea to axis-aligned rectangles in 2D (count, for each rectangle, how many others it overlaps)? What changes in the complement decomposition?
Compute intersections for each segment
Hints
- Counting intersections directly is awkward because the overlap condition couples both endpoints. Count the segments that do NOT intersect segment i and subtract from n-1. Two segments fail to intersect iff one lies entirely to the left of the other.
- A segment j misses segment i in exactly one of two disjoint ways: j ends strictly before i begins (endsAt[j] < startsAt[i]), or j begins strictly after i ends (startsAt[j] > endsAt[i]). These two groups cannot overlap, so count each independently and add them.
- 'How many endsAt[j] < startsAt[i]?' and 'how many startsAt[j] > endsAt[i]?' are order-statistic queries. Sort all start values and all end values once, then answer every query with a binary search (lower_bound / upper_bound). Segment i is never in its own left or right group, so query over all n values and rely on the -1 to remove self.
- Use STRICT inequalities (< and >) so segments that merely touch at a shared endpoint stay counted as intersecting, matching the inclusive-interval semantics.
Solution
Solution
1. The key observation: count the complement
Counting, for each segment , how many other segments intersect it is awkward to attack head-on. Two closed intervals and intersect iff
which couples both endpoints of both segments — hard to count with a single sort.
The complement is much cleaner. Two segments do not intersect iff one lies entirely to the left of the other. For a fixed segment , a different segment misses it in exactly one of two mutually exclusive ways:
- is entirely to the left of : , or
- is entirely to the right of : .
A segment cannot be both to the left and to the right of , so these two sets are disjoint. Therefore, over all other segments:
where
Why strict inequalities are correct. The intervals are inclusive, so segments that merely touch at a single point (e.g. and sharing ) do intersect and must not be subtracted. Using strict < and > ensures a segment whose end equals 's start (or whose start equals 's end) is not placed in the left/right group, so it correctly remains counted as an intersection.
Self-exclusion comes for free. When we compute and we may safely range over all indices, including itself: segment has , so (not in the left group) and (not in the right group). Thus contributes to both and , and the lone in removes the self-pair.
2. Turning the counts into binary searches
asks: how many end-points are strictly less than ? asks: how many start-points are strictly greater than ? These are classic order-statistic queries answered in on a sorted array:
- Sort a copy of all end values,
sortedEnds. Then — the index of the first element , i.e. the number of ends strictly below it. - Sort a copy of all start values,
sortedStarts. Then —upperBoundis the index of the first element , so minus it is the number of starts strictly above it.
Sorting is done once (two arrays), then each of the segments costs two binary searches.
3. Complexity
- Time: two sorts at , then segments two binary searches . Total , comfortably handling .
- Space: for the two sorted copies and the output array. The input is not modified.
4. Reference implementation (Java)
import java.util.Arrays;
public int[] countIntersections(int[] startsAt, int[] endsAt) {
int n = startsAt.length;
int[] counts = new int[n];
if (n == 0) return counts;
int[] sortedStarts = startsAt.clone();
int[] sortedEnds = endsAt.clone();
Arrays.sort(sortedStarts);
Arrays.sort(sortedEnds);
for (int i = 0; i < n; i++) {
// L = number of segments ending strictly before i starts:
// count of sortedEnds[j] < startsAt[i] == index of first element >= startsAt[i]
int left = lowerBound(sortedEnds, startsAt[i]);
// R = number of segments starting strictly after i ends:
// count of sortedStarts[j] > endsAt[i] == n - index of first element > endsAt[i]
int right = n - upperBound(sortedStarts, endsAt[i]);
counts[i] = (n - 1) - left - right;
}
return counts;
}
// first index with a[idx] >= key (count of elements strictly < key)
private int lowerBound(int[] a, int key) {
int lo = 0, hi = a.length;
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (a[mid] < key) lo = mid + 1; else hi = mid;
}
return lo;
}
// first index with a[idx] > key (count of elements <= key)
private int upperBound(int[] a, int key) {
int lo = 0, hi = a.length;
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (a[mid] <= key) lo = mid + 1; else hi = mid;
}
return lo;
}
The same logic in Python (using bisect):
from bisect import bisect_left, bisect_right
def countIntersections(startsAt, endsAt):
n = len(startsAt)
if n == 0:
return []
sorted_starts = sorted(startsAt)
sorted_ends = sorted(endsAt)
counts = [0] * n
for i in range(n):
left = bisect_left(sorted_ends, startsAt[i]) # ends < startsAt[i]
right = n - bisect_right(sorted_starts, endsAt[i]) # starts > endsAt[i]
counts[i] = (n - 1) - left - right
return counts
5. Walking the given example
startsAt = [1, 3], endsAt = [4, 5], so sortedStarts = [1, 3], sortedEnds = [4, 5], .
- Segment 0 : ; .
counts[0] = 1 - 0 - 0 = 1. - Segment 1 : ; .
counts[1] = 1 - 0 - 0 = 1.
Result [1, 1], matching the expected output (the two segments share points and ).
6. Edge cases and how they are handled
- Touching endpoints (e.g. and ): strict
</>keep them out of the left/right groups, so they correctly count as intersecting. - Duplicate / identical segments: identity is by index, not value. Two segments with the same coordinates each satisfy the intersection condition for the other, and since neither is in the other's strict-left or strict-right group, each adds to the other's count — exactly what we want.
- Degenerate single-point segments (): handled identically; the point intersects anything covering it.
- All identical (e.g. all ): every pair intersects, so each
counts[i] = n - 1. Check: , , giving . ✓ - Single segment (): returns
[0]since there are no other segments. ✓ - Integer range: endpoints up to fit in 32-bit
int; counts up to fit inint, so no overflow concerns.
7. Addressing the follow-ups
- Total number of intersecting pairs. counts each intersecting pair twice (once from each endpoint), so the total number of distinct intersecting pairs is . You can also get it directly: total pairs minus the non-intersecting pairs, where non-intersecting pairs are counted by sorting segments and, for each, the number entirely to its left — a single sweep.
- Streaming insertion. Maintain an order-statistic structure (a balanced BST / Fenwick tree over compressed coordinates) of the stored starts and ends. On inserting a new segment, query "ends newStart" and "starts newEnd" in to get its current intersection count, then insert its endpoints. Each operation is .
- Coordinate compression + Fenwick. With segments but coordinates up to , compress the distinct endpoint values to ranks , then use a Fenwick tree (BIT) for prefix counts. This matches the bound and is preferable when the queries are interleaved with updates (the streaming case) rather than answered all at once against a static, pre-sorted array.
- 2D axis-aligned rectangles. Two rectangles overlap iff they overlap on both the - and -projections. The clean 1D complement no longer decomposes into two independent strict-left/right counts, so a plane sweep (sort rectangle edges by , maintain an interval/segment tree over active -intervals, and count active overlaps as you sweep) is the standard generalization.