From a3f9c9d7e8187b3ed95b23e5782f5a326b32167d Mon Sep 17 00:00:00 2001 From: Michael Young Date: Fri, 7 Jun 2024 17:21:31 +0100 Subject: [PATCH 1/6] Add EdgeWeightedDigraphShortestPath(s) --- doc/weights.xml | 113 ++++++++++ doc/z-chap5.xml | 2 + gap/weights.gd | 13 ++ gap/weights.gi | 450 +++++++++++++++++++++++++++++++++++++++ tst/standard/weights.tst | 131 ++++++++++++ tst/testinstall.tst | 10 + 6 files changed, 719 insertions(+) diff --git a/doc/weights.xml b/doc/weights.xml index 1f98ee26d..908d45d15 100644 --- a/doc/weights.xml +++ b/doc/weights.xml @@ -122,3 +122,116 @@ gap> EdgeWeights(T); <#/GAPDoc> + +<#GAPDoc Label="EdgeWeightedDigraphShortestPaths"> + + + + A record. + + If digraph is an edge-weighted digraph, this attribute returns a + record describing the paths of lowest total weight (the shortest + paths) connecting each pair of vertices. If the optional argument + source is specified and is a vertex of digraph, then the + output will only contain information on paths originating from that + vertex.

+ + In the two-argument form, the value returned is a record containing three + components: distances, parents and edges. Each of + these is a list of integers with one entry for each vertex in the + digraph.

+ + + distances[v] is the total weight of the shortest path from + source to v. + + + parents[v] is the final vertex before v on the shortest + path from source to v. + + + edges[v] is the index of the edge of lowest weight going from + parents[v] to v. + + + Using both these components together, you can find the shortest edge + weighted path to all other vertices from a starting vertex.

+ + If no path exists from source to v, then parents[v] and + edges[v] will both be fail. The distance from source + to itself is considered to be 0, and so both parents[source] and + edges[source] are fail.

+ + In the one-argument form, the value returned is also a record containing + components distances, parents and edges, but each of + these will instead be a list of lists in which the ith entry is the + list that corresponds to paths starting at i. In other words, the + following equalities apply. + + + EdgeWeightedDigraphShortestPaths(digraph).distances[source] + = EdgeWeightedDigraphShortestPaths(digraph, source).distances + + + EdgeWeightedDigraphShortestPaths(digraph).parents[source] + = EdgeWeightedDigraphShortestPaths(digraph, source).parents + + + EdgeWeightedDigraphShortestPaths(digraph).edges[source] + = EdgeWeightedDigraphShortestPaths(digraph, source).edges + + + + Edge weights can have negative values, but this operation will fail with an + error if a negative-weighted cycle exists.

+ + For a simple way of finding the shortest path between two specific vertices, + see . See also the non-weighted + operation .

+ + g := EdgeWeightedDigraph([[2, 3], [4], [4], []], [[5, 1], [6], [11], []]); + +gap> EdgeWeightedDigraphShortestPath(g, 1); +rec( distances := [ 0, 5, 1, 11 ], edges := [ fail, 1, 2, 1 ], + parents := [ fail, 1, 1, 2 ] ) +gap> g := EdgeWeightedDigraph([[2], [3], [1]], [[-1], [-2], [-3]]); + +gap> EdgeWeightedDigraphShortestPath(g, 1); +Error, negative cycle exists, +gap> g := EdgeWeightedDigraph([[2], [3], [1]], [[1], [2], [3]]); + +gap> EdgeWeightedDigraphShortestPaths(g); +rec( distances := [ [ 0, 1, 3 ], [ 5, 0, 2 ], [ 3, 4, 0 ] ], + edges := [ [ fail, 1, 1 ], [ 1, fail, 1 ], [ 1, 1, fail ] ], + parents := [ [ fail, 1, 1 ], [ 2, fail, 2 ], [ 3, 3, fail ] ] )]]> + + +<#/GAPDoc> + +<#GAPDoc Label="EdgeWeightedDigraphShortestPath"> + + + A pair of lists, or fail. + + If digraph is an edge-weighted digraph with vertices source + and dest, this operation returns a directed path from source + to dest with the smallest possible total weight. The output is a + pair of lists [v, a] of the form described in .

+ + If source = dest or no path exists, then fail is + returned.

+ + See . + See also the non-weighted operation .

+ D := EdgeWeightedDigraph([[2, 3], [4], [4], []], [[5, 1], [6], [11], []]); + +gap> EdgeWeightedDigraphShortestPath(D, 1, 4); +[ [ 1, 2, 4 ], [ 1, 1 ] ] +gap> EdgeWeightedDigraphShortestPath(D, 3, 2); +fail]]> + + +<#/GAPDoc> diff --git a/doc/z-chap5.xml b/doc/z-chap5.xml index 0baeaf83c..3f5c276a4 100644 --- a/doc/z-chap5.xml +++ b/doc/z-chap5.xml @@ -29,6 +29,8 @@ <#Include Label="EdgeWeightedDigraph"> <#Include Label="EdgeWeightedDigraphTotalWeight"> <#Include Label="EdgeWeightedDigraphMinimumSpanningTree"> + <#Include Label="EdgeWeightedDigraphShortestPaths"> + <#Include Label="EdgeWeightedDigraphShortestPath">

