LaValle, Chapter 6

Combinatorial Motion Planning

Visibility graphs, cell decompositions, and algebraic geometry — the exact algorithms that find paths through C-space without any sampling approximation.

Prerequisites: Chapter 4 (C-space) + Chapter 5 (why sampling). That’s it.
10
Chapters
7+
Simulations
10
Quizzes

Chapter 0: Exact vs. Approximate

You have a 2D robot moving among polygonal obstacles. You want the shortest path from start to goal. Not approximately the shortest. Not probably short. Exactly, provably, optimally shortest.

Chapter 5’s RRT will find some path — maybe twice as long as necessary, maybe winding through unnecessary detours. PRM finds a path through its roadmap, not through the true Cfree. Both are approximations by design. They trade optimality and completeness guarantees for speed in high dimensions.

But there is a class of problems where you can do better. When C-space has special structure — polygonal obstacles, algebraic boundary curves, low dimensionality — you can compute the exact structure of Cfree and find the provably optimal path. These are combinatorial planners (LaValle §6.1): they decompose Cfree into a finite collection of simpler pieces, then search that collection exactly.

The core idea of combinatorics in motion planning: instead of probing C-space randomly, carve it up into a finite number of cells where the topology is simple. Then plan in the abstract graph of cells, not in the continuous space itself. This graph is finite, so search is exact.

What makes combinatorial planning possible?

Three ingredients must align. First, Cobs must have an algebraic description — its boundary is defined by polynomial equations or inequalities, not arbitrary shapes. Second, the dimension of C-space must be small enough that the decomposition has manageable size. Third, you must care about exact answers — the added complexity is only worth paying if approximation is genuinely unacceptable.

When all three hold, combinatorial planning delivers what sampling cannot: completeness (always finds a solution if one exists, always reports failure otherwise) and optimality (among methods that return an exact shortest path).

Two main families

Roadmap Methods
Build a 1D network of curves that captures all of Cfree’s connectivity. Shortest path on the network = shortest path in Cfree. Examples: visibility graph, silhouette curves.
↓ vs
Cell Decomposition Methods
Partition Cfree into convex cells; build an adjacency graph. Any path in Cfree corresponds to a sequence of adjacent cells. Search the graph, then construct a path within that cell sequence.

This chapter covers both, starting with the most beautiful roadmap method: the visibility graph.

C-Space: Exact Boundary vs. Sampled Approximation

A convex polygonal obstacle in 2D workspace. Toggle between exact boundary (combinatorial) and sampled approximation (sampling-based). Notice how sampling misses the sharp corners that define the shortest path.

Showing: exact boundary
What distinguishes a combinatorial planner from a sampling-based planner?

Chapter 1: Visibility Graphs

Imagine you are standing in a 2D room filled with polygonal obstacles. You want to walk from point A to point B along the shortest possible path. The key insight: the shortest path never curves in open space. It is a sequence of straight line segments. And each segment must either start, end, or bend at a vertex of an obstacle.

Why vertices? A shortest path that passes through the interior of an obstacle edge could always be shortcut. A path that bends in free space could always be straightened. The only places a shortest path is forced to change direction are the corners of obstacles — the vertices. This is LaValle’s Theorem 6.1, and it is the foundation of the visibility graph.

The Visibility Graph insight (LaValle §6.2.1): the shortest path from qinit to qgoal through a 2D polygonal environment consists entirely of straight-line segments connecting vertices of the obstacles (plus the start and goal). To find the shortest path, connect every pair of vertices that can “see” each other — with no obstacle in between. The shortest path lives in this graph.

What does “visibility” mean precisely?

Two points p and q are visible to each other if the open line segment pq intersects no obstacle. We allow endpoints to touch obstacle boundaries (since start and goal may lie on edges), but the interior of the segment must be in Cfree.

This condition has an important subtlety: what about line segments that run along an obstacle edge? LaValle handles this carefully — a segment that grazes the boundary of an obstacle (passes through the obstacle’s interior on at least one side) is not considered free. The segment must lie entirely in the closure of Cfree.

Building the visibility graph

