This file provides a solution to the common graph problem of finding the Single Source Shortest Path, through the implementation of Dijkstra's algorithm. Before diving into the actual algorithm's implementation, it is necessary to provide a basic implementation of two ADTs: a graph, and a min-heap. The graph is implemented as a weighted, directed graph, and the min-heap is implemented as a priority queue.
The graph is represented through three families of dynamic facts asserted into the Prolog knowledge base:
graph/1: identifies a graph by name.vertex/2: associates a vertex to its graph.arc/4: associates a directed, weighted arc to its graph, source vertex, destination vertex, and weight.
| Predicate | Brief Description |
|---|---|
new_graph(G) |
Inserts a new graph G into the knowledge base, or succeeds silently if it already exists. |
delete_graph(G) |
Removes graph G along with all its vertices and arcs from the knowledge base. |
new_vertex(G, V) |
Adds vertex V to graph G, provided G exists and V is not already present. |
vertices(G, Vs) |
Unifies Vs with the list of all vertices belonging to graph G. |
list_vertices(G) |
Prints the list of vertices of graph G to the console. |
new_arc(G, U, V, Weight) |
Adds a directed arc from U to V with weight Weight to graph G. |
arcs(G, Es) |
Unifies Es with the list of all arcs belonging to graph G. |
neighbors(G, V, Ns) |
Unifies Ns with the list of direct successors of vertex V in graph G. |
list_arcs(G) |
Prints the list of arcs of graph G to the console. |
list_graph(G) |
Prints both the vertices and arcs of graph G to the console. |
The predicate new_graph(G) succeeds when a graph identified by G already exists in the knowledge base (cutting after the first clause), or otherwise asserts a new graph(G) fact.
Complexity:
This predicate always succeeds (due to the use of retractall/1), and wipes the graph identified by G, along with all associated vertex/2 and arc/4 facts, from the knowledge base.
Complexity:
This predicate succeeds when the graph G exists in the knowledge base and the vertex V is not already present in it. It then asserts a new vertex(G, V) fact at the end of the knowledge base via assertz/1.
Complexity: \+ vertex(G, V).
This predicate uses findall/3 to collect all vertices V such that vertex(G, V) holds, unifying the result with Vs.
Complexity:
This predicate succeeds when the graph G exists, both endpoint vertices U and V exist in G, and no arc between them is already present. It then asserts a new arc(G, U, V, Weight) fact.
Complexity: \+ arc(G, U, V, _).
This predicate uses findall/3 to collect all arcs of the form arc(G, U, V, Weight) such that the corresponding fact holds in the knowledge base.
Complexity:
This predicate uses findall/3 to collect all vertices N such that a direct arc arc(G, V, N, _) exists.
Complexity:
The priority queue needed by Dijkstra's algorithm is implemented as a min-heap. The heap is represented through two families of dynamic facts:
heap/2: stores the heap identifier and its current size.heap_entry/4: stores each element as a quadrupleheap_entry(H, Position, Key, Value), wherePositionencodes the node's location in the logical binary tree.
The binary tree structure is navigated arithmetically: for a node at position
| Predicate | Brief Description |
|---|---|
new_heap(H) |
Inserts a new heap into the Prolog knowledge base. |
delete_heap(H) |
Removes the entire heap (including all entries) from the Prolog knowledge base. |
heap_size(H, S) |
Succeeds when S is the current size of the heap H. |
empty(H) |
Succeeds when the heap H contains no elements. |
not_empty(H) |
Succeeds when the heap H contains at least one element. |
head(H, K, V) |
Succeeds when V is the element of heap H with the minimum key K. |
insert(H, K, V) |
Inserts element V with key K into heap H, restoring the min-heap property. |
extract(H, K, V) |
Removes and returns the minimum-key pair K, V from heap H, restoring the min-heap property. |
modify_key(H, NewKey, OldKey, V) |
Replaces OldKey (associated with value V) with NewKey, restoring the min-heap property. |
list_heap(H) |
Prints the internal state of the heap to the Prolog console. |
The predicate new_heap(H) succeeds when a heap identified by H already exists in the knowledge base (cutting after the first clause), or otherwise asserts a new heap(H, 0) fact, initializing the size to zero.
Complexity:
This predicate always succeeds and wipes the entire heap identified by H from the knowledge base, using retractall/1 to remove all heap_entry/4 facts and the heap/2 fact itself.
Complexity:
This predicate succeeds when S is unified with the size of the heap identified by H, directly by unifying with the heap(H, S) fact.
Complexity:
The predicate empty/1 succeeds when the heap identified by H has size 0. Conversely, not_empty/1 succeeds when the heap size is anything other than 0, verified via \+ heap_size(H, 0).
Complexity:
This predicate succeeds when the heap identified by H is not empty and the heap_entry fact at position 1 (the root of the logical binary tree) exists, unifying K and V with its key and value respectively.
Complexity:
This predicate succeeds when, after a new element V with key K is appended to the heap H at the next available position (i.e., NewS = OldS + 1), the heap property is restored by calling the helper heapify_up/2.
Complexity:
This predicate restores the min-heap property after an insertion by bubbling the newly inserted element upward until its key is no longer smaller than its parent's key. It is structured in three clauses:
Complexity:
This predicate succeeds when the root element (the minimum) of heap H is removed and returned, after which the heap property is restored. It is structured in two clauses:
- Clause A: the heap contains exactly one element: the root
heap_entry(H, 1, K, V)is simply retracted and the heap size is set to0. - Clause B: the heap contains more than one element: the root is retracted, the last element is moved to position
1, the heap size is decremented, andheapify_down/2is called to restore the min-heap property.
Complexity:
This predicate implements a logic version of the MIN-HEAPIFY procedure from [[Introduction To Algorithms — T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein]]. It pushes the element at position ParentP downward until the heap property is restored. It is structured in two clauses:
Complexity:
This predicate determines which of the two children of a node at ParentP has the smaller key, unifying ChildP with the position of the minor child. It is structured in three clauses:
-
Clause A: both children exist and the left child's key is
$\leq$ the right child's key:ChildPis unified with the left child's position. -
Clause B: both children exist and the right child's key is strictly
$<$ the left child's key:ChildPis unified with the right child's position. -
Clause C: only the left child exists (i.e., the right child's position exceeds the heap size):
ChildPis unified with the left child's position.
Complexity:
This predicate succeeds when the element at ParentP has a key strictly greater than the element at ChildP, indicating that a swap is necessary to maintain the min-heap property. It relies on the key_less/2 predicate to handle inf-safe comparisons.
Complexity:
This predicate swaps the heap_entry facts at positions AP and BP within heap H by retracting both entries and re-asserting them with their positions exchanged.
Complexity:
These predicates implement inf-safe key comparisons, handling the special symbolic value inf which represents an infinite distance in the SSSP context:
key_less(A, inf)always succeeds (any finite key is less than infinity).key_less(inf, _)always fails (infinity is never less than anything).key_less(A, B)falls through to arithmeticA < Bfor finite values.
key_leq/2 follows the same logic, using =< for finite comparisons.
Complexity:
This predicate replaces the key OldKey associated with value V in heap H with a new key NewKey, then restores the heap property. It is structured in three clauses:
- Case A (
OldKey = inf): retracts the entry with keyinf, asserts the new key, and callsheapify_up/2, since any finite key is smaller than infinity. - Case B (
NewKey =< OldKey): retracts the old entry, asserts the new key, and callsheapify_up/2, since decreasing a key may violate the heap property upward. - Case C (
NewKey > OldKey): retracts the old entry, asserts the new key, and callsheapify_down/2, since increasing a key may violate the heap property downward.
Complexity:
This predicate prints the internal state of the heap to the Prolog console by first printing the heap/2 fact and then iterating over all entries via the helper print_node/3.
Complexity:
The Single Source Shortest Path is computed via Dijkstra's algorithm, using the min-heap defined above as the priority queue. The algorithm's state is stored in three families of dynamic facts:
distance/3: stores the current best-known distance from the source to each vertex.previous/3: stores the predecessor of each vertex on the shortest path tree.visited/2: marks vertices that have already been extracted from the priority queue.
| Predicate | Brief Description |
|---|---|
dijkstra_sssp(G, Source) |
Runs Dijkstra's algorithm on graph G from vertex Source, populating the distance/3 and previous/3 facts. |
shortest_path(G, Source, V, Path) |
Unifies Path with the list of arcs forming the shortest path from Source to V in graph G, after dijkstra_sssp/2 has been called. |
This is the main entry point for the algorithm. It proceeds in four steps:
- Verifies that the graph
Gand source vertexSourceexist. - Calls
initialize_single_source/2to set all distances toinfand the source distance to0. - Creates a fresh heap and fills it with all vertices, keyed by their initial distances.
- Calls
shortest_subpath/2to run the main Dijkstra loop.
Complexity:
This predicate initializes the algorithm's state by retracting all existing distance/3, previous/3, and visited/2 facts, then setting every vertex's distance to inf via init_distances/2, and finally setting the source vertex's distance to 0 via change_distance/3.
Complexity:
This predicate recurses over the list of vertices, asserting distance(G, V, inf) for each vertex.
Complexity:
This predicate recurses over the list of vertices, inserting each one into the heap keyed by its current distance (initially inf for all vertices except the source, which is 0).
Complexity:
This predicate implements the main loop of Dijkstra's algorithm. At each step it extracts the vertex U with the minimum distance from the priority queue, marks it as visited, collects all unvisited neighbors of U, and calls relax_neighbors/4 to potentially update their distances.
Complexity:
This predicate recurses over the list of unvisited neighbors of vertex U, calling relax/4 for each one.
Complexity:
This predicate attempts to relax the edge V if the path through U is shorter than the current best-known distance. It is structured in three clauses:
- Case A:
U's distance isinf: no relaxation is possible, the predicate cuts immediately. - Case B: the new distance
DU + Wis shorter thanV's current distance:modify_key/4is called to updateV's key in the heap, andchange_distance/3andchange_previous/3update the knowledge base accordingly. - Case C: the new distance is not an improvement: the predicate succeeds trivially without modifying anything.
Complexity: modify_key/4).
This predicate succeeds when NewD represents a strictly shorter distance than OldD. It is structured in two clauses:
- Case A:
OldD = inf: any finite distance is always shorter than infinity. - Case B: both distances are finite: standard arithmetic comparison
NewD < OldD.
Complexity:
This predicate reconstructs the shortest path from Source to V by following the previous/3 chain backwards. It is structured in two clauses:
- Base Case:
Source = V: the path is empty (no arcs needed to reach the source from itself). - Recursive Case: looks up
V's predecessorUviaprevious/3, retrieves the arc connecting them, recursively builds the path toU, and appends the arcarc(G, U, V, W)to it.
Complexity: append/3 calls. This can be reduced to
These predicates update the knowledge base by retracting all existing distance/3 (or previous/3) facts for vertex V in graph G and asserting the new value. The use of retractall/1 guarantees that no stale facts remain.
Complexity: