Computational Geometry contents
Manhattan Distance: Farthest Pair, Rotation and MST
Tricks for the taxicab metric: the farthest pair by trying sign patterns, the 45° rotation to Chebyshev distance, and a Manhattan minimum spanning tree in O(n log n).
Read first: Basic Geometry: Vectors, Dot and Cross Products, Minimum Spanning Tree: Kruskal's Algorithm
Definition
For points and in the plane, the Manhattan (taxicab) distance is
It is the length of the shortest path in a city where you can only travel along horizontal and vertical streets. Many problems with this distance have neat tricks, collected here.
def manhattan(p, q):
return abs(p[0] - q[0]) + abs(p[1] - q[1])
assert manhattan((1, 2), (4, -2)) == 7Farthest pair of points
Given points, find the pair with the maximum Manhattan distance.
The key observation is that : if we guess the sign of each absolute value, wrong guesses only produce smaller values, so they never beat the true answer. In one dimension, ; fixing the sign pattern makes and independent:
So for each of the sign patterns in dimensions, compute the value for every point and take max minus min; the answer is the best over all patterns. Time: .
def farthest_pair_distance(points):
d = len(points[0])
best = 0
for mask in range(1 << d):
values = [sum(p[j] if mask >> j & 1 else -p[j] for j in range(d)) for p in points]
best = max(best, max(values) - min(values))
return best
import random
rnd = random.Random(1)
for d in (1, 2, 3, 4):
for _ in range(200):
pts = [tuple(rnd.randint(-20, 20) for _ in range(d)) for _ in range(rnd.randint(2, 12))]
brute = max(sum(abs(a - b) for a, b in zip(p, q)) for p in pts for q in pts)
assert farthest_pair_distance(pts) == bruteRotation to the Chebyshev distance
For all real : (check the signs of and ). Applying it with , :
The right side is the Chebyshev distance of the transformed points
is a rotation by with a dilation by . So a problem on Manhattan distances (diamonds as circles) can be converted into one on Chebyshev distances (squares as circles), which are usually simpler: the Manhattan "ball" around a point becomes an axis-aligned square.
def chebyshev(p, q):
return max(abs(p[0] - q[0]), abs(p[1] - q[1]))
def rotate45(p):
return (p[0] + p[1], p[1] - p[0])
for _ in range(1000):
p = (rnd.randint(-50, 50), rnd.randint(-50, 50))
q = (rnd.randint(-50, 50), rnd.randint(-50, 50))
assert manhattan(p, q) == chebyshev(rotate45(p), rotate45(q))Example use: "count the points within Manhattan distance of a query" becomes a rectangle query on the rotated points.
points = [(rnd.randint(-30, 30), rnd.randint(-30, 30)) for _ in range(300)]
rotated = [rotate45(p) for p in points]
center, r = (3, -4), 12
cx, cy = rotate45(center)
in_square = sum(1 for (x, y) in rotated if abs(x - cx) <= r and abs(y - cy) <= r)
assert in_square == sum(1 for p in points if manhattan(p, center) <= r)Manhattan minimum spanning tree
Given points (distinct), connect them with a minimum total edge weight, where the weight of an edge is the Manhattan distance. The complete graph has edges, but we can restrict to candidate edges.
Claim. For a point and any two other points in the same octant around (one of the 8 regions between the lines through with slopes , , and ), we have . Hence, in a minimum spanning tree never needs to be connected to both, since replacing the longer edge by shortens the tree. So it suffices to consider, for every point, its nearest neighbor in each of the 8 octants: candidate edges (and by symmetry, only 4 of the octants need to be searched, since an edge found from one end serves both).
Nearest neighbor in one octant, by sweep
Consider the octant "north-north-east" of each point : points with and (that is, is up and to the right of , but closer to the vertical line through than to the diagonal). For such the distance is simply .
Process the points by non-decreasing . Maintain an active set of points that have not yet found their neighbor. When a new point arrives, every active with in its octant gets as its nearest neighbor in it: any later point has a bigger , hence is farther. Those are removed from the active set, and is inserted.
The active set is ordered by ; its elements have increasing as well, so the points that need are consecutive: start at the one with the largest and walk down while . Each point is removed once, so the sweep is in total. Rotating the input by (swap x, y; then negate x; ...) four times covers all the octants.
from bisect import bisect_right
def manhattan_mst_edges(points):
"""Candidate edges (weight, u, v) that contain a Manhattan minimum spanning tree."""
ps = [list(p) for p in points]
ids = list(range(len(ps)))
edges = []
for rot in range(4):
ids.sort(key=lambda i: ps[i][0] + ps[i][1])
keys = [] # x-coordinates of the active points, sorted
active = {} # x -> point id
for i in ids:
xi, yi = ps[i]
pos = bisect_right(keys, xi) - 1 # the largest active x <= x_i
while pos >= 0:
j = active[keys[pos]]
if xi - yi > ps[j][0] - ps[j][1]:
break
edges.append(((xi - ps[j][0]) + (yi - ps[j][1]), i, j))
del active[keys[pos]]
del keys[pos]
pos -= 1
if xi not in active:
keys.insert(bisect_right(keys, xi), xi)
active[xi] = i
for p in ps: # rotate the picture
if rot & 1:
p[0] = -p[0]
else:
p[0], p[1] = p[1], p[0]
return edgesThe list of candidate edges is fed to Kruskal's algorithm with a disjoint set union:
def kruskal(n, edges):
parent = list(range(n))
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
total = 0
for w, u, v in sorted(edges):
ru, rv = find(u), find(v)
if ru != rv:
parent[ru] = rv
total += w
return total
def manhattan_mst(points):
return kruskal(len(points), manhattan_mst_edges(points))
def prim_complete(points):
"""O(n^2) reference: Prim on the complete graph."""
n = len(points)
best = [float("inf")] * n
used = [False] * n
best[0] = 0
total = 0
for _ in range(n):
u = min((i for i in range(n) if not used[i]), key=lambda i: best[i])
used[u] = True
total += best[u]
for v in range(n):
if not used[v]:
best[v] = min(best[v], manhattan(points[u], points[v]))
return total
assert manhattan_mst([(0, 0), (1, 0), (5, 0)]) == 5
assert manhattan_mst([(0, 0), (2, 2), (4, 0), (2, -2)]) == 12 # a diamond: 3 of its 4 sides of length 4
for _ in range(500):
n = rnd.randint(1, 30)
pts = list({(rnd.randint(0, 25), rnd.randint(0, 25)) for _ in range(n)})
assert manhattan_mst(pts) == prim_complete(pts), pts
assert len(manhattan_mst_edges(pts)) <= 4 * len(pts)The number of candidate edges is at most (each point contributes at most one edge per direction of the four sweeps), so Kruskal's sort dominates with . The pts used above are deduplicated; the algorithm assumes distinct points (equal points can be merged first).
The algorithm follows Zhou, Shenoy and Nicholls (2002); Stolfi's divide-and-conquer finds the same nearest neighbors with the same complexity.
Practice problems
- AtCoder Beginner Contest 178E - Dist Max
- CodeForces 1093G - Multidimensional Queries
- CodeForces 944F - Game with Tokens
- AtCoder Code Festival 2017D - Four Coloring
- The 2023 ICPC Asia EC Regionals Online Contest (I) - J. Minimum Manhattan Distance
- Petrozavodsk Winter Training Camp 2016 Contest 4 - B. Airports