14_routing_alternatives

command
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 5, 2026 License: MIT Imports: 11 Imported by: 0

README

Example 14 — Routing alternatives

What it demonstrates

Three flavours of shortest-path computation over one routing graph: classical single-source search.Dijkstra, Yen's k-shortest paths for ranked alternatives (search.YenKShortest), and search.AStar driven by a coordinate-based Euclidean heuristic. The A* section is the focus: it shows how an admissible heuristic lets A* reach the same optimal answer as Dijkstra while expanding fewer nodes.

Domain / scenario

A small directed road network between six European cities, weighted by road distance in kilometres:

lisbon    -> madrid     (624)
lisbon    -> porto      (313)
porto     -> madrid     (568)
madrid    -> barcelona  (622)
porto     -> barcelona  (1200)
madrid    -> paris      (1274)
barcelona -> paris      (1000)
paris     -> berlin     (1054)
barcelona -> berlin     (1500)

Each city is also given a fixed 2D coordinate. The coordinates are an input to the example, not derived from the edge weights, so the heuristic is independent of the costs it is guiding.

The heuristic, and why it is admissible

A* orders its frontier by f(n) = g(n) + h(n), where g(n) is the cost already paid to reach n and h(n) estimates the remaining cost to the destination. The estimate used here is the straight-line (Euclidean) distance between a node's coordinate and the destination's coordinate, multiplied by a constant scale:

h(n) = floor(scale * euclidean(coord[n], coord[dst]))

For A* to return the optimal path, h must be admissible: it may never overestimate the true remaining cost, i.e. h(n) <= true_remaining_road_cost(n) for every n. The coordinates in this example are laid out so that each city's straight-line distance to berlin is already a lower bound on its true shortest remaining road distance, so the scale is 1.0 and the floor only tightens the bound further. Because a constant scale applied to a Euclidean metric also satisfies h(u) <= w(u, v) + h(v) for every edge u -> v, the heuristic is additionally consistent, so A* never re-expands a settled node.

Admissibility is not asserted from theory alone: example_test.go verifies it empirically by checking that A*'s returned cost equals Dijkstra's cost for several source/destination pairs. If a future coordinate change made the heuristic overestimate, those costs would diverge and the test would fail.

A* versus Dijkstra: the expansion count

Dijkstra is exactly A* with h(n) = 0. Without a heuristic it has no sense of direction and settles every node whose distance is below the destination's. The coordinate heuristic biases the frontier towards berlin, so A* settles only the nodes on or near the optimal corridor.

On the lisbon -> berlin query Dijkstra settles all 6 nodes, while A* settles 4 (lisbon -> madrid -> barcelona -> berlin) — the porto and paris detours are pruned. Both return the same optimal cost of 2746 km. The example reports both counts so the speed-up is visible, and cross-checks the instrumented count against the library result so the two cannot drift.

How to run

go run ./examples/14_routing_alternatives

Expected output

Dijkstra lisbon -> berlin: 2746 km
  route: lisbon -> madrid -> barcelona -> berlin

Yen's 3 shortest paths lisbon -> berlin:
  1. 2746 km via lisbon -> madrid -> barcelona -> berlin
  2. 2952 km via lisbon -> madrid -> paris -> berlin
  3. 3003 km via lisbon -> porto -> madrid -> barcelona -> berlin

A* lisbon -> berlin (coordinate-based Euclidean heuristic):
  cost = 2746 km, 3 hops
  route: lisbon -> madrid -> barcelona -> berlin

Nodes expanded (lower is better):
  Dijkstra (zero heuristic) : 6
  A* (Euclidean heuristic)  : 4
  same optimal cost: true (2746 km)

Key APIs

  • graph/adjlist.New / AdjList.AddEdge — build the mutable weighted directed road graph.
  • graph/csr.BuildFromAdjList — freeze the builder into an immutable CSR snapshot for queries.
  • graph/csr.CSR.NeighboursByID — public out-neighbour iterator used by the expansion counter.
  • search.Dijkstra / search.Distances.Distance / search.Distances.Path — single-source shortest paths and route read-back.
  • search.YenKShortest — ranked loopless k-shortest alternatives between two nodes.
  • search.AStar — point-to-point shortest path guided by a caller-supplied admissible heuristic.

Further reading


Documentation

Overview

Example 14_routing_alternatives — compare three flavours of shortest-path computation on the same routing graph: classical Dijkstra, Yen's k-shortest for alternatives, and A* with a coordinate-based Euclidean heuristic.

The A* section is the focus. Each city is given a fixed 2D planar coordinate, and the heuristic is the straight-line (Euclidean) distance from a node to the destination, scaled by a conservative factor so it never overestimates the true remaining road distance. That makes the heuristic admissible (and, being a metric scaled by a constant, consistent), so A* returns the same optimal cost as Dijkstra while expanding no more nodes. The example reports both expansion counts so the speed-up is visible, not just asserted.

Sample output: run `go run ./examples/14_routing_alternatives` and capture the stdout — the output is deterministic for the inputs hard-coded above and serves as the regression baseline a future change should preserve.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL