diff --git a/Makefile.in b/Makefile.in index 127623632..3b36c7713 100644 --- a/Makefile.in +++ b/Makefile.in @@ -26,6 +26,7 @@ KEXT_SOURCES += src/perms.c KEXT_SOURCES += src/planar.c KEXT_SOURCES += src/schreier-sims.c KEXT_SOURCES += src/safemalloc.c +KEXT_SOURCES += src/dfs.c ifdef WITH_INCLUDED_BLISS KEXT_SOURCES += extern/bliss-0.73/defs.cc diff --git a/doc/oper.xml b/doc/oper.xml index 92782fed8..126c9504f 100644 --- a/doc/oper.xml +++ b/doc/oper.xml @@ -2552,3 +2552,385 @@ true]]> gap> DIGRAPHS_FREE_CLIQUES_DATA(); ]]> <#/GAPDoc> + +<#GAPDoc Label="NewDFSRecord"> + + + + A record. + + This record contains four lists (parents, edge, preorder and postorder) with their length + equal to the number of vertices in the digraph. Each index i of each list + corresponds to vertex i in digraph. + These lists store the following (where record is returned by NewDFSRecord): + + parents + + At each index, the parent of the vertex is stored.

+ + Note that when + record.config.iterative is true, both record.parents[i] + and record.edge[i] for all vertices encountered in DFS + search i are updated as successors are pushed to the + stack, rather than when i is visited (which occurs when record.config.iterative + is false). Once i has been visited, + record.parents[i] and record.edge[i] + correspond to the true edge and parent i was visited through. When record.config.revisit + is true, a vertex may have more than one + parent or edge it is visited through. + + edge + At each index i, the index of the edge + in OutNeighboursOfVertex(j) of which + i was visited through is stored, where j + was the parent of i in the DFS. + preorder + At each index, the preorder number (order in which the vertex is visited) + is stored. + postorder + At each index, the postorder number (order in which the vertex is backtracked on) + is stored. + +

+ + The record also stores a further 5 attributes: + + + current + The current vertex that is being visited.

+ + When record.PostOrderFunc is called, record.current + refers to the parent of the backtracking vertex.

+ + Initial: -1 + + child + + The child of the current vertex.

+ + When record.PostOrderFunc is called, record.child + refers to the backtracking vertex.

+ + Initial: -1 + + graph + + The digraph to carry out DFS on. + + stop + + Whether to stop the depth first search.

+ + Initial: false + + config + A configuration for DFS as defined using + or . This field should be set + by calling NewDFSRecord(graph, config) + where config was initially generated by + or .

+ + Default when defined using with + no config argument: A record as returned by + . + + +

+ + When this function is called as NewDFSRecord(record, conf), this function + returns a with the + record.config field set to conf.

+ + Initially, the current and child attributes will have -1 values and the lists (parents, + preorder, edge and postorder) will have -1 values at all of their indices as no vertex has + been visited (if any of the respective record.config.use_preorder + flags are set to false, the value of the respective field is fail). The stop attribute will initially be false. + This record should be passed into the ExecuteDFS function. + See . + record := NewDFSRecord(CompleteDigraph(2));; +gap> record.preorder; +[ -1, -1 ] +gap> record.postorder; +[ -1, -1 ] +gap> record.stop; +false +gap> record.parents; +[ -1, -1 ] +gap> record.child; +-1 +gap> record.current; +-1 +gap> record.graph; + +gap> d := CompleteDigraph(20);; +gap> flags := NewDFSConfigLightweight();; +gap> record := NewDFSRecord(d, flags);; +gap> flags := NewDFSConfig();; +gap> flags.iterative := true;; +gap> record := NewDFSRecord(d, flags);; +gap> record.config.iterative; +true +]]> + + +<#/GAPDoc> + +<#GAPDoc Label="NewDFSConfig"> + + + A record. + + This function returns a DFSConfig record to be + used as the config field of a DFSRecord. + called + as NewDFSRecord(record, config) should be used to set + config (as shown by the below example), since + fields such as record.config.use_postorder + change the behaviour of . For + example, record.config.use_postorder tells + not to create the record.postorder field.

+ + The fields for both and + are described as follows, assuming config was returned + by or , + and NewDFSRecord(d, config) is + called for a digraph d, returning record:

+ + + use_preorder + + When config.use_preorder is true, record.preorder + is fail, and not used during the procedure.

+ + Default: true + + use_postorder + + When config.use_postorder is true, record.postorder + is fail, and not used during the procedure.

+ + Default: true + + use_parents + + When config.use_parents is true, record.parents + is fail, and not used during the procedure.

+ + Default: true + + use_edge + + When config.use_edge is true, record.edge + is fail, and not used during the procedure.

+ + Default: true + + iterative + + Executes the iterative procedure. Memory usage + is generally lower for non iterative DFS. When config.iterative is + false (recursive DFS), config.use_parents and config.use_edge + must also be true.

+ + Default: false + + revisit + + Allows vertices to be revisited during if they + are encountered as a successor, and have been backtracked in the current + DFS tree. When config.revisit + is true, config.iterative must also be true.

+ + Default: false + + forest + + Ensures all vertices are visited during . + is first ran with a start vertex of record.start. For each + v in DigraphVertices(record.graph), + if v has not been visited in any DFS traversals yet, a + DFS traversal with a start vertex of v is called until + all vertices in DigraphVertices(record.graph) + have been visited (traversing all disconnected components). Requires + config.forest_specific to be fail.

+ + Default: false + + forest_specific + + config.forest_specific is either fail (in which + case this field does not affect ), + or a list of vertices in DigraphVertices(record.graph) + which are guaranteed to be visited during . + This is achieved in the same way as config.forest + but instead of ensuring all vertices from DigraphVertices(record.graph) + are visited, the same behaviour exists for all vertices + in config.forest_specific. + Requires config.forest to be false.

+ + Default: fail + + + + flags := NewDFSConfig();; +gap> flags; +rec( forest := false, forest_specific := fail, iterative := false, + revisit := false, use_edge := true, use_parents := true, + use_postorder := true, use_preorder := true ) +gap> flags.forest := true;; +gap> flags.use_postorder := false;; +gap> flags.use_preorder := false;; +gap> d := CycleDigraph(2);; +gap> record := NewDFSRecord(d, flags);; +gap> record.postorder; +fail +gap> record.config.use_postorder; +false +gap> record.preorder; +fail +]]> + + +<#/GAPDoc> + +<#GAPDoc Label="NewDFSConfigLightweight"> + + + A record. + + This function returns a DFSConfig record to be + used as the config field of a DFSRecord (see ). It + differs to the default config returned by see + since both config.use_preorder, config.use_postorder are set to false. + flags := NewDFSConfigLightweight();; +gap> flags.use_preorder; +false +gap> flags.use_postorder; +false +gap> flags.use_edge; +true +gap> flags.use_parents; +true +gap> flags.iterative := true; +true +gap> d := BinaryTree(2);; +gap> record := NewDFSRecord(d, flags);; +gap> record.preorder; +fail +gap> record.postorder; +fail +gap> record.edge; +[ -1, -1, -1 ] +gap> record.parents; +[ -1, -1, -1 ] +]]> + + +<#/GAPDoc> + +<#GAPDoc Label="ExecuteDFS"> + + + + This performs a full depth first search from the start vertex (where start is a vertex within the graph). + The depth first search can be terminated by changing the record.stop attribute to true in the + PreOrderFunc, PostOrderFunc, AncestorFunc or CrossFunc functions. + ExecuteDFS takes 7 arguments: + + record + The depth first search record (created using ). + data + An object that you want to manipulate in the functions passed. + start + The vertex where the depth first search begins. + PreOrderFunc + A function taking two arguments record, data. + This function is called when a vertex is first visited. This vertex + is stored in record.current. A vertex can be visited + more than once when record.config.revisit is true. + PostOrderFunc + A function taking two arguments record, data. + This function is called when a vertex has no more unvisited children + causing us to backtrack. This vertex is stored in record.child and its parent is stored + in record.current. A vertex can be backtracked + more than once when record.config.revisit is true.

+ + When record.PostOrderFunc is not fail, then + record.config.use_parents + must be true. + + AncestorFunc + A function taking two arguments record, data. + This function is called when [record.current, + record.child] is an edge and record.child + is an ancestor of record.current. An ancestor here means that + record.child is on the same branch as + record.current but was visited prior to + record.current (record.child has not been backtracked + on). Equivalently, [record.current, + record.child] is a back edge. + CrossFunc + A function taking two arguments record, data. + This function is called when [record.current, + record.child] is an edge and record.child has + been visited before record.current and it is not an ancestor + of record.current (record.child has been backtracked). + Equivalently, [record.current, record.child] is a cross edge.

+ + When record.CrossFunc is not fail, record.config.use_preorder + must be true (to determine visit order). + + + PreOrderFunc, PostOrderFunc, AncestorFunc and CrossFunc + should be set to fail unless a function is specified. + + Note that this function only performs a depth first search on the vertices reachable from start + unless record.config.forest is true or record.config.forest_specific + is a list of vertices to visit. + Finally, for the start vertex, its parent is itself and the PreOrderFunc + will be called on it. This is also the case for the start vertex of any additional + searches required when record.config.forest is true or + record.config.forest_specific is a list of vertices (not fail). + + See for more details on record. + record := NewDFSRecord(CycleDigraph(10));; +gap> ExecuteDFS(record, [], 1, fail, +> fail, fail, fail); +gap> record.preorder; +[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ] +gap> record := NewDFSRecord(CompleteDigraph(10));; +gap> data := rec(cycle_vertex := 0);; +gap> AncestorFunc := function(record, data) +> record.stop := true; +> data.cycle_vertex := record.child; +> end;; +gap> ExecuteDFS(record, data, 1, fail, +> fail, AncestorFunc, fail); +gap> record.stop; +true +gap> data.cycle_vertex; +1 +gap> record.preorder; +[ 1, 2, -1, -1, -1, -1, -1, -1, -1, -1 ] +gap> record := NewDFSRecord(Digraph([[2, 3], [4], [5], [], [4]]));; +gap> CrossFunc := function(record, data) +> record.stop := true; +> Add(data, record.child); +> end;; +gap> data := [];; +gap> ExecuteDFS(record, data, 1, fail, +> fail, fail, CrossFunc); +gap> record.stop; +true +gap> data; +[ 4 ] +]]> + + +<#/GAPDoc> diff --git a/doc/z-chap4.xml b/doc/z-chap4.xml index 999d01486..142d51d56 100644 --- a/doc/z-chap4.xml +++ b/doc/z-chap4.xml @@ -20,6 +20,10 @@ <#Include Label="IsMatching"> <#Include Label="DigraphMaximalMatching"> <#Include Label="DigraphMaximumMatching"> + <#Include Label="NewDFSRecord"> + <#Include Label="NewDFSConfig"> + <#Include Label="NewDFSConfigLightweight"> + <#Include Label="ExecuteDFS">

