The A* algorithm The A* algorithm is often used for route finding in game AI and is a generalization of Dijkstra's shortest-path algorithm from graph theory. This article describes a simple implementation of the A* algorithm written in OCaml that makes extensive use of the module system including higher-order modules (functors) in order to provide a reusable implementation of this algorithm that is generic over the kinds of graphs that it can handle. Finally, an example application is provided that finds a route around a hill. Introduction Dijkstra's shortest path algorithm finds the shortest path between a pair of nodes in a graph using a breadth-first traversal that incrementally expands a set of nodes around the first node until it includes the second node. Consequently, applying Dijkstra's algorithm to a planar problem results in a roughly circular set of searched nodes. This represents a lot of wasted effort because half of steps taken were in the wrong direction. In many cases, this naive shortest path algorithm may be substantially improved upon by biasing the search towards nodes that are closer according to some heuristic, such as the Euclidean distance between the nodes. In the simplest case of a flat terrain, this heuristic-based algorithm will find the shortest path almost immediately by taking steps from the first node directly towards the second node. This is the A* algorithm. This article describes a simple but efficient and reusable implementation of the A* algorithm in OCaml. Generic implementation In order to abstract our program over the kinds of graphs it can handle, we begin by defining a module signature for a node in a graph that contains the type of each definition required by our algorithm: # module type NODE = sig type t val compare : t -> t -> int val dist : t -> t -> float val neighbors : t -> t list end;; module type NODE = sig type t val compare : t -> t -> int val dist : t -> t -> float val neighbors : t -> t list end
The type t is the abstract type of a node from the point of view of any functor consuming modules of this NODE signature. The compare function is the conventional total ordering over values of the type t . The dist function computes the distance between two nodes and is exact when the nodes are adjacent. The neighbors function returns the list of neighboring nodes of any given node. Now we can define our A* algorithm abstractly as an AStar functor that accepts any module implementing the NODE signature: # module AStar(N: NODE) : sig val search : N.t -> N.t -> N.t list option end = struct
The signature of our functor indicates that it must define a search function that takes a pair of nodes and returns a list including those nodes representing the shortest path between them. An option type is used to convey when no path could be found between the nodes. Our algorithm will use a functional map from nodes to metadata and sets of nodes. The map is easily implemented by applying the Map.Make functor to the N module that was passed to our AStar functor: module Map = Map.Make(N)
One of our sets of nodes, the set of open nodes, must be kept sorted by the estimated path length so we define a Meta module that augments N with the necessary metadata and provides a new compare function that sorts nodes first by f (the estimated path length) and then by the node itself (being careful to reuse the N.compare function rather than the built-in polymorphic compare function which may not provide the correct total ordering over nodes): module Meta = struct type t = { path: N.t list; g: float; f: float; node: N.t } let compare n1 n2 = let c = compare n1.f n2.f in if c<>0 then c else N.compare n1.node n2.node end
The open set is then implemented by applying the Set.Make functor to our Meta module: module OpenSet = struct include Set.Make(Meta) let split_min xs = let x = min_elt xs in x, remove x xs end
As this set is acting as a heap, keeping the nodes sorted by estimated path length, we have augmented it with a split_min function that returns the first node (with the shortest estimated path length) and remaining set. The closed set requires no such special ordering so it is created simply by applying the Set.Make functor to the N module: module ClosedSet = Set.Make(N)
|