Introduction
A scanline is commonly used for geometric figures. As the name suggests, a line sweeps across the figure, and the technique is used for areas, perimeters, and two-dimensional point counting.
Union Area of Rectangles
Given the lower-left and upper-right coordinates of several rectangles in the plane, compute the area of their union.
Procedure
The area can be found by brute force for a small instance. What if the input is large? This is where the scanline algorithm is useful.
Imagine a line sweeping upward from below:
As shown in the figure, divide the rectangle into small rectangles of different colors. Their heights are the distances swept by the line, while their horizontal widths change.
Mark the lower and upper edge of each rectangle with 1 and -1, respectively. Whenever the sweep meets a horizontal edge, add its mark to the weight of the interval covered by that edge on the x-axis.
Note
This resembles traversing a parenthesis sequence: an opening parenthesis adds 1, a closing parenthesis subtracts 1, the “weight” is the current depth, and a positive weight means that the interval is inside parentheses and contributes to the small-rectangle width.
The total width of the small rectangles is the total length of the intervals on the number line whose weight is positive.
Implementation
Use a segment tree to maintain the length covered by a positive number of rectangles. The requirements are:
- Add 1 or subtract 1 from every weight in an interval.
- Compute the total length of the number line whose weight is positive.
A direct segment-tree template can be troublesome. When adding to an interval, even if the modified interval matches a segment-tree node exactly, we cannot determine in constant time how the covered length changes: we do not know how much of the managed interval changes from 1 to 0 or from 0 to 1.
A simple divide-and-conquer solution suffices: maintain v[], the number of fully covering intervals for each node (like a lazy tag that is not pushed down), and w[], the covered length.
Coordinate compression is required.
Luogu P5490 [Template] Scanline & Union Area of Rectangles — reference implementation
#include <algorithm>
#include <iostream>
using ll = long long;
constexpr int N = 1e5 + 1;
int n, a[N * 2], tot; // a[] and tot are used for coordinate compression
ll v[N * 8], w[N * 8]; // full-cover counts and covered lengths
struct St {
ll x1, x2, y, o;
} b[N * 2]; // upper and lower rectangle edges
int f(int y) { // map a coordinate to its index in a
return std::lower_bound(a, a + tot, y) - a;
}
void up(int u, int ul, int ur) { // pushup
if (v[u]) w[u] = a[ur] - a[ul];
// Calling w[u*2+1] on a leaf requires eight times the segment-tree space
// the two horizontal edges of each rectangle make the factor sixteen
else if (ul + 1 == ur)
w[u] = 0;
else
w[u] = w[u * 2 + 1] + w[u * 2 + 2];
}
void add(int lf, int rg, ll o, int u = 0, int ul = 0, int ur = tot - 1) {
// add to an interval
if (lf == ul && rg == ur) return v[u] += o, up(u, ul, ur), void();
int um = (ul + ur) / 2;
if (lf < um) add(lf, std::min(rg, um), o, u * 2 + 1, ul, um);
if (um < rg) add(std::max(lf, um), rg, o, u * 2 + 2, um, ur);
up(u, ul, ur);
}
int main() {
std::cin >> n;
for (int i = 0, x1, x2, y1, y2; i < n; i++) {
// y1 is a local variable, so it cannot conflict with another name
std::cin >> x1 >> y1 >> x2 >> y2;
b[i] = {x1, x2, y1, 1};
b[i + n] = {x1, x2, y2, -1};
a[i] = x1, a[i + n] = x2;
}
std::sort(a, a + n * 2), tot = 1;
for (int i = 1; i < n * 2; i++)
if (a[i] != a[tot - 1]) a[tot++] = a[i]; // coordinate compression
std::sort(b, b + n * 2,
[](St &i, St &j) -> bool { return i.y < j.y; }); // sort the events
ll sum = 0;
add(f(b[0].x1), f(b[0].x2), 1);
for (int i = 1; i < n * 2; i++) {
int x1 = f(b[i].x1), x2 = f(b[i].x2);
sum += (b[i].y - b[i - 1].y) * w[0]; // add each horizontal strip's area
add(x1, x2, b[i].o);
}
std::cout << sum << '\n';
}
[POJ 1151] Atlantis — reference implementation
#include <algorithm>
#include <cstdio>
#include <cstring>
constexpr int MAXN = 300;
using namespace std;
int lazy[MAXN << 3]; // number of times this segment is covered
double s[MAXN << 3];
struct node1 {
double l, r;
double sum;
} cl[MAXN << 3]; // segment tree
struct node2 {
double x, y1, y2;
int flag;
} p[MAXN << 3]; // coordinates
// sort comparator
bool cmp(node2 a, node2 b) { return a.x < b.x; }
// pull information upward
void pushup(int rt) {
if (lazy[rt] > 0)
cl[rt].sum = cl[rt].r - cl[rt].l;
else
cl[rt].sum = cl[rt * 2].sum + cl[rt * 2 + 1].sum;
}
// build the tree
void build(int rt, int l, int r) {
if (r - l > 1) {
cl[rt].l = s[l];
cl[rt].r = s[r];
build(rt * 2, l, (l + r) / 2);
build(rt * 2 + 1, (l + r) / 2, r);
pushup(rt);
} else {
cl[rt].l = s[l];
cl[rt].r = s[r];
cl[rt].sum = 0;
}
return;
}
// update
void update(int rt, double y1, double y2, int flag) {
if (cl[rt].l == y1 && cl[rt].r == y2) {
lazy[rt] += flag;
pushup(rt);
return;
} else {
if (cl[rt * 2].r > y1) update(rt * 2, y1, min(cl[rt * 2].r, y2), flag);
if (cl[rt * 2 + 1].l < y2)
update(rt * 2 + 1, max(cl[rt * 2 + 1].l, y1), y2, flag);
pushup(rt);
}
}
int main() {
int temp = 1, n;
double x1, y1, x2, y2, ans;
while (scanf("%d", &n) && n) {
ans = 0;
for (int i = 0; i < n; i++) {
scanf("%lf %lf %lf %lf", &x1, &y1, &x2, &y2);
p[i].x = x1;
p[i].y1 = y1;
p[i].y2 = y2;
p[i].flag = 1;
p[i + n].x = x2;
p[i + n].y1 = y1;
p[i + n].y2 = y2;
p[i + n].flag = -1;
s[i + 1] = y1;
s[i + n + 1] = y2;
}
sort(s + 1, s + (2 * n + 1)); // coordinate compression
sort(p, p + 2 * n, cmp); // sort rectangle edge x-coordinates
build(1, 1, 2 * n); // build the tree
memset(lazy, 0, sizeof(lazy));
update(1, p[0].y1, p[0].y2, p[0].flag);
for (int i = 1; i < 2 * n; i++) {
ans += (p[i].x - p[i - 1].x) * cl[1].sum;
update(1, p[i].y1, p[i].y2, p[i].flag);
}
printf("Test case #%d\nTotal explored area: %.2lf\n\n", temp++, ans);
}
return 0;
}
Exercises
- 「POJ1177」Picture
- 「POJ3832」Posters
- Luogu P1856 [IOI1998] [USACO5.5] Rectangle Perimeter Picture
- The horizontal-edge contribution is the change in covered length.
- Compute the two directions separately to avoid discussing vertical edges.
- When sorting events, handle coincident edges of two rectangles carefully.
- With the given limits, a quadratic simulation without a segment tree is possible.
B-Dimensional Orthogonal Ranges
A B-dimensional orthogonal range is the set of points inside a B-dimensional Cartesian coordinate system whose -th coordinate lies in an integer interval .
A one-dimensional orthogonal range is usually called an interval, a two-dimensional one a rectangle, and a three-dimensional one a cuboid; two-dimensional point counting is also a two-dimensional orthogonal-range problem.
For a static two-dimensional problem, scan one dimension and use a data structure to maintain the other. As the scanline moves from left to right, updates and queries occur in the dimension maintained by the data structure. If the queried information supports subtraction, use differences directly; otherwise use divide and conquer. Differences are usually maintained with a Fenwick tree or segment tree, and Fenwick trees are common because they are simple and have small constants. Divide and conquer usually means CDQ divide and conquer, which is not discussed here.
Another useful viewpoint is to regard the data as a sequence rather than a plane. The scanline then enumerates the right endpoint and maintains a data structure that, for the current and a given , answers the range from to . In other words, the scanline enumerates right endpoints while the data structure stores the answers for all left endpoints.
The complexity is generally .
Two-Dimensional Point Counting
Given a sequence of length and queries, count for each query the elements in whose values lie in .
This is called two-dimensional point counting. It is equivalent to finding the number of points inside a rectangle in the plane. The simplest method is a scanline together with a Fenwick tree.
This is a static two-dimensional problem, which the scanline converts into a dynamic one-dimensional problem. Maintain the sequence with a data structure; here a Fenwick tree is sufficient.
First coordinate-compress all queries and use a Fenwick tree for the weights. For a query with endpoints and , when the enumeration reaches , record the number of values currently in . Continue the enumeration and, at , record the number in . The answer is .
Example
[Luogu P2163] [SHOI2007] Gardener's Troubles
First perform coordinate compression. Let be the number of points in the rectangle with lower-left corner and upper-right corner . The query answer is the difference .
Code
#include <algorithm>
#include <iostream>
int n, m;
int x[500010], y[500010], ans[500010];
int ax[1500010], ay[1500010], tx, ty; // coordinate compression
struct query {
int a, b, c, d;
} q[500010]; // store queries for compression
struct ope {
int type, x, y, id;
ope(int type = 0, int x = 0, int y = 0, int id = 0) {
this->type = type, this->x = x, this->y = y, this->id = id;
}
bool operator<(const ope& rhs) const {
if (x == rhs.x) return type < rhs.type;
return x < rhs.x;
}
};
ope op[2500010];
int tot; // total number of events
int sum[1500010]; // Fenwick tree
int lowbit(int x) { return x & (-x); }
void add(int x, int k) {
while (x <= 1500000) {
sum[x] = sum[x] + k;
x = x + lowbit(x);
}
}
int getsum(int x) {
int ret = 0;
while (x > 0) {
ret = ret + sum[x];
x = x - lowbit(x);
}
return ret;
}
using std::cin;
using std::cout;
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n >> m, tx = n, ty = n;
for (int i = 1; i <= n; i++) cin >> x[i] >> y[i], ax[i] = x[i], ay[i] = y[i];
for (int i = 1, l, r; i <= m; i++) {
cin >> q[i].a >> q[i].b >> q[i].c >> q[i].d;
ax[++tx] = q[i].a, ay[++ty] = q[i].b, ax[++tx] = q[i].c, ay[++ty] = q[i].d;
}
std::sort(ax + 1, ax + tx + 1), std::sort(ay + 1, ay + ty + 1);
tx = std::unique(ax + 1, ax + tx + 1) - ax - 1;
ty = std::unique(ay + 1, ay + ty + 1) - ay - 1;
for (int i = 1; i <= n; i++) {
x[i] = std::lower_bound(ax + 1, ax + tx + 1, x[i]) - ax;
y[i] = std::lower_bound(ay + 1, ay + ty + 1, y[i]) - ay;
op[++tot] = ope(0, x[i], y[i], i); // add a point
}
for (int i = 1; i <= m; i++) {
q[i].a = std::lower_bound(ax + 1, ax + tx + 1, q[i].a) - ax;
q[i].b = std::lower_bound(ay + 1, ay + ty + 1, q[i].b) - ay;
q[i].c = std::lower_bound(ax + 1, ax + tx + 1, q[i].c) - ax;
q[i].d = std::lower_bound(ay + 1, ay + ty + 1, q[i].d) - ay;
op[++tot] = ope(1, q[i].c, q[i].d, i); // add a differenced query
op[++tot] = ope(1, q[i].a - 1, q[i].b - 1, i);
op[++tot] = ope(2, q[i].a - 1, q[i].d, i);
op[++tot] = ope(2, q[i].c, q[i].b - 1, i);
}
std::sort(op + 1, op + tot + 1); // sort by x, processing point additions first
for (int i = 1; i <= tot; i++) {
if (op[i].type == 0)
add(op[i].y, 1);
else if (op[i].type == 1)
ans[op[i].id] += getsum(op[i].y);
else
ans[op[i].id] -= getsum(op[i].y);
}
for (int i = 1; i <= m; i++) cout << ans[i] << '\n';
return 0;
}
Luogu P1908 Inversion Pairs
Inversion counting can also be viewed as a scanline. Enumerate positions from right to left and count the points in whose values lie in . Since the values can be as large as , first coordinate-compress them. While scanning from right to left, update the Fenwick tree (or segment tree), then count how many values are smaller than the current one; this count is the number of inversions contributed by the current position.
Code
#include <algorithm>
#include <iostream>
using ll = long long;
using namespace std;
struct node {
ll data;
ll num;
} f[500010];
ll n, ans, a[500010];
bool cmp(node a, node b) {
if (a.data == b.data) {
return a.num < b.num;
}
return a.data < b.data;
}
ll sum[500010];
int lowbit(int x) { return x & (-x); }
void add(int x, int k) {
while (x <= n) {
sum[x] = sum[x] + k;
x = x + lowbit(x);
}
}
int getsum(int x) {
int ret = 0;
while (x > 0) {
ret = ret + sum[x];
x = x - lowbit(x);
}
return ret;
}
int main() {
cin >> n;
for (ll i = 1; i <= n; i++) {
cin >> f[i].data;
f[i].num = i;
}
sort(f + 1, f + 1 + n, cmp);
for (int i = 1; i <= n; i++) {
a[f[i].num] = i;
}
for (ll i = n; i > 0; i--) {
ans += getsum(a[i]);
add(a[i], 1);
}
cout << ans;
return 0;
}
[Luogu P1972] [SDOI2009] HH's Necklace
Brief statement: given a sequence, answer how many distinct values occur in each queried interval .
Derive the property and enumerate all right endpoints with a scanline, maintaining the answer for every left endpoint in a data structure. Equivalently, turn it into a rectangle query in the plane.
Let be the previous occurrence position of , or if has not appeared. Repeated occurrences contribute only once; assign the contribution to the first occurrence in the interval. The total contribution is therefore the number of indices with .
The problem becomes: given , answer how many indices in satisfy .
Regard as a point whose horizontal coordinate is and whose vertical coordinate is . The query asks for the number of points in the rectangle with lower-left corner and upper-right corner .
This query supports differences. Subtract the rectangle from to from the rectangle from to , which makes the scanline approach convenient.
Each update costs . There are point additions and query events, so total complexity is .
Code
#include <algorithm>
#include <iostream>
int n, m, a[1000010], ans[1000010];
int pre[1000010], lst[1000010]; // maintain previous occurrences
struct ope {
int type, x, y, id;
ope(int type = 0, int x = 0, int y = 0, int id = 0) {
this->type = type, this->x = x, this->y = y, this->id = id;
}
bool operator<(const ope& rhs) const {
if (x == rhs.x) return type < rhs.type;
return x < rhs.x;
}
};
ope op[2500010];
int tot; // total number of events
int sum[1000010]; // Fenwick tree
int lowbit(int x) { return x & (-x); }
void add(int x, int k) {
x++; // position 0 is updated too, so Fenwick indices are shifted by 1
while (x <= n) {
sum[x] = sum[x] + k;
x = x + lowbit(x);
}
}
int getsum(int x) {
x++;
int ret = 0;
while (x > 0) {
ret = ret + sum[x];
x = x - lowbit(x);
}
return ret;
}
using std::cin;
using std::cout;
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
pre[i] = lst[a[i]], lst[a[i]] = i; // maintain previous occurrences
op[++tot] = ope{0, i, pre[i], i}; // add a point
}
cin >> m;
for (int i = 1, l, r; i <= m; i++) {
cin >> l >> r;
op[++tot] = ope{1, r, l - 1, i}; // add a differenced query
op[++tot] = ope{2, l - 1, l - 1, i};
}
std::sort(op + 1, op + tot + 1); // sort by x, processing point additions first
for (int i = 1; i <= tot; i++) {
if (op[i].type == 0)
add(op[i].y, 1);
else if (op[i].type == 1)
ans[op[i].id] += getsum(op[i].y);
else
ans[op[i].id] -= getsum(op[i].y);
}
for (int i = 1; i <= m; i++) cout << ans[i] << '\n';
return 0;
}
Exercises
- Luogu P8593 (KDOI-02) A Ball's Throw, an application of inversion counting.
- AcWing 4709. Triples, a weakened version of the preceding problem and also an inversion-counting application.
- Luogu P8773 [Lanqiao Cup 2022 Provincial A] Number Selection XOR, a modified version of HH's Necklace.
- Luogu P8844 [Chuanzhi Cup #4 Preliminary] Xiao Ka and Falling Leaves, converting a tree problem into a sequence problem and then applying two-dimensional point counting.
In short, two-dimensional point counting maintains one dimension with a data structure and enumerates the other.