The visibility graph G = (V, E) has:

After construction, run Dijkstra’s algorithm on G. The shortest path in G is the shortest path in 2D Euclidean space through Cfree.

Construction cost: naïvely, for n total vertices (obstacle corners + start + goal), there are O(n2) candidate edges. Each edge requires checking visibility against all O(n) obstacle edges, giving O(n3) total. Rotational sweep algorithms (Lee and Preparata, 1979) reduce this to O(n2 log n) by processing all vertices visible from a fixed source in a single angular sweep.

python
import numpy as np
from itertools import combinations

def segments_intersect(p1, p2, p3, p4):
    """Do open segments p1-p2 and p3-p4 intersect?

    Uses cross-product orientation test.
    Returns True if segments cross in their interiors.
    """
    def cross(o, a, b):
        return (a[0]-o[0])*(b[1]-o[1]) - (a[1]-o[1])*(b[0]-o[0])
    d1, d2 = cross(p3,p4,p1), cross(p3,p4,p2)
    d3, d4 = cross(p1,p2,p3), cross(p1,p2,p4)
    if ((d1>0 and d2<0) or (d1<0 and d2>0)) and \
       ((d3>0 and d4<0) or (d3<0 and d4>0)):
        return True
    return False

def is_visible(u, v, obstacle_edges):
    """True if segment u-v does not cross any obstacle edge."""
    for (p3, p4) in obstacle_edges:
        if segments_intersect(u, v, p3, p4):
            return False
    return True

def build_visibility_graph(vertices, obstacle_edges):
    """
    vertices: list of (x,y) — obstacle corners + start + goal
    obstacle_edges: list of ((x1,y1),(x2,y2)) pairs

    Returns adjacency dict with Euclidean distance weights.

    Worked example: 3 vertices forming a triangle obstacle
      vertices = [(0,0),(1,0),(0.5,1),(−1,0.5),(2,0.5)]
      (last two are start and goal)
    """
    graph = {v: {} for v in vertices}
    for u, v in combinations(vertices, 2):
        if is_visible(u, v, obstacle_edges):
            dist = np.linalg.norm(np.array(u) - np.array(v))
            graph[u][v] = dist
            graph[v][u] = dist
    return graph  # then run Dijkstra on graph
Visibility Graph Explorer

A polygonal environment with obstacles. Teal lines = visible edges. Orange path = shortest path via visibility graph. Toggle edges on/off to see which connections are blocked.

All visibility edges shown
Why does the shortest path in a 2D polygonal environment always pass through obstacle vertices (and never bend in open space)?

Chapter 2: Shortest Paths via Visibility

Building the visibility graph gives you a weighted undirected graph G. Now run Dijkstra’s algorithm on G. The path it returns is the globally shortest path through 2D Euclidean space. Not approximately shortest. Exactly shortest.

Let’s trace Dijkstra on a concrete example. Suppose there are two rectangular obstacles creating a narrow corridor. The visibility graph has 10 vertices (8 obstacle corners, start, goal). Dijkstra maintains a priority queue of (distance, vertex) pairs and a distance table initialized to ∞ except d(start) = 0.

Dijkstra on the visibility graph: step by step

Initialize
d[start] = 0, d[all others] = ∞. Priority queue = {(0, start)}.
Extract minimum
Pop (0, start). For each neighbor v of start visible from start: tentative = 0 + dist(start, v). If tentative < d[v], update d[v] and push (tentative, v).
↓ repeat
Relax edges
Pop next minimum (d, u). Relax all neighbors w of u: if d + dist(u,w) < d[w], update and push. Mark u as settled.
Terminate
When goal is popped from the priority queue, d[goal] is the exact shortest distance. Backtrack parent pointers to recover the path.
Correctness guarantee: Dijkstra on the visibility graph returns the globally shortest path in 2D Euclidean space among all obstacle-free paths. This follows from two facts: (1) any shortest path must pass through obstacle vertices only (Theorem 6.1), and (2) Dijkstra finds the shortest path in any non-negative weighted graph. Together, the visibility graph + Dijkstra is a complete, optimal algorithm for 2D polygonal planning.