Orders diff --git a/gap/weights.gd b/gap/weights.gd index b15f5dd71..071463c21 100644 --- a/gap/weights.gd +++ b/gap/weights.gd @@ -20,3 +20,16 @@ DeclareOperation("EdgeWeightsMutableCopy", [IsDigraph and HasEdgeWeights]); # 3. Minimum Spanning Trees DeclareAttribute("EdgeWeightedDigraphMinimumSpanningTree", IsDigraph and HasEdgeWeights); + +# 4. Shortest Path +DeclareAttribute("EdgeWeightedDigraphShortestPaths", + IsDigraph and HasEdgeWeights); +DeclareOperation("EdgeWeightedDigraphShortestPaths", + [IsDigraph and HasEdgeWeights, IsPosInt]); +DeclareOperation("EdgeWeightedDigraphShortestPath", + [IsDigraph and HasEdgeWeights, IsPosInt, IsPosInt]); + +DeclareGlobalFunction("DIGRAPHS_Edge_Weighted_Johnson"); +DeclareGlobalFunction("DIGRAPHS_Edge_Weighted_FloydWarshall"); +DeclareGlobalFunction("DIGRAPHS_Edge_Weighted_Bellman_Ford"); +DeclareGlobalFunction("DIGRAPHS_Edge_Weighted_Dijkstra"); diff --git a/gap/weights.gi b/gap/weights.gi index f3ef799da..c8faa386b 100644 --- a/gap/weights.gi +++ b/gap/weights.gi @@ -166,3 +166,453 @@ function(digraph) SetEdgeWeightedDigraphTotalWeight(out, total); return out; end); + +############################################################################# +# 4. Shortest Path +############################################################################# +# +# Three different "shortest path" problems are solved: +# - All pairs: DigraphShortestPaths(digraph) +# - Single source: DigraphShortestPaths(digraph, source) +# - Source and destination: DigraphShortestPath (digraph, source, dest) +# +# The "all pairs" problem has two algorithms: +# - Johnson: better for sparse digraphs +# - Floyd-Warshall: better for dense graphs +# +# The "single source" problem has three algorithms: +# - If "all pairs" is already known, extract information for the given source +# - Dijkstra: faster, but cannot handle negative weights +# - Bellman-Ford: slower, but handles negative weights +# +# The "source and destination" problem calls the "single source" problem and +# extracts information for the given destination. +# +# Justification and benchmarks are in Raiyan's MSci thesis, Chapter 6. +# + +InstallMethod(EdgeWeightedDigraphShortestPaths, +"for a digraph with edge weights", +[IsDigraph and HasEdgeWeights], +function(digraph) + local maxNodes, threshold, digraphVertices, nrVertices, nrEdges; + + digraphVertices := DigraphVertices(digraph); + nrVertices := Size(digraphVertices); + nrEdges := DigraphNrEdges(digraph); + + maxNodes := nrVertices * (nrVertices - 1); + + # the boundary for performance is edge weight 0.125 + # so if nr edges for vertices v is less + # than total number of edges in a connected + # graph we use johnson's algorithm + # which performs better on sparse graphs, otherwise + # we use floyd warshall algorithm. + # This information is gathered from benchmarking tests. + threshold := Int(maxNodes / 8); + if nrEdges <= threshold then + return DIGRAPHS_Edge_Weighted_Johnson(digraph); + else + return DIGRAPHS_Edge_Weighted_FloydWarshall(digraph); + fi; +end); + +InstallMethod(EdgeWeightedDigraphShortestPaths, +"for a digraph with edge weights and known shortest paths and a pos int", +[IsDigraph and HasEdgeWeights and HasEdgeWeightedDigraphShortestPaths, IsPosInt], +function(digraph, source) + local all_paths; + if not source in DigraphVertices(digraph) then + ErrorNoReturn("the 2nd argument must be a vertex of the ", + "digraph that is the 1st argument,"); + fi; + # Shortest paths are known for all vertices. Extract the one we want. + all_paths := EdgeWeightedDigraphShortestPaths(digraph); + return rec(distances := all_paths.distances[source], + edges := all_paths.edges[source], + parents := all_paths.parents[source]); +end); + +InstallMethod(EdgeWeightedDigraphShortestPaths, +"for a digraph with edge weights and a pos int", +[IsDigraph and HasEdgeWeights, IsPosInt], +function(digraph, source) + if not source in DigraphVertices(digraph) then + ErrorNoReturn("the 2nd argument must be a vertex of the ", + "digraph that is the 1st argument,"); + fi; + + if IsNegativeEdgeWeightedDigraph(digraph) then + return DIGRAPHS_Edge_Weighted_Bellman_Ford(digraph, source); + else + return DIGRAPHS_Edge_Weighted_Dijkstra(digraph, source); + fi; +end); + +InstallMethod(EdgeWeightedDigraphShortestPath, +"for a digraph with edge weights and two pos ints", +[IsDigraph and HasEdgeWeights, IsPosInt, IsPosInt], +function(digraph, source, dest) + local paths, v, a, current, edge_index; + if not source in DigraphVertices(digraph) then + ErrorNoReturn("the 2nd argument must be a vertex of the ", + "digraph that is the 1st argument,"); + elif not dest in DigraphVertices(digraph) then + ErrorNoReturn("the 3rd argument must be a vertex of the ", + "digraph that is the 1st argument,"); + fi; + + # No trivial paths + if source = dest then + return fail; + fi; + + # Get shortest paths information for this source vertex + paths := EdgeWeightedDigraphShortestPaths(digraph, source); + + # Convert to DigraphPath's [v, a] format by exploring backwards from dest + v := [dest]; + a := []; + current := dest; + while current <> source do + edge_index := paths.edges[current]; + current := paths.parents[current]; + if edge_index = fail or current = fail then + return fail; + fi; + Add(a, edge_index); + Add(v, current); + od; + + return [Reversed(v), Reversed(a)]; +end); + +InstallGlobalFunction(DIGRAPHS_Edge_Weighted_Johnson, +function(digraph) + local vertices, nrVertices, mutableOuts, mutableWeights, new, v, bellman, + bellmanDistances, u, outNeighbours, idx, w, distances, parents, edges, + dijkstra; + vertices := DigraphVertices(digraph); + nrVertices := Size(vertices); + mutableOuts := OutNeighborsMutableCopy(digraph); + mutableWeights := EdgeWeightsMutableCopy(digraph); + + # add new u that connects to all other v with weight 0 + new := nrVertices + 1; + mutableOuts[new] := []; + mutableWeights[new] := []; + + # fill new u + for v in [1 .. nrVertices] do + mutableOuts[new][v] := v; + mutableWeights[new][v] := 0; + od; + + # calculate shortest paths from the new vertex (could be negative) + digraph := EdgeWeightedDigraph(mutableOuts, mutableWeights); + bellman := DIGRAPHS_Edge_Weighted_Bellman_Ford(digraph, new); + bellmanDistances := bellman.distances; + + # new copy of neighbours and weights + mutableOuts := OutNeighborsMutableCopy(digraph); + mutableWeights := EdgeWeightsMutableCopy(digraph); + + # set weight(u, v) equal to weight(u, v) + bell_dist(u) - bell_dist(v) + # for each edge (u, v) + for u in vertices do + outNeighbours := mutableOuts[u]; + for idx in [1 .. Size(outNeighbours)] do + v := outNeighbours[idx]; + w := mutableWeights[u][idx]; + mutableWeights[u][idx] := w + + bellmanDistances[u] - bellmanDistances[v]; + od; + od; + + Remove(mutableOuts, new); + Remove(mutableWeights, new); + + digraph := EdgeWeightedDigraph(mutableOuts, mutableWeights); + distances := EmptyPlist(nrVertices); + parents := EmptyPlist(nrVertices); + edges := EmptyPlist(nrVertices); + + # run dijkstra + for u in vertices do + dijkstra := DIGRAPHS_Edge_Weighted_Dijkstra(digraph, u); + distances[u] := dijkstra.distances; + parents[u] := dijkstra.parents; + edges[u] := dijkstra.edges; + od; + + # correct distances + for u in vertices do + for v in vertices do + if distances[u][v] = fail then + continue; + fi; + distances[u][v] := distances[u][v] + + bellmanDistances[v] - bellmanDistances[u]; + od; + od; + + return rec(distances := distances, parents := parents, edges := edges); +end); + +InstallGlobalFunction(DIGRAPHS_Edge_Weighted_FloydWarshall, +function(digraph) + local weights, adjMatrix, vertices, nrVertices, u, v, edges, outs, idx, + outNeighbours, w, i, k, distances, parents; + weights := EdgeWeights(digraph); + vertices := DigraphVertices(digraph); + nrVertices := Size(vertices); + outs := OutNeighbours(digraph); + + # Create adjacency matrix with (minimum weight, edge index), or a hole + adjMatrix := EmptyPlist(nrVertices); + for u in vertices do + adjMatrix[u] := EmptyPlist(nrVertices); + outNeighbours := outs[u]; + for idx in [1 .. Size(outNeighbours)] do + v := outNeighbours[idx]; # the out neighbour + w := weights[u][idx]; # the weight to the out neighbour + # Use minimum weight edge + if (not IsBound(adjMatrix[u][v])) or (w < adjMatrix[u][v][1]) then + adjMatrix[u][v] := [w, idx]; + fi; + od; + od; + + # Store shortest paths for single edges + distances := EmptyPlist(nrVertices); + parents := EmptyPlist(nrVertices); + edges := EmptyPlist(nrVertices); + for u in vertices do + distances[u] := EmptyPlist(nrVertices); + parents[u] := EmptyPlist(nrVertices); + edges[u] := EmptyPlist(nrVertices); + + for v in vertices do + distances[u][v] := infinity; + parents[u][v] := fail; + edges[u][v] := fail; + + if u = v then + distances[u][v] := 0; + # if the same node, then the node has no parents + parents[u][v] := fail; + edges[u][v] := fail; + elif IsBound(adjMatrix[u][v]) then + w := adjMatrix[u][v][1]; + idx := adjMatrix[u][v][2]; + + distances[u][v] := w; + parents[u][v] := u; + edges[u][v] := idx; + fi; + od; + od; + + # try every triple: distance from u to v via k + for k in vertices do + for u in vertices do + if distances[u][k] < infinity then + for v in vertices do + if distances[k][v] < infinity then + if distances[u][k] + distances[k][v] < distances[u][v] then + distances[u][v] := distances[u][k] + distances[k][v]; + parents[u][v] := parents[u][k]; + edges[u][v] := edges[k][v]; + fi; + fi; + od; + fi; + od; + od; + + # detect negative cycles + for i in vertices do + if distances[i][i] < 0 then + ErrorNoReturn("1st arg contains a negative-weighted cycle,"); + fi; + od; + + # replace infinity with fails + for u in vertices do + for v in vertices do + if distances[u][v] = infinity then + distances[u][v] := fail; + fi; + od; + od; + + return rec(distances := distances, parents := parents, edges := edges); +end); + +InstallGlobalFunction(DIGRAPHS_Edge_Weighted_Dijkstra, +function(digraph, source) + local weights, vertices, nrVertices, adj, u, outNeighbours, idx, v, w, + distances, parents, edges, vertex, visited, queue, node, currDist, + neighbour, edgeInfo, distance, i; + + weights := EdgeWeights(digraph); + vertices := DigraphVertices(digraph); + nrVertices := Size(vertices); + + # Create an adjacancy map for the shortest edges: index and weight + adj := HashMap(); + for u in vertices do + adj[u] := HashMap(); + outNeighbours := OutNeighbors(digraph)[u]; + for idx in [1 .. Size(outNeighbours)] do + v := outNeighbours[idx]; # the out neighbour + w := weights[u][idx]; # the weight to the out neighbour + + # an edge to v already exists + if v in adj[u] then + # check if edge weight is less than current weight, + # and keep track of edge idx + if w < adj[u][v][1] then + adj[u][v] := [w, idx]; + fi; + else # edge doesn't exist already, so add it + adj[u][v] := [w, idx]; + fi; + od; + od; + + distances := ListWithIdenticalEntries(nrVertices, infinity); + parents := EmptyPlist(nrVertices); + edges := EmptyPlist(nrVertices); + + distances[source] := 0; + parents[source] := fail; + edges[source] := fail; + + visited := BlistList(vertices, []); + + # make binary heap by priority of + # index 1 of each element (the cost to get to the node) + queue := BinaryHeap({x, y} -> x[1] > y[1]); + Push(queue, [0, source]); # the source vertex with cost 0 + + while not IsEmpty(queue) do + node := Pop(queue); + + currDist := node[1]; + u := node[2]; + + if visited[u] then + continue; + fi; + + visited[u] := true; + + for neighbour in KeyValueIterator(adj[u]) do + v := neighbour[1]; + edgeInfo := neighbour[2]; + w := edgeInfo[1]; + idx := edgeInfo[2]; + + distance := currDist + w; + + if Float(distance) < Float(distances[v]) then + distances[v] := distance; + + parents[v] := u; + edges[v] := idx; + + if not visited[v] then + Push(queue, [distance, v]); + fi; + fi; + od; + od; + + # show fail if no path is possible + for i in vertices do + if distances[i] = infinity then + distances[i] := fail; + parents[i] := fail; + edges[i] := fail; + fi; + od; + + return rec(distances := distances, parents := parents, edges := edges); +end); + +InstallGlobalFunction(DIGRAPHS_Edge_Weighted_Bellman_Ford, +function(digraph, source) + local edgeList, weights, vertices, nrVertices, distances, u, outNeighbours, idx, v, w, + vertex, edge, parents, edges, d, i, flag, _; + + weights := EdgeWeights(digraph); + vertices := DigraphVertices(digraph); + nrVertices := Size(vertices); + + edgeList := []; + for u in DigraphVertices(digraph) do + outNeighbours := OutNeighbours(digraph)[u]; + for idx in [1 .. Size(outNeighbours)] do + v := outNeighbours[idx]; # the out neighbour + w := weights[u][idx]; # the weight to the out neighbour + Add(edgeList, [w, u, v, idx]); + od; + od; + + distances := ListWithIdenticalEntries(nrVertices, infinity); + parents := EmptyPlist(nrVertices); + edges := EmptyPlist(nrVertices); + + distances[source] := 0; + parents[source] := fail; + edges[source] := fail; + + # relax all edges: update weight with smallest edges + flag := true; + for _ in vertices do + for edge in edgeList do + w := edge[1]; + u := edge[2]; + v := edge[3]; + idx := edge[4]; + + if distances[u] <> infinity + and Float(distances[u]) + Float(w) < Float(distances[v]) then + distances[v] := distances[u] + w; + + parents[v] := u; + edges[v] := idx; + flag := false; + fi; + od; + + if flag then + break; + fi; + od; + + # check for negative cycles + for edge in edgeList do + w := edge[1]; + u := edge[2]; + v := edge[3]; + + if distances[u] <> infinity + and Float(distances[u]) + Float(w) < Float(distances[v]) then + ErrorNoReturn("1st arg contains a negative-weighted cycle,"); + fi; + od; + + # fill lists with fail if no path is possible + for i in vertices do + if distances[i] = infinity then + distances[i] := fail; + parents[i] := fail; + edges[i] := fail; + fi; + od; + + return rec(distances := distances, parents := parents, edges := edges); +end); diff --git a/tst/standard/weights.tst b/tst/standard/weights.tst index 6acc69b38..d38b346f3 100644 --- a/tst/standard/weights.tst +++ b/tst/standard/weights.tst @@ -133,6 +133,137 @@ gap> d := EdgeWeightedDigraph([[2, 2, 2], [1]], [[10, 5, 15], [7]]); gap> EdgeWeightedDigraphMinimumSpanningTree(d); +# Shortest paths: one node +gap> d := EdgeWeightedDigraph([[]], [[]]); + +gap> EdgeWeightedDigraphShortestPaths(d, 1); +rec( distances := [ 0 ], edges := [ fail ], parents := [ fail ] ) + +# Shortest paths: early break when path doesn't exist +gap> d := EdgeWeightedDigraph([[], [1]], [[], [-10]]);; +gap> EdgeWeightedDigraphShortestPaths(d, 1); +rec( distances := [ 0, fail ], edges := [ fail, fail ], + parents := [ fail, fail ] ) + +# Shortest paths: one node and loop +gap> d := EdgeWeightedDigraph([[1]], [[5]]); + +gap> EdgeWeightedDigraphShortestPaths(d, 1); +rec( distances := [ 0 ], edges := [ fail ], parents := [ fail ] ) + +# Shortest paths: two nodes and loop on second node +gap> d := EdgeWeightedDigraph([[2], [1, 2]], [[5], [5, 5]]); + +gap> EdgeWeightedDigraphShortestPaths(d, 1); +rec( distances := [ 0, 5 ], edges := [ fail, 1 ], parents := [ fail, 1 ] ) + +# Shortest paths: cycle +gap> d := EdgeWeightedDigraph([[2], [3], [1]], [[2], [3], [4]]); + +gap> EdgeWeightedDigraphShortestPaths(d, 1); +rec( distances := [ 0, 2, 5 ], edges := [ fail, 1, 1 ], + parents := [ fail, 1, 2 ] ) + +# Shortest paths: parallel edges +gap> d := EdgeWeightedDigraph([[2, 2, 2], [1]], [[10, 5, 15], [7]]); + +gap> EdgeWeightedDigraphShortestPaths(d, 1); +rec( distances := [ 0, 5 ], edges := [ fail, 2 ], parents := [ fail, 1 ] ) + +# Shortest paths: negative edges +gap> d := EdgeWeightedDigraph([[2], [1]], [[-2], [7]]); + +gap> EdgeWeightedDigraphShortestPaths(d, 1); +rec( distances := [ 0, -2 ], edges := [ fail, 1 ], parents := [ fail, 1 ] ) + +# Shortest paths: parallel negative edges +gap> d := EdgeWeightedDigraph([[2, 2, 2], [1]], [[-2, -3, -4], [7]]); + +gap> EdgeWeightedDigraphShortestPaths(d, 1); +rec( distances := [ 0, -4 ], edges := [ fail, 3 ], parents := [ fail, 1 ] ) + +# Shortest paths: negative cycle +gap> d := EdgeWeightedDigraph([[2, 2, 2], [1]], [[-10, 5, -15], [7]]); + +gap> EdgeWeightedDigraphShortestPaths(d, 1); +Error, 1st arg contains a negative-weighted cycle, + +# Shortest paths: source not in graph +gap> d := EdgeWeightedDigraph([[2], [1]], [[2], [7]]); + +gap> EdgeWeightedDigraphShortestPaths(d, 3); +Error, the 2nd argument must be a vertex of the digraph tha\ +t is the 1st argument, +gap> EdgeWeightedDigraphShortestPath(d, 3, 1); +Error, the 2nd argument must be a vertex of the digraph tha\ +t is the 1st argument, +gap> EdgeWeightedDigraphShortestPath(d, 1, 3); +Error, the 3rd argument must be a vertex of the digraph that \ +is the 1st argument, + +# Shortest paths: no path exists +gap> d := EdgeWeightedDigraph([[1], [2]], [[5], [10]]); + +gap> EdgeWeightedDigraphShortestPaths(d, 1); +rec( distances := [ 0, fail ], edges := [ fail, fail ], + parents := [ fail, fail ] ) +gap> EdgeWeightedDigraphShortestPath(d, 1, 2); +fail + +# Shortest paths: no path exists with negative edge weight +gap> d := EdgeWeightedDigraph([[2], [2], []], [[-5], [10], []]); + +gap> r := EdgeWeightedDigraphShortestPaths(d, 1);; +gap> r.distances = [0, -5, fail]; +true +gap> r.edges = [fail, 1, fail]; +true +gap> r.parents = [fail, 1, fail]; +true + +# Shortest paths: parallel edges +gap> d := EdgeWeightedDigraph([[2, 2, 2], []], [[3, 2, 1], []]); + +gap> EdgeWeightedDigraphShortestPaths(d, 1); +rec( distances := [ 0, 1 ], edges := [ fail, 3 ], parents := [ fail, 1 ] ) +gap> EdgeWeightedDigraphShortestPaths(d); +rec( distances := [ [ 0, 1 ], [ fail, 0 ] ], + edges := [ [ fail, 3 ], [ fail, fail ] ], + parents := [ [ fail, 1 ], [ fail, fail ] ] ) +gap> EdgeWeightedDigraphShortestPath(d, 1, 2); +[ [ 1, 2 ], [ 3 ] ] + +# Shortest paths: negative cycle +gap> d := EdgeWeightedDigraph([[2], [3], [1]], [[-3], [-5], [-7]]); + +gap> EdgeWeightedDigraphShortestPaths(d); +Error, 1st arg contains a negative-weighted cycle, + +# Shortest paths: source not in graph neg int +gap> EdgeWeightedDigraphShortestPaths(d, -1); +Error, no method found! For debugging hints type ?Recovery from NoMethodFound +Error, no 1st choice method found for `EdgeWeightedDigraphShortestPaths' on 2 \ +arguments + +# Shortest paths: Johnson +gap> d := EdgeWeightedDigraph([[2], [3], [], [], []], [[3], [5], [], [], []]); + +gap> EdgeWeightedDigraphShortestPaths(d, 1); +rec( distances := [ 0, 3, 8, fail, fail ], edges := [ fail, 1, 1, fail, fail ] + , parents := [ fail, 1, 2, fail, fail ] ) +gap> EdgeWeightedDigraphShortestPaths(d); +rec( distances := [ [ 0, 3, 8, fail, fail ], [ fail, 0, 5, fail, fail ], + [ fail, fail, 0, fail, fail ], [ fail, fail, fail, 0, fail ], + [ fail, fail, fail, fail, 0 ] ], + edges := [ [ fail, 1, 1, fail, fail ], [ fail, fail, 1, fail, fail ], + [ fail, fail, fail, fail, fail ], [ fail, fail, fail, fail, fail ], + [ fail, fail, fail, fail, fail ] ], + parents := [ [ fail, 1, 2, fail, fail ], [ fail, fail, 2, fail, fail ], + [ fail, fail, fail, fail, fail ], [ fail, fail, fail, fail, fail ], + [ fail, fail, fail, fail, fail ] ] ) +gap> EdgeWeightedDigraphShortestPath(d, 1, 3); +[ [ 1, 2, 3 ], [ 1, 1 ] ] + # DIGRAPHS_UnbindVariables gap> Unbind(d); gap> Unbind(tree); diff --git a/tst/testinstall.tst b/tst/testinstall.tst index e165c78c2..f1a2cb3bd 100644 --- a/tst/testinstall.tst +++ b/tst/testinstall.tst @@ -421,6 +421,16 @@ gap> EdgeWeightedDigraphTotalWeight(d); 15 gap> EdgeWeightedDigraphMinimumSpanningTree(d); +gap> d := EdgeWeightedDigraph([[2], [1, 2]], [[5], [5, 5]]); + +gap> EdgeWeightedDigraphShortestPaths(d, 1); +rec( distances := [ 0, 5 ], edges := [ fail, 1 ], parents := [ fail, 1 ] ) +gap> EdgeWeightedDigraphShortestPaths(d); +rec( distances := [ [ 0, 5 ], [ 5, 0 ] ], + edges := [ [ fail, 1 ], [ 1, fail ] ], + parents := [ [ fail, 1 ], [ 2, fail ] ] ) +gap> EdgeWeightedDigraphShortestPath(d, 1, 2); +[ [ 1, 2 ], [ 1 ] ] # Issue 617: bug in DigraphRemoveEdge, wasn't removing edge labels gap> D := DigraphByEdges(IsMutableDigraph, [[1, 2], [2, 3], [3, 4], [4, 1], [1, 1]]);; From 709d3ca9e9e92c0ab632bd5923986523bcc4b552 Mon Sep 17 00:00:00 2001 From: Michael Young Date: Tue, 11 Jun 2024 14:37:49 +0100 Subject: [PATCH 2/6] Fix spelling typo "adjacancy" --- gap/weights.gi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gap/weights.gi b/gap/weights.gi index c8faa386b..390cac4b8 100644 --- a/gap/weights.gi +++ b/gap/weights.gi @@ -460,7 +460,7 @@ function(digraph, source) vertices := DigraphVertices(digraph); nrVertices := Size(vertices); - # Create an adjacancy map for the shortest edges: index and weight + # Create an adjacency map for the shortest edges: index and weight adj := HashMap(); for u in vertices do adj[u] := HashMap(); From c41064a0dc44cb8d8043440516f109ab42076e36 Mon Sep 17 00:00:00 2001 From: Michael Young Date: Tue, 11 Jun 2024 14:38:14 +0100 Subject: [PATCH 3/6] Simplify documentation --- doc/weights.xml | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/doc/weights.xml b/doc/weights.xml index 908d45d15..a3440737f 100644 --- a/doc/weights.xml +++ b/doc/weights.xml @@ -138,8 +138,8 @@ gap> EdgeWeights(T); In the two-argument form, the value returned is a record containing three components: distances, parents and edges. Each of - these is a list of integers with one entry for each vertex in the - digraph.

