Another one.
It uses library(assoc)
association lists to store the path associated to each vertex. This avoids having to code a list scan. The list of "vertex that shall be visited next" is still a vanilla list, but we need to eliminate duplicated vertex and retain those vertexes with minimal distances only after each visit, then sort the list by vertex distance so that the next vertex that shall be visitited is at the head.
(SWI-Prolog has a built-in associative data structure, the SWI-Prolog dict
, which I'm not using here. Does library(assoc)
exist in GNU Prolog? It should...)
?- main.
VisitThese is currently: [0-a]
Neighbors of vertex a : [s-7,d-4,b-3]
Dirty visitations : [7-s,4-d,3-b], Clean visitations : [3-b,4-d,7-s]
VisitThese is currently: [3-b,4-d,7-s]
Neighbors of vertex b : [d-4,a-3,s-2,h-1]
Dirty visitations : [5-s,4-h,4-d,7-s], Clean visitations : [4-d,4-h,5-s]
VisitThese is currently: [4-d,4-h,5-s]
Neighbors of vertex d : [a-4,f-5,b-4]
Dirty visitations : [9-f,4-h,5-s], Clean visitations : [4-h,5-s,9-f]
VisitThese is currently: [4-h,5-s,9-f]
Neighbors of vertex h : [f-3,g-2,b-1]
Dirty visitations : [7-f,6-g,5-s,9-f], Clean visitations : [5-s,6-g,7-f]
VisitThese is currently: [5-s,6-g,7-f]
Neighbors of vertex s : [c-3,b-2,a-7]
Dirty visitations : [8-c,6-g,7-f], Clean visitations : [6-g,7-f,8-c]
VisitThese is currently: [6-g,7-f,8-c]
Neighbors of vertex g : [h-2,e-2]
Dirty visitations : [8-e,7-f,8-c], Clean visitations : [7-f,8-c,8-e]
VisitThese is currently: [7-f,8-c,8-e]
Neighbors of vertex f : [d-5,h-3]
Dirty visitations : [8-c,8-e], Clean visitations : [8-c,8-e]
VisitThese is currently: [8-c,8-e]
Neighbors of vertex c : [l-2,s-3]
Dirty visitations : [10-l,8-e], Clean visitations : [8-e,10-l]
VisitThese is currently: [8-e,10-l]
Neighbors of vertex e : [g-2,k-5]
Dirty visitations : [13-k,10-l], Clean visitations : [10-l,13-k]
VisitThese is currently: [10-l,13-k]
Neighbors of vertex l : [i-4,j-4,c-2]
Dirty visitations : [14-i,14-j,13-k], Clean visitations : [13-k,14-i,14-j]
VisitThese is currently: [13-k,14-i,14-j]
Neighbors of vertex k : [e-5,i-4,j-4]
Dirty visitations : [14-i,14-j], Clean visitations : [14-i,14-j]
VisitThese is currently: [14-i,14-j]
Neighbors of vertex i : [j-6,k-4,l-4]
Dirty visitations : [14-j], Clean visitations : [14-j]
VisitThese is currently: [14-j]
Neighbors of vertex j : [k-4,l-4,i-6]
Dirty visitations : [], Clean visitations : []
Vertex a is at distance 0 via [a-0]
Vertex b is at distance 3 via [a-0,b-3]
Vertex c is at distance 8 via [a-0,b-3,s-5,c-8]
Vertex d is at distance 4 via [a-0,d-4]
Vertex e is at distance 8 via [a-0,b-3,h-4,g-6,e-8]
Vertex f is at distance 7 via [a-0,b-3,h-4,f-7]
Vertex g is at distance 6 via [a-0,b-3,h-4,g-6]
Vertex h is at distance 4 via [a-0,b-3,h-4]
Vertex i is at distance 14 via [a-0,b-3,s-5,c-8,l-10,i-14]
Vertex j is at distance 14 via [a-0,b-3,s-5,c-8,l-10,j-14]
Vertex k is at distance 13 via [a-0,b-3,h-4,g-6,e-8,k-13]
Vertex l is at distance 10 via [a-0,b-3,s-5,c-8,l-10]
Vertex s is at distance 5 via [a-0,b-3,s-5]
As computed by:
% =========
% Graph definition
% =========
% ---------
% "Asymmetric connection relation"
% ---------
% "Connection relation" between vertices. Each edge is labeled with
% a "distance" (cost)
%
% connection(?VertexName1,?VertexName2,?Cost).
%
% This also indirectly defines the set of vertices which are simply
% given by their names, which are atoms.
%
% This relation is not symmetric. We make it symmetric by defining
% a symmetric relations "on top". Improvement: This relation could
% be made "canonical" in that a unique representation would be
% enforced by demanding that VertexName1 @=< VertexName (i.e. the
% vertex names would appear sorted by the standard order of terms).
connection(s, c, 3).
connection(c, l, 2).
connection(l, i, 4).
connection(l, j, 4).
connection(i, j, 6).
connection(i, k, 4).
connection(j, k, 4).
connection(k, e, 5).
connection(e, g, 2).
connection(g, h, 2).
connection(h, f, 3).
connection(f, d, 5).
connection(d, a, 4).
connection(b, d, 4).
connection(b, a, 3).
connection(b, s, 2).
connection(b, h, 1).
connection(a, s, 7).
% ---------
% "Symmetric connection relation"
% ---------
sym_connection(Vertex1,Vertex2,Cost) :- connection(Vertex1,Vertex2,Cost).
sym_connection(Vertex1,Vertex2,Cost) :- connection(Vertex2,Vertex1,Cost).
% =========
% Start measuring paths and their cost from vertex 'a'.
%
% Initially we only know about vertex 'a' itself
%
% - It is the only member in the list of vertices to be visited next,
% at distance/cost 0. This is represented by a single pair in list
% VisitThese: it is a list containing only [0-a].
% - We have a path to 'a' with overall distance/cost 0, containing only
% vertex 'a' found at cost 0: the path is [a-0].
% PathContainerIn maps 'a', the destination vertex, to that path
% [[a-0]].
% The container is implemented by an "association list" (a "map")
% from library(assoc); other abstract data types are possible, in
% particular SWI-Prolog's "dict" if this were running in SWI-Prolog.
% =========
main :-
list_to_assoc([a-[a-0]],PathContainerIn),
VisitThese=[0-a],
% Do it!
dijsktra(VisitThese,PathContainerIn,PathContainerOut),
% We are done! we just need to print out...
assoc_to_list(PathContainerOut,Pairs),
print_list(Pairs).
print_list([]).
print_list([Vertex-Path|More]) :-
reverse(Path,ReversedPath),
Path=[_-Dist|_],
format("Vertex ~q is at distance ~d via ~q~n",[Vertex,Dist,ReversedPath]),
print_list(More).
% =========
% dijsktra([CurCost-CurVertex|More],PathContainerIn,PathContainerOut).
%
% - The first argument is the "list of vertexes to be visited next", named
% "VisitThese" (i.e. the "boundary" of the search), sorted by the distance/cost
% of their path from the initial vertex, ascending (so we always need
% to just grab the head of "VisitThese" to find the vertex which is guaranteed
% nearest the start vertex on visit).
% - The second argument is the "container of the best-path-known-so-far to
% vertexes already seen (all of those visited or tentatively visited in
% handle_neighbors/7, for which a best-path-known-so-far could be determined)
% Note that a cheaper representation than keeping the full path would be
% to just keep the last edge of the path.
% The container is used to create a new container, accumulator-style, which
% is the third argument, which contains all the best paths to all the vertices
% at success-time.
% =========
dijsktra([],PathContainer,PathContainer) :- !.
dijsktra(VisitThese,PathContainerIn,PathContainerOut) :-
VisitThese = [CurCost-CurVertex|MoreToVisit],
format("VisitThese is currently: ~q ~n",[VisitThese]),
% bagof/3 fails if no neighbors, but that's only the case if the initial vertex is unconnected
bagof(LocalNeighbor-LocalCost,
sym_connection(CurVertex,LocalNeighbor,LocalCost),
Neighbors),
format("Neighbors of vertex ~q : ~q ~n",[CurVertex,Neighbors]),
get_assoc(CurVertex,PathContainerIn,CurPath),
% format("Found path for current vertex ~q: ~q~n",[CurVertex,CurPath]),
handle_neighbors(CurVertex,CurPath,CurCost,Neighbors,PathContainerIn,PathContainer2,VisitTheseToo),
append(VisitTheseToo,MoreToVisit,Dirty),
clean_visit_these_list(Dirty,Clean),
format("Dirty visitations : ~q, Clean visitations : ~q~n",[Dirty,Clean]),
dijsktra(Clean,PathContainer2,PathContainerOut).
% =========
% "Tentatively visit" all the neighbors of "CurVertex" ("CurVertex" can be reached through
% "CurPath" at cost "CurCost"), determining for each a path and overall cost
%
% We may reach a neighbor of "CurVertex":
%
% - for the first time if no path to it is stored in PathContainer yet.
% Then a new path is stored in PathContainer and the vertex is added to
% the list of vertexes-to-be-visted-next, "VisitThese" (which will have
% to be sorted by overall path cost before the recursive call to dijkstar/3)
% - for a not-the-first time if a path to it is stored in PathContainer yet
% (so the neighbor has been visited (and its old path is - by construction -
% cheaper and it shall not be visited again) or it has been only
% tentatively visited (and its old path *may* be costlier and thus demand
% replacement by the new path as well as addition of a cheaper
% entry in the list of vertexes to be visited next)
%
% Note that the list "Neighbors" also contains the neighboring vertex on
% the path "CurPath" through which "CurVertex" was reached in the first place.
% But there is no need to handle that case in a special way. This is just a
% vertex that has been visited previously and the old path is cheaper.
%
% handle_neighbors(+CurVertex,+CurPath,+CurCost,
% +Neighbors,
% +PathContainerIn,-PathContainerOut,
% -VisitThese)
%
% =========
% case of "all neighbor vertices handled, we are done"
handle_neighbors(_CurVertex,_CurPath,_CurCost,[],PathContainer,PathContainer,[]).
% case of "neighbor vertex already has a path but the new path is costlier
handle_neighbors(CurVertex,CurPath,CurCost,Neighbors,PathContainerIn,PathContainerOut,VisitThese) :-
Neighbors=[Vertex-LocalCost|More], % grab the next neighbor
get_assoc(Vertex,PathContainerIn,[Vertex-BestCostSoFar|_]), % grab its known path, if it exists (fails if not)
NewCost is CurCost+LocalCost,
NewCost >= BestCostSoFar, % the new path is costlier
!, % do nothing, move to next neighbor
handle_neighbors(CurVertex,CurPath,CurCost,More,PathContainerIn,PathContainerOut,VisitThese).
% case of "neighbor vertex already has a path but the new path is cheaper; note that it is added to the "VisitThese" list
handle_neighbors(CurVertex,CurPath,CurCost,Neighbors,PathContainerIn,PathContainerOut,[NewCost-Vertex|VisitThese]) :-
Neighbors=[Vertex-LocalCost|More], % grab the next neighbor
get_assoc(Vertex,PathContainerIn,[Vertex-BestCostSoFar|_]), % grab its known path, if it exists (fails if not)
NewCost is CurCost+LocalCost,
NewCost < BestCostSoFar, % new path is cheaper
!, % replace path, retain neighbor as "to be visited"
put_assoc(Vertex,PathContainerIn,[Vertex-NewCost|CurPath],PathContainer2),
handle_neighbors(CurVertex,CurPath,CurCost,More,PathContainer2,PathContainerOut,VisitThese).
% case of "neighbor vertex has no path yet"; note that it is added to the "VisitThese" list
handle_neighbors(CurVertex,CurPath,CurCost,Neighbors,PathContainerIn,PathContainerOut,[NewCost-Vertex|VisitThese]) :-
Neighbors=[Vertex-LocalCost|More], % grab the next neighbor
\+ get_assoc(Vertex,PathContainerIn,_), % vertex has no path yet
!, % the cut is not really needed
NewCost is CurCost+LocalCost,
put_assoc(Vertex,PathContainerIn,[Vertex-NewCost|CurPath],PathContainer2),
handle_neighbors(CurVertex,CurPath,CurCost,More,PathContainer2,PathContainerOut,VisitThese).
% ---
% Transfrom list of elements "Cost-Vertex"
%
% ... which is the list of vertexes and their "best cost yet" that
% shall be expanded by the Dijkstra algorithm, but in which several
% entries for the same "Vertex" may appear
% ... into a new list where there is always only exactly one entry for
% any "Vertex" appaearing in the original list, and the "Cost"
% associated to it is the minimum cost for that "Vertex".
% ---
clean_visit_these_list(Dirty,SortedByCostAsc) :-
predsort(sort_pairs,Dirty,SortedByVertexFirstCostSecond),
keep_best(SortedByVertexFirstCostSecond,DuplicatesRemovedMinCostRetained),
keysort(DuplicatesRemovedMinCostRetained,SortedByCostAsc).
sort_pairs(Order,Cost1-Vertex,Cost2-Vertex) :-
!, % Same vertex - order depends on associated cost
compare(Order,Cost1,Cost2).
sort_pairs(Order,_-Vertex1,_-Vertex2) :-
Vertex1 \== Vertex2,
!, % Different vertex - Sort lexicographically (cut is not needed actually)
compare(Order,Vertex1,Vertex2).
keep_best([Cost-Vertex,_-Vertex|More],Out) :-
!, % Vertex appears twice - retain only first entry (with smallest cost)
keep_best([Cost-Vertex|More],Out).
keep_best([Cost1-Vertex1,Cost2-Vertex2|More],[Cost1-Vertex1|Out]) :-
Vertex1 \== Vertex2,
!, % Different vertex - Retain first vertex and its cost, move on
keep_best([Cost2-Vertex2|More],Out).
keep_best([X],[X]). % Termination case #1
keep_best([],[]). % Termination case #2
% ---
% Test clean_visit_these_list/2
% ---
:- begin_tests(clean_visit_these_list).
test(1,true(Result == [])) :- clean_visit_these_list([],Result).
test(2,true(Result == [3-b])) :- clean_visit_these_list([3-b],Result).
test(2,true(Result == [3-b])) :- clean_visit_these_list([4-b,3-b,5-b],Result).
test(3,true(Result == [3-b,8-v,9-a,10-w])) :- clean_visit_these_list([8-v,10-w,9-a,3-b],Result).
test(4,true(Result == [2-w,3-b,8-v,9-a])) :- clean_visit_these_list([8-v,10-w,9-a,4-w,3-b,2-w,12-v],Result).
:- end_tests(clean_visit_these_list).