Complexity analysis

Let n = number of obstacle vertices (plus start and goal). The visibility graph has O(n) vertices and O(n2) edges in the worst case (every pair of vertices is visible). Dijkstra with a binary heap runs in O((V + E) log V) = O(n2 log n). Building the graph first costs O(n3) naïvely (O(n2) pairs × O(n) intersection tests each) or O(n2 log n) with the Lee–Preparata rotational sweep.

So the overall algorithm runs in O(n2 log n). For a robot navigating a building with 200 wall corners, n = 200, n2 log n ≈ 200,000 × 7.6 ≈ 1.5 million operations. That’s microseconds on modern hardware.

The rotational sweep for visibility

The O(n2 log n) algorithm for building the visibility graph uses a rotational sweep from each vertex. Fix a source vertex s. Sort all other vertices by their polar angle around s. Maintain a set of obstacle edges that intersect the current sweep ray, ordered by distance from s. As the ray sweeps past each vertex v:

This processes all vertices visible from s in O(n log n) time. Running from all n sources gives O(n2 log n) total.

python
import heapq

def dijkstra(graph, start, goal):
    """
    graph: dict of {vertex: {neighbor: distance}}
    Returns (shortest_distance, path_list).

    Worked example on visibility graph with 5 vertices:
      graph = {
        'S': {'A': 2.0, 'B': 3.5},
        'A': {'S': 2.0, 'B': 1.2, 'G': 4.1},
        'B': {'S': 3.5, 'A': 1.2, 'G': 2.8},
        'G': {'A': 4.1, 'B': 2.8}
      }
      dijkstra(graph, 'S', 'G')
      -> (5.5, ['S', 'B', 'G'])   # S->B = 3.5, B->G = 2.8
      # Not S->A->G = 6.1, not S->A->B->G = 7.7
    """
    dist  = {v: float('inf') for v in graph}
    prev  = {v: None for v in graph}
    dist[start] = 0.0
    pq = [(0.0, start)]

    while pq:
        d, u = heapq.heappop(pq)
        if d > dist[u]: continue   # stale entry
        if u == goal: break
        for v, w in graph[u].items():
            nd = d + w
            if nd < dist[v]:
                dist[v] = nd
                prev[v] = u
                heapq.heappush(pq, (nd, v))

    # Backtrack path
    path, node = [], goal
    while node is not None:
        path.append(node)
        node = prev[node]
    return dist[goal], path[::-1]
Dijkstra on the Visibility Graph

Watch Dijkstra explore the visibility graph. Settled nodes = popped from queue. Frontier = in queue. Orange = shortest path found.

Ready — press Step or Run
A visibility graph for n obstacle vertices has O(n2) edges. Why doesn’t this make the algorithm impractical for typical indoor environments?

Chapter 3: Vertical Cell Decomposition

The visibility graph finds the shortest path. But sometimes you just need any path — and you need an approach that generalizes beyond Euclidean distance. Enter cell decomposition: carve Cfree into simple pieces, build a graph of adjacencies, search the graph.

The simplest and most beautiful cell decomposition is the vertical decomposition (LaValle §6.2.2, Figure 6.7). Imagine sweeping a vertical line from left to right across the 2D workspace. Every time the sweep line hits an obstacle vertex, it triggers a vertical cut that splits Cfree into a new piece.

The sweep line intuition: think of Cfree as a sheet of paper with obstacle-shaped holes cut out. Now draw vertical lines through every obstacle corner, from floor to ceiling (or until hitting another obstacle). These lines partition Cfree into vertical slabs. Each slab is either fully free (a rectangle or trapezoid) or absent (inside an obstacle).

What the sweep line produces

When the vertical line hits a vertex v of an obstacle, LaValle’s algorithm (Algorithm 6.1) shoots two rays from v: one straight up and one straight down, until each ray hits an obstacle boundary or the workspace boundary. This creates a vertical slice through Cfree at the x-coordinate of v.

Between consecutive event x-coordinates (obstacle vertices sorted by x), each connected piece of Cfree is a trapezoid (or triangle, or rectangle — all special cases of trapezoid). The left and right sides are vertical segments from the slice events; the top and bottom are portions of obstacle edges or workspace boundary.

The event queue

The sweep line algorithm maintains two data structures:

Event queue Q (sorted by x): the x-coordinates of all obstacle vertices. Each event triggers a vertical slice and potentially creates/destroys trapezoids.
Status structure T (sorted by y): the obstacle edges currently intersected by the sweep line, ordered vertically. Enables O(log n) lookup of what is above/below any point.

When the sweep line reaches a vertex v at position (xv, yv), the algorithm classifies v by its connectivity: is it a left endpoint, right endpoint, or interior point of the obstacles incident to it? Each case triggers different trapezoid creation and termination events.

Three types of events

Event typeTriggerAction
Start vertexBoth adjacent obstacle edges go to the rightStart two new trapezoids (one above, one below the new obstacle)
End vertexBoth adjacent edges come from the leftClose two trapezoids; merge them into one if adjacent
Pass-through vertexOne edge comes from left, one goes rightClose current trapezoid(s) and open new one(s)
Vertical Sweep Line Animation

Watch the vertical line sweep from left to right. At each obstacle vertex, vertical cuts are made. Teal regions = completed trapezoids in Cfree. Orange line = sweep line position.

Press “Next Event” to start sweep
In vertical cell decomposition, what event triggers a vertical slice through Cfree?

Chapter 4: Trapezoidal Decomposition

After the sweep line completes, Cfree has been partitioned into trapezoids. Now the key question: how do you turn this geometric partition into a path?

The answer is the adjacency graph (LaValle Definition 6.2): two trapezoids are adjacent if they share a vertical edge segment. Build a graph where each trapezoid is a node and each shared vertical wall is an edge. Then finding a path from qinit to qgoal in Cfree is equivalent to finding a path in this finite graph — any graph search (BFS, DFS, Dijkstra) works.

The abstraction payoff: the adjacency graph has O(n) nodes and O(n) edges (each trapezoid has at most 2 vertical walls, each shared by exactly 2 trapezoids). BFS on this graph runs in O(n) time. You’ve reduced an infinite-dimensional continuous problem to a linear-time graph search.

From cell sequence to actual path

A BFS path gives you a sequence of adjacent trapezoids: T1, T2, ..., Tk. This tells you which regions to pass through, but not the actual waypoints. To construct a path:

Step 1: Find entry points
For each consecutive pair (Ti, Ti+1), the shared vertical edge gives a line segment. Choose a point on that segment (e.g., midpoint) as the transition waypoint.
Step 2: Connect waypoints
The path visits qinit, then the midpoints of shared walls, then qgoal. Each segment between consecutive waypoints stays inside the corresponding trapezoid.
Step 3 (optional): Smooth
The midpoint path may be jagged. Apply a local shortcutting pass: if the segment from waypoint i to waypoint i+2 is collision-free, remove waypoint i+1.

This is not the shortest path (that requires the visibility graph). But it is a valid path guaranteed to exist whenever a path exists — completeness is free.

Handling multiple connected components

If Cfree has multiple disconnected regions (some obstacles completely enclose subregions), the adjacency graph is disconnected. BFS from qinit’s trapezoid will never reach qgoal’s trapezoid, correctly reporting “no path exists.” This is the completeness guarantee in action: the algorithm never returns a false negative.

python
from collections import deque

def build_adjacency_graph(trapezoids):
    """
    trapezoids: list of Trapezoid objects.
    Each Trapezoid has:
      .left_x, .right_x      : x-extent
      .top_edge, .bot_edge   : y(x) functions for top/bottom
      .left_wall, .right_wall: (y_lo, y_hi) of vertical walls

    Returns: dict of {trap_id: [adjacent_trap_ids]}

    Two trapezoids are adjacent iff their shared vertical wall
    has non-zero length overlap.
    """
    adj = {i: [] for i in range(len(trapezoids))}
    for i, t1 in enumerate(trapezoids):
        for j, t2 in enumerate(trapezoids):
            if i >= j: continue
            # Adjacent iff one's right_x == other's left_x
            # AND their vertical walls overlap in y
            if abs(t1.right_x - t2.left_x) < 1e-9:
                overlap = min(t1.right_wall[1], t2.left_wall[1]) - \
                          max(t1.right_wall[0], t2.left_wall[0])
                if overlap > 1e-9:
                    adj[i].append(j); adj[j].append(i)
    return adj

def bfs_trapezoids(adj, start_trap, goal_trap):
    """BFS through adjacency graph. Returns list of trap ids or None."""
    queue = deque([[start_trap]])
    visited = {start_trap}
    while queue:
        path = queue.popleft()
        node = path[-1]
        if node == goal_trap: return path
        for nbr in adj[node]:
            if nbr not in visited:
                visited.add(nbr)
                queue.append(path + [nbr])
    return None  # no path exists
Trapezoidal Adjacency Graph & Path

After decomposition, each trapezoid becomes a node. BFS finds the cell sequence, then waypoints are placed at shared wall midpoints. Green dot = start, red dot = goal.

Press “Show Cells” to begin
After finding a cell sequence via BFS on the adjacency graph, how do you construct the actual continuous path?

Chapter 5: General Cell Decompositions

Trapezoidal decomposition is one specific way to carve up Cfree. But what if Cfree isn’t described by polygonal obstacles? What if the boundary curves are smooth? What if you’re working in 3D or higher dimensions? The vertical sweep idea generalizes into a broader theory of cell decompositions that LaValle develops in §6.3.

A cell decomposition is a partition of Cfree into a finite collection of open cells {C1, C2, ..., Cm} where each cell is homeomorphic to an open ball — no holes, no handles, topologically simple. Two cells are adjacent if their closures share a piece of boundary. The adjacency graph G = (V, E) has one node per cell and one edge per adjacency.

The key theorem (LaValle Proposition 6.2): a path exists from qinit to qgoal in Cfree if and only if a path exists in the adjacency graph between the cell containing qinit and the cell containing qgoal. This is the mathematical reason cell decompositions give complete planners.

Exact vs. approximate decompositions

LaValle makes a critical distinction between two kinds of cell decompositions. In an exact decomposition, the cells perfectly cover Cfree — no cell overlaps Cobs, and every point of Cfree belongs to some cell. A path in the adjacency graph guarantees a collision-free path in C-space. Trapezoidal decomposition is exact.

In an approximate decomposition, cells are simpler (usually axis-aligned cubes) but they may overlap Cobs. Some cells are labeled “mixed” — partly free, partly occupied. These cells are either discarded (safe but incomplete) or subdivided recursively (adaptive resolution). This is the basis of octree and quadtree planners in robotics.

PropertyExact decompositionApproximate decomposition
Cell shapesTrapezoids, curved regionsCubes, voxels
CompletenessCompleteResolution complete
Boundary accuracyPerfect±ε at finest resolution
ScalabilityHarder in high dimensionsBetter (octree is adaptive)
Mixed cellsNoneMust be handled (subdivide or discard)

The connectivity graph encodes all topology

Once you have any valid cell decomposition, the adjacency graph captures everything that matters for path planning. The continuous geometry is abstracted away. You can add obstacles by marking cells as occupied; you can query connectivity via BFS in O(m) where m is the number of cells. If the decomposition has m = O(nd) cells in d-dimensional space, BFS costs O(nd) — exponential in dimension but guaranteed.

python
from collections import deque

class Cell:
    """A cell in the decomposition.

    status: 'free' | 'occupied' | 'mixed'
    neighbors: list of adjacent Cell objects
    """
    def __init__(self, bounds, status='free'):
        self.bounds = bounds      # (x_lo,x_hi,y_lo,y_hi,...)
        self.status = status
        self.neighbors = []
        self.center = [0.5*(b[0]+b[1]) for b in bounds]

