🚀 Alguni Start learning

Unit 24: Shapes and Sweeps

One sign answers most of geometry.

Unit 24 of 25 in Competitive programming for kids. Its 4 lessons are Left or Right?, Do These Two Cross?, The Elastic Band and The Sweep Line — below is everything each one explains, and a question or two from it to try.

Every sample on this page was run through real Python before it shipped, and prints exactly what it says it prints.

This unit opens with a fortnight’s trial of everything — no card needed — or with a family plan, bought in the iPhone app. The first two units of every track are free for ever. Try it in the app.

📐 Left or Right?

The cross product, and what its sign means

For two steps (x1, y1) and (x2, y2), the cross product is x1 * y2 - x2 * y1.

Positive means the second step turns left from the first, negative means right, and zero means they are in line. That sign is most of contest geometry.

Python

def cross(ax, ay, bx, by):
    return ax * by - bx * ay

print(cross(1, 0, 0, 1))
print(cross(1, 0, 0, -1))
print(cross(1, 0, 2, 0))

It prints

1
-1
0

Which side of a line is a point on?

Take the step from a to b, and the step from a to the point. The cross product of those two says which side the point is on — and zero says it is exactly on the line.

With whole-number coordinates this is exact. No angles, no square roots, nothing to round.

Python

def side(ax, ay, bx, by, px, py):
    value = (bx - ax) * (py - ay) - (px - ax) * (by - ay)
    if value > 0:
        return "left"
    if value < 0:
        return "right"
    return "on the line"

print(side(0, 0, 4, 0, 2, 3))
print(side(0, 0, 4, 0, 2, -3))
print(side(0, 0, 4, 0, 2, 0))

It prints

left
right
on the line

And it gives you area for free

The cross product of the two sides of a triangle is twice its area, with a sign for which way round the corners go.

Keeping the doubled value avoids ever dividing by 2, so the arithmetic stays in whole numbers — which is exactly what you want a judge comparing your answer to.

Python

def twice_area(ax, ay, bx, by, cx, cy):
    return abs((bx - ax) * (cy - ay) - (cx - ax) * (by - ay))

print(twice_area(0, 0, 4, 0, 0, 3))
print(twice_area(0, 0, 4, 0, 2, 3))
print(twice_area(0, 0, 4, 0, 8, 0))

It prints

12
12
0

Distances: compare the squares

The distance between two points needs a square root, which is a decimal, which rounds. If all you are doing is comparing — which point is nearer, is this within r — compare the squared distances instead and stay exact.

Only take a square root when the answer itself has to be printed as a length.

Python

def squared(ax, ay, bx, by):
    return (ax - bx) ** 2 + (ay - by) ** 2

print(squared(0, 0, 3, 4))
print(squared(0, 0, 5, 0))
print(squared(0, 0, 3, 4) == squared(0, 0, 5, 0))

It prints

25
25
True

Try it yourself

The cross product of two steps is 0. What does that mean?

  • They are the same length
  • They point along the same line
  • They are at right angles
  • One of them is zero

What does this print?

Python

def cross(ax, ay, bx, by):
    return ax * by - bx * ay

print(cross(2, 3, 4, 6), cross(2, 3, 6, 4))

Answer them in the app

❌ Do These Two Cross?

Four turns decide it

Two segments cross when each one has the other's ends on opposite sides of it.

So four cross products: the two ends of the second segment against the first, and the two ends of the first against the second. If both pairs have opposite signs, they cross.

Python

def turn(ax, ay, bx, by, px, py):
    value = (bx - ax) * (py - ay) - (px - ax) * (by - ay)
    if value > 0:
        return 1
    if value < 0:
        return -1
    return 0

def crosses(a, b, c, d):
    t1 = turn(a[0], a[1], b[0], b[1], c[0], c[1])
    t2 = turn(a[0], a[1], b[0], b[1], d[0], d[1])
    t3 = turn(c[0], c[1], d[0], d[1], a[0], a[1])
    t4 = turn(c[0], c[1], d[0], d[1], b[0], b[1])
    return t1 * t2 < 0 and t3 * t4 < 0

print(crosses((0, 0), (4, 4), (0, 4), (4, 0)))
print(crosses((0, 0), (1, 1), (2, 2), (3, 3)))
print(crosses((0, 0), (4, 0), (2, 1), (2, 5)))

It prints

True
False
False

The awkward cases are the zeroes

A cross product of 0 means an end sits exactly on the other segment — touching rather than crossing. The strict test above says False for that, which is right for some problems and wrong for others.

Read the statement. "Touching counts as intersecting" is a sentence that decides your code, and it is easy to skim past.

Python

def turn(ax, ay, bx, by, px, py):
    value = (bx - ax) * (py - ay) - (px - ax) * (by - ay)
    if value > 0:
        return 1
    if value < 0:
        return -1
    return 0

print(turn(0, 0, 4, 0, 2, 0))
print(turn(0, 0, 4, 0, 4, 0))
print(turn(0, 0, 4, 0, 2, 1))

It prints

0
0
1

Counting crossings without checking every pair

A thousand segments make half a million pairs — fine. A hundred thousand make five thousand million, which is not.

That is what lesson 4 is for: sort the ends and sweep, so only segments that overlap in the sweep direction are ever compared.

Try it yourself

