From 202c414277a42ab7dccf13984d4b0e226b57aeeb Mon Sep 17 00:00:00 2001 From: Michael Young Date: Fri, 7 Jun 2024 17:21:31 +0100 Subject: [PATCH 1/8] 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 4c4f293a2ae2a9d1e573c62b97fe1d2f3ab93081 Mon Sep 17 00:00:00 2001 From: Michael Young Date: Tue, 11 Jun 2024 14:37:49 +0100 Subject: [PATCH 2/8] 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 408c51a0d44afde42e5978173ecb450f27fac6e4 Mon Sep 17 00:00:00 2001 From: Michael Young Date: Tue, 11 Jun 2024 14:38:14 +0100 Subject: [PATCH 3/8] 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 05078ec6e763efba440a413bb3691206ae8bacb9 Mon Sep 17 00:00:00 2001 From: Michael Young Date: Tue, 11 Jun 2024 14:38:28 +0100 Subject: [PATCH 4/8] 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 3621098a6979af604ca1dedf996bc31549284af9 Mon Sep 17 00:00:00 2001 From: Michael Young Date: Tue, 11 Jun 2024 14:55:42 +0100 Subject: [PATCH 5/8] 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 96103f84e63556521e55a32a43e727bd387c4bdb Mon Sep 17 00:00:00 2001 From: Michael Young Date: Wed, 12 Jun 2024 12:12:59 +0100 Subject: [PATCH 6/8] 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 ] ] From f6e9006fba5855b788135872ae775ef010839e4f Mon Sep 17 00:00:00 2001 From: Markus Pfeiffer Date: Wed, 28 Aug 2024 14:35:29 +0100 Subject: [PATCH 7/8] Add iterator for K-Shortest-Paths in a weighted graph --- gap/weights.gd | 1 + gap/weights.gi | 72 ++++++++++++++++++++++++++++++++++++++++ tst/standard/weights.tst | 8 ++++- 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/gap/weights.gd b/gap/weights.gd index 071463c21..6c50101ad 100644 --- a/gap/weights.gd +++ b/gap/weights.gd @@ -33,3 +33,4 @@ DeclareGlobalFunction("DIGRAPHS_Edge_Weighted_Johnson"); DeclareGlobalFunction("DIGRAPHS_Edge_Weighted_FloydWarshall"); DeclareGlobalFunction("DIGRAPHS_Edge_Weighted_Bellman_Ford"); DeclareGlobalFunction("DIGRAPHS_Edge_Weighted_Dijkstra"); +DeclareGlobalFunction("DIGRAPHS_ShortestPathsIterator"); diff --git a/gap/weights.gi b/gap/weights.gi index 5c5ef640a..988f43043 100644 --- a/gap/weights.gi +++ b/gap/weights.gi @@ -616,3 +616,75 @@ function(digraph, source) return rec(distances := distances, parents := parents, edges := edges); end); + +############################################################################# +# 4. Shortest Paths Iterator +############################################################################# +# +# returns an iterator that generates the (possibly empty) sequence of paths +# between source and dest +# +# the iterator needs to store +# - found paths +# - candidates +# - +# rec( found_paths := [], +InstallGlobalFunction(DIGRAPHS_ShortestPathsIterator, +function(digraph, source, dest) + local currentIterator, findNextPath; + + currentIterator := rec( + candidates := BinaryHeap(), + foundPaths := [ + EdgeWeightedDigraphShortestPath(digraph, source, dest) + ]); + + findNextPath := function(iter) + local currentShortestPath, currentShortestPathLength, spurNode, rootPath, + rootPathNode, modifiedGraph, foundPaths, i, p, spurPath, totalPath, + nextPath; + + currentShortestPath := Last(iter.foundPaths); + currentShortestPathLength := Length(currentShortestPath[1]); + foundPaths := iter.foundPaths; + + for i in [1 .. currentShortestPathLength] do + modifiedGraph := fail; + + spurNode := currentShortestPath[1][i]; + rootPath := [ + currentShortestPath[1]{[1..i]}, + currentShortestPath[2]{[1..i-1]} + ]; + + for p in foundPaths do + if rootPath = p[1]{[1..i]} then + # remove p[2][i] from Graph; + fi; + od; + + for rootPathNode in rootPath[1] do + if rootPathNode <> spurNode then + # remove rootPathNode from Graph; + fi; + od; + + spurPath := EdgeWeightedDigraphShortestPath(modifiedGraph, spurNode, dest); + totalPath := [ Concatenation(rootPath[1], spurPath[1]), + Concatenation(rootPath[2], spurPath[2]) ]; + + Push(iter.candidatePaths, totalPath); + od; + + if IsEmpty(iter.candidatePaths) then + return fail; + fi; + + nextPath := Pop(iter.candidatePaths); + Push(iter.foundPaths, nextPath); + + return nextPath; + end; + + return findNextPath(currentIterator); +end); diff --git a/tst/standard/weights.tst b/tst/standard/weights.tst index 9e5d2abdf..3fd851d34 100644 --- a/tst/standard/weights.tst +++ b/tst/standard/weights.tst @@ -272,10 +272,16 @@ t is the 1st argument, gap> EdgeWeightedDigraphShortestPath(d, 1, 3); [ [ 1, 2, 3 ], [ 1, 1 ] ] +# K Shortest Paths +gap> d := EdgeWeightedDigraph([[2], [3], [4], []], [[1], [1], [1], []]); +gap> shortest_path := EdgeWeightedDigraphShortestPath(d, 1, 4); +[ [ 1, 2, 3, 4 ], [ 1, 1, 1 ] ] +gap> iter := DIGRAPHS_ShortestPathsIterator(d, 1, 4); + # DIGRAPHS_UnbindVariables gap> Unbind(d); gap> Unbind(tree); # gap> DIGRAPHS_StopTest(); -gap> STOP_TEST("Digraphs package: standard/weights.tst", 0); \ No newline at end of file +gap> STOP_TEST("Digraphs package: standard/weights.tst", 0); From acaea65c5901185c509c791dd5a450970f894dfb Mon Sep 17 00:00:00 2001 From: Markus Pfeiffer Date: Thu, 29 Aug 2024 22:23:35 +0100 Subject: [PATCH 8/8] Separate code into testable functions --- gap/weights.gi | 157 ++++++++++++++++++++++++++------------- tst/standard/weights.tst | 1 + 2 files changed, 107 insertions(+), 51 deletions(-) diff --git a/gap/weights.gi b/gap/weights.gi index 988f43043..e5467a363 100644 --- a/gap/weights.gi +++ b/gap/weights.gi @@ -622,69 +622,124 @@ end); ############################################################################# # # returns an iterator that generates the (possibly empty) sequence of paths -# between source and dest +# between source and dest by increasing weight. # # the iterator needs to store # - found paths # - candidates -# - +# - reference to the digraph # rec( found_paths := [], -InstallGlobalFunction(DIGRAPHS_ShortestPathsIterator, -function(digraph, source, dest) - local currentIterator, findNextPath; - - currentIterator := rec( - candidates := BinaryHeap(), - foundPaths := [ - EdgeWeightedDigraphShortestPath(digraph, source, dest) - ]); - - findNextPath := function(iter) - local currentShortestPath, currentShortestPathLength, spurNode, rootPath, - rootPathNode, modifiedGraph, foundPaths, i, p, spurPath, totalPath, - nextPath; - - currentShortestPath := Last(iter.foundPaths); - currentShortestPathLength := Length(currentShortestPath[1]); - foundPaths := iter.foundPaths; - - for i in [1 .. currentShortestPathLength] do - modifiedGraph := fail; - - spurNode := currentShortestPath[1][i]; - rootPath := [ - currentShortestPath[1]{[1..i]}, - currentShortestPath[2]{[1..i-1]} - ]; - - for p in foundPaths do - if rootPath = p[1]{[1..i]} then - # remove p[2][i] from Graph; - fi; - od; - for rootPathNode in rootPath[1] do - if rootPathNode <> spurNode then - # remove rootPathNode from Graph; - fi; - od; +DIGRAPHS_SPI := function(digraph, source, dest) + local iter; - spurPath := EdgeWeightedDigraphShortestPath(modifiedGraph, spurNode, dest); - totalPath := [ Concatenation(rootPath[1], spurPath[1]), - Concatenation(rootPath[2], spurPath[2]) ]; + iter := rec( + NextIterator := function(iter) + local shortestPath; - Push(iter.candidatePaths, totalPath); + if IsEmpty(iter!.foundPaths) then + shortestPath := EdgeWeightedDigraphShortestPath(iter!.digraph, iter!.source, iter!.dest); + Add(iter!.foundPaths, shortestPath); + else + + fi; + end, + IsDoneIterator := function(iter) + return IsEmpty(iter!.candidatePaths); + end, + ShallowCopy := function(iter) + # TODO + return iter; + end, + PrintObj := function(iter) + Print(""); + end, + + foundPaths := [], + candidatePaths := BinaryHeap(), + digraph := digraph, + source := source, + dest := dest + ); + return IteratorByFunctions(iter); +end; + +# FIXME: find out how often paths are used as objects in +# their own right. +DIGRAPHS_ConcatenatePaths := function(a, b) + local a_length; + + a_length := Length(a); + + if a[1][a_length] <> b[1][1] then + ErrorNoReturn("concatenatePaths: last vertex on `a` is not equal to first vertex of `b`"); + fi; + + if a_length = 0 then + return StructuralCopy(b); + else + return [ Concatenation(a[1]{[1..a_length-1]}, b[1]), + Concatenation(b[2], b[2]) ]; + fi; +end; + +DIGRAPHS_ModifyGraph := function(digraph, root, foundPaths) + mutableWeights := EdgeWeightsMutableCopy(digraph); + for p in foundPaths do + if rootPath = p[1]{[1..i]} then + mutableWeights[p[2][i]] := infinity; + fi; od; - if IsEmpty(iter.candidatePaths) then - return fail; + rootNodes := currentShortestPath[1]{[1..i-1]}; + + o := OutNeighbours(digraph); + for i in [1..Length(o)] do + for j in [1..Length(o[i])] do + if o[i][j] in rootNodes then + mutableWeights[i][j] := infinity; + fi; + od; + od; + return EdgeWeightedDigraph(digraph, mutableWeights); +end; + +DIGRAPHS_NextShortestPath := function(iter) + local currentShortestPath, currentShortestPathLength, spurNode, rootPath, + rootPathNode, modifiedGraph, foundPaths, i, p, spurPath, totalPath, + nextPath, mutableWeights, mutableOuts, rootNodes, j, o; + + currentShortestPath := Last(iter.foundPaths); + currentShortestPathLength := Length(currentShortestPath[1]); + foundPaths := iter.foundPaths; + + for i in [1 .. currentShortestPathLength] do + spurNode := currentShortestPath[1][i]; + rootPath := [ + currentShortestPath[1]{[1..i]}, + currentShortestPath[2]{[1..i-1]} + ]; + + modifiedGraph := DIGRAPHS_ModifyGraph(digraph, rootPath, iter.foundPaths); + spurPath := EdgeWeightedDigraphShortestPath(modifiedGraph, spurNode, dest); + + if spurPath <> fail then + totalPath := DIGRAPHS_ConcatenatePaths(rootPath, spurPath); + Push(iter.candidatePaths, totalPath); fi; + od; + + if IsEmpty(iter.candidatePaths) then + return fail; + fi; - nextPath := Pop(iter.candidatePaths); - Push(iter.foundPaths, nextPath); + nextPath := Pop(iter.candidatePaths); + Add(iter.foundPaths, nextPath); - return nextPath; - end; + return nextPath; +end; - return findNextPath(currentIterator); +InstallGlobalFunction(DIGRAPHS_ShortestPathsIterator, +function(digraph, source, dest) + ErrorNoReturn("Not implemented yet"); end); diff --git a/tst/standard/weights.tst b/tst/standard/weights.tst index 3fd851d34..31cfb8c30 100644 --- a/tst/standard/weights.tst +++ b/tst/standard/weights.tst @@ -274,6 +274,7 @@ gap> EdgeWeightedDigraphShortestPath(d, 1, 3); # K Shortest Paths gap> d := EdgeWeightedDigraph([[2], [3], [4], []], [[1], [1], [1], []]); + gap> shortest_path := EdgeWeightedDigraphShortestPath(d, 1, 4); [ [ 1, 2, 3, 4 ], [ 1, 1, 1 ] ] gap> iter := DIGRAPHS_ShortestPathsIterator(d, 1, 4);