def bfs_cells(start_cell, goal_cell):
    """BFS through cell adjacency graph.

    Only traverses 'free' cells.
    Returns list of cells from start to goal, or None.

    Worked example: 3-cell hallway:
      A(free) -- B(free) -- C(free)
      bfs_cells(A, C) -> [A, B, C]

    With obstacle:
      A(free) -- B(occupied) -- C(free)
      bfs_cells(A, C) -> None
    """
    queue = deque([[start_cell]])
    visited = {start_cell}
    while queue:
        path = queue.popleft()
        cell = path[-1]
        if cell is goal_cell: return path
        for nbr in cell.neighbors:
            if nbr not in visited and nbr.status == 'free':
                visited.add(nbr)
                queue.append(path + [nbr])
    return None
Approximate Cell Decomposition (Quadtree)

A quadtree recursively splits mixed cells. Teal = free. Red = occupied. Yellow = mixed (split). Increase resolution to see finer decomposition near obstacle boundaries.

Resolution 2
Resolution: 4 cells × 4 cells
What is a “mixed” cell in an approximate cell decomposition?

Chapter 6: Showcase — Cylindrical Algebraic Decomposition

So far we have decomposed 2D polygonal environments using geometric sweep lines. Now consider a harder problem: C-space obstacles defined by polynomial inequalities, not polygons. The robot configuration space might be described by equations like x2 + y2 ≤ 1 (a disk obstacle) or (x − 0.3)2 + y2 ≤ 0.04. How do you decompose Cfree exactly when the boundaries are curves, not line segments?

The answer is Cylindrical Algebraic Decomposition (CAD), developed by Collins (1975) and central to LaValle’s §6.4. The idea: project the obstacles onto a lower-dimensional space, decompose that space, then “lift” each piece back up. In 2D, project onto the x-axis; in 3D, project onto the xy-plane; and so on recursively.

The core insight of CAD: a semi-algebraic set in n dimensions can be decomposed into finitely many cells, each described by a sign condition on a fixed set of polynomials. Within each cell, the algebraic structure is constant — no topological changes occur. This is the algebraic analogue of “no obstacle vertices between consecutive sweep events.”

What “cylindrical” means

A decomposition is cylindrical if every pair of cells either is disjoint or one is a “cylinder” over the other — meaning the projection of the higher-dimensional cell onto the lower-dimensional base space maps entirely onto a single lower-dimensional cell. This nested structure means you can plan in the projected space and then lift back up safely.

For 2D C-space with polynomial obstacles, the CAD algorithm:

1. Projection
Compute the polynomials in x only (projection onto x-axis) that describe where the obstacle boundary has critical points — where the y-extent of the obstacle changes. These are called discriminants.
2. Isolate x-cells
The roots of the projected polynomials divide the x-axis into intervals (cells) and points (boundaries). Between consecutive roots, the topology of the 2D obstacle cross-section does not change.
3. Lift
For each x-interval, compute the y-extent of Cobs at a sample x value. The region between consecutive boundary curves is either all-free or all-occupied. These are the 2D cells.
4. Adjacency & search
Build the adjacency graph of 2D cells (same as before). BFS from start cell to goal cell. Lift the path.

Showcase: Interactive CAD on Circular Obstacles

The simulation below applies the CAD idea to circular obstacles (defined by x2 + y2 = r2). Place obstacles by clicking the canvas; see the decomposition recomputed and a path found (or failure reported) in real time.

Interactive Cell Decomposition — Place Obstacles

Click the canvas to place circular obstacles. The decomposition recomputes immediately. Green dot = start (left). Red dot = goal (right). Orange path = found path through free cells. Teal = free cells.

Obs radius 0.09
Click canvas to place obstacles. Start = left edge, Goal = right edge.
Complexity of CAD: for n polynomials in d variables each of degree at most k, Collins’ algorithm produces O((nk)2d) cells — doubly exponential in dimension. In practice, CAD is feasible for d ≤ 6 or 7 with careful implementation. For d = 2, it is polynomial in n. This is why the technique is powerful for low-dimensional C-spaces and impractical for high-dimensional ones.

Chapter 7: Complexity of Motion Planning