Both ends of segment B come out on the same side of segment A. What follows?

  • They cross
  • They cannot cross
  • They touch
  • You need the other two turns to know

What does this print?

Python

def turn(ax, ay, bx, by, px, py):
    value = (bx - ax) * (py - ay) - (px - ax) * (by - ay)
    if value > 0:
        return 1
    if value < 0:
        return -1
    return 0

print(turn(0, 0, 4, 4, 0, 4), turn(0, 0, 4, 4, 4, 0))

Answer them in the app

🔷 The Elastic Band

Stretch a band round the points and let go

The shape it snaps to is the convex hull — the smallest shape containing every point, with no dents.

It answers "which points are on the outside", "what is the widest pair", and half the questions about a cloud of points.

Sort by x, then walk along keeping only left turns

Take the points in order of x. Push each onto a list, and while the last three make a right turn, throw the middle one away — it was a dent.

That builds the bottom of the hull. Do it again from the other end for the top, and join them.

Python

points = [(0, 0), (1, 1), (2, 0), (2, 3), (4, 3), (4, 0)]

def cross(o, a, b):
    return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])

def half(order):
    chain = []
    for p in order:
        while len(chain) >= 2 and cross(chain[-2], chain[-1], p) <= 0:
            chain.pop()
        chain.append(p)
    return chain

ordered = sorted(points)
lower = half(ordered)
upper = half(ordered[::-1])
print(lower)
print(upper)
print(lower[:-1] + upper[:-1])

It prints

[(0, 0), (4, 0), (4, 3)]
[(4, 3), (2, 3), (0, 0)]
[(0, 0), (4, 0), (4, 3), (2, 3)]

Four corners, two thrown away

The points (1, 1) and (2, 0) are inside the shape, so the band never touches them. The hull is (0, 0), (4, 0), (4, 3), (2, 3).

The cost is the sorting — n log n — because each point is pushed once and popped at most once.

The comparison decides what happens to flat points

<= 0 throws away a point that is exactly on an edge; < 0 keeps it. Some problems want the corners only, some want every point on the boundary.

That single character is the whole difference, and it is a classic wrong answer.

Try it yourself

Why is a right turn popped off the chain?

  • To save memory
  • It is a dent — the band would not touch that point
  • Because the points are sorted
  • To keep the list short

What does this print?

Python

def cross(o, a, b):
    return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])

print(cross((0, 0), (1, 0), (2, 1)))
print(cross((0, 0), (1, 0), (2, -1)))
print(cross((0, 0), (1, 0), (2, 0)))

Answer them in the app

🏆 The Sweep Line

A line moving across the picture

Imagine a vertical line sliding left to right. Nothing happens except at the moments something starts or ends.

So sort those moments, walk through them in order, and keep a count. Everything between two events is unchanged, so there is nothing to look at there.

How many things overlap at once?

Five bookings for a room. Turn each into two events — a +1 where it starts and a -1 where it ends — sort them, and run a total.

The biggest the total ever gets is the answer: three rooms needed. No pair of bookings was ever compared.

Python

bookings = [(1, 4), (2, 6), (5, 8), (7, 9), (3, 5)]

events = []
for start, end in bookings:
    events.append((start, 1))
    events.append((end, -1))
events.sort()

now = 0
best = 0
for position, change in events:
    now += change
    best = max(best, now)
    print(position, change, now)

print("most at once", best)

It prints

1 1 1
2 1 2
3 1 3
4 -1 2
5 -1 1
5 1 2
6 -1 1
7 1 2
8 -1 1
9 -1 0
most at once 3

The tie at position 5 is not an accident

One booking ends at 5 and another starts at 5. Sorting puts -1 before +1, because -1 is smaller — so the room is freed before it is taken, and the count stays at 2.

If a problem says a booking ending at 5 still clashes with one starting at 5, you must sort the other way round. One character, two different answers.

Counting overlapping pairs, still without pairs

Walk the same events. When something starts, it overlaps everything currently open — so add the current count.

Five bookings, five overlapping pairs, and the loop never looked at a pair.

Python

bookings = [(1, 4), (2, 6), (5, 8), (7, 9), (3, 5)]

events = []
for start, end in bookings:
    events.append((start, 1))
    events.append((end, -1))
events.sort()

now = 0
pairs = 0
for position, change in events:
    if change == 1:
        pairs += now
        now += 1
    else:
        now -= 1

slow = 0
for i in range(len(bookings)):
    for j in range(i + 1, len(bookings)):
        if bookings[i][0] < bookings[j][1] and bookings[j][0] < bookings[i][1]:
            slow += 1

print(pairs, slow)

It prints

5 5

What the sweep really bought

The slow loop is n * n. The sweep is the cost of sorting, n log n, and answers the same question exactly.

That is the same bargain as unit 4's two pointers and unit 5's binary search: put the data in order, then walk it once. Most of this track is that sentence.

Try it yourself

Why can the sweep ignore everything between two events?

  • It is not important
  • Nothing starts or ends there, so the count cannot change
  • It is too slow to check
  • It cannot ignore it

What does this print?

Python

bookings = [(1, 5), (2, 3), (4, 6)]
events = []
for start, end in bookings:
    events.append((start, 1))
    events.append((end, -1))
events.sort()

now = 0
best = 0
for position, change in events:
    now += change
    best = max(best, now)

print(best)

Answer them in the app