Skip to the chapter
←DSAGGroundworkΣDSA
DSA/Advanced/A13
47 / 48 written chapters
/

Levels and plan
  1. B1Complexity analysis
  2. B2JavaScript toolkit
  3. B3Arrays & strings
  4. B4Prefix sums
  5. B5Hashing
  6. B6Two pointers
  7. B7Sliding window
  8. B8Binary search
  9. B9Basic recursion
  10. B10Sorting algorithms
  11. B11Stacks & queues
  12. B12Linked lists
  1. I1Trees
  2. I2BST operations
  3. I3Tree problems in depth
  4. I4Heaps & priority queues
  5. I5Graphs: representation
  6. I6BFS on grids
  7. I7Graph problems
  8. I8Topological patterns
  9. I9Backtracking
  10. I10DP: 1D
  11. I11DP: 2D
  12. I12DP state machines
  13. I13Greedy algorithms
  14. I14Intervals
  15. I15Bit manipulation
  16. I16Matrix problems
  17. I17Tries
  18. I18Monotonic stack & queue
  19. I19Math for interviews
  20. I20Design & simulation
  21. I21Divide and conquer
  22. I22Randomization
  1. A1Advanced DP
  2. A2Union-Find
  3. A3Advanced graph algorithms
  4. A4Minimum Spanning Tree
  5. A5Advanced graphs
  6. A6Segment & Fenwick trees
  7. A7Sparse tables
  8. A8String algorithms
  9. A9Design problems
  10. A10Advanced backtracking
  11. A11Games & probability DP
  12. A12Ordered structures
  13. A13Sweep line & geometry
  14. A14Interview strategy

ΣA13 · Part 3 · Advanced

Sweep line, offline queries and geometry

Sort the moments when something changes, keep a summary of what is active, and skylines, offline queries and most geometry questions become one pass.

56 min read2 diagrams15 sections17 exercises
47 of 48

In this chapter

  • The sweep-line recipe: events, a tie-break and an active set
  • The skyline problem: a heap with lazy deletion
  • Offline queries: answer them in the order that suits you
  • Coordinate compression, exactly
  • Rectangle union: the simple way and the segment tree

and 10 more

about 56 min
15On this page
  1. The sweep-line recipe: events, a tie-break and an active set
  2. The skyline problem: a heap with lazy deletion
  3. Offline queries: answer them in the order that suits you
  4. Coordinate compression, exactly
  5. Rectangle union: the simple way and the segment tree
  6. Geometry in integers: cross product and orientation
  7. Polygon area by the shoelace formula
  8. Point in polygon by ray casting
  9. Rectangles: overlap, area and the perfect rectangle
  10. Maximum points on a line: hash the direction, not the slope
  11. Valid square without a single square root
  12. Convex hull by monotone chain (erect the fence)
  13. Closest pair of points
  14. Floating point: why geometry stays in integers
  15. Recognizing it in an unseen problem

The sweep-line recipe: events, a tie-break and an active set

Picture a vertical line that starts at the far left of the plane and moves right. Almost everywhere nothing happens as it moves: the same shapes are under it a moment later as a moment ago. It only matters at a handful of places, where a shape begins or ends. A sweep line is the habit of looking only at those places. You stop at each one, update what the line is touching, and answer the question from that. The line can just as well be a timeline, a pointer over sorted queries, or a row scanning a grid; the idea is to sort the moments that matter and keep a summary of "what is active right now".

The intervals chapter already uses this with the simplest possible summary, a counter: meeting rooms, car pooling and difference arrays are all sweeps where the active set is just a number. This chapter starts where a number stops being enough. The recipe is the same four steps every time:

  1. Make events. Turn each object into the coordinates where it starts or stops mattering, carrying whatever the update needs (a height, a weight, an index).
  2. Sort the events, with an explicit rule for what happens when two events share a coordinate.
  3. Walk the events in order and update the active set.
  4. Read the answer from the active set at the right moments: after every event, after every group of events at one coordinate, or between events when the answer accumulates over a stretch.

What changes from problem to problem is the structure that holds the active set. Choose it by the question you will ask of the set:

The question asked of the active setStructureWhere it appears
How many are active?a countermeeting rooms, car pooling (intervals chapter)
What is the largest or smallest value among the active ones?a heap, with lazy deletionskyline, minimum interval for each query (below)
What is the nearest active value above or below this one, or remove one specific entry?an ordered setthe ordered structures chapter
How much total length is covered?a segment tree over compressed coordinatesrectangle union (below)

The tie-break is part of the algorithm

When a start and an end fall on the same coordinate, which one is processed first changes the answer, and the right choice comes from the question and not from a trick. Ask whether a thing that ends at x is still there at x. If the interval is closed, [1, 2] still contains 2, so a start at 2 should see it: process starts first. If it is half-open, [1, 2) no longer contains 2, so process ends first, which is what the intervals chapter does. Writing the order as named numbers keeps the choice visible in the code:

const START = 0, END = 1; // the order of these two numbers IS the tie-break

function maxOverlapClosed(intervals) {
  const events = [];
  for (const [from, to] of intervals) {
    events.push([from, START]);
    events.push([to, END]);
  }
  events.sort((a, b) => a[0] - b[0] || a[1] - b[1]); // same time: starts first, so [1, 2] and [2, 3] touch
  let active = 0, best = 0;
  for (const [, kind] of events) {
    active += kind === START ? 1 : -1;
    best = Math.max(best, active);
  }
  return best;
}

For [[1, 2], [2, 3]] the events sort to (1, START), (2, START), (2, END), (3, END), so the count reaches 2: the two closed intervals share the point 2. Swapping the two constants gives 1, the answer for half-open intervals. The sort is O(n log n) and the walk is linear.

There are two ways to avoid tie-breaking altogether, and both appear below. One is to process all events at a coordinate before reading the answer, as the skyline does. The other is to read the answer only between coordinates, as the area sweep does: between two events at the same coordinate no width passes, so their order cannot change the total.

⚠ Sort numbers with a comparator that returns a number [10, 9, 1, 100].sort() returns [1, 10, 100, 9], because the default sort compares text. A comparator that returns a boolean, such as (a, b) => a > b, never returns a negative number and leaves [3, 1, 2] unsorted. Write (a, b) => a - b, and chain tie-breaks with || as above.
Say it like this → "I'll turn each object into two events, sort them with an explicit rule for ties, and sweep while keeping the active ones in a structure that can answer the one question I need, like the largest height right now."

The skyline problem: a heap with lazy deletion

Each building is [left, right, height]: it stands on the ground from left up to, but not including, right. Draw them all and trace the outline against the sky. Report that outline as the list of key points [x, height], where each key point is the left end of a flat stretch, and the last one has height 0. The rule that makes the answer unique: when two neighbouring stretches have the same height, they are one stretch, so no key point appears between them.

[2, 10] [3, 15] [7, 12] [12, 0] 10 15 12 2 3 5 7 9 x
Buildings [2, 9, 10], [3, 7, 15] and [5, 12, 12]. The outline changes only where a building starts or ends, and only when that changes the tallest one.

Why the obvious way fails. You could compute the height at every integer x by scanning all buildings, then compress runs. That costs the width of the whole range times n, and the range can be 231 wide.

The idea. The height at x is the maximum height over the buildings with left ≤ x < right. That maximum can only change at a coordinate where some building starts or ends, so the outline is decided by at most 2n coordinates. Sweep over them in order, keep the buildings covering the line in a structure that can say "the tallest", and whenever the tallest height differs from the height just before, record [x, height].

The active set needs three operations: insert a building when its left edge is reached, read the maximum height, and remove a building when its right edge is passed. A heap does the first two in O(log n) and O(1), but it cannot remove an arbitrary entry cheaply. The fix is lazy deletion: do not remove it. Leave the dead building where it is, and discard it only when it reaches the top, where it is about to be read. This is correct for a simple reason: you only ever read the top. A dead entry underneath a live top cannot change the maximum, and a dead entry on top is thrown away before you look. It also requires that death is permanent, and it is: the sweep only moves right, so a building that has ended never comes back.

class Heap {
  constructor(before) {
    this.before = before; // before(a, b) is true when a must leave the heap earlier than b
    this.items = [];
  }

  get size() {
    return this.items.length;
  }

