A graph is planar if it can be drawn in the plane with no crossing edges. That one geometric fact makes many problems cheaper. Shortest paths can run in linear time, maximum flow becomes a shortest-path problem, NP-hard problems get approximation schemes that come within any fixed factor of optimal, and some counting problems that are #P-hard on general graphs become polynomial. Road networks (approximately), circuit layouts, meshes, image grids and maps all have planar or nearly planar structure, so these algorithms show up in real systems.
This article assumes you already know how to test planarity, which is covered in Planarity Testing in Linear Time. It is about what to do after the test says yes. It covers faces and duality, a tested minimum cut computed as a dual shortest path, separators, the landmark fast algorithms, Baker's technique, failure modes and a checklist.
What planarity buys you
Everything starts with Euler's formula. For a connected plane graph with V vertices, E edges and F faces (counting the unbounded outer face), V - E + F = 2. Each face is bounded by at least three edges and each edge borders at most two faces, so for a simple planar graph with at least three vertices, E <= 3V - 6. Three consequences matter algorithmically:
- Sparsity. E is O(V), so any O(E) algorithm is O(V) on a planar graph and adjacency lists are always small.
- A low-degree vertex always exists. Average degree is below 6, so some vertex has degree at most 5, which lets greedy coloring 6-color any planar graph in linear time; 5 and 4 colors are also achievable (see Graph Coloring).
- Closure under minors. Deleting or contracting edges keeps a graph planar, so recursive algorithms can shrink the graph and stay inside the class.
The bigger gains come from the drawing itself, which the next sections use.
Embeddings, rotation systems and faces
An algorithm cannot use a picture, so a drawing is stored combinatorially as a rotation system: for each vertex, the cyclic order of its neighbours around it. A planarity tester such as Boyer-Myrvold returns exactly this. When you already have a straight-line drawing (vertices with coordinates, no crossings), you can build it by sorting neighbours by angle.
Faces then fall out of a simple walk. Treat each undirected edge as two darts, (u, v) and (v, u). From dart (u, v), the next dart on the same face is (v, w), where w follows u when turning clockwise around v. Each dart belongs to exactly one face. Under this convention the outer face is the walk with negative signed area, and checking V - E + F = 2 afterwards catches a corrupted rotation system.
import heapq, math
from collections import defaultdict
def rotation_system(pos, edges):
"""Neighbours of each vertex sorted counter-clockwise by angle (straight-line drawing)."""
nbrs = defaultdict(list)
for u, v in edges:
nbrs[u].append(v); nbrs[v].append(u)
rot = {}
for v, ns in nbrs.items():
x0, y0 = pos[v]
rot[v] = sorted(ns, key=lambda w: math.atan2(pos[w][1] - y0, pos[w][0] - x0))
return rot
def faces(rot):
"""Trace every face. Dart (u, v) is followed by (v, w), w = clockwise successor of u at v."""
face_of, walks = {}, []
for u in rot:
for v in rot[u]:
if (u, v) in face_of:
continue
walk, d = [], (u, v)
while d not in face_of:
face_of[d] = len(walks); walk.append(d)
a, b = d
ring = rot[b]
w = ring[(ring.index(a) - 1) % len(ring)]
d = (b, w)
walks.append(walk)
return face_of, walks
def outer_face(walks, pos):
"""Under this convention the outer face is the one walk with negative signed area."""
area = lambda w: sum(pos[a][0] * pos[b][1] - pos[b][0] * pos[a][1] for a, b in w)
return min(range(len(walks)), key=lambda i: area(walks[i]))
The dual graph
The dual graph G* has one vertex per face of G and one edge for every edge of G, joining the two faces that the edge separates. Dual edges carry the primal edge's weight. The key correspondence is that a set of edges forms a simple cycle in G* exactly when the same edges form a minimal cut (a bond) in G. Intuitively, a closed curve through faces separates the vertices inside it from those outside, and the edges it crosses are the cut.
So cut problems in G become path or cycle problems in G*, which are easier. G* has F = E - V + 2 vertices, so it is also O(n), and it is built in one pass over the dart-to-face map.
Minimum cut as a shortest path in the dual
Take the simplest useful case. The graph is undirected, edges have capacities, and s and t both lie on the outer face. Cut the outer face into two pieces at s and t: arc A runs along the boundary from s to t, and arc B runs back from t to s. Any s-t cut must separate the two boundary arcs, so in the dual it is a path from A to B. Hence min s-t cut = shortest A-to-B path in the dual, and by max-flow min-cut it also equals the max-flow value. This is Hassin's observation, building on Itai and Shiloach's st-planar flow algorithm, and it needs one Dijkstra run instead of an augmenting-path loop. Here it is, continuing the listing above. cap holds each undirected edge once, as {(u, v): capacity}:
def min_cut_dual(rot, cap, s, t, outer):
"""Min s-t cut when s and t both lie on face `outer`: shortest path in the dual."""
face_of, walks = faces(rot)
side, cur = {}, -1 # split the outer face: arc A = -1, arc B = -2
walk = walks[outer]
k = next(i for i, (a, _) in enumerate(walk) if a == s)
for d in walk[k:] + walk[:k]:
if d[0] == t:
cur = -2
side[d] = cur
node = lambda d: side.get(d, face_of[d])
adj = defaultdict(list)
for (u, v), c in cap.items():
f, g = node((u, v)), node((v, u))
adj[f].append((g, c)); adj[g].append((f, c))
dist, pq = {-1: 0}, [(0, -1)]
while pq:
d, f = heapq.heappop(pq)
if f == -2:
return d
if d > dist[f]:
continue
for g, c in adj[f]:
if d + c < dist.get(g, math.inf):
dist[g] = d + c; heapq.heappush(pq, (d + c, g))
# usage
face_of, walks = faces(rot)
value = min_cut_dual(rot, cap, s, t, outer_face(walks, pos))Before embedding it, the listing was checked against an Edmonds-Karp max-flow on 2,000 random grids of up to 6 x 6 vertices, with random diagonals and random integer capacities, and s and t sampled from the outer face. Every value matched, and V - E + F = 2 held for every traced embedding. Three limits are deliberate. It returns the cut value; per-edge flows need the dual distances as potentials (Hassin's second step). It assumes the outer boundary is a simple cycle. And it needs s and t on a common face; otherwise the minimum cut is the shortest dual cycle separating them, which needs heavier machinery.
Worked example: a 3 x 4 grid
Use the figure's 3 x 4 grid with s at the top-left corner and t at the bottom-right. Tracing gives V = 12, E = 17 and F = 7: six square faces plus the outer face, and 12 - 17 + 7 = 2. The outer walk has 10 darts. The two obvious cuts are the edges around s (5 + 6 = 11) and around t (9 + 1 = 10). Dijkstra in the dual finds something cheaper. It leaves arc A across the right-hand edge of capacity 1 into the top-right face, drops across the edge of capacity 4 into the face below, and exits across the bottom edge of capacity 1 into arc B, for a total of 6.
Edmonds-Karp on the primal agrees: the max flow is 6. One Dijkstra over at most eight dual nodes replaced repeated augmenting-path searches, a difference that matters on pixel grids with millions of vertices.
Separators and divide and conquer
The second big idea is the planar separator theorem of Lipton and Tarjan (1979). Every n-vertex planar graph has a set of at most 2√2 · √n vertices whose removal leaves no component larger than 2n/3, and that set can be found in linear time. The construction: run BFS and group vertices into levels; small levels near the median cut out a band of bounded depth; triangulate it, and some fundamental cycle of the BFS tree, of length O(depth), splits the band in a balanced way.
Separators turn into algorithms by recursion: solve each side, then patch the answer across the O(√n) boundary vertices. Two uses dominate:
- Nested dissection. Order the unknowns of a sparse linear system whose sparsity pattern is planar, such as a 2D finite-element mesh, by numbering separator vertices last. Gaussian elimination then creates O(n log n) fill and needs O(n3/2) operations, instead of the dense blow-up from a naive ordering. Sparse direct solvers use this family of orderings.
- r-divisions. Frederickson's r-division recursively splits the graph into O(n/r) regions, each with O(r) vertices and O(√r) boundary vertices. Algorithms precompute inside regions and then work only on the much smaller boundary graph. Most fast planar shortest-path and distance-oracle results are built on this.
The fast algorithms, and when to use them
Here are the landmark results, with the general-graph baseline for comparison. Use them to recognise when it is worth exploiting planarity.
| Problem | Planar result | General-graph baseline |
|---|---|---|
| Single-source shortest paths, non-negative weights | O(n), Henzinger, Klein, Rao and Subramanian (1997) | O(m + n log n) Dijkstra with Fibonacci heaps |
| Shortest paths with negative weights | O(n log2 n / log log n), Mozes and Wulff-Nilsen (2010) | O(nm) Bellman-Ford |
| Max flow, s and t on one face | One dual shortest-path computation (Hassin) | General max-flow algorithms |
| Max flow, directed, arbitrary s and t | O(n log n), Borradaile and Klein (2009) | General max-flow algorithms |
| Counting perfect matchings | Polynomial via Kasteleyn's Pfaffian orientation | #P-complete (Valiant) |
| Max cut | Polynomial (Hadlock, 1975) | NP-hard |
| Isomorphism | Linear time (Hopcroft and Wong, 1974) | No polynomial algorithm known |
For shortest paths, compare with Dijkstra's algorithm in depth. The planar algorithms carry large constants and live mostly in research code, so a tuned binary-heap Dijkstra is the practical baseline to beat.
Approximation with Baker&#x27;s technique
Many NP-hard problems remain NP-hard on planar graphs, including maximum independent set, minimum vertex cover and minimum dominating set. Baker's technique (1994) gives a polynomial-time approximation scheme for them. Layer the embedded graph by face distance from the outer face: layer 1 is the outer boundary, layer 2 is what becomes outer after removing layer 1, and so on. Pick a parameter k. For each offset i from 0 to k, delete every layer congruent to i modulo k + 1. What remains splits into k-outerplanar pieces, and k-outerplanar graphs have treewidth at most 3k - 1, so each piece is solved exactly by dynamic programming over a tree decomposition.
For maximum independent set, one of the k + 1 offsets deletes at most a 1/(k + 1) share of an optimal solution, so the best of the k + 1 answers is at least k/(k + 1) of optimal. The running time is 2O(k) n, so one knob trades accuracy for time.
Planarity in practice
Start from a library planarity test that returns an embedding: NetworkX's check_planarity returns a flag and a PlanarEmbedding, and Boost Graph Library has boyer_myrvold_planarity_test and planar_face_traversal. Real data is rarely perfectly planar. Road networks have bridges and tunnels. Pixel grids are planar with 4-connectivity, but adding both diagonals creates crossings. For the BFS levels separators use, see BFS and DFS; for the general flow algorithms the dual trick replaces, see Ford-Fulkerson max flow.
Failure modes
- The embedding does not match the graph. Angle-sorting coordinates from a drawing that actually has crossings produces a rotation system that is not planar. Face tracing still terminates but V - E + F is not 2. Always assert Euler's formula after tracing.
- Parallel edges and loops. Multigraphs are fine combinatorially, but angle sorting gives two parallel edges the same angle and an arbitrary order. Merge parallel edges first by summing their capacities.
- s and t not on a common face. The simple dual shortest path then gives a wrong answer silently. Check that both appear on the same face walk before using it.
- Directed capacities. The listing treats edges as undirected. For directed planar flow, dual edges become directed with asymmetric weights, and arbitrary s and t require Borradaile-Klein-style algorithms.
What to do next
- Confirm your input is really planar with a library test, and keep the embedding it returns.
- Trace faces and assert V - E + F = 2 for each connected component.
- If you need a min cut with s and t on one face, run the dual shortest path above and cross-check it once against a max-flow solver.
- For repeated shortest-path queries on large planar graphs, prototype a separator-based r-division and compare it with tuned Dijkstra on your data.
- For NP-hard problems on planar inputs, try Baker's layering with k = 2 or 3 before reaching for an ILP solver.
- Document which properties your code assumes (simple, 2-connected, undirected, s and t on one face) and validate them at the API boundary.