+ these is a list of integers with one entry for each vertex v as + follows:

distances[v] is the total weight of the shortest path from @@ -154,36 +154,20 @@ gap> EdgeWeights(T); parents[v] to v. - Using both these components together, you can find the shortest edge + Using these three components together, you can find the shortest edge weighted path to all other vertices from a starting vertex.

If no path exists from source to v, then parents[v] and edges[v] will both be fail. The distance from source to itself is considered to be 0, and so both parents[source] and - edges[source] are fail.

+ edges[source] are fail. + Edge weights can have negative values, but this operation will fail with an + error if a negative-weighted cycle exists.

In the one-argument form, the value returned is also a record containing components distances, parents and edges, but each of these will instead be a list of lists in which the ith entry is the - list that corresponds to paths starting at i. In other words, the - following equalities apply. - - - EdgeWeightedDigraphShortestPaths(digraph).distances[source] - = EdgeWeightedDigraphShortestPaths(digraph, source).distances - - - EdgeWeightedDigraphShortestPaths(digraph).parents[source] - = EdgeWeightedDigraphShortestPaths(digraph, source).parents - - - EdgeWeightedDigraphShortestPaths(digraph).edges[source] - = EdgeWeightedDigraphShortestPaths(digraph, source).edges - - - - Edge weights can have negative values, but this operation will fail with an - error if a negative-weighted cycle exists.