  peek() {
    return this.items[0];
  }

  push(value) {
    const a = this.items;
    a.push(value);
    let i = a.length - 1;
    while (i > 0) {
      const parent = (i - 1) >> 1;
      if (!this.before(a[i], a[parent])) break;
      [a[parent], a[i]] = [a[i], a[parent]];
      i = parent;
    }
  }

  pop() {
    const a = this.items;
    const top = a[0];
    const last = a.pop();
    if (a.length > 0) {
      a[0] = last;
      let i = 0;
      for (;;) {
        const left = 2 * i + 1, right = left + 1;
        let first = i;
        if (left < a.length && this.before(a[left], a[first])) first = left;
        if (right < a.length && this.before(a[right], a[first])) first = right;
        if (first === i) break;
        [a[first], a[i]] = [a[i], a[first]];
        i = first;
      }
    }
    return top;
  }
}
function getSkyline(buildings) {
  const xs = [...new Set(buildings.flatMap(([left, right]) => [left, right]))].sort((a, b) => a - b);
  const byLeft = [...buildings].sort((a, b) => a[0] - b[0]);
  const alive = new Heap((a, b) => a[0] > b[0]); // entries [height, right]; the tallest leaves first
  const skyline = [];
  let next = 0, previous = 0;
  for (const x of xs) {
    while (next < byLeft.length && byLeft[next][0] <= x) {
      const [, right, height] = byLeft[next++];
      alive.push([height, right]);
    }
    while (alive.size > 0 && alive.peek()[1] <= x) alive.pop(); // lazy deletion: only a dead TOP has to go
    const height = alive.size > 0 ? alive.peek()[0] : 0;
    if (height !== previous) {
      skyline.push([x, height]);
      previous = height;
    }
  }
  return skyline;
}

The heap holds [height, right] pairs, so a building carries its own end with it, and "dead" simply means right <= x. The comparison is <= because the building is half-open: at x === right it no longer covers anything. All buildings that start at or before x are pushed first and all dead tops are popped next, and only then is the height read. That order is what makes ties harmless: a building ending at x and another starting at x are both dealt with before the single read at x.

Dry run: [[2, 9, 10], [3, 7, 15], [5, 12, 12], [15, 20, 10], [19, 24, 8]]

xpushedpoppedheap after, tallest firstheightkey point
2[10, 9]none[10, 9]10[2, 10]
3[15, 7]none[15, 7] [10, 9]15[3, 15]
5[12, 12]none[15, 7] [12, 12] [10, 9]15none, same height
7none[15, 7][12, 12] [10, 9]12[7, 12]
9nonenone[12, 12] [10, 9]12none; [10, 9] is dead but buried
12none[12, 12] and [10, 9]empty0[12, 0]
15[10, 20]none[10, 20]10[15, 10]
19[8, 24]none[10, 20] [8, 24]10none
20none[10, 20][8, 24]8[20, 8]
24none[8, 24]empty0[24, 0]

The answer is [[2, 10], [3, 15], [7, 12], [12, 0], [15, 10], [20, 8], [24, 0]]. Watch x = 9: the building [10, 9] has ended, but it sits under [12, 12], so nobody notices and nobody pays for it. It is only removed at x = 12, when it surfaces together with the building on top of it. That is lazy deletion working as intended.

Cost. Sorting the coordinates is O(n log n). Each building is pushed once and popped at most once, each in O(log n), so the sweep is O(n log n) too. The heap never holds more than n entries, so the space is O(n).

The ordered-set alternative. Instead of a heap you can keep the heights of the active buildings in a sorted multiset: insert at the left edge, remove that exact height at the right edge, read the largest. Nothing is left dead, so there is no lazy deletion, and the same O(n log n) holds. JavaScript has no built-in ordered set, which is why the heap version is the one to write in an interview; the ordered structures chapter builds the structure if you want the other version.

⚠ Skyline pitfalls Emit a key point only when the height differs from the previous emitted height; otherwise two equal-height buildings that touch produce a bogus point. Compare with right <= x and not <, or a building that has just ended still holds the outline up for one more key point. Keep the final drop to 0: the heap becoming empty gives height 0, which differs from the last height, so the closing point appears by itself. And sort the coordinates with a numeric comparator.

Offline queries: answer them in the order that suits you

A static pile of intervals and a list of questions about it: how many cover this time, which one is the smallest that contains this point. Answering each question by scanning the intervals costs O(n) per question and O(n · q) in all. The way out is the word offline. If you get all the questions before you must answer any, you are free to answer them in any order you like, as long as each answer is written back to its original slot. The order worth choosing is usually sorted by the thing being asked, because then the sweep above applies: the "line" is the query value moving upward.

To answer in a different order, sort indices rather than values: order = queries.map((_, i) => i).sort((a, b) => queries[a] - queries[b]). Then answer[order[k]] is where the k-th smallest query's answer goes. Sorting the query values themselves loses the positions and you cannot undo it.

Number of flowers in full bloom: a count is a difference of two counts

Each flower is in bloom on the closed range [start, end]. For each visitor arriving at time t, report how many flowers are in bloom. A flower is in bloom at t when start ≤ t and t ≤ end. Count the flowers that have opened by t (start ≤ t) and subtract the ones that have closed before t (end < t). A flower that closed before t opened before t too, so every subtracted flower was counted, and what remains is exactly the flowers in bloom. Each of the two counts is "how many values in a sorted array are at most (or below) t", which is a binary search.

function countAtMost(sorted, t) {
  let lo = 0, hi = sorted.length;
  while (lo < hi) {
    const mid = (lo + hi) >> 1;
    if (sorted[mid] <= t) lo = mid + 1;
    else hi = mid;
  }
  return lo; // how many values are <= t
}

function countBelow(sorted, t) {
  let lo = 0, hi = sorted.length;
  while (lo < hi) {
    const mid = (lo + hi) >> 1;
    if (sorted[mid] < t) lo = mid + 1;
    else hi = mid;
  }
  return lo; // how many values are < t
}
function fullBloomFlowers(flowers, people) {
  const starts = flowers.map(([start]) => start).sort((a, b) => a - b);
  const ends = flowers.map(([, end]) => end).sort((a, b) => a - b);
  return people.map((t) => countAtMost(starts, t) - countBelow(ends, t)); // opened by t, minus closed before t
}

Dry run: flowers [[1, 6], [3, 7], [9, 12], [4, 13]], visitors [2, 3, 7, 11]

starts = [1, 3, 4, 9], ends = [6, 7, 12, 13]

tstarts ≤ tends < tin bloom
21 (just 1)01
32 (1 and 3)02
73 (1, 3, 4)1 (just 6)2
1142 (6 and 7)2

The answer is [1, 2, 2, 2]. Look at t = 7: the flower [3, 7] ends at 7 and is still in bloom, which is why the second count uses end < t and not end <= t. Using the same comparison in both helpers is the standard off-by-one here. Sorting is O(n log n) and each visitor costs two binary searches, so the total is O((n + q) log n), with O(n) extra space. This version does not even need the queries sorted, because binary search answers any order.

Minimum interval to include each query: the sweep with a heap

Now the question changes to: for each query point q, what is the size (right - left + 1) of the smallest interval that contains it, or -1 if there is none? A count could be split into two counts; a minimum cannot. So keep the intervals that currently contain q in a min-heap keyed by size, and sort the queries so that q only grows.

Sorted this way, two things happen as q increases. Intervals with left ≤ q become candidates, and they stay candidates until they end. Intervals with right < q are useless for this query and also for every later one, since later queries are larger. That permanent death is exactly what lazy deletion needs. So: push every interval whose left end is at or before q, pop dead tops, and the top is the answer. The code reuses the Heap class from the skyline, here ordered so that the smallest size comes out first.

function minInterval(intervals, queries) {
  const byLeft = [...intervals].sort((a, b) => a[0] - b[0]);
  const order = queries.map((_, i) => i).sort((a, b) => queries[a] - queries[b]); // query indices, smallest query first
  const open = new Heap((a, b) => a[0] < b[0]); // entries [size, right]; the smallest size leaves first
  const answer = new Array(queries.length).fill(-1);
  let next = 0;
  for (const qi of order) {
    const q = queries[qi];
    while (next < byLeft.length && byLeft[next][0] <= q) {
      const [left, right] = byLeft[next++];
      open.push([right - left + 1, right]);
    }
    while (open.size > 0 && open.peek()[1] < q) open.pop(); // ended before q, so it also ends before every later query
    if (open.size > 0) answer[qi] = open.peek()[0];
  }
  return answer;
}

