Skip to content
CCC Python Course

Coordinate compression and sweep line

Module
M6.8
Lesson
1 of 1
Reading time
5 min

In this lesson

  • Compress large coordinates into a dense rank map.
  • Build a 2D difference array on a compressed grid weighted by real dimensions.
  • Sweep over sorted events to compute coverage, not just a count.
  • Solve rectangle and interval problems that span large coordinate ranges.

A common geometry problem gives you many rectangles on a 2D plane and asks how much total area they cover. The coordinates can run as high as 10910^9, so a 2D array indexed directly by coordinate is out of the question. coordinate compressionMapping large coordinate values to small ranks to enable dense array storage for sparse 2D problems.In the glossary maps those large coordinates down to a small, dense range, and a 2D difference array does the counting on that dense grid instead.

Coordinate compression

Compression replaces each distinct coordinate with its rank in sorted order. Take the x-coordinates [1, 5, 10, 3]. Sort them to [1, 3, 5, 10], then map 1 -> 0, 3 -> 1, 5 -> 2, 10 -> 3. Every value now fits inside an array indexed 0 to 3, no matter how far apart the original values were.

To compress a list, collect the distinct coordinates, sort them, and build a dictionary from each value to its position in that sorted list.

examples/compress_coordinates.py
import sys

def main() -> None:    input_data = sys.stdin.read().split()    n = int(input_data[0])    coords = list(map(int, input_data[1:n + 1]))
    sorted_coords = sorted(set(coords))    rank_map = {v: i for i, v in enumerate(sorted_coords)}
    compressed = [rank_map[c] for c in coords]    print(" ".join(map(str, compressed)))

if __name__ == "__main__":    main()

Input

4
1 5 10 3

Output

0 2 3 1
Map large coordinate values to small integer ranks

Compression only helps when you also remember the real gap between consecutive ranks. Take two rectangles: one from x=0 to 10 and y=0 to 10, the other from x=5 to 15 and y=5 to 15. The distinct x-coordinates are [0, 5, 10, 15], and so are the y-coordinates. The first rectangle spans x-ranks 0 to 2 and y-ranks 0 to 2; the second spans x-ranks 1 to 3 and y-ranks 1 to 3. A 4x4 compressed grid now stands in for a 15x15 region, and the real width of the cell between rank i and rank i + 1 is sorted_x[i + 1] - sorted_x[i].

A 2D difference array on the compressed grid

A 2D difference array extends the 1D idea from earlier stages. To add a rectangle to it, increment the top-left corner, decrement the two corners that share an edge with it, and increment the bottom-right corner. Once every rectangle is added, a 2D prefix sum recovers, for each compressed cell, how many rectangles cover it.

examples/rectangle_overlap.py
import sys

def main() -> None:    input_data = sys.stdin.read().split()    n = int(input_data[0])
    xs = []    ys = []    rects = []
    for i in range(n):        x1 = int(input_data[1 + i * 4])        y1 = int(input_data[1 + i * 4 + 1])        x2 = int(input_data[1 + i * 4 + 2])        y2 = int(input_data[1 + i * 4 + 3])        rects.append((x1, y1, x2, y2))        xs.extend([x1, x2])        ys.extend([y1, y2])
    xs = sorted(set(xs))    ys = sorted(set(ys))
    # Create difference array    diff = [[0] * len(ys) for _ in range(len(xs))]
    # Add rectangles to difference array    for x1, y1, x2, y2 in rects:        xi1 = xs.index(x1)        yi1 = ys.index(y1)        xi2 = xs.index(x2)        yi2 = ys.index(y2)
        diff[xi1][yi1] += 1        diff[xi2][yi1] -= 1        diff[xi1][yi2] -= 1        diff[xi2][yi2] += 1
    # Compute 2D prefix sum    area = 0    for i in range(len(xs) - 1):        for j in range(len(ys) - 1):            if i > 0:                diff[i][j] += diff[i - 1][j]            if j > 0:                diff[i][j] += diff[i][j - 1]            if i > 0 and j > 0:                diff[i][j] -= diff[i - 1][j - 1]
            if diff[i][j] > 0:                width = xs[i + 1] - xs[i]                height = ys[j + 1] - ys[j]                area += width * height
    print(area)

if __name__ == "__main__":    main()

Input

2
0 0 10 10
5 5 15 15

Output

175
Compute the total area covered by two overlapping rectangles