Now for the bad news. We have been building increasingly powerful exact planners — visibility graphs for 2D polygons, trapezoidal decomposition, cylindrical algebraic decomposition. Each works beautifully in its domain. But how hard is motion planning in general?

In 1979, Reif proved that the general motion planning problem is PSPACE-hard (LaValle §6.5, citing Reif 1979). This is a fundamental lower bound: no algorithm can solve all motion planning instances efficiently unless PSPACE = P, which is widely believed to be false. The proof uses a reduction from the QSAT problem (quantified Boolean satisfiability), which is PSPACE-complete.

What PSPACE-hard means in practice: PSPACE ⊇ NP ⊇ P. PSPACE-hard problems are at least as hard as any problem solvable with polynomial space (including all NP-complete problems). An algorithm for general motion planning that runs in polynomial time would imply PSPACE = P — considered extremely unlikely. The best known algorithms for the general case are exponential in the number of degrees of freedom.

Complexity by problem type

The hardness depends critically on what kind of C-space and obstacles you are working with. LaValle §6.5 surveys the landscape:

Problem variantComplexityAlgorithm
2D, point robot, polygonal obstacles, translationalO(n2 log n)Visibility graph + Dijkstra
2D, polygonal robot, polygonal obstacles, translationalO(n2 log n)C-space Minkowski sum, then visibility
2D, polygonal robot, translation + rotationPolynomial (O(n5) known)Algebraic decomposition of SE(2)
3D, polygonal robot, translation onlyPolynomial3D visibility or CAD
Multiple robots in 2D (coupled)PSPACE-hardNo efficient general algorithm known
General d-DOF, algebraic obstaclesPSPACE-hard (Reif 1979)CAD (doubly exponential in d)

The Reif reduction: why robots are hard

Reif’s 1979 proof constructs a physical system that simulates a QSAT formula. Each Boolean variable is a gear whose rotation corresponds to True or False. Each quantifier corresponds to a robot arm that must be moved in a specific order. Finding a path for all the arms simultaneously is equivalent to deciding QSAT — known to be PSPACE-complete. Therefore motion planning is PSPACE-hard.

The implication for practice: efficient complete algorithms exist for low-dimensional C-spaces with special structure (polygon, algebraic). For high-dimensional C-spaces (robot arms with 6+ DOF), sampling-based planners are the only practical option — they sacrifice completeness and optimality to remain computationally feasible.

Hardness even for simple-looking problems

Adding rotation to a 2D disk robot moving among 2D obstacles takes the problem from polynomial to the harder end of the spectrum. Adding one more rigid body (two coupled robots in 2D) makes it PSPACE-hard. The jump from “polynomial” to “exponential” can happen from a single change in the problem specification. This is why motion planning complexity is studied so carefully: small changes in the problem statement can radically change what’s computationally feasible.

Complexity Landscape Visualizer

Select a problem type to see where it falls in the complexity hierarchy and roughly how many operations are needed for n=100 obstacle vertices.

Select a problem type above
Why did Reif’s 1979 PSPACE-hardness proof matter for motion planning?

Chapter 8: Exact vs. Sampling — When to Use Each

You now know both families: sampling-based planners (Chapter 5) and combinatorial planners (this chapter). They are not rivals — they are tools for different jobs. Choosing the wrong one is expensive. This chapter makes the trade-offs concrete.

The fundamental tension

Combinatorial planners give you guarantees: completeness, optimality (for visibility graphs), and exact answers. But they scale poorly. The visibility graph for n vertices costs O(n2 log n); trapezoidal decomposition costs O(n log n); CAD costs doubly exponential in dimension. For a 6-DOF robot arm with smooth algebraic obstacles, the exact decomposition would have 1030+ cells. Infeasible.

Sampling-based planners scale gracefully. RRT builds a tree of n nodes in O(n log n) with a k-d tree. PRM builds a roadmap in O(n log n). Neither has any dependence on the dimensionality of C-space in their per-iteration cost (only in the number of iterations needed for coverage). But they cannot guarantee optimality or, in finite time, completeness.