Dry run: intervals [[1, 4], [2, 4], [3, 6], [4, 4]], queries [2, 3, 4, 5]

Heap entries are [size, right]; the queries are already in sorted order here.

qpushedpoppedheap after, smallest firstanswer
2[4, 4] and [3, 4]none[3, 4] [4, 4]3
3[4, 6]none[3, 4] [4, 4] [4, 6]3
4[1, 4]none[1, 4] [3, 4] [4, 4] [4, 6]1
5none[1, 4] and [3, 4][4, 6] on top, with the dead [4, 4] buried under it4

The result is [3, 3, 1, 4]. At q = 5 the entries [1, 4] and [3, 4] sit on top and both ended at 4, so both are popped. Two entries of size 4 remain: [4, 6], which is still alive, and [4, 4], which is dead. This run leaves the live one on top, so the loop stops and the answer is 4; had the dead one been on top, it would have been popped next and the answer would be the same. A dead entry that stays buried is harmless, which is the whole point of deleting lazily.

The cost is one sort of the intervals, one sort of the query indices, and a push and a pop per interval, so O((n + q) log(n + q)) time and O(n + q) space. The pitfalls are the ones to rehearse. Write the answer at answer[qi], not at the sorted position. Pop on right < q, not <=, because the range is closed. And do not forget the -1 default for a query that no interval reaches.

When offline is not available

Offline needs every query up front, and no query may depend on an earlier answer. If the queries arrive one at a time and each must be answered before the next appears, there is no sorted order to exploit, and you need a structure that answers any query on demand, such as the trees in the segment and Fenwick tree chapter. Ask the interviewer which case you are in. "Are all queries given up front?" is often the most valuable single question in these problems.

Say it like this → "Since all the queries are known up front, I'll sort them and sweep. As the query value only increases, an interval that has ended never matters again, so I can delete lazily from the heap, and I'll keep each query's original index to put the answers back in order."

Coordinate compression, exactly

Many of these problems give coordinates up to 109 but only a few hundred or thousand distinct ones. The algorithm cannot index an array by such a coordinate, but it does not need to: a sweep, a sort or a segment tree only asks whether one coordinate is smaller than another. Coordinate compression replaces each value by its rank among the distinct values, which keeps every comparison and shrinks the range to at most the number of distinct values.

function compress(values) {
  const sorted = [...new Set(values)].sort((a, b) => a - b); // the comparator matters: the default sort is textual
  const rank = new Map(sorted.map((value, i) => [value, i]));
  return { sorted, rank };
}

function rankTransform(array) {
  const { rank } = compress(array);
  return array.map((value) => rank.get(value) + 1);
}

For [100, 7, 1000000000, 7, 42] the distinct sorted values are [7, 42, 100, 1000000000], so the ranks are 0, 1, 2 and 3, and rankTransform returns [3, 1, 4, 1, 2] with ranks starting at 1. Equal values get equal ranks, and the order is preserved. The cost is O(n log n) for the sort, and each later lookup in the Map is O(1) on average. If you need the rank of a value that may not be in the set, such as a query coordinate, binary search the sorted array instead: the number of distinct values below it, countBelow(sorted, v) from above, is its rank.

What compression keeps and what it destroys. Order survives; distances do not. Two coordinates one apart and two coordinates a billion apart can be neighbours after compression. Anything that depends on a distance must go back to the original values, and the sorted array is how: the length of the gap between rank i and rank i + 1 is sorted[i + 1] - sorted[i]. The rectangle union below uses exactly that.

⚠ Compression pitfalls Include both endpoints of every interval or rectangle, not only the starts. Remove duplicates before ranking, or equal values get different ranks. Sort with (a, b) => a - b. And mind closed against half-open: if closed integer intervals [l, r] must keep the unit gaps between them, add r + 1 to the values and treat them as [l, r + 1).

Rectangle union: the simple way and the segment tree

Given n axis-aligned rectangles [x1, y1, x2, y2], find the area of their union, with overlaps counted once. Adding the areas counts overlaps several times, and correcting for them by inclusion and exclusion is exponential. Counting grid cells fails when the coordinates reach 109.

The idea. Sweep along x. Between two consecutive distinct x coordinates (a slab) the set of rectangles that cover the slab does not change, so the covered height is one number for the whole slab, and the slab contributes width × covered height. The covered height is the total length of the union of the y-ranges of those rectangles, which is the interval merge from the intervals chapter. There are at most 2n - 1 slabs.

function unionAreaSimple(rects) {
  const xs = [...new Set(rects.flatMap(([x1, , x2]) => [x1, x2]))].sort((a, b) => a - b);
  const byBottom = [...rects].sort((a, b) => a[1] - b[1]); // sorted by y once, so each slab needs no sort
  let total = 0n;
  for (let i = 0; i + 1 < xs.length; i++) {
    const left = xs[i], right = xs[i + 1];
    let covered = 0, reach = -Infinity;
    for (const [x1, y1, x2, y2] of byBottom) {
      if (x1 > left || x2 < right || y2 <= reach) continue; // not covering this slab, or already inside what is covered
      covered += y2 - Math.max(y1, reach); // count only the part above what is already covered
      reach = y2;
    }
    total += BigInt(right - left) * BigInt(covered); // width times height can pass 2 ** 53
  }
  return total;
}

Sorting the rectangles by bottom edge once means every slab can merge the y-ranges in one pass, with reach as the highest point covered so far: a range lying entirely below reach adds nothing, and any other adds only the part above reach. There are up to 2n - 1 slabs, and each scans n rectangles, so this is O(n²) time and O(n) space. For rectangles [[0, 0, 2, 2], [1, 0, 2, 3], [1, 0, 3, 1]] the slabs are [0, 1], [1, 2] and [2, 3] with covered heights 2, 3 and 1, so the area is 2 + 3 + 1 = 6.

Faster: let a segment tree keep the covered length. The quadratic version recomputes the covered length from scratch for every slab, though only one or two rectangles enter or leave between slabs. What is needed is a structure that supports "add 1 to the cover of the y-range [a, b)", "subtract 1 from it", and "total covered length right now". Compress the y coordinates first. The elementary segments are the gaps between neighbouring distinct y values, numbered 0 to m - 1, and a rectangle covers the segments from the rank of its bottom to the rank of its top, exclusive.

Each tree node stands for a range of elementary segments and stores two numbers. cover is how many rectangles have this node as one of the pieces their y-range was split into, so each such rectangle covers the node's whole range. length is the covered length inside the node's range. If cover is positive, the whole range is covered and the length is ys[hi] - ys[lo]. Otherwise the length is whatever the two children report, or 0 at a leaf. Nothing has to be pushed down to children, because every removal subtracts from exactly the nodes where the matching insertion added, and after every update the root's length is the covered height.

function unionArea(rects) {
  const ys = [...new Set(rects.flatMap(([, y1, , y2]) => [y1, y2]))].sort((a, b) => a - b);
  const yIndex = new Map(ys.map((y, i) => [y, i]));
  const segments = ys.length - 1; // the gaps between neighbouring y values
  if (segments < 1) return 0n;
  const cover = new Array(4 * segments).fill(0); // how many rectangles cover this node's whole range
  const length = new Array(4 * segments).fill(0); // covered length inside this node's range

  function update(node, lo, hi, from, to, delta) { // node stands for gaps lo .. hi - 1
    if (to <= lo || hi <= from) return;
    if (from <= lo && hi <= to) {
      cover[node] += delta;
    } else {
      const mid = (lo + hi) >> 1;
      update(2 * node, lo, mid, from, to, delta);
      update(2 * node + 1, mid, hi, from, to, delta);
    }
    if (cover[node] > 0) length[node] = ys[hi] - ys[lo];
    else if (hi - lo === 1) length[node] = 0;
    else length[node] = length[2 * node] + length[2 * node + 1];
  }

  const events = [];
  for (const [x1, y1, x2, y2] of rects) {
    if (x1 === x2 || y1 === y2) continue;
    events.push([x1, 1, yIndex.get(y1), yIndex.get(y2)]);
    events.push([x2, -1, yIndex.get(y1), yIndex.get(y2)]);
  }
  events.sort((a, b) => a[0] - b[0]); // ties do not matter: no width passes between them
  let total = 0n, previousX = events.length > 0 ? events[0][0] : 0;
  for (const [x, delta, from, to] of events) {
    total += BigInt(x - previousX) * BigInt(length[1]); // the covered length held since the last event
    previousX = x;
    update(1, 0, segments, from, to, delta);
  }
  return total;
}

Dry run: [[0, 0, 2, 2], [1, 0, 2, 3], [1, 0, 3, 1]], with y values 0, 1, 2, 3

Event at xGaps changedArea added before itTotalCovered height after
0: add rectangle 1[0, 2)002
1: add rectangle 2[0, 3)1 × 2 = 223
1: add rectangle 3[0, 1)0 × 3 = 023
2: remove rectangle 1[0, 2)1 × 3 = 353
2: remove rectangle 2[0, 3)0 × 3 = 051
3: remove rectangle 3[0, 1)1 × 1 = 160

The total is 6, matching the slab version. Two events at x = 1 and two at x = 2 are processed in whatever order the sort leaves them, and the second of each pair adds area 0, so the order does not matter. Each update touches O(log m) nodes, and there are 2n updates, so the whole sweep is O(n log n) after sorting, with O(n) space.

⚠ Union-area pitfalls Areas overflow. A single rectangle with coordinates up to 109 has area 1018, far beyond 253, which is why the code multiplies in BigInt. A problem that asks for the area modulo 109 + 7 wants Number(area % 1000000007n) at the very end. Use the same [from, to) pair for a rectangle's removal as for its insertion, or the counts do not cancel. And skip rectangles with zero width or height: they add nothing, and a zero-height one would build an empty range.

Geometry in integers: cross product and orientation

The rest of the chapter is plane geometry, and nearly all of it rests on one operation. For vectors u = (ux, uy) and v = (vx, vy), the cross product is the number ux · vy - uy · vx. To see what it means, write the vectors by length and angle, u = |u| (cos α, sin α) and v = |v| (cos β, sin β). Then the cross product is |u| |v| (cos α sin β - sin α cos β), which is |u| |v| sin(β - α). Its sign is the sign of the sine of the angle you turn through to get from u to v: positive for a counter-clockwise turn of less than a half turn, negative for clockwise, and zero when the vectors are parallel or one is zero. Its size is the area of the parallelogram the two vectors span.

For three points, take the vectors from o to a and from o to b. Walking o → a → b turns left if the cross product is positive, right if negative, and goes straight (or back on itself) if zero.

o a b o a b o a b cross > 0: left turn cross < 0: right turn cross = 0: collinear
The sign of the cross product is the turn direction, drawn here with y pointing up.
function cross(o, a, b) {
  return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
}

function orientation(o, a, b) {
  const c = cross(o, a, b);
  return c > 0 ? 1 : c < 0 ? -1 : 0; // 1: left turn, -1: right turn, 0: collinear
}

orientation reduces the number to -1, 0 or 1. Keep to that sign whenever you only need the direction: the size of cross is easy to overflow, while its sign is all the algorithms below use. One caution about pictures: on a screen, y grows downward, which mirrors the plane, so a "left turn" in the formula looks clockwise there. Decide by the sign and the formula, not by how the points look on screen.

Exactness. With integer coordinates every operation here is an integer operation, so the result is exact, and zero really means collinear. A Number holds integers exactly up to 253, about 9 · 1015. If every coordinate has absolute value at most M, each difference is at most 2M, each product at most 4M², and the cross product at most 8M². That stays below 253 while M is up to about 3 · 107. Beyond that, use BigInt, shown in the floating-point section.

Collinear points and the point-on-segment test

Three points are collinear when cross(a, b, c) === 0. That says c is on the line through a and b, not on the segment between them. To restrict it to the segment, add the bounding box check: c must lie between the endpoints in x and in y.

function onSegment(a, b, p) {
  return (
    cross(a, b, p) === 0 &&
    Math.min(a[0], b[0]) <= p[0] && p[0] <= Math.max(a[0], b[0]) &&
    Math.min(a[1], b[1]) <= p[1] && p[1] <= Math.max(a[1], b[1])
  );
}

Why the box is enough once the point is on the line: along a line, x and y both change steadily, so a point between the endpoints in the coordinate that varies is between them on the line. For a vertical segment the x test is trivially true and the y test does the work; for a horizontal one it is the other way round. A degenerate "segment" with a === b has cross product 0 for every p, and the box shrinks to a single point, so only p === a passes, which is correct. This is O(1).

Do two segments intersect?

Segment ab and segment cd cross properly when c and d are on opposite sides of the line through a and b, and a and b are on opposite sides of the line through c and d. Opposite sides means the two orientations differ. Both conditions are needed: the first says that segment cd crosses the line through ab, but not that it crosses the segment ab itself, and the second closes that gap from the other side.

Write the comparison as "the two orientations differ", not as "the product of two cross products is negative". A zero is then its own side, and the same test also accepts an endpoint that lies on the other segment (a T) and two segments that share an endpoint, which is what interviewers like to probe. What it cannot see is the case where both endpoints of one segment lie on the other's line, so two orientations are 0 and equal: the segments are collinear and may overlap or leave a gap. A segment that has shrunk to a single point lands there too. For those, check whether an endpoint of one segment lies inside the other: orientation 0 and inside its bounding box.

function segmentsIntersect(a, b, c, d) {
  const o1 = orientation(a, b, c), o2 = orientation(a, b, d);
  const o3 = orientation(c, d, a), o4 = orientation(c, d, b);
  if (o1 !== o2 && o3 !== o4) return true; // each segment straddles the other's line
  if (o1 === 0 && onSegment(a, b, c)) return true; // an endpoint lies on the other segment
  if (o2 === 0 && onSegment(a, b, d)) return true;
  if (o3 === 0 && onSegment(c, d, a)) return true;
  if (o4 === 0 && onSegment(c, d, b)) return true;
  return false;
}

Dry run on seven cases

Casea, b, c, do1, o2, o3, o4What decidesResult
X crossing(0,0) (4,4) (0,4) (4,0)1, -1, -1, 1o1 ≠ o2 and o3 ≠ o4true
T: an end on the other(0,0) (4,0) (2,0) (2,3)0, 1, 1, -1o1 ≠ o2 and o3 ≠ o4, although o1 is 0true
shared endpoint(0,0) (2,2) (2,2) (4,0)0, -1, -1, 0o1 ≠ o2 and o3 ≠ o4true
collinear, overlapping(0,0) (3,3) (2,2) (5,5)0, 0, 0, 0the first test cannot fire; (2,2) is on abtrue
collinear, a gap(0,0) (1,1) (2,2) (3,3)0, 0, 0, 0no endpoint lies inside the other segmentfalse
parallel(0,0) (4,0) (0,1) (4,1)1, 1, -1, -1o1 = o2, and nothing is 0false
near miss: lines cross, segments do not(0,0) (4,0) (5,-1) (5,1)-1, 1, 1, 1o3 = o4: a and b are on the same side of cdfalse

In the T case c is on the line of ab while d is off it, and a and b fall on opposite sides of the line through cd, so the first test reports the touch even though o1 is 0. In the two all-zero rows the first test cannot fire, since o1 === o2, and only the endpoint checks can say true. Two collinear segments that overlap always have an endpoint of one inside the other, so those four checks are enough. Across 200,000 random pairs of segments on a 5 × 5 grid, including collinear, touching and single-point segments, the function matched an independent exact calculation every time, and it gave the same answer with the two segments swapped or their endpoints reversed.

⚠ Segment pitfalls Do not test cross1 * cross2 < 0: the product of two large cross products can overflow even when each is exact, and it also loses the zero case. Compare the orientations, which are only -1, 0 and 1. Do not stop after the first test; the collinear case needs the four endpoint checks. And do not assume the endpoints come in a convenient order: swapping a with b, or one segment with the other, must not change the answer.
Say it like this → "I'll use integer cross products so the result is exact. Segments cross if each one's endpoints are on opposite sides of the other's line. Then I'll handle the zero cases: an endpoint on the other segment, and the collinear overlap, with a bounding-box check."