+ list that corresponds to paths starting at i.

For a simple way of finding the shortest path between two specific vertices, see . See also the non-weighted From 35c662de7dfb3a0c5ea6a4deb4985d4597876d26 Mon Sep 17 00:00:00 2001 From: Michael Young Date: Tue, 11 Jun 2024 14:38:28 +0100 Subject: [PATCH 4/6] Fix manual examples --- doc/weights.xml | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/doc/weights.xml b/doc/weights.xml index a3440737f..119bc1fbf 100644 --- a/doc/weights.xml +++ b/doc/weights.xml @@ -174,20 +174,17 @@ gap> EdgeWeights(T); operation .

g := EdgeWeightedDigraph([[2, 3], [4], [4], []], [[5, 1], [6], [11], []]); +gap> D := EdgeWeightedDigraph([[2, 3], [4], [4], []], +> [[5, 1], [6], [11], []]); -gap> EdgeWeightedDigraphShortestPath(g, 1); -rec( distances := [ 0, 5, 1, 11 ], edges := [ fail, 1, 2, 1 ], +gap> EdgeWeightedDigraphShortestPaths(D, 1); +rec( distances := [ 0, 5, 1, 11 ], edges := [ fail, 1, 2, 1 ], parents := [ fail, 1, 1, 2 ] ) -gap> g := EdgeWeightedDigraph([[2], [3], [1]], [[-1], [-2], [-3]]); +gap> D := EdgeWeightedDigraph([[2], [3], [1]], [[1], [2], [3]]); -gap> EdgeWeightedDigraphShortestPath(g, 1); -Error, negative cycle exists, -gap> g := EdgeWeightedDigraph([[2], [3], [1]], [[1], [2], [3]]); - -gap> EdgeWeightedDigraphShortestPaths(g); -rec( distances := [ [ 0, 1, 3 ], [ 5, 0, 2 ], [ 3, 4, 0 ] ], - edges := [ [ fail, 1, 1 ], [ 1, fail, 1 ], [ 1, 1, fail ] ], +gap> EdgeWeightedDigraphShortestPaths(D); +rec( distances := [ [ 0, 1, 3 ], [ 5, 0, 2 ], [ 3, 4, 0 ] ], + edges := [ [ fail, 1, 1 ], [ 1, fail, 1 ], [ 1, 1, fail ] ], parents := [ [ fail, 1, 1 ], [ 2, fail, 2 ], [ 3, 3, fail ] ] )]]> @@ -210,7 +207,8 @@ rec( distances := [ [ 0, 1, 3 ], [ 5, 0, 2 ], [ 3, 4, 0 ] ], See . See also the non-weighted operation .

D := EdgeWeightedDigraph([[2, 3], [4], [4], []], [[5, 1], [6], [11], []]); +gap> D := EdgeWeightedDigraph([[2, 3], [4], [4], []], +> [[5, 1], [6], [11], []]); gap> EdgeWeightedDigraphShortestPath(D, 1, 4); [ [ 1, 2, 4 ], [ 1, 1 ] ] From 997d57006d2333dac8eb41219888d3666d33ccb2 Mon Sep 17 00:00:00 2001 From: Michael Young Date: Tue, 11 Jun 2024 14:55:42 +0100 Subject: [PATCH 5/6] Linting --- gap/weights.gi | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/gap/weights.gi b/gap/weights.gi index 390cac4b8..5c5ef640a 100644 --- a/gap/weights.gi +++ b/gap/weights.gi @@ -313,7 +313,7 @@ function(digraph) digraph := EdgeWeightedDigraph(mutableOuts, mutableWeights); bellman := DIGRAPHS_Edge_Weighted_Bellman_Ford(digraph, new); bellmanDistances := bellman.distances; - + # new copy of neighbours and weights mutableOuts := OutNeighborsMutableCopy(digraph); mutableWeights := EdgeWeightsMutableCopy(digraph); @@ -453,8 +453,8 @@ end); InstallGlobalFunction(DIGRAPHS_Edge_Weighted_Dijkstra, function(digraph, source) local weights, vertices, nrVertices, adj, u, outNeighbours, idx, v, w, - distances, parents, edges, vertex, visited, queue, node, currDist, - neighbour, edgeInfo, distance, i; + distances, parents, edges, visited, queue, node, currDist, neighbour, + edgeInfo, distance, i; weights := EdgeWeights(digraph); vertices := DigraphVertices(digraph); @@ -544,13 +544,13 @@ end); InstallGlobalFunction(DIGRAPHS_Edge_Weighted_Bellman_Ford, function(digraph, source) - local edgeList, weights, vertices, nrVertices, distances, u, outNeighbours, idx, v, w, - vertex, edge, parents, edges, d, i, flag, _; + local edgeList, weights, vertices, nrVertices, distances, u, outNeighbours, + idx, v, w, edge, parents, edges, i, flag, _; weights := EdgeWeights(digraph); vertices := DigraphVertices(digraph); nrVertices := Size(vertices); - + edgeList := []; for u in DigraphVertices(digraph) do outNeighbours := OutNeighbours(digraph)[u]; @@ -579,7 +579,7 @@ function(digraph, source) idx := edge[4]; if distances[u] <> infinity - and Float(distances[u]) + Float(w) < Float(distances[v]) then + and Float(distances[u]) + Float(w) < Float(distances[v]) then distances[v] := distances[u] + w; parents[v] := u; From 4b88960c14f5aa03b23cc413595b2190738ba6c1 Mon Sep 17 00:00:00 2001 From: Michael Young Date: Wed, 12 Jun 2024 12:12:59 +0100 Subject: [PATCH 6/6] Add edge cases to tests --- tst/standard/weights.tst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tst/standard/weights.tst b/tst/standard/weights.tst index d38b346f3..9e5d2abdf 100644 --- a/tst/standard/weights.tst +++ b/tst/standard/weights.tst @@ -245,6 +245,11 @@ Error, no method found! For debugging hints type ?Recovery from NoMethodFound Error, no 1st choice method found for `EdgeWeightedDigraphShortestPaths' on 2 \ arguments +# Shortest path: same vertex +gap> d := EdgeWeightedDigraph([[2], [3], [1]], [[-3], [-5], [-7]]);; +gap> EdgeWeightedDigraphShortestPath(d, 2, 2); +fail + # Shortest paths: Johnson gap> d := EdgeWeightedDigraph([[2], [3], [], [], []], [[3], [5], [], [], []]); @@ -261,6 +266,9 @@ rec( distances := [ [ 0, 3, 8, fail, fail ], [ fail, 0, 5, fail, fail ], parents := [ [ fail, 1, 2, fail, fail ], [ fail, fail, 2, fail, fail ], [ fail, fail, fail, fail, fail ], [ fail, fail, fail, fail, fail ], [ fail, fail, fail, fail, fail ] ] ) +gap> EdgeWeightedDigraphShortestPaths(d, 6); +Error, the 2nd argument must be a vertex of the digraph tha\ +t is the 1st argument, gap> EdgeWeightedDigraphShortestPath(d, 1, 3); [ [ 1, 2, 3 ], [ 1, 1 ] ]