Skip to content

Commit 2a04cf1

Browse files
DigraphCycleBasis (#610)
* Add "DigraphCycleBasis" to oper.gi, 9. Connectivity * Fitting into length 80 * Linting change * Linting change 2 * Adding tests * Ignore walk of length 0 * silly mistake fixed * something * Documentation added (not tested) * Lint * L * Fundamental basis is guaranteed to be linear independent * L * L * Applying some suggested changes * undo oper.gi changes? * Adding comments only * fixed missing fi; * Changing the expected error msg accordingly * Complete Rewrite * L * L * L * Avoid "Last" function? * L * L * line break mess * Doc update * some more comments * Fix formatting and comments in oper.xml and oper.gi * Fix typo and improve clarity in documentation and code comments * Fix path initialization in oper.gi * Fix path initialization in oper.gi * Update Digraph in oper.tst * Conjuring up a particularly incidious test case * Update DigraphCycleBasis function tests in oper.tst * Fix DigraphCycleBasis output test formatting * Refactor loop error handling in oper.gi * Fix variable declaration in DigraphCycleBasis function * Linting local variables decl * Remove redundant loop check in oper.gi * ~9% speed up * Minor speed up * L * Adding the test for the 8GB warning in the extreme tests * Almost there * Documentation update related to the time complexity * More RAM conservative extreme test * Remove the check for 8GB warning * Updated the doc about explaining the cycle space * Apply suggestions from code review * applying most suggested changes * Applying the rest * Remove roots local variable * added a missing comma --------- Co-authored-by: James Mitchell <james-d-mitchell@users.noreply.github.com>
1 parent 31fb62a commit 2a04cf1

5 files changed

Lines changed: 281 additions & 0 deletions

File tree

doc/oper.xml

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2269,6 +2269,92 @@ true
22692269
</ManSection>
22702270
<#/GAPDoc>
22712271

2272+
<#GAPDoc Label="DigraphCycleBasis">
2273+
<ManSection>
2274+
<Oper Name="DigraphCycleBasis" Arg="digraph"/>
2275+
<Returns>A list and a list of GF(2) vectors</Returns>
2276+
<Description>
2277+
If <A>digraph</A> is a symmetric loopless digraph with no multiple edges, then
2278+
<C>DigraphCycleBasis</C> calculates the <E>fundamental cycle basis</E> of
2279+
<A>digraph</A> and returns a list of edges and a list of a basis vectors.
2280+
If <A>digraph</A> contains a loop, this function will return an error. If <A>digraph</A>
2281+
is a multi-digraph, then multiple edges incident to the same vertices are treated as a single edge by this function.
2282+
See <Ref Prop="IsSymmetricDigraph"/>, <Ref Prop="DigraphHasLoops"/>, and <Ref
2283+
Prop="IsMultiDigraph"/>.
2284+
The first list returned contains the out-neighours that provide the ordering
2285+
on the edges with the symmetric edges appearing only once;
2286+
these are the same as <E>OutNeighbours(MaximalAntiSymmetricSubdigraph(G))</E>.
2287+
See <Ref Oper="MaximalAntiSymmetricSubdigraph"/>
2288+
<Ref Oper="OutNeighbours"/>.
2289+
The second list returned contains the basis vectors of the cycle space of the
2290+
digraph. These vectors belongs to <M>(GF(2))^m</M> where <M>m</M> is the
2291+
length of the list of edges.<P/>
2292+
2293+
A graph is <E>eulerian</E> if every vertex has an even degree. A <E>cycle
2294+
space</E> of a graph is a subspace of <M>(GF(2))^m</M> representing the set
2295+
of all <E>eulerian</E> subgraphs without isolated vertices.
2296+
An eulerian subgraph without isolated vertices can be uniquely
2297+
identified using the edges that is contains. Therefore, given some ordering
2298+
on the edges of the graph, a subgraph can be represented by a vector in
2299+
<M>(GF(2))^m</M> where the <M>i</M>-th entry is <M>1</M> if the <M>i</M>-th edge
2300+
is in the subgraph and <M>0</M> otherwise. In this function, the ordering of the
2301+
edges is returned as the first list which corresponds to
2302+
<C>OutNeighbours(MaximalAntiSymmetricSubdigraph(G))</C>.
2303+
See <Ref Oper="MaximalAntiSymmetricSubdigraph"/>
2304+
and <Ref Oper="OutNeighbours"/>.
2305+
The first basis vector for the complete digraph with 4 vertices shown below
2306+
represents the edges <C>[1, 2]</C>, <C>[1, 3]</C> and <C>[2, 3]</C> i.e. cycle subgraph
2307+
between the vertices <C>1</C>, <C>2</C> and <C>3</C>
2308+
2309+
The cycle space is closed under the symmetric difference of the edges of
2310+
the graph. This nicely corresponds to the addition of vectors in
2311+
<M>(GF(2))^m</M> which makes the vector space formulation of the cycle
2312+
space very natural.<P/>
2313+
2314+
A <E>cycle basis</E> is a basis of the cycle space.
2315+
A <E>fundamental cycle basis</E> is a special
2316+
kind of basis of the cycle space where there is a specific spanning tree
2317+
(forest, when the graph is disconnected) of the graph and each basis
2318+
corresponds to the unique cycle created by adding an edge outside the
2319+
spanning tree of the graph to the spanning tree. In this function, the
2320+
spanning forest is rooted in the vertex with the smallest label for each
2321+
connected component of the graph. <P/>
2322+
2323+
The fundamental cycle basis is unique up to reordering of the basis vectors.
2324+
The number of basis vectors in the fundamental cycle basis is
2325+
<M>m - n + c</M>, where <M>m</M> is the number
2326+
of edges, <M>n</M> is the number of vertices, and <M>c</M> is the number of
2327+
connected components. See <Ref Oper="DigraphConnectedComponents"/>.<P/>
2328+
2329+
This function performs a depth first traversal of the input digraph with complexity <M>O(m + n)</M> and
2330+
the complexity of the computation of the basis is <M>O(m^2)</M> where <M>m</M>
2331+
is the number of edges in the input digraph.
2332+
2333+
2334+
<Example><![CDATA[
2335+
gap> D := CycleGraph(4);
2336+
<immutable symmetric digraph with 4 vertices, 8 edges>
2337+
gap> res := DigraphCycleBasis(D);
2338+
[ [ [ 2, 4 ], [ 3 ], [ 4 ], [ ] ], [ <a GF2 vector of length 4> ] ]
2339+
gap> List(res[2][1]);
2340+
[ Z(2)^0, Z(2)^0, Z(2)^0, Z(2)^0 ]
2341+
gap> D := CompleteDigraph(4);
2342+
<immutable complete digraph with 4 vertices>
2343+
gap> res := DigraphCycleBasis(D);
2344+
[ [ [ 2, 3, 4 ], [ 3, 4 ], [ 4 ], [ ] ],
2345+
[ <a GF2 vector of length 6>, <a GF2 vector of length 6>,
2346+
<a GF2 vector of length 6> ] ]
2347+
gap> List(res[2][1]);
2348+
[ Z(2)^0, 0*Z(2), Z(2)^0, 0*Z(2), Z(2)^0, 0*Z(2) ]
2349+
gap> List(res[2][2]);
2350+
[ 0*Z(2), Z(2)^0, Z(2)^0, 0*Z(2), 0*Z(2), Z(2)^0 ]
2351+
gap> List(res[2][3]);
2352+
[ Z(2)^0, Z(2)^0, 0*Z(2), Z(2)^0, 0*Z(2), 0*Z(2) ]
2353+
]]></Example>
2354+
</Description>
2355+
</ManSection>
2356+
<#/GAPDoc>
2357+
22722358
<#GAPDoc Label="IsOrderIdeal">
22732359
<ManSection>
22742360
<Oper Name="IsOrderIdeal" Arg="D, subset" Label="for a digraph and list"/>

doc/z-chap4.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
<#Include Label="HamiltonianPath">
8383
<#Include Label="NrSpanningTrees">
8484
<#Include Label="DigraphDijkstra">
85+
<#Include Label="DigraphCycleBasis">
8586
</Section>
8687

8788
<Section><Heading>Cayley graphs of groups</Heading>

gap/oper.gd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ DeclareOperation("VerticesReachableFrom", [IsDigraph, IsList]);
141141
DeclareOperation("IsOrderIdeal", [IsDigraph, IsList]);
142142
DeclareOperation("Dominators", [IsDigraph, IsPosInt]);
143143
DeclareOperation("DominatorTree", [IsDigraph, IsPosInt]);
144+
DeclareOperation("DigraphCycleBasis", [IsDigraph]);
144145

145146
# 10. Operations for vertices . . .
146147
DeclareOperation("PartialOrderDigraphJoinOfVertices",

gap/oper.gi

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2216,6 +2216,140 @@ function(D, root)
22162216
return result;
22172217
end);
22182218

2219+
# Computes the fundamental cycle basis of a symmetric digraph
2220+
# First, notice that the cycle space is composed of orthogonal subspaces
2221+
# corresponding to the cycle spaces of the connected components.
2222+
# e.g. if G has G1, G2 and G3 connected
2223+
# components with B1, B2 and B3 cycle basis matrix respectively, then the
2224+
# resulting cycle basis matrix of G is
2225+
# [[ B1, 0, 0],
2226+
# [ 0, B2, 0],
2227+
# [ 0, 0, B3]]
2228+
# up to some permutation on the order of the edges.
2229+
# As a result, we can compute the fundamental cycle basis of each connected
2230+
# component and then combine them.
2231+
2232+
# For each connected component, a spanning tree is computed rooted at 1. Then,
2233+
# there is one to one correspondence between the edges not in the spanning tree
2234+
# and the fundamental cycles basis.
2235+
# (See : https://en.wikipedia.org/wiki/Cycle_basis#Fundamental_cycles)
2236+
# The set of edges that form the base cycle is computed by finding the path
2237+
# from the root to the each sides of the edge and then adding the edge to the
2238+
# path. Then, it is converted to a binary vector where the i-th entry is 1 if
2239+
# the i-th edge in the 'EdgesList' is in the cycle and 0 otherwise.
2240+
# Related paper : https://dl.acm.org/doi/pdf/10.1145/363219.363232
2241+
InstallMethod(DigraphCycleBasis, "for a digraph",
2242+
[IsDigraph],
2243+
function(G)
2244+
local OutNbr, InNbr, n, partialSum, m, visited, unusedEdges, i, c, s, stack,
2245+
z, u, v, p, B;
2246+
2247+
# Check for loops
2248+
if DigraphHasLoops(G) then
2249+
ErrorNoReturn("the 1st argument (a digraph) must not have any loops");
2250+
fi;
2251+
2252+
G := MaximalAntiSymmetricSubdigraph(G);
2253+
OutNbr := OutNeighbors(G);
2254+
InNbr := InNeighbors(G);
2255+
Unbind(G);
2256+
2257+
n := Length(OutNbr);
2258+
2259+
# Quick early return for too few vertices
2260+
if n < 3 then
2261+
return [OutNbr, []];
2262+
fi;
2263+
# Find the partial sum of each row of OutNbr
2264+
partialSum := [0];
2265+
for i in [1 .. n - 1] do
2266+
Add(partialSum, partialSum[i] + Length(OutNbr[i]));
2267+
od;
2268+
m := partialSum[n] + Length(OutNbr[n]);
2269+
# Quick early return for too few edges
2270+
if m < 3 then
2271+
return [OutNbr, []];
2272+
fi;
2273+
2274+
# Traverse the graph, depth first search
2275+
s := 1;
2276+
visited := BlistList([1 .. n], []);
2277+
unusedEdges := [];
2278+
while s <> fail do
2279+
visited[s] := [0, s];
2280+
stack := [s];
2281+
while not IsEmpty(stack) do
2282+
u := Remove(stack);
2283+
for p in [1 .. Length(OutNbr[u])] do
2284+
v := OutNbr[u][p];
2285+
i := partialSum[u] + p;
2286+
if visited[v] = false then
2287+
visited[v] := [i, u];
2288+
Add(stack, v);
2289+
elif v in stack then
2290+
Add(unusedEdges, [u, i, visited[v][1], visited[v][2]]);
2291+
fi;
2292+
od;
2293+
for v in InNbr[u] do
2294+
p := Position(OutNbr[v], u);
2295+
i := partialSum[v] + p;
2296+
if visited[v] = false then
2297+
visited[v] := [i, u];
2298+
Add(stack, v);
2299+
elif v in stack then
2300+
Add(unusedEdges, [u, i, visited[v][1], visited[v][2]]);
2301+
fi;
2302+
od;
2303+
od;
2304+
s := Position(visited, false, s);
2305+
od;
2306+
2307+
c := Length(unusedEdges);
2308+
2309+
# Warning for large matrix
2310+
# The warning is printed roughly when the result matrix will take up
2311+
# more than 8GB of RAM.
2312+
if (10 ^ 11) / 2 < m * c then
2313+
Info(InfoWarning, 1, StringFormatted(
2314+
"The resulting matrix is going to be large of size {} \x {} ",
2315+
m, c));
2316+
fi;
2317+
2318+
# Create the matrix B
2319+
# The algorithm so far is O(m). However, the creation of the matrix B is
2320+
# O(m * c) ~= O(m^2) which is the most expensive part of the function.
2321+
# Hence, a nice thing to do would be to create an object that would
2322+
# lazy compute the matrix B on demand.
2323+
# For implementation of such an object, it would need to know :
2324+
# - m : The number of edges
2325+
# - unusedEdges : The list of unused edges to be converted to a basis vector
2326+
# - visited : The result of the depth first search above
2327+
2328+
# TODO : In the case the Digraph package requires GAP 4.12 or over,
2329+
# remove the following if statement.
2330+
if CompareVersionNumbers(GAPInfo.Version, "4.12") then
2331+
B := List([1 .. c], i -> NewZeroVector(IsGF2VectorRep, GF(2), m));
2332+
else
2333+
z := List([1 .. m], i -> Zero(GF(2)));
2334+
B := List([1 .. c], i -> Vector(GF(2), z));
2335+
fi;
2336+
2337+
for i in [1 .. c] do
2338+
u := unusedEdges[i][1];
2339+
v := unusedEdges[i][4];
2340+
2341+
while u <> v do
2342+
B[i][visited[u][1]] := Z(2);
2343+
u := visited[u][2];
2344+
od;
2345+
2346+
B[i][unusedEdges[i][2]] := Z(2);
2347+
B[i][unusedEdges[i][3]] := Z(2);
2348+
od;
2349+
2350+
return [OutNbr, B];
2351+
end);
2352+
22192353
#############################################################################
22202354
# 10. Operations for vertices
22212355
#############################################################################

tst/standard/oper.tst

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2839,6 +2839,64 @@ gap> D := CycleDigraph(5);;
28392839
gap> IsOrderIdeal(D, [1]);
28402840
Error, the 1st argument (a digraph) must be a partial order digraph
28412841

2842+
# DigraphCycleBasis
2843+
gap> D := NullDigraph(0);
2844+
<immutable empty digraph with 0 vertices>
2845+
gap> DigraphCycleBasis(D);
2846+
[ [ ], [ ] ]
2847+
gap> D := NullDigraph(6);
2848+
<immutable empty digraph with 6 vertices>
2849+
gap> DigraphCycleBasis(D);
2850+
[ [ [ ], [ ], [ ], [ ], [ ], [ ] ], [ ] ]
2851+
gap> D := Digraph([[1]]);
2852+
<immutable digraph with 1 vertex, 1 edge>
2853+
gap> DigraphCycleBasis(D);
2854+
Error, the 1st argument (a digraph) must not have any loops
2855+
gap> D := CompleteDigraph(5);
2856+
<immutable complete digraph with 5 vertices>
2857+
gap> res := DigraphCycleBasis(D);
2858+
[ [ [ 2, 3, 4, 5 ], [ 3, 4, 5 ], [ 4, 5 ], [ 5 ], [ ] ],
2859+
[ <a GF2 vector of length 10>, <a GF2 vector of length 10>,
2860+
<a GF2 vector of length 10>, <a GF2 vector of length 10>,
2861+
<a GF2 vector of length 10>, <a GF2 vector of length 10> ] ]
2862+
gap> List(res[2], x -> List(x));
2863+
[ [ Z(2)^0, 0*Z(2), 0*Z(2), Z(2)^0, 0*Z(2), 0*Z(2), Z(2)^0, 0*Z(2), 0*Z(2),
2864+
0*Z(2) ],
2865+
[ 0*Z(2), Z(2)^0, 0*Z(2), Z(2)^0, 0*Z(2), 0*Z(2), 0*Z(2), 0*Z(2), Z(2)^0,
2866+
0*Z(2) ],
2867+
[ 0*Z(2), 0*Z(2), Z(2)^0, Z(2)^0, 0*Z(2), 0*Z(2), 0*Z(2), 0*Z(2), 0*Z(2),
2868+
Z(2)^0 ],
2869+
[ Z(2)^0, 0*Z(2), Z(2)^0, 0*Z(2), 0*Z(2), Z(2)^0, 0*Z(2), 0*Z(2), 0*Z(2),
2870+
0*Z(2) ],
2871+
[ 0*Z(2), Z(2)^0, Z(2)^0, 0*Z(2), 0*Z(2), 0*Z(2), 0*Z(2), Z(2)^0, 0*Z(2),
2872+
0*Z(2) ],
2873+
[ Z(2)^0, Z(2)^0, 0*Z(2), 0*Z(2), Z(2)^0, 0*Z(2), 0*Z(2), 0*Z(2), 0*Z(2),
2874+
0*Z(2) ] ]
2875+
gap> D := DigraphSymmetricClosure(ChainDigraph(10));
2876+
<immutable symmetric digraph with 10 vertices, 18 edges>
2877+
gap> DigraphCycleBasis(D);
2878+
[ [ [ 2 ], [ 3 ], [ 4 ], [ 5 ], [ 6 ], [ 7 ], [ 8 ], [ 9 ], [ 10 ], [ ] ],
2879+
[ ] ]
2880+
gap> D := Digraph([[6], [3, 1, 6], [2], [6, 5], [4, 3, 2, 6], [4, 1, 5, 2]]);
2881+
<immutable digraph with 6 vertices, 15 edges>
2882+
gap> res := DigraphCycleBasis(D);
2883+
[ [ [ 6 ], [ 1, 3, 6 ], [ ], [ 5, 6 ], [ 2, 3, 6 ], [ ] ],
2884+
[ <a GF2 vector of length 9>, <a GF2 vector of length 9>,
2885+
<a GF2 vector of length 9>, <a GF2 vector of length 9> ] ]
2886+
gap> List(res[2], x -> List(x));
2887+
[ [ Z(2)^0, Z(2)^0, 0*Z(2), Z(2)^0, 0*Z(2), 0*Z(2), 0*Z(2), 0*Z(2), 0*Z(2) ],
2888+
[ 0*Z(2), 0*Z(2), Z(2)^0, 0*Z(2), 0*Z(2), 0*Z(2), Z(2)^0, Z(2)^0, 0*Z(2) ],
2889+
[ Z(2)^0, Z(2)^0, 0*Z(2), 0*Z(2), 0*Z(2), 0*Z(2), Z(2)^0, 0*Z(2), Z(2)^0 ],
2890+
[ Z(2)^0, Z(2)^0, 0*Z(2), 0*Z(2), Z(2)^0, Z(2)^0, Z(2)^0, 0*Z(2), 0*Z(2) ] ]
2891+
gap> D := DigraphDisjointUnion(CycleGraph(3), CycleGraph(4));
2892+
<immutable digraph with 7 vertices, 14 edges>
2893+
gap> res := DigraphCycleBasis(D);
2894+
[ [ [ 2, 3 ], [ 3 ], [ ], [ 5, 7 ], [ 6 ], [ 7 ], [ ] ],
2895+
[ <a GF2 vector of length 7>, <a GF2 vector of length 7> ] ]
2896+
gap> List(res[2], x -> List(x));
2897+
[ [ Z(2)^0, Z(2)^0, Z(2)^0, 0*Z(2), 0*Z(2), 0*Z(2), 0*Z(2) ],
2898+
[ 0*Z(2), 0*Z(2), 0*Z(2), Z(2)^0, Z(2)^0, Z(2)^0, Z(2)^0 ] ]
2899+
28422900
# DIGRAPHS_UnbindVariables
28432901
gap> Unbind(C);
28442902
gap> Unbind(D);
@@ -2892,6 +2950,7 @@ gap> Unbind(p2);
28922950
gap> Unbind(path);
28932951
gap> Unbind(qr);
28942952
gap> Unbind(r);
2953+
gap> Unbind(res);
28952954
gap> Unbind(rtclosure);
28962955
gap> Unbind(t);
28972956
gap> Unbind(tclosure);

0 commit comments

Comments
 (0)