Polygon area by the shoelace formula

The cross product gives the area of a triangle: for the triangle o, a, b it is half of cross(o, a, b), with a sign. A polygon with vertices p0, p1, ..., p(n-1) given in order around its boundary can be cut into the fan of triangles (p0, p1, p2), (p0, p2, p3), .... Adding their signed areas gives the polygon's area, and this holds for concave polygons too. A fan triangle can stick out of a concave polygon, but each triangle carries the sign of its own turn, so a piece that sticks out is counted once forwards and once backwards by the triangles of the fan, and cancels. It is called the shoelace formula because, written out in coordinates, the products cross like laces.

function doubledArea(polygon) {
  let sum = 0;
  for (let i = 1; i + 1 < polygon.length; i++) {
    sum += cross(polygon[0], polygon[i], polygon[i + 1]); // the triangle fan from vertex 0
  }
  return sum; // positive for counter-clockwise, negative for clockwise
}

The function returns twice the area, as an integer, which avoids the halves and keeps the arithmetic exact; divide by 2 at the end if you need the area itself. The sign tells the orientation: positive when the vertices run counter-clockwise, negative for clockwise, so Math.abs gives the area and the sign is a free orientation test. For the rectangle [[0, 0], [4, 0], [4, 3], [0, 3]] the fan has two triangles with doubled areas 12 and 12, so the result is 24 and the area is 12. For the concave polygon [[0, 0], [6, 0], [6, 6], [3, 2], [0, 6]] it returns 48, an area of 24, and reversing the vertex order gives -48. Time is O(n), space O(1).

A cross-check you can trust. For a polygon whose vertices all have integer coordinates, Pick's theorem says that area = I + B / 2 - 1, where I is the number of lattice points strictly inside and B the number on the boundary. B is the sum of gcd(|dx|, |dy|) over the edges, using the gcd from the math chapter: an edge from one lattice point to another contributes exactly that many lattice points, counting one of its ends. In the chapter's checks, the shoelace result equalled 2I + B - 2 on three thousand random polygons.

⚠ Shoelace pitfalls The vertices must be in boundary order, not sorted. A polygon whose edges cross itself does not have an "area" this formula can report; the signed pieces cancel in ways that mean nothing. Repeating the first vertex at the end adds a degenerate triangle of area 0, which is harmless, and fewer than 3 vertices give 0. For large coordinates the cross products can leave the safe range, so the BigInt version applies.

Point in polygon by ray casting

Is a point inside a polygon, possibly a concave one? Shoot a ray from the point to the right, to infinity, and count how many edges it crosses. Each crossing switches between outside and inside, and the far end of the ray is outside, so the point is inside exactly when the count is odd. That is the whole idea. The difficulty is in the edge cases, and an exact integer test removes all of them.

Three things go wrong if you code it carelessly:

  • The ray passes through a vertex. Two edges meet there, and a naive test counts the crossing twice, or not at all.
  • An edge lies along the ray. A horizontal edge on the ray's row has no single crossing point.
  • The point is on the boundary. Neither "inside" nor "outside" is obviously right.

The first two are solved by one half-open rule. Count an edge only when (a.y > p.y) !== (b.y > p.y): exactly one endpoint is strictly above the ray's row. This treats a vertex exactly on the row as if it were slightly below the ray, so the ray never actually touches a vertex. An edge ending on the row is counted only if its other end is above, once, and a horizontal edge, which has both ends on the same side, is never counted. For an edge that passes the test, decide whether the crossing is to the right of the point with the cross product sign: for an edge going up, the point must be on its left (cross > 0); for an edge going down, on its right (cross < 0). No division and no rounding are involved. The boundary is handled first, with onSegment.

function pointInPolygon(p, polygon) {
  let crossings = 0;
  for (let i = 0; i < polygon.length; i++) {
    const a = polygon[i], b = polygon[(i + 1) % polygon.length];
    if (onSegment(a, b, p)) return "boundary";
    if ((a[1] > p[1]) !== (b[1] > p[1])) { // the edge straddles the row of p; a flat edge never does
      const side = cross(a, b, p);
      if (b[1] > a[1] ? side > 0 : side < 0) crossings++; // the crossing lies to the right of p
    }
  }
  return crossings % 2 === 1 ? "inside" : "outside";
}

Dry run: the diamond [[2, 0], [4, 2], [2, 4], [0, 2]] and the point (1, 2)

The ray from (1, 2) runs right along y = 2, straight through the vertex (4, 2).

EdgeOne end strictly above y = 2?cross(a, b, p)Counted?
(2,0) to (4,2)no: 0 and 2 are both not above 2not computedno
(4,2) to (2,4)yes: only (2,4) is above6, edge goes up, positiveyes
(2,4) to (0,2)yes: only (2,4) is above2, edge goes down, needs negativeno
(0,2) to (2,0)nonot computedno

One crossing, an odd count, so the point is inside, which is right. The vertex (4, 2) was used by two edges but counted once. A naive rule that counts an edge whenever the row lies between its ends inclusively counts both edges at (4, 2) and gets 2, an even number, so it calls the point outside. The same function reports (5, 2) as outside, (4, 2) as the boundary and (2, 2) as inside. Time is O(n) per point and space is O(1).

⚠ Point-in-polygon pitfalls Decide with the interviewer whether a boundary point counts as inside; the code returns three answers precisely so the caller can choose. Keep the onSegment test before the ray count, or a point on an edge is classified inconsistently: the half-open rule calls it inside on one side of the polygon and outside on the other. Use the identical comparison, > p.y, on both ends of every edge: mixing > and >= from one edge to the next makes a shared vertex count differently for its two edges. For a convex polygon with counter-clockwise vertices there is a simpler test: the point is inside or on the boundary exactly when cross(a, b, p) >= 0 for every edge.

Rectangles: overlap, area and the perfect rectangle

An axis-aligned rectangle is [x1, y1, x2, y2] with x1 < x2 and y1 < y2: bottom-left and top-right corners. Two rectangles overlap in a region of positive area exactly when their x-ranges overlap and their y-ranges overlap. Two ranges (a1, a2) and (b1, b2) overlap when a1 < b2 and b1 < a2, the same test as the My Calendar check in the intervals chapter.

function rectanglesOverlap(a, b) {
  return a[0] < b[2] && b[0] < a[2] && a[1] < b[3] && b[1] < a[3]; // strict: sharing an edge is not overlap
}

function overlapArea(a, b) {
  const width = Math.min(a[2], b[2]) - Math.max(a[0], b[0]);
  const height = Math.min(a[3], b[3]) - Math.max(a[1], b[1]);
  return Math.max(0, width) * Math.max(0, height); // clamp each side separately, then multiply
}

function totalArea(a, b) {
  const area = (r) => (r[2] - r[0]) * (r[3] - r[1]);
  return area(a) + area(b) - overlapArea(a, b);
}

The comparisons are strict, so two rectangles that share only an edge or a corner do not overlap. If a problem counts touching as overlap, use <=, and say so. For the area, each side of the overlap is the smaller of the two top values minus the larger of the two bottom values, and that can be negative when the rectangles are apart. Clamp each side at 0 before multiplying. Clamping only the product is the classic bug: two negative sides multiply to a positive number, and rectangles that are apart in both directions would report a phantom overlap. The union of two rectangles has area A + B - overlap. All three functions are O(1), and they agreed with a cell-by-cell count on twenty thousand random pairs.

The perfect rectangle: two checks, each catching what the other misses

Given n rectangles, do they fit together to cover one larger rectangle exactly, with no gaps and no overlaps? Two tests, both linear:

  1. The areas add up. The sum of the rectangle areas must equal the area of the bounding box, the smallest rectangle containing all of them.
  2. The corners cancel. Look at every corner of every rectangle. In a perfect tiling, a point where tiles meet is a corner of an even number of them: a point inside a straight edge of one tile is the corner of two neighbours, and a point where four tiles meet is the corner of all four. Only the four corners of the whole bounding box belong to exactly one tile. So toggle each corner in a set, adding it if absent and removing it if present. At the end, exactly the four bounding-box corners must be left.
