One-dimensional sweep line
Sweeping a line over one dimension: sorting coordinates, maintaining the active set, the union of line segments, covered length and the earliest conflict.
Learning goals
- Sweep over sorted endpoints and maintain a structure for the active intervals
- Compute the total length covered by a union of segments
- Compute the length covered at least k times
- Detect the earliest conflict in a booking system, offline and online
Prerequisites
The sweep line is the general method: stop only where something changes, and keep just enough about the active objects to answer the question at hand.
Coordinate sorting
A one-dimensional sweep starts by turning the objects into a sorted list of **coordinates**, each tagged with what happens there (a segment starts, a segment ends) and which object it belongs to. Everything else is a loop over that list. Sorting dominates the cost: O(n log n) for n segments.
Sort the pairs of coordinate and event type, and decide the tie rule once. The union animation sorts the segments by start and marches across them: each segment either extends the current covered run or ends it and starts a new one.
1// each endpoint is {coordinate, type, id}; type 0 = end, 1 = start, so ends sort first at equal coordinates2static int[][] endpoints(int[][] segments) {3int[][] e = new int[segments.length * 2][];4for (int i = 0; i < segments.length; i++) {5e[2 * i] = new int[]{segments[i][0], 1, i};6e[2 * i + 1] = new int[]{segments[i][1], 0, i};7}8Arrays.sort(e, (x, y) -> x[0] != y[0] ? Integer.compare(x[0], y[0]) : Integer.compare(x[1], y[1]));9return e;10}
Total length covered by a set of segments
Interactive · edit the inputsorted by start
covered runs
Sort by start and sweep, keeping the current run [start, end]. A segment that starts inside the run extends it; a segment that starts after the run ends closes it and begins a new one.
1static int coveredLength(int[][] seg) {2Arrays.sort(seg, Comparator.comparingInt(a -> a[0])); // sweep from left to right3int total = 0, curStart = seg[0][0], curEnd = seg[0][1];4for (int i = 1; i < seg.length; i++) {5if (seg[i][0] <= curEnd) curEnd = Math.max(curEnd, seg[i][1]); // overlaps the open run: extend it6else { total += curEnd - curStart; curStart = seg[i][0]; curEnd = seg[i][1]; } // a gap: close the run7}8return total + (curEnd - curStart);9}
Live state
start
1
end
4
total
0
Execution trace (dry run table)Show 8 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | start=1 end=4 total=0 | Sort by start and sweep, keeping the current run [start, end]. A segment that starts inside the run extends it; a segment that starts after the run ends closes it and begins a new one. |
| 2 | L5 | segment=[3, 6] run=[1, 4] total=0 | [3, 6] starts inside the run: extend it to 6. |
| 3 | L5 | segment=[8, 10] run=[1, 6] total=0 | [8, 10] starts after the run ends at 6: there is a gap. |
| 4 | L6 | total=5 | Close the run [1, 6]: add its length 5. Total covered 5. |
| 5 | L5 | segment=[9, 12] run=[8, 10] total=5 | [9, 12] starts inside the run: extend it to 12. |
| 6 | L5 | segment=[14, 15] run=[8, 12] total=5 | [14, 15] starts after the run ends at 12: there is a gap. |
| 7 | L6 | total=9 | Close the run [8, 12]: add its length 4. Total covered 9. |
| 8 | L8 | return=10 | Total covered length 10. The uncovered gaps are exactly the spaces between runs. |
Maintain active set
While sweeping, keep a structure holding the objects that currently cross the line. What the structure is depends on the question. A counter is enough for 'how many'. A **min-heap of end times** answers 'when does the next one finish'. A **sorted map** answers 'which active object is just before or after this one', which is how conflicts are found when bookings arrive online, one at a time.
An online calendar stores each booking's start and end in a TreeMap keyed by start. A new booking [s, e) conflicts only with the booking that starts at or before s (if it ends after s) or the booking that starts after s (if it starts before e). Looking at those two neighbours is O(log n); no other booking can conflict, because the stored bookings are disjoint.
1// bookings are half-open [start, end); returns false (and does not store) on a conflict2static boolean book(TreeMap<Integer, Integer> calendar, int start, int end) {3Map.Entry<Integer, Integer> before = calendar.floorEntry(start); // starts at or before 'start'4if (before != null && before.getValue() > start) return false;5Map.Entry<Integer, Integer> after = calendar.ceilingEntry(start); // starts at or after 'start'6if (after != null && after.getKey() < end) return false;7calendar.put(start, end);8return true;9}
Start/end events: active count and maximum concurrency
Interactive · edit the inputintervals
events in sweep order
active count after each event
Turn every interval into two events, +1 at its start and −1 at its end, sort them, and sweep once keeping a running count. At equal coordinates ends come first, because [s, e) does not contain e: back-to-back intervals do not overlap.
1static int maxConcurrent(int[][] iv, boolean closed) {2List<int[]> events = new ArrayList<>(); // {coordinate, +1 start / -1 end}3for (int[] x : iv) { events.add(new int[]{x[0], +1}); events.add(new int[]{x[1], -1}); }4events.sort((a, b) -> a[0] != b[0] ? Integer.compare(a[0], b[0])5: closed ? Integer.compare(b[1], a[1]) : Integer.compare(a[1], b[1])); // tie: closed -> start first; half-open -> end first6int active = 0, best = 0;7for (int[] e : events) {8active += e[1]; // a start opens one, an end closes one9best = Math.max(best, active);10}11return best;12}
Live state
intervals
4
events
8
Execution trace (dry run table)Show 10 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | intervals=4 events=8 | Turn every interval into two events, +1 at its start and −1 at its end, sort them, and sweep once keeping a running count. At equal coordinates ends come first, because [s, e) does not contain e: back-to-back intervals do not overlap. |
| 2 | L8 | x=0 event=start active=1 best=1 | At 0: an interval starts, so 1 is active. New maximum: 1. |
| 3 | L8 | x=5 event=start active=2 best=2 | At 5: an interval starts, so 2 are active. New maximum: 2. |
| 4 | L8 | x=10 event=end active=1 best=2 | At 10: an interval ends, so 1 is active. |
| 5 | L8 | x=10 event=start active=2 best=2 | At 10: an interval starts, so 2 are active. |
| 6 | L8 | x=15 event=end active=1 best=2 | At 15: an interval ends, so 1 is active. |
| 7 | L8 | x=15 event=start active=2 best=2 | At 15: an interval starts, so 2 are active. |
| 8 | L8 | x=20 event=end active=1 best=2 | At 20: an interval ends, so 1 is active. |
| 9 | L8 | x=30 event=end active=0 best=2 | At 30: an interval ends, so 0 are active. |
| 10 | L11 | return=2 | Maximum concurrency 2: that many rooms (or platforms, or CPUs) are needed. Cost O(n log n) for the sort, O(n) for the sweep. |
Line segment unions
The **union** of segments is the set of points covered by at least one of them. Sort by start and sweep, keeping the current covered run [runStart, runEnd]: if the next segment starts after runEnd, the run is finished (add its length) and a new run begins; otherwise extend runEnd. The gaps are exactly the spaces between runs.
The “Overlaps and gaps” preset covers [1, 6], [8, 12] and [14, 15] for a total of 5 + 4 + 1 = 10. “Nested” covers 10 even though three segments are given, and “Touching” (1-3 and 3-6) joins into one run of length 5.
1static int unionLength(int[][] segments) {2int[][] a = segments.clone();3Arrays.sort(a, Comparator.comparingInt(x -> x[0]));4int total = 0;5int runStart = a[0][0], runEnd = a[0][1];6for (int i = 1; i < a.length; i++) {7if (a[i][0] > runEnd) { // gap: close the run8total += runEnd - runStart;9runStart = a[i][0];10runEnd = a[i][1];11} else {12runEnd = Math.max(runEnd, a[i][1]);13}14}15return total + (runEnd - runStart);16}
Best
O(n log n)
Average
O(n log n)
Worst
O(n log n)
Space
O(1) beyond the sorted copy
Total length covered by a set of segments
Interactive · edit the inputsorted by start
covered runs
Sort by start and sweep, keeping the current run [start, end]. A segment that starts inside the run extends it; a segment that starts after the run ends closes it and begins a new one.
1static int coveredLength(int[][] seg) {2Arrays.sort(seg, Comparator.comparingInt(a -> a[0])); // sweep from left to right3int total = 0, curStart = seg[0][0], curEnd = seg[0][1];4for (int i = 1; i < seg.length; i++) {5if (seg[i][0] <= curEnd) curEnd = Math.max(curEnd, seg[i][1]); // overlaps the open run: extend it6else { total += curEnd - curStart; curStart = seg[i][0]; curEnd = seg[i][1]; } // a gap: close the run7}8return total + (curEnd - curStart);9}
Live state
start
1
end
4
total
0
Execution trace (dry run table)Show 8 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | start=1 end=4 total=0 | Sort by start and sweep, keeping the current run [start, end]. A segment that starts inside the run extends it; a segment that starts after the run ends closes it and begins a new one. |
| 2 | L5 | segment=[3, 6] run=[1, 4] total=0 | [3, 6] starts inside the run: extend it to 6. |
| 3 | L5 | segment=[8, 10] run=[1, 6] total=0 | [8, 10] starts after the run ends at 6: there is a gap. |
| 4 | L6 | total=5 | Close the run [1, 6]: add its length 5. Total covered 5. |
| 5 | L5 | segment=[9, 12] run=[8, 10] total=5 | [9, 12] starts inside the run: extend it to 12. |
| 6 | L5 | segment=[14, 15] run=[8, 12] total=5 | [14, 15] starts after the run ends at 12: there is a gap. |
| 7 | L6 | total=9 | Close the run [8, 12]: add its length 4. Total covered 9. |
| 8 | L8 | return=10 | Total covered length 10. The uncovered gaps are exactly the spaces between runs. |
Coverage length
The union counts points covered **at least once**. Sometimes you need the length covered at least k times, or exactly k times. The event sweep gives it directly: between two consecutive event coordinates the active count is constant, so add the gap length to the total whenever the count at that stretch is at least k.
With k = 1 it reproduces the union length. With k = 2 it gives the length where segments overlap each other, which is useful for conflict reports.
1// length covered by at least k of the half-open segments; segments must have start < end2static long lengthCoveredAtLeast(int[][] segments, int k) {3TreeMap<Integer, Integer> delta = new TreeMap<>();4for (int[] s : segments) { delta.merge(s[0], +1, Integer::sum); delta.merge(s[1], -1, Integer::sum); }5long total = 0;6int active = 0;7Integer prev = null;8for (Map.Entry<Integer, Integer> e : delta.entrySet()) {9if (prev != null && active >= k) total += e.getKey() - prev; // the stretch (prev, key) has constant count10active += e.getValue();11prev = e.getKey();12}13return total;14}
Best
O(n log n)
Average
O(n log n)
Worst
O(n log n)
Space
O(n)
Total length covered by a set of segments
Interactive · edit the inputsorted by start
covered runs
Sort by start and sweep, keeping the current run [start, end]. A segment that starts inside the run extends it; a segment that starts after the run ends closes it and begins a new one.
1static int coveredLength(int[][] seg) {2Arrays.sort(seg, Comparator.comparingInt(a -> a[0])); // sweep from left to right3int total = 0, curStart = seg[0][0], curEnd = seg[0][1];4for (int i = 1; i < seg.length; i++) {5if (seg[i][0] <= curEnd) curEnd = Math.max(curEnd, seg[i][1]); // overlaps the open run: extend it6else { total += curEnd - curStart; curStart = seg[i][0]; curEnd = seg[i][1]; } // a gap: close the run7}8return total + (curEnd - curStart);9}
Live state
start
1
end
4
total
0
Execution trace (dry run table)Show 8 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | start=1 end=4 total=0 | Sort by start and sweep, keeping the current run [start, end]. A segment that starts inside the run extends it; a segment that starts after the run ends closes it and begins a new one. |
| 2 | L5 | segment=[3, 6] run=[1, 4] total=0 | [3, 6] starts inside the run: extend it to 6. |
| 3 | L5 | segment=[8, 10] run=[1, 6] total=0 | [8, 10] starts after the run ends at 6: there is a gap. |
| 4 | L6 | total=5 | Close the run [1, 6]: add its length 5. Total covered 5. |
| 5 | L5 | segment=[9, 12] run=[8, 10] total=5 | [9, 12] starts inside the run: extend it to 12. |
| 6 | L5 | segment=[14, 15] run=[8, 12] total=5 | [14, 15] starts after the run ends at 12: there is a gap. |
| 7 | L6 | total=9 | Close the run [8, 12]: add its length 4. Total covered 9. |
| 8 | L8 | return=10 | Total covered length 10. The uncovered gaps are exactly the spaces between runs. |
Earliest conflict
Given a list of bookings, find the first moment at which two of them conflict. Sort the bookings by start and keep the largest end seen so far; the first booking that starts **before** that end is in conflict, and the conflict begins at its start. The sweep can stop immediately, which is what the animation reports.
The “Conflict at 11” preset (bookings 9-10, 14-16, 10-12, 11-13) finds that B4 starts at 11 while B3 (10-12) is still active. “No conflict” shows back-to-back bookings, and “Two start together” shows that equal starts conflict at once, at time 5.
1// bookings are half-open {start, end}; returns {time, indexOfLaterBooking, indexOfEarlierBooking} or null2static int[] earliestConflict(int[][] bookings) {3Integer[] order = new Integer[bookings.length];4for (int i = 0; i < order.length; i++) order[i] = i;5Arrays.sort(order, Comparator.comparingInt(i -> bookings[i][0]));6int holder = -1; // booking with the largest end so far7for (int i : order) {8if (holder >= 0 && bookings[i][0] < bookings[holder][1]) return new int[]{bookings[i][0], i, holder};9if (holder < 0 || bookings[i][1] > bookings[holder][1]) holder = i;10}11return null;12}
Best
O(n log n)
Average
O(n log n)
Worst
O(n log n)
Space
O(n) for the index array
Earliest scheduling conflict by sweeping
Interactive · edit the inputbookings by start
Sort by start time and sweep. Remember the booking with the largest end so far. The next booking conflicts exactly when it starts before that end, because everything earlier ended no later.
1static int[] firstConflict(int[][] bookings) { // returns {start of the conflict, index of the earlier booking}2Integer[] idx = new Integer[bookings.length]; for (int i = 0; i < idx.length; i++) idx[i] = i;3Arrays.sort(idx, Comparator.comparingInt(i -> bookings[i][0]));4int latest = -1, latestEnd = Integer.MIN_VALUE; // booking with the largest end so far5for (int i : idx) {6if (latest >= 0 && bookings[i][0] < latestEnd) return new int[]{bookings[i][0], latest}; // starts before something ends7if (bookings[i][1] > latestEnd) { latest = i; latestEnd = bookings[i][1]; }8}9return null; // no two bookings overlap10}
Live state
bookings
4
Execution trace (dry run table)Show 5 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L3 | bookings=4 | Sort by start time and sweep. Remember the booking with the largest end so far. The next booking conflicts exactly when it starts before that end, because everything earlier ended no later. |
| 2 | L6 | booking=B1 start=9 latest end=none | B1 is the first booking. |
| 3 | L6 | booking=B3 start=10 latest end=10 | B3 starts at 10, at or after 10: no conflict so far. |
| 4 | L6 | booking=B4 start=11 latest end=12 | B4 starts at 11, before B3 ends at 12: this is the earliest conflict. |
| 5 | L6 | return=11 (with B3) | Conflict starting at time 11 between B4 and B3. Stop: the sweep finds it as soon as it happens. |
Practice ladder
Now try it yourself
Easy
- Meeting Rooms
- My Calendar I
Medium
- My Calendar II
- Merge Intervals
- Divide Intervals Into Minimum Number of Groups
Advanced
- My Calendar III
- Rectangle Area II
Key takeaways
- A sweep sorts coordinates once and then processes only the places where something changes.
- The structure for the active set is chosen by the question: a counter, a heap of ends or a sorted map.
- The union length adds the length of each run of overlapping segments.
- Covered-at-least-k length adds each gap between events whose active count is at least k.
- The earliest conflict is the first booking, in start order, that starts before the largest end so far.