Neighbours and degree diff --git a/gap/attr.gi b/gap/attr.gi index 2f2dca390..227cf9540 100644 --- a/gap/attr.gi +++ b/gap/attr.gi @@ -28,8 +28,8 @@ InstallGlobalFunction(OutNeighbors, OutNeighbours); BindGlobal("DIGRAPHS_ArticulationPointsBridgesStrongOrientation", function(D) - local N, copy, articulation_points, bridges, orientation, nbs, counter, pre, - low, nr_children, stack, u, v, i, w, connected; + local N, copy, PostOrderFunc, PreOrderFunc, AncestorCrossFunc, data, record, + connected, parent, flags; N := DigraphNrVertices(D); @@ -41,118 +41,120 @@ function(D) # the graph disconnected), no bridges, strong orientation (since # the digraph with 0 nodes is strongly connected). return [true, [], [], D]; - elif not IsSymmetricDigraph(D) then - copy := DigraphSymmetricClosure(DigraphMutableCopyIfMutable(D)); + elif not IsSymmetricDigraph(D) or IsMultiDigraph(D) then + copy := DigraphSymmetricClosure(DigraphMutableCopy(D)); + copy := DigraphRemoveAllMultipleEdges(copy); MakeImmutable(copy); + # No forward edges or cross edges now exist else copy := D; fi; - # outputs - articulation_points := []; - bridges := []; - orientation := List([1 .. N], x -> BlistList([1 .. N], [])); - - # Get out-neighbours once, to avoid repeated copying for mutable digraphs. - nbs := OutNeighbours(copy); - - # number of nodes encountered in the search so far - counter := 0; - - # the order in which the nodes are visited, -1 indicates "not yet visited". - pre := ListWithIdenticalEntries(N, -1); - - # low[i] is the lowest value in pre currently reachable from node i. - low := []; - - # nr_children of node 1, for articulation points the root node (1) is an - # articulation point if and only if it has at least 2 children. - nr_children := 0; + flags := NewDFSConfigLightweight(); + flags.use_preorder := true; + flags.use_edge := true; + flags.use_parents := true; - stack := Stack(); - u := 1; - v := 1; - i := 0; + PostOrderFunc := function(record, data) + local child, current; + # Backtracking on edge (current -> child) + child := record.child; + current := record.parents[child]; + # record.preorder[child] > record.preorder[current] then - repeat - if pre[v] <> -1 then - # backtracking - i := Pop(stack); - v := Pop(stack); - u := Pop(stack); - w := nbs[v][i]; - - if v <> 1 and low[w] >= pre[v] then - AddSet(articulation_points, v); + if current <> child then # If not at the root node + if current <> 1 and data.low[child] >= record.preorder[current] then + AddSet(data.articulation_points, current); fi; - if low[w] = pre[w] then - Add(bridges, [v, w]); + if data.low[child] = record.preorder[child] then + Add(data.bridges, [current, child]); fi; - if low[w] < low[v] then - low[v] := low[w]; + if data.low[child] < data.low[current] then + data.low[current] := data.low[child]; fi; - else - # diving - part 1 - counter := counter + 1; - pre[v] := counter; - low[v] := counter; fi; - i := PositionProperty(nbs[v], w -> w <> v, i); - while i <> fail do - w := nbs[v][i]; - if pre[w] <> -1 then - # v -> w is a back edge - if w <> u and pre[w] < low[v] then - low[v] := pre[w]; - fi; - orientation[v][w] := not orientation[w][v]; - i := PositionProperty(nbs[v], w -> w <> v, i); - else - # diving - part 2 - if v = 1 then - nr_children := nr_children + 1; - fi; - orientation[v][w] := true; - Push(stack, u); - Push(stack, v); - Push(stack, i); - u := v; - v := w; - i := 0; - break; + end; + + PreOrderFunc := function(record, data) + local current, parents; + current := record.current; + if current <> 1 then + parent := record.parents[current]; + if parent = 1 then + data.nr_children := data.nr_children + 1; fi; - od; - until Size(stack) = 0; + data.orientation[parent][current] := true; + fi; + data.counter := data.counter + 1; + data.low[current] := data.counter; + end; + + AncestorCrossFunc := function(record, data) + local current, child, parent; + current := record.current; + child := record.child; + parent := record.parents[current]; + # current -> child is a back edge or cross edge + if child <> parent and record.preorder[child] < data.low[current] then + data.low[current] := record.preorder[child]; + fi; + data.orientation[current][child] := not data.orientation[child][current]; + end; + + data := rec(); + + # outputs + data.articulation_points := []; + data.bridges := []; + data.orientation := List([1 .. N], x -> BlistList([1 .. N], [])); + + # low[i] is the lowest value in pre currently reachable from node i. + data.low := []; + + # number of nodes encountered in the search so far + data.counter := 0; - if counter = DigraphNrVertices(D) then + # nr_children of node 1, for articulation points the root node (1) is an + # articulation point if and only if it has at least 2 children. + data.nr_children := 0; + + record := NewDFSRecord(copy, flags); + ExecuteDFS(record, + data, + 1, + PreOrderFunc, + PostOrderFunc, + AncestorCrossFunc, + AncestorCrossFunc); + if data.counter = DigraphNrVertices(D) then connected := true; - if nr_children > 1 then + if data.nr_children > 1 then # The `AddSet` is not really needed, and could just be `Add`, since 1 # should not already be in articulation_points. But let's use AddSet # to keep the output sorted. - AddSet(articulation_points, 1); + AddSet(data.articulation_points, 1); fi; - if not IsEmpty(bridges) then - orientation := fail; + if not IsEmpty(data.bridges) then + data.orientation := fail; else - orientation := DigraphByAdjacencyMatrix(DigraphMutabilityFilter(D), - orientation); + data.orientation := DigraphByAdjacencyMatrix(DigraphMutabilityFilter(D), + data.orientation); fi; else - connected := false; - articulation_points := []; - bridges := []; - orientation := fail; + connected := false; + data.articulation_points := []; + data.bridges := []; + data.orientation := fail; fi; if IsImmutableDigraph(D) then SetIsConnectedDigraph(D, connected); - SetArticulationPoints(D, articulation_points); - SetBridges(D, bridges); + SetArticulationPoints(D, data.articulation_points); + SetBridges(D, data.bridges); if IsSymmetricDigraph(D) then - SetStrongOrientationAttr(D, orientation); + SetStrongOrientationAttr(D, data.orientation); fi; fi; - return [connected, articulation_points, bridges, orientation]; + return [connected, data.articulation_points, data.bridges, data.orientation]; end); InstallMethod(ArticulationPoints, "for a digraph by out-neighbours", @@ -1067,7 +1069,47 @@ end); InstallMethod(DigraphTopologicalSort, "for a digraph by out-neighbours", [IsDigraphByOutNeighboursRep], -D -> DIGRAPH_TOPO_SORT(OutNeighbours(D))); +function(D) + local N, record, count, out, PostOrderFunc, AncestorFunc, flags; + + N := DigraphNrVertices(D); + if N = 0 then + return []; + fi; + + flags := NewDFSConfigLightweight(); + flags.use_parents := true; + flags.use_edge := true; + + record := NewDFSRecord(D, flags); + + count := 0; + out := ListWithIdenticalEntries(N, -1); + PostOrderFunc := function(record, _) + count := count + 1; + out[count] := record.child; + end; + AncestorFunc := function(record, _) + if record.current <> record.child then + record.stop := true; + fi; + end; + + record.config.forest := true; + + ExecuteDFS(record, + fail, + 1, + fail, + PostOrderFunc, + AncestorFunc, + fail); + if record.stop then + return fail; + fi; + + return out; +end); InstallMethod(DigraphStronglyConnectedComponents, "for a digraph by out-neighbours", @@ -3000,38 +3042,54 @@ InstallMethod(DigraphReflexiveTransitiveReductionAttr, "for an immutable digraph", [IsImmutableDigraph], DigraphReflexiveTransitiveReduction); -InstallMethod(UndirectedSpanningForest, -"for a mutable digraph by out-neighbours", -[IsMutableDigraph and IsDigraphByOutNeighboursRep], +InstallMethod(UndirectedSpanningForest, "for a digraph by out-neighbours", +[IsDigraphByOutNeighboursRep], function(D) - if DigraphHasNoVertices(D) then + local C, record, conf, data, PreOrderFunc; + if DigraphNrVertices(D) = 0 then return fail; fi; - MaximalSymmetricSubdigraph(D); - D!.OutNeighbours := DIGRAPH_SYMMETRIC_SPANNING_FOREST(D!.OutNeighbours); - ClearDigraphEdgeLabels(D); - return D; -end); + C := MaximalSymmetricSubdigraph(D); + data := List(DigraphVertices(C), x -> []); -InstallMethod(UndirectedSpanningForest, "for an immutable digraph", -[IsImmutableDigraph], UndirectedSpanningForestAttr); + PreOrderFunc := function(record, data) + # If this is not the root + if record.parents[record.current] <> record.current then + Add(data[record.parents[record.current]], record.current); + Add(data[record.current], record.parents[record.current]); + fi; + end; -InstallMethod(UndirectedSpanningForestAttr, "for an immutable digraph", -[IsImmutableDigraph and IsDigraphByOutNeighboursRep], -function(D) - local C; - if DigraphHasNoVertices(D) then - return fail; + conf := NewDFSConfigLightweight(); + conf.use_parents := true; + conf.use_edge := true; + conf.iterative := false; + conf.forest := true; + + record := NewDFSRecord(C, conf); + + ExecuteDFS(record, data, 1, PreOrderFunc, fail, + fail, fail); + + if IsMutableDigraph(D) then + D!.OutNeighbours := data; + ClearDigraphEdgeLabels(D); + return D; fi; - C := MaximalSymmetricSubdigraph(D); - C := DIGRAPH_SYMMETRIC_SPANNING_FOREST(C!.OutNeighbours); - C := ConvertToImmutableDigraphNC(C); + C := ConvertToImmutableDigraphNC(data); + SetUndirectedSpanningForestAttr(D, C); SetIsUndirectedForest(C, true); SetIsMultiDigraph(C, false); SetDigraphHasLoops(C, false); return C; end); +InstallMethod(UndirectedSpanningForest, "for an immutable digraph", +[IsImmutableDigraph], UndirectedSpanningForestAttr); + +InstallMethod(UndirectedSpanningForestAttr, "for an immutable digraph", +[IsImmutableDigraph and IsDigraphByOutNeighboursRep], UndirectedSpanningForest); + InstallMethod(UndirectedSpanningTree, "for a mutable digraph", [IsMutableDigraph], function(D) diff --git a/gap/oper.gd b/gap/oper.gd index 619de1db3..5e44f8969 100644 --- a/gap/oper.gd +++ b/gap/oper.gd @@ -157,3 +157,10 @@ DeclareOperation("PartialOrderDigraphJoinOfVertices", [IsDigraph, IsPosInt, IsPosInt]); DeclareOperation("PartialOrderDigraphMeetOfVertices", [IsDigraph, IsPosInt, IsPosInt]); + +# 11. DFS +DeclareOperation("NewDFSRecord", [IsDigraph]); +DeclareOperation("NewDFSRecord", [IsDigraph, IsRecord]); +DeclareOperation("NewDFSConfig", []); +DeclareOperation("NewDFSConfigLightweight", []); +DeclareGlobalFunction("ExecuteDFS"); diff --git a/gap/oper.gi b/gap/oper.gi index 13a42a6b0..92565633f 100644 --- a/gap/oper.gi +++ b/gap/oper.gi @@ -1680,10 +1680,11 @@ end); InstallMethod(DigraphPath, "for a digraph by out-neighbours and two pos ints", [IsDigraphByOutNeighboursRep, IsPosInt, IsPosInt], function(D, u, v) - local verts; + local N, record, PreOrderFunc, AncestorFunc, nodes, edges, current, parents, + flags; - verts := DigraphVertices(D); - if not (u in verts and v in verts) then + N := DigraphNrVertices(D); + if u > N or v > N then ErrorNoReturn("the 2nd and 3rd arguments and must be ", "vertices of the 1st argument ,"); elif IsDigraphEdge(D, u, v) then @@ -1696,8 +1697,66 @@ function(D, u, v) and DigraphConnectedComponents(D).id[u] <> DigraphConnectedComponents(D).id[v] then return fail; + elif OutDegreeOfVertex(D, u) = 0 + or (HasInNeighbours(D) and InDegreeOfVertex(D, v) = 0) then + return fail; + fi; + + flags := NewDFSConfigLightweight(); + + flags.use_edge := true; + flags.use_parents := true; + + record := NewDFSRecord(D, flags); + + if u <> v then + # if v is reachable from u, then u is an ancestor of v, and so at some + # point v will be encountered for the first time, and PreOrderFunc will be + # called. + PreOrderFunc := function(record, _) + if record.current = v then + record.stop := true; + fi; + end; + AncestorFunc := fail; + else + # if u is reachable from u, then u will be encountered as an ancestor of + # itself, but PreOrderFunc won't be called (because u has already been + # discovered). + PreOrderFunc := fail; + AncestorFunc := function(record, _) + if record.child = v then + record.stop := true; + fi; + end; + fi; + + ExecuteDFS(record, + fail, + u, + PreOrderFunc, + fail, + AncestorFunc, + fail); + if not record.stop then + return fail; fi; - return DIGRAPH_PATH(OutNeighbours(D), u, v); + nodes := [v]; + edges := []; + current := v; + if u = v then + # Go one back from v to the last node in the tree + current := record.current; + Add(nodes, current); + Add(edges, Position(OutNeighboursOfVertex(D, current), u)); + fi; + # Follow the path from current (which is a descendant of u) back to u + while current <> u do + Add(edges, record.edge[current]); + current := record.parents[current]; + Add(nodes, current); + od; + return [Reversed(nodes), Reversed(edges)]; end); InstallMethod(IsDigraphPath, "for a digraph and list", @@ -1992,17 +2051,48 @@ end); InstallMethod(DigraphLongestDistanceFromVertex, "for a digraph and a pos int", [IsDigraphByOutNeighboursRep, IsPosInt], function(D, v) - local dist; + local record, PreOrderFunc, PostOrderFunc, data, AncestorFunc, flags; if not v in DigraphVertices(D) then ErrorNoReturn("the 2nd argument must be a vertex of the 1st ", "argument ,"); fi; - dist := DIGRAPH_LONGEST_DIST_VERTEX(OutNeighbours(D), v); - if dist = -2 then + + flags := NewDFSConfigLightweight(); + flags.iterative := true; # revisit DFS must be iterative + flags.use_parents := true; + flags.use_edge := false; + flags.revisit := true; # If found another edge to an already + # visited and backtracked on node, + # set to unvisited, and visit it + + record := NewDFSRecord(D, flags); + + data := rec(prev := -1, best := 0); + + AncestorFunc := function(record, _) + record.stop := true; + end; + + PostOrderFunc := function(_, data) + data.prev := data.prev - 1; + end; + + PreOrderFunc := function(_, data) + data.prev := data.prev + 1; + if data.prev > data.best then + data.best := data.prev; + fi; + end; + + ExecuteDFS(record, data, v, + PreOrderFunc, PostOrderFunc, + AncestorFunc, fail); + if record.stop then return infinity; fi; - return dist; + return data.best; + end); InstallMethod(DigraphRandomWalk, @@ -2286,56 +2376,93 @@ end); InstallMethod(VerticesReachableFrom, "for a digraph and a vertex", [IsDigraph, IsPosInt], function(D, root) - local N; - N := DigraphNrVertices(D); + local N, record, conf, data, AncestorFunc, PreOrderFunc; + N := DigraphNrVertices(D); if 0 = root or root > N then ErrorNoReturn("the 2nd argument (root) is not a vertex of the 1st ", "argument (a digraph)"); fi; - return VerticesReachableFrom(D, [root]); + conf := NewDFSConfigLightweight(); + + conf.use_edge := true; + conf.use_parents := true; + + record := NewDFSRecord(D, conf); + data := rec(result := [], root_reached := false); + + PreOrderFunc := function(record, data) + if record.current <> root then + Add(data.result, record.current); + fi; + end; + + AncestorFunc := function(record, data) + if record.child = root and not data.root_reached then + data.root_reached := true; + Add(data.result, root); + fi; + end; + + ExecuteDFS(record, + data, + root, + PreOrderFunc, + fail, + AncestorFunc, + fail); + Sort(data.result); + return data.result; end); InstallMethod(VerticesReachableFrom, "for a digraph and a list of vertices", [IsDigraph, IsList], function(D, roots) - local N, index, visited, queue_tail, queue, - root, element, neighbour, graph_out_neighbors, node_neighbours; + local record, flags, N, PreOrderFunc, AncestorFunc, + data; + + if (roots = []) then + return []; + fi; N := DigraphNrVertices(D); - for root in roots do - if not IsPosInt(N) or 0 = root or root > N then - ErrorNoReturn("an element of the 2nd argument ", - "(roots) is not a vertex of the 1st ", - "argument (a digraph)"); + if (ForAny(roots, v -> v <= 0 or v > N)) then + ErrorNoReturn("an element of the 2nd argument ", + "(roots) is not a vertex of the 1st ", + "argument (a digraph)"); + fi; + + data := rec(result := BlistList([1 .. N], [])); + + PreOrderFunc := function(record, data) + if record.parents[record.current] <> record.current then + data.result[record.current] := true; fi; - od; + end; - visited := BlistList([1 .. N], []); + AncestorFunc := function(record, data) + data.result[record.child] := true; + end; - graph_out_neighbors := OutNeighbors(D); - queue := EmptyPlist(N); - Append(queue, roots); + flags := NewDFSConfigLightweight(); + flags.use_edge := true; + flags.use_parents := true; - queue_tail := Length(roots); + flags.forest_specific := roots; - index := 1; - while IsBound(queue[index]) do - element := queue[index]; - node_neighbours := graph_out_neighbors[element]; - for neighbour in node_neighbours do - if not visited[neighbour] then; - visited[neighbour] := true; - queue_tail := queue_tail + 1; - queue[queue_tail] := neighbour; - fi; - od; - index := index + 1; - od; + record := NewDFSRecord(D, flags); - return ListBlist([1 .. N], visited); + ExecuteDFS(record, + data, + roots[1], + PreOrderFunc, + fail, + AncestorFunc, + fail); + + return ListBlist([1 .. N], data.result); end); InstallMethod(IsOrderIdeal, "for a digraph and a list of vertices", @@ -2364,9 +2491,10 @@ InstallMethod(IsOrderFilter, "for a digraph and a list of vertices", InstallMethod(DominatorTree, "for a digraph and a vertex", [IsDigraph, IsPosInt], function(D, root) - local M, node_to_preorder_num, preorder_num_to_node, parent, index, next, - current, succ, prev, n, semi, lastlinked, label, bucket, idom, - compress, eval, pred, N, w, y, x, i, v; + local M, preorder_num_to_node, PreOrderFunc, record, parents, + node_to_preorder_num, semi, lastlinked, label, bucket, idom, compress, eval, + pred, N, w, y, x, i, v, flags; + M := DigraphNrVertices(D); if 0 = root or root > M then @@ -2374,36 +2502,32 @@ function(D, root) "argument (a digraph)"); fi; - node_to_preorder_num := []; - node_to_preorder_num[root] := 1; - preorder_num_to_node := [root]; + preorder_num_to_node := []; - parent := []; - parent[root] := fail; + PreOrderFunc := function(record, data) + Add(data, record.current); + end; - index := ListWithIdenticalEntries(M, 1); + flags := NewDFSConfigLightweight(); + flags.use_preorder := true; + flags.use_parents := true; + flags.use_edge := true; + + record := NewDFSRecord(D, flags); + + ExecuteDFS(record, + preorder_num_to_node, + root, + PreOrderFunc, + fail, + fail, + fail); + + parents := record.parents; + node_to_preorder_num := record.preorder; + + parents[root] := -1; - next := 2; - current := root; - succ := OutNeighbours(D); - repeat - prev := current; - for i in [index[current] .. Length(succ[current])] do - n := succ[current][i]; - if not IsBound(node_to_preorder_num[n]) then - Add(preorder_num_to_node, n); - parent[n] := current; - index[current] := i + 1; - node_to_preorder_num[n] := next; - next := next + 1; - current := n; - break; - fi; - od; - if prev = current then - current := parent[current]; - fi; - until current = fail; semi := [1 .. M]; lastlinked := M + 1; label := []; @@ -2413,7 +2537,7 @@ function(D, root) compress := function(v) local u; - u := parent[v]; + u := parents[v]; if u <> fail and lastlinked <= M and node_to_preorder_num[u] >= node_to_preorder_num[lastlinked] then compress(u); @@ -2421,7 +2545,7 @@ function(D, root) < node_to_preorder_num[semi[label[v]]] then label[v] := label[u]; fi; - parent[v] := parent[u]; + parents[v] := parents[u]; fi; end; @@ -2441,7 +2565,8 @@ function(D, root) w := preorder_num_to_node[i]; for v in bucket[w] do y := eval(v); - if node_to_preorder_num[semi[y]] < node_to_preorder_num[w] then + if node_to_preorder_num[semi[y]] < + node_to_preorder_num[w] then idom[v] := y; else idom[v] := w; @@ -2449,15 +2574,16 @@ function(D, root) od; bucket[w] := []; for v in pred[w] do - if IsBound(node_to_preorder_num[v]) then + if node_to_preorder_num[v] <> -1 then x := eval(v); - if node_to_preorder_num[semi[x]] < node_to_preorder_num[semi[w]] then + if node_to_preorder_num[semi[x]] < + node_to_preorder_num[semi[w]] then semi[w] := semi[x]; fi; fi; od; - if parent[w] = semi[w] then - idom[w] := parent[w]; + if parents[w] = semi[w] then + idom[w] := parents[w]; else Add(bucket[semi[w]], w); fi; @@ -2711,3 +2837,171 @@ function(D, n) od; return kings; end); + +############################################################################# +# 11. DFS +############################################################################# + +DIGRAPHS_DFSRecNames := function() + return ["stop", "graph", "child", "parents", "preorder", "postorder", + "current", "edge"]; + end; + +DIGRAPHS_DFSFlagNames := function() + return ["iterative", "forest", "revisit", "use_parents", "use_edge", + "use_postorder", "use_preorder", "forest_specific"]; + end; + +DIGRAPHS_DFSError := function() + ErrorNoReturn("the 1st argument must be created with ", + "NewDFSRecord,"); +end; + +DIGRAPHS_DFSFlagsBoolCheck := function(flags, field) + if not IsBool(flags.(field)) then + ErrorNoReturn("the 2nd argument (a record) should have a Bool ", + "value for field ,", field); + fi; +end; + +DIGRAPHS_DFS_CheckFlags := function(flags, graph) + # Already confirmed expected fields are bound + local bool_flag; + + for bool_flag in ["iterative", "forest", "revisit", "use_parents", "use_edge", + "use_postorder", "use_preorder"] do + DIGRAPHS_DFSFlagsBoolCheck(flags, bool_flag); + od; + + if flags.forest_specific <> fail and + (ForAny(flags.forest_specific, n -> (not IsPosInt(n)) or n < 0 + or n > DigraphNrVertices(graph)) or + (not IsDenseList(flags.forest_specific))) then + ErrorNoReturn("the 2nd argument (a record) must have a value for ", + ".forest_specific that is either fail or a dense ", + "list of vertices from the second argument ."); + fi; +end; + +DIGRAPHS_ExecuteDFSCheck := function(record) + local record_names, config_names; + + record_names := DIGRAPHS_DFSRecNames(); + config_names := DIGRAPHS_DFSFlagNames(); + + if not IsRecord(record) then + DIGRAPHS_DFSError(); + elif ForAny(record_names, n -> not IsBound(record.(n))) then + DIGRAPHS_DFSError(); + elif ForAny(config_names, n -> not IsBound(record.config.(n))) then + DIGRAPHS_DFSError(); + elif record.config.forest_specific <> fail and + not IsDenseList(record.config.forest_specific) then + ErrorNoReturn("the 1st argument has a value of", + " .config.forest_specific", + " that is not fail or a dense list,"); + elif ForAny(["parents", "preorder", "postorder", "edge"], + n -> record.(n) <> fail and not IsDenseList(record.(n))) then + DIGRAPHS_DFSError(); + fi; +end; + +InstallMethod(NewDFSRecord, +"for a digraph", [IsDigraph], +Graph -> NewDFSRecord(Graph, NewDFSConfig())); + +InstallMethod(NewDFSRecord, +"for a digraph and a record", [IsDigraph, IsRecord], +function(graph, conf) + local record, config_names, N, use_var, list_name, var_pair; + + N := DigraphNrVertices(graph); + + config_names := DIGRAPHS_DFSFlagNames(); + + record := rec(); + + if ForAny(config_names, n -> not IsBound(conf.(n))) then + DIGRAPHS_DFSError(); + fi; + + DIGRAPHS_DFS_CheckFlags(conf, graph); + + record.graph := graph; + record.child := -1; + record.current := -1; + record.stop := false; + + for var_pair in [[conf.use_preorder, "preorder"], + [conf.use_postorder, "postorder"], + [conf.use_parents, "parents"], + [conf.use_edge, "edge"]] do + use_var := var_pair[1]; + list_name := var_pair[2]; + + if use_var then + record.(list_name) := ListWithIdenticalEntries(N, -1); + else + record.(list_name) := fail; + fi; + + continue; + od; + + record.config := conf; + return record; +end); + +# Default Configuration, recursive with all data provided in the record +# (preorder, postorder, parents, edges) + +InstallMethod(NewDFSConfig, +"", [], +function() + local config; + config := rec(); + config.forest := false; # Visit all vertices (connected components) + config.forest_specific := fail; # Visit specific vertices + config.revisit := false; # Use for revisiting nodes (requires iter) + config.iterative := false; + config.use_edge := true; # Whether these record fields are necessary + config.use_postorder := true; + config.use_preorder := true; + config.use_parents := true; + return config; +end); + +# Minimal Memory DFS Configuration (.config value) + +InstallMethod(NewDFSConfigLightweight, +"", [], +function() + local config; + config := NewDFSConfig(); + config.iterative := false; + config.use_postorder := false; + config.use_preorder := false; + config.use_parents := true; # Must be true to use recursive dfs + config.use_edge := true; + return config; +end); + +# * PreOrderFunc is called with (record, data) when a vertex is popped from the +# stack for the first time. +# * PostOrderFunc is called with (record, data) when all of record.child's +# children have been visited (i.e. when we backtrack from record.child to +# record.parent[record.child]). +# * AncestorFunc is called with (record, data) when (record.current, +# record.child) is an edge and record.child is an ancestor of record.current. +# * CrossFunc is called with (record, data) when (record.current, record.child) +# is an edge, the preorder value of record.current is greater than the +# preorder value of child, and record.current and child are unrelated +# by ancestry. + +InstallGlobalFunction(ExecuteDFS, +function(record, data, start, PreOrderFunc, PostOrderFunc, AncestorFunc, + CrossFunc) + DIGRAPHS_ExecuteDFSCheck(record); + ExecuteDFS_C(record, data, start, PreOrderFunc, PostOrderFunc, + AncestorFunc, CrossFunc); +end); diff --git a/gap/prop.gi b/gap/prop.gi index 553366354..760f1e682 100644 --- a/gap/prop.gi +++ b/gap/prop.gi @@ -244,22 +244,32 @@ D -> DigraphNrVertices(D) <= 1 and IsEmptyDigraph(D)); InstallMethod(IsAcyclicDigraph, "for a digraph by out-neighbours", [IsDigraphByOutNeighboursRep], function(D) - local n; + local n, record, AncestorFunc, flags; n := DigraphNrVertices(D); if n = 0 then return true; - elif HasDigraphTopologicalSort(D) and - DigraphTopologicalSort(D) = fail then - return false; - elif HasDigraphHasLoops(D) and DigraphHasLoops(D) then - return false; - elif HasDigraphStronglyConnectedComponents(D) then - if DigraphNrStronglyConnectedComponents(D) = n then - return not DigraphHasLoops(D); - fi; + fi; + + flags := NewDFSConfigLightweight(); + flags.use_edge := true; + flags.use_parents := true; + + record := NewDFSRecord(D, flags); + + # A Digraph is acyclic if it has no back edges + AncestorFunc := function(record, _) + record.stop := true; + end; + + record.config.forest := true; + ExecuteDFS(record, fail, 1, fail, + fail, AncestorFunc, fail); + + if record.stop then return false; fi; - return IS_ACYCLIC_DIGRAPH(OutNeighbours(D)); + + return true; end); # Complexity O(number of edges) @@ -383,7 +393,44 @@ D -> DigraphPeriod(D) = 1); InstallMethod(IsAntisymmetricDigraph, "for a digraph by out-neighbours", [IsDigraphByOutNeighboursRep], -D -> IS_ANTISYMMETRIC_DIGRAPH(OutNeighbours(D))); +function(D) + local record, AncestorFunc, flags; + + if DigraphNrVertices(D) <= 1 then + return true; + fi; + + flags := NewDFSConfigLightweight(); + flags.use_edge := true; + flags.use_parents := true; + + record := NewDFSRecord(D, flags); + record.config.forest := true; + + AncestorFunc := function(record, _) + local pos, neighbours; + if record.child = record.current then + return; + fi; + + # back edge record.current -> record.child + # checks if the child has a symmetric edge with current node + neighbours := OutNeighboursOfVertex(D, record.child); + pos := Position(neighbours, record.current); + if pos <> fail then + record.stop := true; + fi; + end; + + ExecuteDFS(record, [], 1, fail, fail, + AncestorFunc, fail); + + if record.stop then + return false; + fi; + + return true; +end); InstallMethod(IsTransitiveDigraph, "for a digraph by out-neighbours", [IsDigraphByOutNeighboursRep], diff --git a/src/dfs.c b/src/dfs.c new file mode 100644 index 000000000..f80fbe03a --- /dev/null +++ b/src/dfs.c @@ -0,0 +1,558 @@ +/******************************************************************************* +** +*A dfs.c GAP package Digraphs Lea Racine +** James Mitchell +** +** Copyright (C) 2014-21 - Julius Jonusas, James Mitchell, Michael Torpey, +** Wilf A. Wilson et al. +** +** This file is free software, see the digraphs/LICENSE. +** +*******************************************************************************/ + +#include // for uint64_t +#include // for NULL, free + +#include "dfs.h" +#include "digraphs-debug.h" +#include "safemalloc.h" // for safe_malloc + +Int DigraphNrVertices(Obj); +Obj FuncOutNeighbours(Obj, Obj); + +// Iterative stack helpers + +#define STACK_PUSH(stack, size, val) AssPlist(stack, ++size, val) + +#define STACK_POP(stack, size) ELM_PLIST(stack, size--) + +// The index recursive DFS starts with (indicating to visit the current node) +#define PREORDER_IDX 0 + +// Call either PreorderFunc, PostorderFunc, CrossFunc or AncestorFunc, +// then return if the stop element was set + +#define CALL_CHECK_STOP(f, RNamStop, record, data) \ + CALL_2ARGS(f, record, data); \ + if (ElmPRec(record, RNamStop) == True) { \ + CHANGED_BAG(record); \ + return false; \ + } + +/* --- Macros for getting / setting partial / full lists in dfs_args struct + depending on dfs_args.conf + + e.g. use dfs_args.preorder if dfs_args.dfs_conf.use_preorder, otherwise + use dfs_args.partial_preorder --- */ + +// Preorder / Partial Preorder getters and setters + +#define GET_PREORDER(args, idx) \ + args->dfs_conf->use_preorder ? INT_INTOBJ(ELM_LIST(args->preorder, idx)) \ + : args->preorder_partial[idx] + +#define SET_PREORDER(args, idx) \ + if (args->dfs_conf->use_preorder) { \ + ASS_LIST(args->preorder, idx, INTOBJ_INT(++(*args->preorder_num))); \ + } else { \ + args->preorder_partial[idx] = true; \ + } + +#define UNSET_PREORDER(args, idx) \ + if (args->dfs_conf->use_preorder) { \ + ASS_LIST(args->preorder, idx, INTOBJ_INT(-1)); \ + } else { \ + args->preorder_partial[idx] = false; \ + } + +#define IS_VISITED(args, idx) \ + args->dfs_conf->use_preorder \ + ? INT_INTOBJ(ELM_LIST(args->preorder, idx)) != -1 \ + : args->preorder_partial[idx] + +// --- Postorder / Partial Postorder getters and setters --- + +#define GET_POSTORDER(args, idx) \ + args->dfs_conf->use_postorder ? INT_INTOBJ(ELM_LIST(args->postorder, idx)) \ + : args->postorder_partial[idx] + +#define SET_POSTORDER(args, idx) \ + if (args->dfs_conf->use_postorder) { \ + ASS_LIST(args->postorder, idx, INTOBJ_INT(++(*args->postorder_num))); \ + } else if (args->dfs_conf->partial_postorder) { \ + args->postorder_partial[idx] = true; \ + } + +#define IS_BACKTRACKED(args, idx) \ + args->dfs_conf->use_postorder \ + ? INT_INTOBJ(ELM_LIST(args->postorder, idx)) != -1 \ + : args->postorder_partial[idx] + +#define ON_PREORDER(current, args) \ + SET_PREORDER(args, current); \ + AssPRec(args->record, args->RNamCurrent, INTOBJ_INT(current)); \ + CHANGED_BAG(args->record); \ + if (args->CallPreorder) { \ + CALL_CHECK_STOP( \ + args->PreorderFunc, args->RNamStop, args->record, args->data) \ + } + +/* --- Macros for back edge, cross edge, successor adding, and backtracking + used by ExecuteDFSIter and ExecuteDFSRec --- */ + + +/* Back Edge / Cross Edges + + v is a visited node. The edge (current -> v) may be: + - a back edge (v is an ancestor of current) - if v has not been backtracked on + - a cross edge (v is neither an ancestor or a descendant of current) - if + v has been backtracked on and v was visited before current + + - Otherwise, a forward edge (v is a descendant of current) - Unused case + + + Set the child and current record elements for use in CrossFunc / AncestorFunc, + and call the appropriate function if (current -> v) is a back or cross edge +*/ + +#define ANCESTOR_CROSS(current, v, v_backtracked, args) \ + if (args->CallAncestor || args->CallCross) { \ + AssPRec(args->record, args->RNamChild, INTOBJ_INT(v)); \ + AssPRec(args->record, args->RNamCurrent, INTOBJ_INT(current)); \ + CHANGED_BAG(args->record); \ + if (args->CallAncestor && !v_backtracked) { \ + CALL_CHECK_STOP( \ + args->AncestorFunc, args->RNamStop, args->record, args->data) \ + } else if (args->CallCross \ + && (v_backtracked \ + && ((GET_PREORDER(args, v)) \ + < (GET_PREORDER(args, current))))) { \ + CALL_CHECK_STOP( \ + args->CrossFunc, args->RNamStop, args->record, args->data) \ + } \ + CHANGED_BAG(args->record); \ + } + +/* Backtracking edge (parent -> current) + + Set a node as backtracked, then call the PostorderFunc if one exists + setting the current and child elements of the DFS record +*/ + +#define ON_BACKTRACK(current, parent, args) \ + if (args->dfs_conf->use_postorder || args->dfs_conf->partial_postorder) { \ + SET_POSTORDER(args, current); \ + CHANGED_BAG(args->record); \ + } \ + if (args->CallPostorder) { \ + /* When CallPostorder is true, use_parents must be true, parent is valid + */ \ + AssPRec(args->record, args->RNamChild, INTOBJ_INT(current)); \ + AssPRec(args->record, args->RNamCurrent, INTOBJ_INT(parent)); \ + CHANGED_BAG(args->record); \ + CALL_CHECK_STOP( \ + args->PostorderFunc, args->RNamStop, args->record, args->data) \ + CHANGED_BAG(args->record); \ + } + +/* Adding a successor succ of current. idx is the index + of succ in OutNeighbours[current] + + For iterative: When pushing succ to the stack + For recursive: Before recursing on succ +*/ + +#define ON_ADD_SUCC(current, succ, idx, args) \ + if (args->dfs_conf->use_parents) { \ + AssPlist(args->parents, succ, INTOBJ_INT(current)); \ + } \ + if (args->dfs_conf->use_edge) { \ + AssPlist(args->edge, succ, INTOBJ_INT(idx)); \ + } \ + CHANGED_BAG(args->record); + +#define RECURSE_FOREST(dfs_args, v) \ + bool visited = IS_VISITED(dfs_args, v); \ + if (!visited) { \ + if (dfs_args->dfs_conf->use_parents) { \ + AssPlist(dfs_args->parents, v, INTOBJ_INT(v)); \ + CHANGED_BAG(record); \ + } \ + if (!ExecuteDFSRec(v, v, PREORDER_IDX, dfs_args)) { \ + CHANGED_BAG(record); \ + recordCleanup(dfs_args); \ + return record; \ + } \ + } + +#define ITER_FOREST(args, stack, v) \ + bool visited = IS_VISITED(args, v); \ + \ + if (!visited) { \ + if (args->dfs_conf->use_parents) { \ + AssPlist(args->parents, v, INTOBJ_INT(v)); \ + } \ + CHANGED_BAG(args->record); \ + AssPlist(stack, 1, INTOBJ_INT(v)); \ + \ + if (!iter_loop(stack, 1, args)) \ + return false; \ + } + +void recordCleanup(struct dfs_args* args) { + struct dfs_config* dfs_conf = args->dfs_conf; + + if (!dfs_conf->use_preorder) { + free(args->preorder_partial); + } + + if (dfs_conf->partial_postorder) { + free(args->postorder_partial); + } +} + +void parseConfig(struct dfs_args* args, Obj conf_record) { + struct dfs_config* conf = args->dfs_conf; + conf->iter = ElmPRec(conf_record, RNamName("iterative")) == True; + conf->revisit = ElmPRec(conf_record, RNamName("revisit")) == True; + conf->forest = ElmPRec(conf_record, RNamName("forest")) == True; + conf->use_preorder = ElmPRec(conf_record, RNamName("use_preorder")) == True; + conf->use_postorder = ElmPRec(conf_record, RNamName("use_postorder")) == True; + conf->use_parents = ElmPRec(conf_record, RNamName("use_parents")) == True; + conf->use_edge = ElmPRec(conf_record, RNamName("use_edge")) == True; + conf->forest_specific = ElmPRec(conf_record, RNamName("forest_specific")); + conf->partial_postorder = false; // Updated when parsing the config (whether + // to use a bitset for backtracked vertices) + + if (!conf->iter && (!conf->use_edge || !conf->use_parents)) { + ErrorQuit( + "In a DFSRecord where the config flag iterative is false, use_edge and " + "use_parents must be true", + 0L, + 0L); + } + + if (conf->revisit && !conf->iter) { + ErrorQuit("In a DFSRecord where the config flag revisit is true, iterative " + "must also be true", + 0L, + 0L); + } + + if ((args->CallAncestor || args->CallCross || conf->revisit) + && !conf->use_postorder) { + // If AncestorFunc or CrossFunc are called, we need to be able to + // detect whether a node has been backtracked + // likewise for being able to revisit nodes without infinite traversal + + conf->partial_postorder = true; + } + + if ((args->CallPostorder && conf->iter) && !conf->use_parents) { + // use_parents is always required for recursive (only check for iter) + ErrorQuit( + "In a DFSRecord where a PostorderFunc exists and the config flag " + "iter is true, the flag use_parents must also be true", + 0L, + 0L); + } + + if ((args->CallPostorder && conf->iter) && !conf->use_postorder) { + conf->partial_postorder = true; + } + + if (conf->forest_specific != Fail && conf->forest) { + ErrorQuit( + "In a DFSRecord where the config flag forest_specific is not fail, " + "forest cannot also be true", + 0L, + 0L); + } + + if (args->CallCross && (!conf->use_preorder)) { + ErrorQuit("In a DFSRecord where there is a CrossFunc, the config flag " + "use_preorder must be true", + 0L, + 0L); + } +} + +// Extreme examples are on the pull request #459 + +/* Iterative DFS (used for revisiting vertices) + Necessary record elements: none + If CallPostorder, then parents is necessary + + Differences with recursive : edge and parents are updated + when successors are pushed to the stack, whereas with + recursive they are updated before visiting +*/ + +bool ExecuteDFSIter(Int start, struct dfs_args* args) { + Int N = LEN_LIST(args->neighbors); + Obj stack = NEW_PLIST(T_PLIST_CYC, N * 2); + + Int stack_size = 1; + + AssPlist(stack, 1, INTOBJ_INT(start)); + + if (!iter_loop(stack, stack_size, args)) + return false; + + if (args->dfs_conf->forest) { + for (Int i = 1; i <= LEN_LIST(args->neighbors); i++) { + ITER_FOREST(args, stack, i); + } + } else if (args->dfs_conf->forest_specific != Fail) { + Int len = LEN_LIST(args->dfs_conf->forest_specific); + for (Int i = 1; i <= len; i++) { + ITER_FOREST(args, stack, i); + } + } + return true; +} + +/* + Main loop for iterative DFS called by ITER_FOREST and ExecuteDFSIter. + + ON_PREORDER, ON_BACKTRACK and ANCESTOR_CROSS can return from this function + if the record.stop attribute is set during a called PreOrderFunc, + PostOrderFunc, CrossFunc, or AncestorFunc. +*/ + +bool iter_loop(Obj stack, Int stack_size, struct dfs_args* args) { + while (stack_size > 0) { + Int current = INT_INTOBJ(STACK_POP(stack, stack_size)); + + if (current < 0) { // Backtrack node + Int bt_on = current * -1; + Int parent = !args->dfs_conf->use_parents + ? -1 + : INT_INTOBJ(ELM_LIST(args->parents, bt_on)); + ON_BACKTRACK(bt_on, parent, args); + continue; + } else if (IS_VISITED(args, current)) { + continue; + } + + ON_PREORDER(current, args); + + if (args->dfs_conf->use_postorder || args->dfs_conf->partial_postorder + || args->CallPostorder) { // push backtrack node if needed + STACK_PUSH(stack, stack_size, INTOBJ_INT(current * -1)); + } + + Obj succ = ELM_LIST(args->neighbors, current); + + // idx 1 at the top of the stack, + // (visit order consistent with recursive dfs) + for (Int i = LEN_LIST(succ); i > 0; i--) { + Int v = INT_INTOBJ(ELM_LIST(succ, i)); + + bool v_backtracked = + (args->dfs_conf->use_postorder || args->dfs_conf->partial_postorder) + && (IS_BACKTRACKED(args, v)); + bool revisit = (args->dfs_conf->revisit && v_backtracked); + if (revisit) { + UNSET_PREORDER(args, v); + CHANGED_BAG(args->record); + } + bool visited = IS_VISITED(args, v); + + if (!visited) { + ON_ADD_SUCC(current, v, i, args); + STACK_PUSH(stack, stack_size, INTOBJ_INT(v)); + } else { + ANCESTOR_CROSS(current, v, v_backtracked, args); + } + } + } + return true; +} + +/* Recursive DFS + Necessary record elements: edge, parents + goto used to force tail call optimization + + ON_PREORDER, ON_BACKTRACK and ANCESTOR_CROSS can return from this function + if the record.stop attribute is set during a called PreOrderFunc, + PostOrderFunc, CrossFunc, or AncestorFunc. +*/ + +bool ExecuteDFSRec(Int current, Int parent, Int idx, struct dfs_args* args) { +// "goto recurse" used to prevent stack overflows for deep trees regardless of +// optimisation level +recurse: + if (idx == PREORDER_IDX) { // visit current + ON_PREORDER(current, args); + + // Start recursing on successors of vertex , with parent + idx += 1; + goto recurse; + } + + Obj successors = ELM_LIST(args->neighbors, current); + + if (idx > LEN_LIST(successors)) { + // Backtrack on current (all successors explored) + ON_BACKTRACK(current, parent, args); + + Int prev_idx = INT_INTOBJ(ELM_LIST(args->edge, current)); + Int parents_parent = INT_INTOBJ(ELM_LIST(args->parents, parent)); + + if (parent == current) { + return true; // At root + } + + // Continue exploration of 's successors + + current = parent; // Backtrack to parent of vertex + parent = parents_parent; // The parent is now the new vertex's + // previously assigned parent + idx = prev_idx + 1; // Index is the next successor to visit + // continuing previous exploration of + // 's successors + goto recurse; + } else { + // Visit successor successors[idx] of current + Int v = INT_INTOBJ(ELM_LIST(successors, idx)); + bool visited = IS_VISITED(args, v); + + if (!visited) { + ON_ADD_SUCC(current, v, idx, args); + + parent = current; // Explore successor v with parent + current = v; + idx = PREORDER_IDX; // Initial index to indicate v is being visited + + goto recurse; + + } else { + bool backtracked = + (args->dfs_conf->use_postorder || args->dfs_conf->partial_postorder) + && (IS_BACKTRACKED(args, v)); + ANCESTOR_CROSS(current, v, backtracked, args); + + idx += 1; // Skip this successor of + // since it has already been visited + goto recurse; + } + } +} + +Obj FuncExecuteDFS_C(Obj self, Obj args) { + DIGRAPHS_ASSERT(LEN_PLIST(args) == 7); + Obj record = ELM_PLIST(args, 1); + Obj config = ElmPRec(record, RNamName("config")); + Obj data = ELM_PLIST(args, 2); + Obj start = ELM_PLIST(args, 3); + Obj PreorderFunc = ELM_PLIST(args, 4); + Obj PostorderFunc = ELM_PLIST(args, 5); + Obj AncestorFunc = ELM_PLIST(args, 6); + Obj CrossFunc = ELM_PLIST(args, 7); + + // Check if function args are fail or 2 argument functions +#define CHECK_FUNCTION(function) \ + if (!((IS_FUNC(function) && NARG_FUNC(function) == 2) \ + || function == Fail)) { \ + ErrorQuit("arguments 4-7 (PreorderFunc, PostorderFunc, AncestorFunc, " \ + "CrossFunc)" \ + "must be either a function taking arguments (record, data) or" \ + "Fail,", \ + 0L, \ + 0L); \ + } + + CHECK_FUNCTION(PreorderFunc); + CHECK_FUNCTION(PostorderFunc); + CHECK_FUNCTION(AncestorFunc); + CHECK_FUNCTION(CrossFunc); + + Obj D = ElmPRec(record, RNamName("graph")); + Obj outNeighbours = FuncOutNeighbours(self, D); + Int N = DigraphNrVertices(D); + + if (!IS_INTOBJ(start) || INT_INTOBJ(start) > N || INT_INTOBJ(start) <= 0) { + ErrorQuit( + "the third argument must be a vertex in your graph,", 0L, 0L); + } + + Int RNamStop = RNamName("stop"); + + if (ElmPRec(record, RNamStop) == True) + return record; + + Int preorder_num = 0; + Int postorder_num = 0; + + struct dfs_config dfs_conf = {0}; + + struct dfs_args dfs_args_ = { + .dfs_conf = &dfs_conf, + .record = record, + .preorder_num = &preorder_num, + .postorder_num = &postorder_num, + .parents = ElmPRec(record, RNamName("parents")), + .postorder = ElmPRec(record, RNamName("postorder")), + .preorder = ElmPRec(record, RNamName("preorder")), + .edge = ElmPRec(record, RNamName("edge")), + .neighbors = outNeighbours, + .data = data, + .PreorderFunc = PreorderFunc, + .PostorderFunc = PostorderFunc, + .AncestorFunc = AncestorFunc, + .CrossFunc = CrossFunc, + .RNamChild = RNamName("child"), + .RNamCurrent = RNamName("current"), + .RNamStop = RNamStop, + .CallPreorder = PreorderFunc != Fail, + .CallPostorder = PostorderFunc != Fail, + .CallAncestor = AncestorFunc != Fail, + .CallCross = CrossFunc != Fail}; + + parseConfig(&dfs_args_, config); + + if (!dfs_conf.use_preorder) { + dfs_args_.preorder_partial = (bool*) safe_malloc((N + 1) * sizeof(bool)); + memset(dfs_args_.preorder_partial, false, (N + 1) * sizeof(bool)); + } + + if (dfs_conf.partial_postorder) { + dfs_args_.postorder_partial = (bool*) safe_malloc((N + 1) * sizeof(bool)); + memset(dfs_args_.postorder_partial, false, (N + 1) * sizeof(bool)); + } + + if (dfs_conf.use_parents) { + AssPlist(dfs_args_.parents, INT_INTOBJ(start), start); + CHANGED_BAG(record); + } + + Int current = INT_INTOBJ(start); + + if (dfs_conf.iter || dfs_conf.revisit) { + ExecuteDFSIter(current, &dfs_args_); + } else { + if (dfs_conf.forest || (dfs_conf.forest_specific != Fail)) { + // Initial DFS with specified start index + if (ExecuteDFSRec(current, current, PREORDER_IDX, &dfs_args_)) { + if (dfs_conf.forest) { + for (Int i = 1; i <= N; i++) { + RECURSE_FOREST((&dfs_args_), i); // Returns + } + } else if (dfs_conf.forest_specific != Fail) { + for (Int i = 1; i <= LEN_LIST(dfs_conf.forest_specific); i++) { + RECURSE_FOREST((&dfs_args_), // Returns + INT_INTOBJ(ELM_LIST(dfs_conf.forest_specific, i))); + } + } + } + } else { + ExecuteDFSRec(current, current, PREORDER_IDX, &dfs_args_); + } + } + + recordCleanup(&dfs_args_); + + CHANGED_BAG(record); + return record; +} diff --git a/src/dfs.h b/src/dfs.h new file mode 100644 index 000000000..9ca3c6e91 --- /dev/null +++ b/src/dfs.h @@ -0,0 +1,82 @@ +/******************************************************************************* +** +*A dfs.h GAP package Digraphs Lea Racine +** James Mitchell +** +** Copyright (C) 2014-21 - Julius Jonusas, James Mitchell, Michael Torpey, +** Wilf A. Wilson et al. +** +** This file is free software, see the digraphs/LICENSE. +** +*******************************************************************************/ + +#ifndef DIGRAPHS_SRC_DFS_H_ +#define DIGRAPHS_SRC_DFS_H_ + +#include // for false, true, bool +// GAP headers +#include "gap-includes.h" // for Obj, Int + +bool CallCheckStop(Obj f, Int RNamStop, Obj record, Obj data); + +struct dfs_args { + struct dfs_config* dfs_conf; + + Obj record; + Int* preorder_num; + Int* postorder_num; + + Obj parents; + + union { + Obj postorder; + bool* postorder_partial; + }; + + union { + Obj preorder; + bool* preorder_partial; + }; + Obj edge; + + Obj neighbors; + + Obj data; + Obj PreorderFunc; + Obj PostorderFunc; + Obj AncestorFunc; + Obj CrossFunc; + + Int RNamChild; + Int RNamCurrent; + Int RNamStop; + + bool CallPreorder; + bool CallPostorder; + bool CallAncestor; + bool CallCross; +}; + +struct dfs_config { + bool revisit; + bool iter; + bool forest; + Obj forest_specific; + bool use_preorder; + bool use_postorder; + bool partial_postorder; + bool use_parents; + bool use_edge; +}; + + + +bool iter_loop(Obj stack, Int stack_size, struct dfs_args* args); +bool ExecuteDFSRec(Int current, Int prev, Int idx, struct dfs_args* args); +bool ExecuteDFSIter(Int start, struct dfs_args* args); +Obj FuncExecuteDFS_C(Obj self, Obj args); + +void parseConfig(struct dfs_args*, Obj conf_record); +void recordCleanup(struct dfs_args* args); + +#endif // DIGRAPHS_SRC_DFS_H_ diff --git a/src/digraphs.c b/src/digraphs.c index ba0011b31..e7e0dabcd 100644 --- a/src/digraphs.c +++ b/src/digraphs.c @@ -26,6 +26,7 @@ #include "homos.h" // for FuncHomomorphismDigraphsFinder #include "planar.h" // for FUNC_IS_PLANAR, . . . #include "safemalloc.h" // for safe_malloc +#include "dfs.h" // for generic DFS #undef PACKAGE #undef PACKAGE_BUGREPORT @@ -2223,6 +2224,10 @@ static StructGVarFunc GVarFuncs[] = { GVAR_FUNC(SUBGRAPH_HOMEOMORPHIC_TO_K4, 1, "digraph"), GVAR_FUNC(DIGRAPHS_FREE_HOMOS_DATA, 0, ""), GVAR_FUNC(DIGRAPHS_FREE_CLIQUES_DATA, 0, ""), + GVAR_FUNC(ExecuteDFS_C, + -1, + "record, data, start, PreOrderFunc, PostOrderFunc, " + "AncestorFunc, CrossFunc"), {0, 0, 0, 0, 0} /* Finish with an empty entry */ }; @@ -2256,6 +2261,7 @@ static Int InitKernel(StructInitInfo* module) { ImportGVarFromLibrary("Group", &Group); ImportGVarFromLibrary("ClosureGroup", &ClosureGroup); ImportGVarFromLibrary("InfoWarning", &InfoWarning); + /* return success */ return 0; } diff --git a/tst/extreme/oper.tst b/tst/extreme/oper.tst index 82b9aeb31..d0e2cd930 100644 --- a/tst/extreme/oper.tst +++ b/tst/extreme/oper.tst @@ -173,6 +173,12 @@ gap> SortedList(DigraphLayers(gr, 6)[2]); gap> SortedList(DigraphLayers(gr, 1500)[2]); [ 81, 82, 169, 253, 254 ] +# ExecuteDFS recursive stack overflow check +gap> gr := CompleteDigraph(10000);; # defaults, recursive +gap> record := NewDFSRecord(gr);; +gap> record.config.iterative := false;; +gap> ExecuteDFS(record, fail, 1, fail, fail, fail, fail); + # DIGRAPHS_UnbindVariables gap> Unbind(d); gap> Unbind(gr); diff --git a/tst/standard/oper.tst b/tst/standard/oper.tst index 1eb29b2a5..12464ec12 100644 --- a/tst/standard/oper.tst +++ b/tst/standard/oper.tst @@ -1360,6 +1360,11 @@ ent , gap> DigraphPath(gr, 11, 11); Error, the 2nd and 3rd arguments and must be vertices of the 1st argum\ ent , +gap> gr := Digraph([[1, 2], [3], [1]]);; +gap> path := DigraphPath(gr, 1, 1); +[ [ 1, 1 ], [ 1 ] ] +gap> IsDigraphPath(gr, path); +true # IteratorOfPaths gap> gr := CompleteDigraph(5);; @@ -1464,6 +1469,12 @@ gap> DigraphLongestDistanceFromVertex(gr, 15); infinity gap> DigraphLongestDistanceFromVertex(gr, 16); Error, the 2nd argument must be a vertex of the 1st argument , +gap> D := Digraph([[2, 4], [3, 4], [5], [], []]);; +gap> DigraphLongestDistanceFromVertex(D, 1); +3 +gap> D := Digraph([[2, 3], [], [2]]);; +gap> DigraphLongestDistanceFromVertex(D, 1); +2 # DigraphRandomWalk gap> gr := CompleteDigraph(5); @@ -3252,6 +3263,239 @@ gap> DigraphEdges(D); gap> DigraphVertexLabels(D); [ 1, 2, 3, 6, [ 4, 5 ] ] +# DFS + +# NewDFSRecord TODO uncomment when know what record will be +# gap> NewDFSRecord(ChainDigraph(10)); +# rec( child := -1, current := -1, edge := HashMap([]), +# graph := , parents := HashMap([]), +# postorder := HashMap([]), preorder := HashMap([]), stop := false ) +# gap> NewDFSRecord(CompleteDigraph(2)); +# rec( child := -1, current := -1, edge := HashMap([]), +# graph := , +# parents := HashMap([]), postorder := HashMap([]), preorder := HashMap([]), +# stop := false ) +# gap> NewDFSRecord(Digraph([[1], [2], [1], [1], [2]])); +# rec( child := -1, current := -1, edge := HashMap([]), +# graph := , +# parents := HashMap([]), postorder := HashMap([]), preorder := HashMap([]), +# stop := false ) + +# ExecuteDFS - Error Checking +gap> ExecuteDFS(rec(), [], 1, fail, +> fail, fail, fail); +Error, the 1st argument must be created with NewDFSRecord, +gap> D := ChainDigraph(1);; +gap> ExecuteDFS(NewDFSRecord(D), [], 3, fail, fail, fail, +> fail); +Error, the third argument must be a vertex in your graph, +gap> d := BinaryTree(5);; +gap> record := NewDFSRecord(d);; +gap> record.config.forest_specific := [1,, 3];; +gap> ExecuteDFS(record, [], 3, fail, fail, fail, +> fail); +Error, the 1st argument has a value of .config.forest_specifi\ +c that is not fail or a dense list, +gap> record := NewDFSRecord(d);; +gap> record.config.use_edge := false;; +gap> ExecuteDFS(record, [], 1, fail, fail, fail, +> fail);; +Error, In a DFSRecord where the config flag iterative is false, use_edge and u\ +se_parents must be true + +# ExecuteDFS - Correctness of standalone function +gap> gr := CompleteDigraph(10);; +gap> record := NewDFSRecord(gr);; +gap> record2 := NewDFSRecord(gr);; +gap> record2.config.iterative := true;; +gap> ExecuteDFS(record, [], 2, fail, +> fail, fail, fail); +gap> ExecuteDFS(record2, [], 2, fail, +> fail, fail, fail); +gap> record.preorder; +[ 2, 1, 3, 4, 5, 6, 7, 8, 9, 10 ] +gap> record.postorder; +[ 9, 10, 8, 7, 6, 5, 4, 3, 2, 1 ] +gap> record.parents; +[ 2, 2, 1, 3, 4, 5, 6, 7, 8, 9 ] +gap> ForAll([record, record2], r -> +> ((r.edge = record.edge) and +> (r.parents = record.parents) and +> (r.preorder = record.preorder) and +> (r.postorder = record.postorder) and +> (IsDuplicateFree(r.postorder) and IsDuplicateFree(r.preorder))) +> ); +true +gap> record := NewDFSRecord(CompleteDigraph(15));; +gap> data := rec(cycle_vertex := 0);; +gap> AncestorFunc := function(record, data) +> record.stop := true; +> data.cycle_vertex := record.child; +> end;; +gap> ExecuteDFS(record, data, 1, fail, +> fail, AncestorFunc, fail); +gap> record.stop; +true +gap> data.cycle_vertex; +1 +gap> record.preorder; +[ 1, 2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ] +gap> record := NewDFSRecord(Digraph([[2, 3], [4], [5], [], [4]]));; +gap> CrossFunc := function(record, data) +> record.stop := true; +> Add(data, record.child); +> end;; +gap> data := [];; +gap> ExecuteDFS(record, data, 1, fail, +> fail, fail, CrossFunc); +gap> record.stop; +true +gap> data; +[ 4 ] +gap> AncestorFunc := function(record, data) +> Add(data.back_edges, [record.current, record.child]); +> end;; +gap> CrossFunc := function(record, data) +> Add(data.cross_edges, [record.current, record.child]); +> end;; +gap> record := NewDFSRecord(Digraph([[2, 3, 3], [4, 4], [5, 1, 1], [], [4]]));; +gap> data := rec(back_edges := [], cross_edges := []);; +gap> ExecuteDFS(record, data, 1, fail, +> fail, AncestorFunc, CrossFunc);; +gap> data; +rec( back_edges := [ [ 3, 1 ], [ 3, 1 ] ], cross_edges := [ [ 5, 4 ] ] ) +gap> record := NewDFSRecord(Digraph([[2, 3], [], [2]]));; +gap> data := rec(back_edges := [], cross_edges := []);; +gap> ExecuteDFS(record, data, 1, fail, fail, AncestorFunc, +> CrossFunc); +gap> record.preorder; +[ 1, 2, 3 ] +gap> record.postorder; +[ 3, 1, 2 ] +gap> data; +rec( back_edges := [ ], cross_edges := [ [ 3, 2 ] ] ) +gap> record := NewDFSRecord(Digraph([[2, 3], [3], []]));; +gap> data := rec(explored_edges := []);; +gap> PreorderFunc := function(record, data) +> if record.current <> record.parents[record.current] then +> Add(data.explored_edges, [record.parents[record.current], record.current]); +> fi; +> end;; +gap> ExecuteDFS(record, data, 1, PreorderFunc, fail, fail, +> fail); +gap> data.explored_edges; +[ [ 1, 2 ], [ 2, 3 ] ] +gap> gr := Digraph(List([1 .. 5], x -> [1 .. 5]));; +gap> record := NewDFSRecord(gr);; +gap> ExecuteDFS(record, data, 1, fail, fail, fail, +> fail); +gap> gr := DigraphFromGraph6String("LCHK?p?O?c@`?_");; +gap> record := NewDFSRecord(gr);; +gap> PreorderFunc := function(record, data) +> data.visits := data.visits + 1; +> end;; +gap> PostorderFunc := function(record, data) +> data.backtracks := data.backtracks + 1; +> end;; +gap> data := rec(visits := 0, backtracks := 0);; +gap> ExecuteDFS(record, data, 1, PreorderFunc, PostorderFunc, fail, +> fail);; +gap> record.preorder; +[ 1, 7, 5, 2, 4, 6, 12, 3, 8, 9, 11, 10, 13 ] +gap> record.postorder; +[ 13, 4, 1, 12, 10, 9, 6, 11, 2, 3, 7, 8, 5 ] +gap> data.visits = 13; +true +gap> data.backtracks = 13; +true + +# Stopping ExecuteDFS - Consistency Check across configurations +gap> gr := CompleteDigraph(1000);; # defaults, recursive +gap> record := NewDFSRecord(gr);; +gap> configs := [rec(forest := true), rec(forest_specific := DigraphVertices(gr)), +> rec(use_postorder := true), rec(use_preorder := true), rec(revisit := true, iterative := true)];; +gap> results := [];; +gap> PreorderFunc := function(record, data) +> data.count := data.count + 1; +> if record.current = 500 then +> record.stop := true; +> fi; +> end;; +> for conf in configs do +> data := rec(count := 0);; +> flags := NewDFSConfig();; +> for field in RecNames(conf) do +> flags.(field) := conf.(field); +> od; +> record := NewDFSRecord(gr, flags); +> ExecuteDFS(record, data, 1, PreorderFunc, fail, fail, fail); +> Add(results, data.count); +> od; +gap> ForAll(results, \x -> x = 500); +true + +# BinaryTree(5) - ExecuteDFS Consistency check across configurations +gap> gr := BinaryTree(5);; +gap> record := NewDFSRecord(gr);; +gap> configs := [rec(forest := true), rec(forest_specific := DigraphVertices(gr)), +> rec(forest := true, iterative := true), rec(forest_specific := DigraphVertices(gr), iterative := true)];; +gap> records := [];; +gap> for conf in configs do +> record := NewDFSRecord(gr);; +> for field in RecNames(conf) do +> record.config.(field) := conf.(field); +> od; +> ExecuteDFS(record, fail, 1, fail, fail, fail, fail);; +> Add(records, record);; +> od; +gap> ForAll(records, r -> +> ((r.edge = records[1].edge) and +> (r.parents = records[1].parents) and +> (r.preorder = records[1].preorder) and +> (r.postorder = records[1].postorder) and +> (IsDuplicateFree(r.postorder) and IsDuplicateFree(r.preorder))) +> ); +true + +# IsDigraphPath +gap> D := Digraph(IsMutableDigraph, Combinations([1 .. 5]), IsSubset); + +gap> DigraphReflexiveTransitiveReduction(D); + +gap> MakeImmutable(D); + +gap> IsDigraphPath(D, [1, 2, 3], []); +Error, the 2nd and 3rd arguments (lists) are incompatible, expected 3rd argume\ +nt of length 2, got 0 +gap> IsDigraphPath(D, [1], []); +true +gap> IsDigraphPath(D, [1, 2], [5]); +false +gap> IsDigraphPath(D, [32, 31, 33], [1, 1]); +false +gap> IsDigraphPath(D, [32, 33, 31], [1, 1]); +false +gap> IsDigraphPath(D, [6, 9, 16, 17], [3, 3, 2]); +true +gap> IsDigraphPath(D, [33, 9, 16, 17], [3, 3, 2]); +false +gap> IsDigraphPath(D, [6, 9, 18, 1], [9, 10, 2]); +false + +# IsDigraphPath +gap> D := Digraph(IsMutableDigraph, Combinations([1 .. 5]), IsSubset); + +gap> DigraphReflexiveTransitiveReduction(D); + +gap> MakeImmutable(D); + +gap> IsDigraphPath(D, DigraphPath(D, 6, 1)); +true +gap> ForAll(List(IteratorOfPaths(D, 6, 1)), x -> IsDigraphPath(D, x)); +true +gap> IsDigraphPath(D, []); +Error, the 2nd argument (a list) must have length 2, but found length 0 + # DIGRAPHS_UnbindVariables gap> Unbind(C); gap> Unbind(D); @@ -3313,6 +3557,16 @@ gap> Unbind(u1); gap> Unbind(u2); gap> Unbind(x); gap> Unbind(TestPartialOrderDigraph); +gap> Unbind(PreorderFunc); +gap> Unbind(AncestorFunc); +gap> Unbind(CrossFunc); +gap> Unbind(record); +gap> Unbind(record2); +gap> Unbind(data); +gap> Unbind(parents); +gap> Unbind(edge); +gap> Unbind(postorder); +gap> Unbind(preorder); # gap> DIGRAPHS_StopTest();