function isRectangleCover(rects) {
  let left = Infinity, bottom = Infinity, right = -Infinity, top = -Infinity, area = 0;
  const corners = new Set();
  const toggle = (x, y) => {
    const key = x + "," + y;
    if (corners.has(key)) corners.delete(key);
    else corners.add(key);
  };
  for (const [x1, y1, x2, y2] of rects) {
    left = Math.min(left, x1);
    bottom = Math.min(bottom, y1);
    right = Math.max(right, x2);
    top = Math.max(top, y2);
    area += (x2 - x1) * (y2 - y1);
    toggle(x1, y1);
    toggle(x1, y2);
    toggle(x2, y1);
    toggle(x2, y2);
  }
  if (area !== (right - left) * (top - bottom)) return false;
  return (
    corners.size === 4 &&
    corners.has(left + "," + bottom) && corners.has(left + "," + top) &&
    corners.has(right + "," + bottom) && corners.has(right + "," + top)
  );
}

Neither test is enough alone, and it is worth seeing why. The two rectangles [0, 0, 1, 2] and [0, 0, 2, 1] overlap in a unit square and leave a unit gap at the top right, so the area is 2 + 2 = 4, which equals the 2 × 2 bounding box and passes the first check; but the corner (0, 0) is toggled twice and disappears, so the second check fails. Three copies of [0, 0, 1, 1] toggle every corner three times, so exactly the four box corners remain and the second check passes, but the area 3 is not 1, so the first check fails. Together they catch both.

A note on how much to trust the pair. The necessity of both tests is the argument above. That the two together are also sufficient is true, but the proof is longer than the code. What this chapter relies on is a brute-force comparison: every set of up to 5 rectangles on a 3 × 3 grid and up to 4 on a 4 × 4 grid, about 5.3 million sets in all, plus twenty thousand tilings with one rectangle removed, duplicated, stretched, shrunk or shifted, checked cell by cell. It never disagreed. Time is O(n) and space is O(n) for the set.

Dry run: [[1, 1, 3, 3], [3, 1, 4, 2], [3, 2, 4, 4], [1, 3, 2, 4], [2, 3, 3, 4]]

RectangleAreaCorners toggled
[1, 1, 3, 3]4(1,1) (1,3) (3,1) (3,3)
[3, 1, 4, 2]1(3,1) removed; (3,2) (4,1) (4,2) added
[3, 2, 4, 4]2(3,2) removed; (3,4) added; (4,2) removed; (4,4) added
[1, 3, 2, 4]1(1,3) removed; (1,4) (2,3) (2,4) added
[2, 3, 3, 4]1(2,3) removed; (2,4) removed; (3,3) removed; (3,4) removed

The areas sum to 9, the bounding box is 3 × 3 = 9, and the corners left are (1,1), (1,4), (4,1) and (4,4), which are exactly the four box corners. The answer is true. The input [[1, 1, 2, 3], [1, 3, 2, 4], [3, 1, 4, 2], [3, 2, 4, 4]] leaves the middle column empty: its areas sum to 6 against a box of 9, so the first check already fails.

⚠ Perfect-rectangle pitfalls The tests assume every rectangle has positive width and height. A zero-area rectangle would still toggle corners. Do not forget to compare the box corners by value and not only the count of 4: four leftover points that are not the box corners fail. Build set keys as strings, such as x + "," + y, rather than packing two numbers into one, which can collide. And remember an empty list has no bounding box: the code above returns false because the infinite bounds never match an area of 0.

Maximum points on a line: hash the direction, not the slope

Given n points, find the largest number that lie on one straight line. Testing every triple with the cross product is O(n³). The better question is: fix one point as an anchor. Every other point gives a direction from the anchor, and the points that line up with the anchor are exactly the ones with the same direction. So count points per direction with a Map, and the best line through the anchor holds the largest count plus the anchor. Repeat for every anchor: O(n²) lookups.

The direction is the whole problem. The slope dy / dx looks natural, but it fails twice. A vertical line has dx = 0 and needs a special case, and a division of two integers can give the same floating-point number for two different slopes. The exact key is the pair (dy, dx) after two normalisations. Reduce it by dividing both by their gcd, taken from the math chapter, so that (2, 4), (1, 2) and (3, 6) all become (1, 2): two integer vectors are parallel exactly when they are the same vector after dividing out the gcd and fixing the sign. Fix the sign, so that (1, 2) and (-1, -2), which point opposite ways along the same line, get one key: make dx positive, and when dx is 0, make dy positive.

function maxPoints(points) {
  const n = points.length;
  if (n <= 2) return n;
  let best = 1;
  for (let i = 0; i < n; i++) {
    const slopes = new Map();
    let duplicates = 0, onBestLine = 0;
    for (let j = i + 1; j < n; j++) {
      let dx = points[j][0] - points[i][0];
      let dy = points[j][1] - points[i][1];
      if (dx === 0 && dy === 0) {
        duplicates++; // the same point again belongs to every line through i
        continue;
      }
      const g = gcd(dx, dy);
      dx /= g;
      dy /= g;
      if (dx < 0 || (dx === 0 && dy < 0)) { // one direction, not two opposite ones
        dx = -dx;
        dy = -dy;
      }
      const key = dy + "/" + dx;
      const count = (slopes.get(key) ?? 0) + 1;
      slopes.set(key, count);
      onBestLine = Math.max(onBestLine, count);
    }
    best = Math.max(best, onBestLine + duplicates + 1);
  }
  return best;
}

Two details deserve a sentence each. The loop looks only at points after the anchor (j > i): the best line's lowest-indexed point, used as the anchor, sees every other point on that line later in the array, so nothing is missed. And a point identical to the anchor lies on every line through the anchor, so it is counted separately in duplicates and added to whichever direction wins. If every point is the same, onBestLine stays 0 and the answer is the number of duplicates plus the anchor, which is n.

Dry run: anchor (1, 4) among (1,4) (1,1) (3,2) (5,3) (4,1) (2,3)

Point(dx, dy)gcdKey after reducing and fixing the signCount so far
(1,1)(0, -3)31/01
(3,2)(2, -2)2-1/11
(5,3)(4, -1)1-1/41
(4,1)(3, -3)3-1/12
(2,3)(1, -1)1-1/13

The key is written dy/dx. Three points share -1/1, so the line through the anchor holds 3 + 1 = 4 points, the answer for this input. The row for (1,1) shows the sign rule: the vertical direction (0, -3) becomes 1/0, not -1/0. Time is O(n² log C), where C bounds the coordinate differences, because each gcd takes a logarithmic number of steps, and the space is O(n) for one map.

⚠ Slope-hashing pitfalls The single most tested trap: the points (0, 0), (94911150, 94911151) and (94911149, 94911150) are not on one line, but the slopes 94911151 / 94911150 and 94911150 / 94911149 are the same Number, so a floating-point key reports 3 points where the answer is 2. The reduced integer key gives 2. Also: skip the gcd when both differences are zero, because gcd(0, 0) is 0 and dividing by it gives NaN; normalise the sign after the division; and let the vertical case produce the ordinary key "1/0", a string, with no Infinity and no special branch.

Valid square without a single square root

Four points are given in any order. Do they form a square? Trying the orderings and checking right angles works but is clumsy. A neater test uses distances. A square with side s has six pairwise distances: four equal sides, and two equal diagonals. In squared form, which stays in integers, the sides are s² and the diagonals are 2s² by Pythagoras. So compute the six squared distances, sort them, and require four equal smallest values that are not zero, then two equal larger values, each twice the smaller.

function squaredDistance(a, b) {
  return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2;
}

function validSquare(p1, p2, p3, p4) {
  const points = [p1, p2, p3, p4];
  const d = [];
  for (let i = 0; i < 4; i++) {
    for (let j = i + 1; j < 4; j++) d.push(squaredDistance(points[i], points[j]));
  }
  d.sort((a, b) => a - b);
  return d[0] > 0 && d[0] === d[3] && d[4] === d[5] && d[4] === 2 * d[0]; // four equal sides, two diagonals of twice the square
}

For (0,0) (1,1) (1,0) (0,1) the six squared distances sorted are [1, 1, 1, 1, 2, 2]: four of 1, two of 2, and 2 is twice 1, so the answer is true. A tilted square such as (0,0) (2,1) (1,3) (-1,2) gives [5, 5, 5, 5, 10, 10] and is accepted. The parallelogram (0,0) (2,0) (3,1) (1,1), whose sides have two different lengths, gives [2, 2, 2, 4, 4, 10] and is rejected. The cost is 6 distances and a sort of 6 numbers: O(1).