Here the two rectangles from the example above overlap in the region from x=5 to 10 and y=5 to 10. Each covers 100 square units on its own, their overlap is 25, so the total area covered by at least one of them is 100 + 100 - 25 = 175, which is what the program prints. The prefix sum step turns the four corner updates into a covering count per cell, and multiplying each covered cell by its real width and height, then summing, converts that count into a real area instead of a cell count.

Sweeping across sorted events

A sweep line is an imaginary line that moves across the plane in sorted order, updating a running total each time it crosses an event. On a 1D line, the events are interval starts and ends.

examples/sweep_line_coverage.py
import sys

def main() -> None:    input_data = sys.stdin.read().split()    n = int(input_data[0])
    events = []    for i in range(n):        start = int(input_data[1 + i * 2])        end = int(input_data[1 + i * 2 + 1])        events.append((start, 1))        events.append((end, -1))
    events.sort()
    total_coverage = 0    active_count = 0    prev_pos = events[0][0]
    for pos, delta in events:        if active_count > 0 and pos > prev_pos:            # This stretch is covered by at least one interval, so it counts            # once no matter how many intervals overlap it here.            total_coverage += pos - prev_pos
        active_count += delta        prev_pos = pos
    print(total_coverage)

if __name__ == "__main__":    main()

Input

3
0 5
3 8
6 10

Output

10
Compute the union length of overlapping intervals

Sort every start and end by position, then walk through them left to right, keeping a count of how many intervals are currently active. Between two consecutive events, the count does not change, so that stretch is either fully covered or fully uncovered. With intervals [0, 5], [3, 8], and [6, 10], the covered stretches are [0, 5], [3, 8], and [6, 10], which together cover every point from 0 to 10 without a gap. The total union length is 10.

Notice that the running total only adds a stretch's length once, regardless of how many intervals are active there. Multiplying the stretch length by the active count would answer a different question, the sum of each interval's own length, which needs no sweep at all. The active count exists to tell you whether a stretch is covered, not to weight it.

Why compression works and where it stops helping

Compression works because only the relative order of coordinates matters for computing areas and overlaps, not their absolute values. A thousand rectangles contribute at most 2,000 distinct x-coordinates, one endpoint each on the left and right. Mapping those to ranks 0 through 1,999 needs a 2,000 by 2,000 array, about 4 million cells, instead of an array sized 10910^9 by 10910^9, which no computer could hold.

The same compression assumes a dense grid built from every distinct coordinate is worth building. If a problem only ever asks about the coordinates that actually appear, the dense grid does the job. If it asks about many additional query points that were never among the original coordinates, you need to insert those points into the sorted coordinate list too, or the compressed ranks will not line up with the real question being asked.

Common mistakes

The most common mistake is forgetting to weight by real dimensions. The 2D prefix sum over a compressed grid gives you a covering count per cell, not an area. Before summing, multiply each covered cell by sorted_x[i + 1] - sorted_x[i] and sorted_y[j + 1] - sorted_y[j]. Skip that step and you get the number of cell intersections, a number with no direct meaning in the original coordinates.

A second mistake is getting the four corners of a rectangle update wrong. Adding a rectangle needs an increment at its top-left corner, a decrement at each of the two corners that share exactly one of its edges, and an increment at its bottom-right corner. Missing one of these four, or using the wrong rank for one of them, corrupts every prefix sum computed afterward.

A third mistake is confusing a rank with the coordinate it stands for. After sorting [1, 5, 10, 3] to [1, 3, 5, 10], rank 1 stands for the value 3, not the value 1. A rectangle from x=3 to x=5 uses ranks 1 to 2, not the literal numbers 3 and 5. Keep the value-to-rank dictionary next to the compressed array and always go through it, rather than trying to do rank arithmetic from memory.

Recap

Coordinate compression turns coordinates that span up to a billion into small, dense ranks, so a 2D difference array can answer aggregate questions about them. Weighting each compressed cell by its real width and height converts a covering count back into a real area. A sweep line answers a different kind of question, walking sorted events left to right and updating a running total exactly when the state changes, which is how you get a union length instead of a sum of individual pieces.

Practice

Try this on the judge. The link opens the problem on DMOJ.

  1. 2014 S4
    Tinted Glass Window (opens on DMOJ in a new tab) DMOJ

    Find the total window area covered once several tinted panes overlap.