The decision rule: use combinatorial planning when d ≤ 3, the obstacle boundaries are algebraic, and you need exact answers or guaranteed completeness. Use sampling-based planning when d ≥ 4, or when the C-space obstacles have no algebraic description, or when approximate answers are acceptable.

The narrow passage problem favors exact methods

Sampling-based planners struggle with narrow passages — regions of Cfree that are necessary to pass through but have very small measure (volume). A random sample has probability proportional to volume of landing in the passage; narrow passages have near-zero volume. Exponentially many samples may be needed.

Exact planners don’t suffer this. The trapezoidal decomposition captures every passage exactly, no matter how narrow. If a passage is 10⊃−⊃¹⊃&sup0; wide, it still gets a cell in the decomposition. One BFS step traverses it. This is the major practical advantage of exact methods in low dimensions.

Structured comparison

PropertyVisibility GraphCell DecompositionPRMRRT
CompletenessCompleteCompleteProb. completeProb. complete
OptimalityGlobally optimalAny pathAsymptotically optimal (PRM*)Not optimal
Narrow passagesHandles exactlyHandles exactlyStrugglesStruggles
High dimensionsFails (exp cost)Fails (exp cost)Scales wellScales well
Dynamic obstaclesRebuild requiredRebuild requiredPartial reuseEasy replan
Typical use case2D navigation, games2D planning, GISMulti-query, static envManipulation, high-DOF
Narrow Passage: Exact vs. Sampling

A narrow corridor between two obstacles. Exact: trapezoidal decomp finds it immediately. Sampling: watch how many samples are needed before one lands in the corridor. Adjust corridor width to see the effect.

Corridor width 0.10
Choose Exact Path or start sampling
A mobile robot navigates a 2D floor plan with 50 polygonal walls (n=200 vertices). Which planner is the best choice, and why?

Chapter 9: Connections & What Comes Next

Combinatorial planning is a complete, principled approach to motion planning that delivers exact answers. But it is also a lens: every sampling-based planner can be seen as an attempt to approximate some combinatorial structure without paying its full computational cost. PRM builds an approximate roadmap; RRT builds an approximate tree. The theoretical guarantees of combinatorial methods illuminate why sampling methods work and where they fail.

Where combinatorial ideas show up elsewhere

Video games and navigation meshes. A navmesh is a trapezoidal/convex decomposition of the walkable area in a game level. NPCs find paths by BFS on the adjacency graph of navmesh cells — exactly the trapezoidal method, applied at 60 fps.
Computational geometry. The Bentley–Ottmann sweep line algorithm for computing all intersections of n line segments runs in O((n + k) log n) where k is the output size — a direct extension of the sweep ideas in vertical decomposition.
GIS routing. Road network routing (GPS navigation) uses a precomputed hierarchical decomposition of geographic space — a form of cell decomposition on the road graph — to achieve sub-millisecond shortest-path queries on continental-scale networks.
Verification and control. Cylindrical algebraic decomposition underlies tools like Mathematica’s Reduce and formal verification of hybrid systems. Any time you ask “for all x in this region, does this polynomial inequality hold?”, you are implicitly doing CAD.

Limitations of combinatorial planning

Comparison with related chapters

ChapterCore ideaCompletenessBest for
Ch 2 (Discrete Planning)Graph search on finite state spaceCompleteAlready discrete; discrete approximations
Ch 5 (Sampling)Probe Cfree via random samplesProbabilistically completeHigh-DOF, complex geometry
Ch 6 (Combinatorial)Exact decomposition of CfreeCompleteLow-DOF, algebraic obstacles, exact answers
Ch 7 (Extensions)Kinodynamic, differential constraintsVariesSystems with dynamics (cars, quadrotors)

Related micro-lessons

The ideas in this chapter connect to several other lessons in Parminces and the microLearning series:

“If you can solve a problem with an exact algorithm, you should. But first make sure you actually have the problem the exact algorithm solves.”
— paraphrasing the practical wisdom of Steven LaValle, Planning Algorithms, 2006
A robot with 7 degrees of freedom navigates among smooth, polynomial-boundary obstacles. Which planning approach should you use, and why?