Why the pattern is sufficient. That a square produces it is clear. The converse needs an argument, because the test never looks at angles. Suppose four of the six squared distances equal d > 0 and the other two equal 2d. The two long pairs either share a point or are disjoint.

  • Disjoint long pairs, say AC and BD: then the four other pairs AB, BC, CD, DA have squared length d. In triangle ABC, AB² + BC² = 2d = AC², so the angle at B is a right angle, and it is an isosceles right triangle on the hypotenuse AC. The same holds for triangle ADC. There are only two points that make an isosceles right triangle on a given hypotenuse, one on each side of it; D cannot equal B because BD² = 2d > 0, so D is the other one, and ABCD is a square.
  • Long pairs sharing a point, say PQ and PR: then PS, QR, QS, RS all have squared length d, so Q, R and S form an equilateral triangle of side √d, and P is at distance √d from S and √(2d) from both Q and R. P is then on the perpendicular bisector of QR, which passes through S, at distance √d from S: either on the far side of S from QR or on the same side as QR. Measuring from the midpoint M of QR, S is (√3/2)√d from M, so those two positions are (√3/2 + 1)√d and (1 - √3/2)√d from M, while PQ² = 2d demands PM² = 2d - d/4, that is (√7/2)√d. Neither matches, so this case cannot occur.

I also checked the test against a permutation-and-right-angle definition on all 65,536 ordered quadruples of points from a 4 × 4 grid, and on 100,000 random ones, with no disagreement.

⚠ Valid-square pitfalls Forgetting the d[0] > 0 check accepts four identical points, whose six distances are all 0 and satisfy every equality. A rhombus has four equal sides but unequal diagonals, so it fails the equal-diagonals check, and a rectangle has two different side lengths, so it has no four equal smallest distances. Skip square roots: Math.sqrt rounds, and the squared comparison is exact.

Convex hull by monotone chain (erect the fence)

The convex hull of a set of points is the smallest convex polygon that contains all of them: stretch a rubber band around the points and let go. "Erect the fence" asks for every point that lies on that band, including points in the middle of a straight stretch. The monotone chain method builds the hull in two halves and uses nothing but the cross product.

Sort the points by x, breaking ties by y. Build the lower hull going left to right with a stack: for each new point p, while the stack has two or more points and the last two points a, b together with p make a right turn (cross(a, b, p) < 0), pop b; then push p. Then do the same over the points in reverse order for the upper hull. The two chains joined give the boundary in counter-clockwise order.

Why the cross-product test is the right one. The lower hull is the part of the boundary you see from below: going left to right it only ever turns left (or goes straight), because a right turn would make a dent pointing into the hull. Look at what a right turn means for the three points a, b, p, whose x values do not decrease. Then b lies strictly above the segment from a to p (when x values tie, the same holds with "above" read as the vertical order). The point on that segment directly below b is a blend of a and p, both in the set, so the hull reaches strictly below b there, and b cannot be on the lower boundary. Popping it loses nothing. After the pops stop, the last two stack points and p make a left turn or go straight, and the invariant, "every consecutive triple on the stack turns left or goes straight", holds again with p on top. p itself belongs on the stack because nothing to its right has been seen yet and it is the newest rightmost point of the prefix.

A straight triple, cross === 0, is the one decision that differs between versions. The middle point is on the hull boundary but is not a corner. "Erect the fence" wants it, so pop only when the cross product is strictly negative. A problem that wants only the corners pops on <= 0 as well.

function convexHull(points, keepCollinear) {
  const sorted = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
  const pts = sorted.filter((p, i) => i === 0 || p[0] !== sorted[i - 1][0] || p[1] !== sorted[i - 1][1]);
  if (pts.length <= 2) return pts;
  const mustPop = keepCollinear ? (turn) => turn < 0 : (turn) => turn <= 0;

  function chain(list) {
    const stack = [];
    for (const p of list) {
      while (stack.length >= 2 && mustPop(cross(stack[stack.length - 2], stack[stack.length - 1], p))) stack.pop();
      stack.push(p);
    }
    return stack;
  }

  const lower = chain(pts);
  const upper = chain([...pts].reverse());
  const seen = new Set();
  const hull = [];
  for (const p of [...lower, ...upper]) {
    const key = p[0] + "," + p[1];
    if (!seen.has(key)) {
      seen.add(key);
      hull.push(p);
    }
  }
  return hull;
}

Dry run: [[1,1],[2,0],[2,2],[2,4],[3,3],[4,2]], keeping collinear points

Sorted by x then y, the order is (1,1) (2,0) (2,2) (2,4) (3,3) (4,2).

ChainNew pointPops (cross < 0)Stack afterwards
lower(1,1), (2,0)none(1,1) (2,0)
lower(2,2)none: cross 2(1,1) (2,0) (2,2)
lower(2,4)none: cross 0, straight, kept(1,1) (2,0) (2,2) (2,4)
lower(3,3)(2,4) with -2, (2,2) with -2(1,1) (2,0) (3,3)
lower(4,2)(3,3) with -4(1,1) (2,0) (4,2)
upper(4,2), (3,3), (2,4), (2,2), (2,0)none, with a straight triple kept(4,2) (3,3) (2,4) (2,2) (2,0)
upper(1,1)(2,0) with -2, (2,2) with -2(4,2) (3,3) (2,4) (1,1)

Joining the lower chain (1,1) (2,0) (4,2) with the upper chain (4,2) (3,3) (2,4) (1,1) and dropping repeats gives (1,1) (2,0) (4,2) (3,3) (2,4). The point (2,2) is correctly absent: it is inside the hull. If every point is on one line, both chains hold all of them, and removing repeats leaves each once.

Cost. The sort is O(n log n). In each chain every point is pushed once and popped at most once, so building both chains is O(n), and the total is O(n log n) time and O(n) space. The same code with keepCollinear set to false returns only the corners: for the bottom-edge example (0,0) (1,0) (2,0) (2,2) (0,2) it returns four points, and with true it returns five, including (1,0).

⚠ Convex-hull pitfalls Remove duplicate points first; otherwise a repeated point makes a zero turn that the "strictly negative" rule keeps. Handle fewer than three distinct points before building chains. Choose < or <= on purpose and say which you chose: corners only against every boundary point is the whole difference between two problems. The output order is counter-clockwise starting at the leftmost-lowest point, and some checkers accept any order while others do not, so ask.

Closest pair of points

Given n points, find the smallest distance between any two. Comparing all pairs is O(n²). Two ways reach O(n log n). The divide and conquer version splits the points by x, solves both halves, and repairs the pair that straddles the split; it belongs with the other recursive splits in the divide and conquer chapter. The sweep version fits this chapter: process the points in x order and keep the already-seen points whose x is within the best distance d found so far, ordered by y. For a new point, only the active points whose y is within d of it can improve the answer, and a packing argument says there are a small constant number of them (the argument below gives at most eight): all pairs among the processed points are at least d apart, a box d wide and 2d tall splits into eight squares of side d / 2, and two points in one such square would be closer than d. Finding those neighbours in the y-ordered set needs an ordered structure, which the ordered structures chapter covers.

A short version needs no special structure and is usually fast. Sort by x, and for each point scan backwards through the earlier points, stopping as soon as the gap in x alone is no smaller than the best distance found so far, because every earlier point is at least that far away in x.

function closestPairSquared(points) {
  const pts = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
  let best = Infinity;
  for (let i = 1; i < pts.length; i++) {
    for (let j = i - 1; j >= 0; j--) {
      const dx = pts[i][0] - pts[j][0];
      if (dx * dx >= best) break; // everything further left is at least this far away in x
      const dy = pts[i][1] - pts[j][1];
      best = Math.min(best, dx * dx + dy * dy);
    }
  }
  return best;
}

It returns the squared distance, which is exact in integers and avoids the square root; take Math.sqrt only for display. It returns Infinity for fewer than two points. The honest cost is O(n²) in the worst case: if all points share one x, the gap in x is 0, the early exit never fires, and every pair is compared. On well-spread input it does far less, but when an interviewer asks for the guaranteed bound, use the sweep with an ordered set or the divide and conquer.

Floating point: why geometry stays in integers

JavaScript numbers are 64-bit floating point. Every integer up to 253 is stored exactly, and so is every sum, difference and product of integers whose result stays within that range. Geometry with integer input stays exact as long as no intermediate value leaves that range. Once it does, the damage is silent: no error, just a wrong sign. Here is a concrete case. The points o = (0, 0), a = (94911150, 94911151) and b = (94911149, 94911150) are not collinear, since the exact cross product is 94911150 · 94911150 - 94911151 · 94911149 = 1. In floating point the two products are both 9008126394322500, and the cross product comes out as 0, "collinear". The slopes 94911151 / 94911150 and 94911150 / 94911149 are also the same double. When coordinates can be this large, BigInt restores exactness:

function crossBig(o, a, b) {
  const ax = BigInt(a[0]) - BigInt(o[0]), ay = BigInt(a[1]) - BigInt(o[1]);
  const bx = BigInt(b[0]) - BigInt(o[0]), by = BigInt(b[1]) - BigInt(o[1]);
  return ax * by - ay * bx; // exact whatever the size; compare it with 0n
}

crossBig(o, a, b) for the points above returns 1n. A BigInt is slower and cannot be mixed with a Number, so convert the coordinates up front, and compare the result with 0n, > 0n and < 0n. Use it only when the coordinates can exceed about 3 · 107; below that, Number is exact and much faster.

The rules that follow:

  • Compare, do not divide. Ask whether two slopes are equal by cross-multiplying, dy1 * dx2 === dy2 * dx1, while the products stay in the safe range, or by reducing with the gcd, not by comparing quotients.
  • Compare squared distances. Skip Math.sqrt and Math.hypot: a² + b² < c² is exact for integers and the root is rounded.
  • Do not use an epsilon on integer input. A tolerance such as Math.abs(x - y) < 1e-9 is not transitive: with 0, 0.8e-9 and 1.6e-9, the first two are "equal", the last two are "equal", and the first and last are not. Its right size also depends on the scale of the data. Epsilon belongs to genuinely real-valued input, where you should state the tolerance you chose.
  • Sort by angle with the cross product, not with Math.atan2, when you can: split the plane into two half-planes first, then compare within a half-plane by the sign of the cross product. Two different directions can have the same atan2: the vectors (94911151, 94911150) and (94911150, 94911149) do.
  • Leave a note on the range. "Coordinates up to 104, so the cross product is at most about 8 · 108 and fits" is a sentence that earns trust in an interview.

Two small -0 facts, since negative zero surfaces whenever a zero is negated, as in the sign fix of a direction key. 0 * -1 is -0, which is === to 0 and prints as "0" in a string key, and a Map treats -0 and 0 as one key. So it is harmless in comparisons and hash keys, but Object.is(0 * -1, 0) is false, which matters only to equality checks built on Object.is.

Say it like this → "The coordinates are integers up to a bound I'll state, so I'll keep everything in integers: cross products for orientation, squared distances for comparisons, and a gcd-reduced pair instead of a slope. That avoids every floating-point question, and if the bound were larger I would switch the cross product to BigInt."

Recognizing it in an unseen problem

  • Buildings, intervals, rectangles or events with start and end coordinates, and a question about "the tallest", "the smallest" or "the total covered" at each moment → a sweep line, with a heap, an ordered set or a segment tree as the active set
  • The answer changes only at the endpoints → look at the 2n endpoints only, not the whole range
  • Many queries against a fixed set, all known up front → offline: sort the queries (keeping their indices) and sweep; or turn a count into a difference of two prefix counts with binary search
  • Coordinates up to 109 with few distinct values, and only comparisons matter → coordinate compression, going back to the sorted values when a length is needed
  • "Area of the union of rectangles" → a sweep over x with a segment tree over compressed y, or the quadratic slab version when n is small
  • "Do these segments touch or cross", "which side of the line", "is this a left turn" → the cross product and orientation, handling the zero cases explicitly
  • "Is the point inside this polygon" → ray casting with the half-open rule; "area of this polygon" → the shoelace sum
  • "Most points on a line" or "group points by direction" → a gcd-reduced, sign-normalised pair as the key, not a floating-point slope
  • "Do these four points form a square" → six squared distances; "which points enclose all the rest" → monotone chain
  • Any of the above with huge coordinates → check whether a cross product can pass 253, and switch to BigInt if it can
Before you move on
  • List the four steps of the sweep-line recipe, and say which structure you would pick for "how many", "the largest", "the nearest value" and "the total covered length".
  • Explain how the order of a start and an end at the same coordinate follows from closed against half-open, and name two ways to avoid tie-breaking altogether.
  • Write the skyline with a heap of [height, right] pairs, and say why lazy deletion is correct and why the sweep must read the height only after all events at one x.
  • Answer "flowers in full bloom" as a difference of two binary searches, and say why one bound is <= and the other <.
  • Solve the minimum interval for each query offline, and say why sorting the queries makes deleting an ended interval permanent.
  • Write coordinate compression from memory and say what it preserves and what it destroys.
  • Explain the slab idea behind rectangle union, then how the segment tree's cover and length fields give the covered height without pushing anything down.
  • Write cross and orientation, explain why the sign is the turn direction, and state the coordinate range in which Number is exact.
  • Write the segment intersection test and trace the T, shared-endpoint, collinear-overlap and collinear-gap cases.
  • Compute a polygon's doubled area with the fan of triangles, and read the orientation from its sign.
  • Cast a ray with the half-open rule, and show how a vertex on the ray is counted once.
  • Compute rectangle overlap area with each side clamped at 0, and explain why the perfect-rectangle test needs both the area check and the corner check.
  • Key a direction by a gcd-reduced pair with a fixed sign, and give the example where a floating-point slope fails.
  • Test a square by six squared distances, and say why four equal distances plus two double ones is enough.
  • Build a convex hull with the monotone chain, explain the pop condition with the cross product, and say which comparison keeps collinear boundary points.
  • Say why the closest-pair sweep checks only a constant number of neighbours, and what the short scan does in the worst case.

17Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 8 testsMeeting Rooms IIintermediate
  • 8 testsCar Poolingintermediate
  • 10 testsThe Skyline Problemadvanced
  • 9 testsNumber of Flowers in Full Bloomintermediate
  • 9 testsMinimum Interval to Include Each Queryadvanced
  • 8 testsRank Transform of an Arrayintermediate
  • 10 testsArea of the Union of Rectanglesadvanced
  • 10 testsDo Two Segments Intersect?intermediate
  • 9 testsPolygon Area by the Shoelace Formulaintermediate
  • 9 testsPoint in Polygonadvanced
  • 8 testsTotal Area of Two Rectanglesintermediate
  • 9 testsPerfect Rectangleadvanced
  • 10 testsMax Points on a Lineadvanced
  • 8 testsValid Squareintermediate
  • 9 testsErect the Fenceadvanced
  • 10 testsClosest Pair of Pointsadvanced
  • 8 testsK Closest Points to Originintermediate
All 159 for this level →

✓Check yourself

5 questions, pass at 4. The check runs in your browser and the result stays on this device, so it is a self-check and not a certificate.

A13

Finished reading?

0 of 48 chapters read in this series

Mark as read

→Next stop

  1. A12Previous · A12Ordered sets without a TreeMap
  2. A13you are here
  3. Next · A14Interview strategyThe algorithm is half the score — the other half is everything you say before, during and after writing it.45 min read

01On this page

15 sections · 0%

  1. 01The sweep-line recipe: events, a tie-break and an active set
  2. 02The skyline problem: a heap with lazy deletion
  3. 03Offline queries: answer them in the order that suits you
  4. 04Coordinate compression, exactly
  5. 05Rectangle union: the simple way and the segment tree
  6. 06Geometry in integers: cross product and orientation
  7. 07Polygon area by the shoelace formula
  8. 08Point in polygon by ray casting
  9. 09Rectangles: overlap, area and the perfect rectangle
  10. 10Maximum points on a line: hash the direction, not the slope
  11. 11Valid square without a single square root
  12. 12Convex hull by monotone chain (erect the fence)
  13. 13Closest pair of points
  14. 14Floating point: why geometry stays in integers
  15. 15Recognizing it in an unseen problem
Mark as read

[] chapters · t top