1231 lines
42 KiB
Python
1231 lines
42 KiB
Python
"""
|
|
========================
|
|
Cycle finding algorithms
|
|
========================
|
|
"""
|
|
|
|
from collections import Counter, defaultdict
|
|
from itertools import combinations, product
|
|
from math import inf
|
|
|
|
import networkx as nx
|
|
from networkx.utils import not_implemented_for, pairwise
|
|
|
|
__all__ = [
|
|
"cycle_basis",
|
|
"simple_cycles",
|
|
"recursive_simple_cycles",
|
|
"find_cycle",
|
|
"minimum_cycle_basis",
|
|
"chordless_cycles",
|
|
"girth",
|
|
]
|
|
|
|
|
|
@not_implemented_for("directed")
|
|
@not_implemented_for("multigraph")
|
|
@nx._dispatch
|
|
def cycle_basis(G, root=None):
|
|
"""Returns a list of cycles which form a basis for cycles of G.
|
|
|
|
A basis for cycles of a network is a minimal collection of
|
|
cycles such that any cycle in the network can be written
|
|
as a sum of cycles in the basis. Here summation of cycles
|
|
is defined as "exclusive or" of the edges. Cycle bases are
|
|
useful, e.g. when deriving equations for electric circuits
|
|
using Kirchhoff's Laws.
|
|
|
|
Parameters
|
|
----------
|
|
G : NetworkX Graph
|
|
root : node, optional
|
|
Specify starting node for basis.
|
|
|
|
Returns
|
|
-------
|
|
A list of cycle lists. Each cycle list is a list of nodes
|
|
which forms a cycle (loop) in G.
|
|
|
|
Examples
|
|
--------
|
|
>>> G = nx.Graph()
|
|
>>> nx.add_cycle(G, [0, 1, 2, 3])
|
|
>>> nx.add_cycle(G, [0, 3, 4, 5])
|
|
>>> nx.cycle_basis(G, 0)
|
|
[[3, 4, 5, 0], [1, 2, 3, 0]]
|
|
|
|
Notes
|
|
-----
|
|
This is adapted from algorithm CACM 491 [1]_.
|
|
|
|
References
|
|
----------
|
|
.. [1] Paton, K. An algorithm for finding a fundamental set of
|
|
cycles of a graph. Comm. ACM 12, 9 (Sept 1969), 514-518.
|
|
|
|
See Also
|
|
--------
|
|
simple_cycles
|
|
"""
|
|
gnodes = dict.fromkeys(G) # set-like object that maintains node order
|
|
cycles = []
|
|
while gnodes: # loop over connected components
|
|
if root is None:
|
|
root = gnodes.popitem()[0]
|
|
stack = [root]
|
|
pred = {root: root}
|
|
used = {root: set()}
|
|
while stack: # walk the spanning tree finding cycles
|
|
z = stack.pop() # use last-in so cycles easier to find
|
|
zused = used[z]
|
|
for nbr in G[z]:
|
|
if nbr not in used: # new node
|
|
pred[nbr] = z
|
|
stack.append(nbr)
|
|
used[nbr] = {z}
|
|
elif nbr == z: # self loops
|
|
cycles.append([z])
|
|
elif nbr not in zused: # found a cycle
|
|
pn = used[nbr]
|
|
cycle = [nbr, z]
|
|
p = pred[z]
|
|
while p not in pn:
|
|
cycle.append(p)
|
|
p = pred[p]
|
|
cycle.append(p)
|
|
cycles.append(cycle)
|
|
used[nbr].add(z)
|
|
for node in pred:
|
|
gnodes.pop(node, None)
|
|
root = None
|
|
return cycles
|
|
|
|
|
|
@nx._dispatch
|
|
def simple_cycles(G, length_bound=None):
|
|
"""Find simple cycles (elementary circuits) of a graph.
|
|
|
|
A `simple cycle`, or `elementary circuit`, is a closed path where
|
|
no node appears twice. In a directed graph, two simple cycles are distinct
|
|
if they are not cyclic permutations of each other. In an undirected graph,
|
|
two simple cycles are distinct if they are not cyclic permutations of each
|
|
other nor of the other's reversal.
|
|
|
|
Optionally, the cycles are bounded in length. In the unbounded case, we use
|
|
a nonrecursive, iterator/generator version of Johnson's algorithm [1]_. In
|
|
the bounded case, we use a version of the algorithm of Gupta and
|
|
Suzumura[2]_. There may be better algorithms for some cases [3]_ [4]_ [5]_.
|
|
|
|
The algorithms of Johnson, and Gupta and Suzumura, are enhanced by some
|
|
well-known preprocessing techniques. When G is directed, we restrict our
|
|
attention to strongly connected components of G, generate all simple cycles
|
|
containing a certain node, remove that node, and further decompose the
|
|
remainder into strongly connected components. When G is undirected, we
|
|
restrict our attention to biconnected components, generate all simple cycles
|
|
containing a particular edge, remove that edge, and further decompose the
|
|
remainder into biconnected components.
|
|
|
|
Note that multigraphs are supported by this function -- and in undirected
|
|
multigraphs, a pair of parallel edges is considered a cycle of length 2.
|
|
Likewise, self-loops are considered to be cycles of length 1. We define
|
|
cycles as sequences of nodes; so the presence of loops and parallel edges
|
|
does not change the number of simple cycles in a graph.
|
|
|
|
Parameters
|
|
----------
|
|
G : NetworkX DiGraph
|
|
A directed graph
|
|
|
|
length_bound : int or None, optional (default=None)
|
|
If length_bound is an int, generate all simple cycles of G with length at
|
|
most length_bound. Otherwise, generate all simple cycles of G.
|
|
|
|
Yields
|
|
------
|
|
list of nodes
|
|
Each cycle is represented by a list of nodes along the cycle.
|
|
|
|
Examples
|
|
--------
|
|
>>> edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)]
|
|
>>> G = nx.DiGraph(edges)
|
|
>>> sorted(nx.simple_cycles(G))
|
|
[[0], [0, 1, 2], [0, 2], [1, 2], [2]]
|
|
|
|
To filter the cycles so that they don't include certain nodes or edges,
|
|
copy your graph and eliminate those nodes or edges before calling.
|
|
For example, to exclude self-loops from the above example:
|
|
|
|
>>> H = G.copy()
|
|
>>> H.remove_edges_from(nx.selfloop_edges(G))
|
|
>>> sorted(nx.simple_cycles(H))
|
|
[[0, 1, 2], [0, 2], [1, 2]]
|
|
|
|
Notes
|
|
-----
|
|
When length_bound is None, the time complexity is $O((n+e)(c+1))$ for $n$
|
|
nodes, $e$ edges and $c$ simple circuits. Otherwise, when length_bound > 1,
|
|
the time complexity is $O((c+n)(k-1)d^k)$ where $d$ is the average degree of
|
|
the nodes of G and $k$ = length_bound.
|
|
|
|
Raises
|
|
------
|
|
ValueError
|
|
when length_bound < 0.
|
|
|
|
References
|
|
----------
|
|
.. [1] Finding all the elementary circuits of a directed graph.
|
|
D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975.
|
|
https://doi.org/10.1137/0204007
|
|
.. [2] Finding All Bounded-Length Simple Cycles in a Directed Graph
|
|
A. Gupta and T. Suzumura https://arxiv.org/abs/2105.10094
|
|
.. [3] Enumerating the cycles of a digraph: a new preprocessing strategy.
|
|
G. Loizou and P. Thanish, Information Sciences, v. 27, 163-182, 1982.
|
|
.. [4] A search strategy for the elementary cycles of a directed graph.
|
|
J.L. Szwarcfiter and P.E. Lauer, BIT NUMERICAL MATHEMATICS,
|
|
v. 16, no. 2, 192-204, 1976.
|
|
.. [5] Optimal Listing of Cycles and st-Paths in Undirected Graphs
|
|
R. Ferreira and R. Grossi and A. Marino and N. Pisanti and R. Rizzi and
|
|
G. Sacomoto https://arxiv.org/abs/1205.2766
|
|
|
|
See Also
|
|
--------
|
|
cycle_basis
|
|
chordless_cycles
|
|
"""
|
|
|
|
if length_bound is not None:
|
|
if length_bound == 0:
|
|
return
|
|
elif length_bound < 0:
|
|
raise ValueError("length bound must be non-negative")
|
|
|
|
directed = G.is_directed()
|
|
yield from ([v] for v, Gv in G.adj.items() if v in Gv)
|
|
|
|
if length_bound is not None and length_bound == 1:
|
|
return
|
|
|
|
if G.is_multigraph() and not directed:
|
|
visited = set()
|
|
for u, Gu in G.adj.items():
|
|
multiplicity = ((v, len(Guv)) for v, Guv in Gu.items() if v in visited)
|
|
yield from ([u, v] for v, m in multiplicity if m > 1)
|
|
visited.add(u)
|
|
|
|
# explicitly filter out loops; implicitly filter out parallel edges
|
|
if directed:
|
|
G = nx.DiGraph((u, v) for u, Gu in G.adj.items() for v in Gu if v != u)
|
|
else:
|
|
G = nx.Graph((u, v) for u, Gu in G.adj.items() for v in Gu if v != u)
|
|
|
|
# this case is not strictly necessary but improves performance
|
|
if length_bound is not None and length_bound == 2:
|
|
if directed:
|
|
visited = set()
|
|
for u, Gu in G.adj.items():
|
|
yield from (
|
|
[v, u] for v in visited.intersection(Gu) if G.has_edge(v, u)
|
|
)
|
|
visited.add(u)
|
|
return
|
|
|
|
if directed:
|
|
yield from _directed_cycle_search(G, length_bound)
|
|
else:
|
|
yield from _undirected_cycle_search(G, length_bound)
|
|
|
|
|
|
def _directed_cycle_search(G, length_bound):
|
|
"""A dispatch function for `simple_cycles` for directed graphs.
|
|
|
|
We generate all cycles of G through binary partition.
|
|
|
|
1. Pick a node v in G which belongs to at least one cycle
|
|
a. Generate all cycles of G which contain the node v.
|
|
b. Recursively generate all cycles of G \\ v.
|
|
|
|
This is accomplished through the following:
|
|
|
|
1. Compute the strongly connected components SCC of G.
|
|
2. Select and remove a biconnected component C from BCC. Select a
|
|
non-tree edge (u, v) of a depth-first search of G[C].
|
|
3. For each simple cycle P containing v in G[C], yield P.
|
|
4. Add the biconnected components of G[C \\ v] to BCC.
|
|
|
|
If the parameter length_bound is not None, then step 3 will be limited to
|
|
simple cycles of length at most length_bound.
|
|
|
|
Parameters
|
|
----------
|
|
G : NetworkX DiGraph
|
|
A directed graph
|
|
|
|
length_bound : int or None
|
|
If length_bound is an int, generate all simple cycles of G with length at most length_bound.
|
|
Otherwise, generate all simple cycles of G.
|
|
|
|
Yields
|
|
------
|
|
list of nodes
|
|
Each cycle is represented by a list of nodes along the cycle.
|
|
"""
|
|
|
|
scc = nx.strongly_connected_components
|
|
components = [c for c in scc(G) if len(c) >= 2]
|
|
while components:
|
|
c = components.pop()
|
|
Gc = G.subgraph(c)
|
|
v = next(iter(c))
|
|
if length_bound is None:
|
|
yield from _johnson_cycle_search(Gc, [v])
|
|
else:
|
|
yield from _bounded_cycle_search(Gc, [v], length_bound)
|
|
# delete v after searching G, to make sure we can find v
|
|
G.remove_node(v)
|
|
components.extend(c for c in scc(Gc) if len(c) >= 2)
|
|
|
|
|
|
def _undirected_cycle_search(G, length_bound):
|
|
"""A dispatch function for `simple_cycles` for undirected graphs.
|
|
|
|
We generate all cycles of G through binary partition.
|
|
|
|
1. Pick an edge (u, v) in G which belongs to at least one cycle
|
|
a. Generate all cycles of G which contain the edge (u, v)
|
|
b. Recursively generate all cycles of G \\ (u, v)
|
|
|
|
This is accomplished through the following:
|
|
|
|
1. Compute the biconnected components BCC of G.
|
|
2. Select and remove a biconnected component C from BCC. Select a
|
|
non-tree edge (u, v) of a depth-first search of G[C].
|
|
3. For each (v -> u) path P remaining in G[C] \\ (u, v), yield P.
|
|
4. Add the biconnected components of G[C] \\ (u, v) to BCC.
|
|
|
|
If the parameter length_bound is not None, then step 3 will be limited to simple paths
|
|
of length at most length_bound.
|
|
|
|
Parameters
|
|
----------
|
|
G : NetworkX Graph
|
|
An undirected graph
|
|
|
|
length_bound : int or None
|
|
If length_bound is an int, generate all simple cycles of G with length at most length_bound.
|
|
Otherwise, generate all simple cycles of G.
|
|
|
|
Yields
|
|
------
|
|
list of nodes
|
|
Each cycle is represented by a list of nodes along the cycle.
|
|
"""
|
|
|
|
bcc = nx.biconnected_components
|
|
components = [c for c in bcc(G) if len(c) >= 3]
|
|
while components:
|
|
c = components.pop()
|
|
Gc = G.subgraph(c)
|
|
uv = list(next(iter(Gc.edges)))
|
|
G.remove_edge(*uv)
|
|
# delete (u, v) before searching G, to avoid fake 3-cycles [u, v, u]
|
|
if length_bound is None:
|
|
yield from _johnson_cycle_search(Gc, uv)
|
|
else:
|
|
yield from _bounded_cycle_search(Gc, uv, length_bound)
|
|
components.extend(c for c in bcc(Gc) if len(c) >= 3)
|
|
|
|
|
|
class _NeighborhoodCache(dict):
|
|
"""Very lightweight graph wrapper which caches neighborhoods as list.
|
|
|
|
This dict subclass uses the __missing__ functionality to query graphs for
|
|
their neighborhoods, and store the result as a list. This is used to avoid
|
|
the performance penalty incurred by subgraph views.
|
|
"""
|
|
|
|
def __init__(self, G):
|
|
self.G = G
|
|
|
|
def __missing__(self, v):
|
|
Gv = self[v] = list(self.G[v])
|
|
return Gv
|
|
|
|
|
|
def _johnson_cycle_search(G, path):
|
|
"""The main loop of the cycle-enumeration algorithm of Johnson.
|
|
|
|
Parameters
|
|
----------
|
|
G : NetworkX Graph or DiGraph
|
|
A graph
|
|
|
|
path : list
|
|
A cycle prefix. All cycles generated will begin with this prefix.
|
|
|
|
Yields
|
|
------
|
|
list of nodes
|
|
Each cycle is represented by a list of nodes along the cycle.
|
|
|
|
References
|
|
----------
|
|
.. [1] Finding all the elementary circuits of a directed graph.
|
|
D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975.
|
|
https://doi.org/10.1137/0204007
|
|
|
|
"""
|
|
|
|
G = _NeighborhoodCache(G)
|
|
blocked = set(path)
|
|
B = defaultdict(set) # graph portions that yield no elementary circuit
|
|
start = path[0]
|
|
stack = [iter(G[path[-1]])]
|
|
closed = [False]
|
|
while stack:
|
|
nbrs = stack[-1]
|
|
for w in nbrs:
|
|
if w == start:
|
|
yield path[:]
|
|
closed[-1] = True
|
|
elif w not in blocked:
|
|
path.append(w)
|
|
closed.append(False)
|
|
stack.append(iter(G[w]))
|
|
blocked.add(w)
|
|
break
|
|
else: # no more nbrs
|
|
stack.pop()
|
|
v = path.pop()
|
|
if closed.pop():
|
|
if closed:
|
|
closed[-1] = True
|
|
unblock_stack = {v}
|
|
while unblock_stack:
|
|
u = unblock_stack.pop()
|
|
if u in blocked:
|
|
blocked.remove(u)
|
|
unblock_stack.update(B[u])
|
|
B[u].clear()
|
|
else:
|
|
for w in G[v]:
|
|
B[w].add(v)
|
|
|
|
|
|
def _bounded_cycle_search(G, path, length_bound):
|
|
"""The main loop of the cycle-enumeration algorithm of Gupta and Suzumura.
|
|
|
|
Parameters
|
|
----------
|
|
G : NetworkX Graph or DiGraph
|
|
A graph
|
|
|
|
path : list
|
|
A cycle prefix. All cycles generated will begin with this prefix.
|
|
|
|
length_bound: int
|
|
A length bound. All cycles generated will have length at most length_bound.
|
|
|
|
Yields
|
|
------
|
|
list of nodes
|
|
Each cycle is represented by a list of nodes along the cycle.
|
|
|
|
References
|
|
----------
|
|
.. [1] Finding All Bounded-Length Simple Cycles in a Directed Graph
|
|
A. Gupta and T. Suzumura https://arxiv.org/abs/2105.10094
|
|
|
|
"""
|
|
G = _NeighborhoodCache(G)
|
|
lock = {v: 0 for v in path}
|
|
B = defaultdict(set)
|
|
start = path[0]
|
|
stack = [iter(G[path[-1]])]
|
|
blen = [length_bound]
|
|
while stack:
|
|
nbrs = stack[-1]
|
|
for w in nbrs:
|
|
if w == start:
|
|
yield path[:]
|
|
blen[-1] = 1
|
|
elif len(path) < lock.get(w, length_bound):
|
|
path.append(w)
|
|
blen.append(length_bound)
|
|
lock[w] = len(path)
|
|
stack.append(iter(G[w]))
|
|
break
|
|
else:
|
|
stack.pop()
|
|
v = path.pop()
|
|
bl = blen.pop()
|
|
if blen:
|
|
blen[-1] = min(blen[-1], bl)
|
|
if bl < length_bound:
|
|
relax_stack = [(bl, v)]
|
|
while relax_stack:
|
|
bl, u = relax_stack.pop()
|
|
if lock.get(u, length_bound) < length_bound - bl + 1:
|
|
lock[u] = length_bound - bl + 1
|
|
relax_stack.extend((bl + 1, w) for w in B[u].difference(path))
|
|
else:
|
|
for w in G[v]:
|
|
B[w].add(v)
|
|
|
|
|
|
@nx._dispatch
|
|
def chordless_cycles(G, length_bound=None):
|
|
"""Find simple chordless cycles of a graph.
|
|
|
|
A `simple cycle` is a closed path where no node appears twice. In a simple
|
|
cycle, a `chord` is an additional edge between two nodes in the cycle. A
|
|
`chordless cycle` is a simple cycle without chords. Said differently, a
|
|
chordless cycle is a cycle C in a graph G where the number of edges in the
|
|
induced graph G[C] is equal to the length of `C`.
|
|
|
|
Note that some care must be taken in the case that G is not a simple graph
|
|
nor a simple digraph. Some authors limit the definition of chordless cycles
|
|
to have a prescribed minimum length; we do not.
|
|
|
|
1. We interpret self-loops to be chordless cycles, except in multigraphs
|
|
with multiple loops in parallel. Likewise, in a chordless cycle of
|
|
length greater than 1, there can be no nodes with self-loops.
|
|
|
|
2. We interpret directed two-cycles to be chordless cycles, except in
|
|
multi-digraphs when any edge in a two-cycle has a parallel copy.
|
|
|
|
3. We interpret parallel pairs of undirected edges as two-cycles, except
|
|
when a third (or more) parallel edge exists between the two nodes.
|
|
|
|
4. Generalizing the above, edges with parallel clones may not occur in
|
|
chordless cycles.
|
|
|
|
In a directed graph, two chordless cycles are distinct if they are not
|
|
cyclic permutations of each other. In an undirected graph, two chordless
|
|
cycles are distinct if they are not cyclic permutations of each other nor of
|
|
the other's reversal.
|
|
|
|
Optionally, the cycles are bounded in length.
|
|
|
|
We use an algorithm strongly inspired by that of Dias et al [1]_. It has
|
|
been modified in the following ways:
|
|
|
|
1. Recursion is avoided, per Python's limitations
|
|
|
|
2. The labeling function is not necessary, because the starting paths
|
|
are chosen (and deleted from the host graph) to prevent multiple
|
|
occurrences of the same path
|
|
|
|
3. The search is optionally bounded at a specified length
|
|
|
|
4. Support for directed graphs is provided by extending cycles along
|
|
forward edges, and blocking nodes along forward and reverse edges
|
|
|
|
5. Support for multigraphs is provided by omitting digons from the set
|
|
of forward edges
|
|
|
|
Parameters
|
|
----------
|
|
G : NetworkX DiGraph
|
|
A directed graph
|
|
|
|
length_bound : int or None, optional (default=None)
|
|
If length_bound is an int, generate all simple cycles of G with length at
|
|
most length_bound. Otherwise, generate all simple cycles of G.
|
|
|
|
Yields
|
|
------
|
|
list of nodes
|
|
Each cycle is represented by a list of nodes along the cycle.
|
|
|
|
Examples
|
|
--------
|
|
>>> sorted(list(nx.chordless_cycles(nx.complete_graph(4))))
|
|
[[1, 0, 2], [1, 0, 3], [2, 0, 3], [2, 1, 3]]
|
|
|
|
Notes
|
|
-----
|
|
When length_bound is None, and the graph is simple, the time complexity is
|
|
$O((n+e)(c+1))$ for $n$ nodes, $e$ edges and $c$ chordless cycles.
|
|
|
|
Raises
|
|
------
|
|
ValueError
|
|
when length_bound < 0.
|
|
|
|
References
|
|
----------
|
|
.. [1] Efficient enumeration of chordless cycles
|
|
E. Dias and D. Castonguay and H. Longo and W.A.R. Jradi
|
|
https://arxiv.org/abs/1309.1051
|
|
|
|
See Also
|
|
--------
|
|
simple_cycles
|
|
"""
|
|
|
|
if length_bound is not None:
|
|
if length_bound == 0:
|
|
return
|
|
elif length_bound < 0:
|
|
raise ValueError("length bound must be non-negative")
|
|
|
|
directed = G.is_directed()
|
|
multigraph = G.is_multigraph()
|
|
|
|
if multigraph:
|
|
yield from ([v] for v, Gv in G.adj.items() if len(Gv.get(v, ())) == 1)
|
|
else:
|
|
yield from ([v] for v, Gv in G.adj.items() if v in Gv)
|
|
|
|
if length_bound is not None and length_bound == 1:
|
|
return
|
|
|
|
# Nodes with loops cannot belong to longer cycles. Let's delete them here.
|
|
# also, we implicitly reduce the multiplicity of edges down to 1 in the case
|
|
# of multiedges.
|
|
if directed:
|
|
F = nx.DiGraph((u, v) for u, Gu in G.adj.items() if u not in Gu for v in Gu)
|
|
B = F.to_undirected(as_view=False)
|
|
else:
|
|
F = nx.Graph((u, v) for u, Gu in G.adj.items() if u not in Gu for v in Gu)
|
|
B = None
|
|
|
|
# If we're given a multigraph, we have a few cases to consider with parallel
|
|
# edges.
|
|
#
|
|
# 1. If we have 2 or more edges in parallel between the nodes (u, v), we
|
|
# must not construct longer cycles along (u, v).
|
|
# 2. If G is not directed, then a pair of parallel edges between (u, v) is a
|
|
# chordless cycle unless there exists a third (or more) parallel edge.
|
|
# 3. If G is directed, then parallel edges do not form cycles, but do
|
|
# preclude back-edges from forming cycles (handled in the next section),
|
|
# Thus, if an edge (u, v) is duplicated and the reverse (v, u) is also
|
|
# present, then we remove both from F.
|
|
#
|
|
# In directed graphs, we need to consider both directions that edges can
|
|
# take, so iterate over all edges (u, v) and possibly (v, u). In undirected
|
|
# graphs, we need to be a little careful to only consider every edge once,
|
|
# so we use a "visited" set to emulate node-order comparisons.
|
|
|
|
if multigraph:
|
|
if not directed:
|
|
B = F.copy()
|
|
visited = set()
|
|
for u, Gu in G.adj.items():
|
|
if directed:
|
|
multiplicity = ((v, len(Guv)) for v, Guv in Gu.items())
|
|
for v, m in multiplicity:
|
|
if m > 1:
|
|
F.remove_edges_from(((u, v), (v, u)))
|
|
else:
|
|
multiplicity = ((v, len(Guv)) for v, Guv in Gu.items() if v in visited)
|
|
for v, m in multiplicity:
|
|
if m == 2:
|
|
yield [u, v]
|
|
if m > 1:
|
|
F.remove_edge(u, v)
|
|
visited.add(u)
|
|
|
|
# If we're given a directed graphs, we need to think about digons. If we
|
|
# have two edges (u, v) and (v, u), then that's a two-cycle. If either edge
|
|
# was duplicated above, then we removed both from F. So, any digons we find
|
|
# here are chordless. After finding digons, we remove their edges from F
|
|
# to avoid traversing them in the search for chordless cycles.
|
|
if directed:
|
|
for u, Fu in F.adj.items():
|
|
digons = [[u, v] for v in Fu if F.has_edge(v, u)]
|
|
yield from digons
|
|
F.remove_edges_from(digons)
|
|
F.remove_edges_from(e[::-1] for e in digons)
|
|
|
|
if length_bound is not None and length_bound == 2:
|
|
return
|
|
|
|
# Now, we prepare to search for cycles. We have removed all cycles of
|
|
# lengths 1 and 2, so F is a simple graph or simple digraph. We repeatedly
|
|
# separate digraphs into their strongly connected components, and undirected
|
|
# graphs into their biconnected components. For each component, we pick a
|
|
# node v, search for chordless cycles based at each "stem" (u, v, w), and
|
|
# then remove v from that component before separating the graph again.
|
|
if directed:
|
|
separate = nx.strongly_connected_components
|
|
|
|
# Directed stems look like (u -> v -> w), so we use the product of
|
|
# predecessors of v with successors of v.
|
|
def stems(C, v):
|
|
for u, w in product(C.pred[v], C.succ[v]):
|
|
if not G.has_edge(u, w): # omit stems with acyclic chords
|
|
yield [u, v, w], F.has_edge(w, u)
|
|
|
|
else:
|
|
separate = nx.biconnected_components
|
|
|
|
# Undirected stems look like (u ~ v ~ w), but we must not also search
|
|
# (w ~ v ~ u), so we use combinations of v's neighbors of length 2.
|
|
def stems(C, v):
|
|
yield from (([u, v, w], F.has_edge(w, u)) for u, w in combinations(C[v], 2))
|
|
|
|
components = [c for c in separate(F) if len(c) > 2]
|
|
while components:
|
|
c = components.pop()
|
|
v = next(iter(c))
|
|
Fc = F.subgraph(c)
|
|
Fcc = Bcc = None
|
|
for S, is_triangle in stems(Fc, v):
|
|
if is_triangle:
|
|
yield S
|
|
else:
|
|
if Fcc is None:
|
|
Fcc = _NeighborhoodCache(Fc)
|
|
Bcc = Fcc if B is None else _NeighborhoodCache(B.subgraph(c))
|
|
yield from _chordless_cycle_search(Fcc, Bcc, S, length_bound)
|
|
|
|
components.extend(c for c in separate(F.subgraph(c - {v})) if len(c) > 2)
|
|
|
|
|
|
def _chordless_cycle_search(F, B, path, length_bound):
|
|
"""The main loop for chordless cycle enumeration.
|
|
|
|
This algorithm is strongly inspired by that of Dias et al [1]_. It has been
|
|
modified in the following ways:
|
|
|
|
1. Recursion is avoided, per Python's limitations
|
|
|
|
2. The labeling function is not necessary, because the starting paths
|
|
are chosen (and deleted from the host graph) to prevent multiple
|
|
occurrences of the same path
|
|
|
|
3. The search is optionally bounded at a specified length
|
|
|
|
4. Support for directed graphs is provided by extending cycles along
|
|
forward edges, and blocking nodes along forward and reverse edges
|
|
|
|
5. Support for multigraphs is provided by omitting digons from the set
|
|
of forward edges
|
|
|
|
Parameters
|
|
----------
|
|
F : _NeighborhoodCache
|
|
A graph of forward edges to follow in constructing cycles
|
|
|
|
B : _NeighborhoodCache
|
|
A graph of blocking edges to prevent the production of chordless cycles
|
|
|
|
path : list
|
|
A cycle prefix. All cycles generated will begin with this prefix.
|
|
|
|
length_bound : int
|
|
A length bound. All cycles generated will have length at most length_bound.
|
|
|
|
|
|
Yields
|
|
------
|
|
list of nodes
|
|
Each cycle is represented by a list of nodes along the cycle.
|
|
|
|
References
|
|
----------
|
|
.. [1] Efficient enumeration of chordless cycles
|
|
E. Dias and D. Castonguay and H. Longo and W.A.R. Jradi
|
|
https://arxiv.org/abs/1309.1051
|
|
|
|
"""
|
|
blocked = defaultdict(int)
|
|
target = path[0]
|
|
blocked[path[1]] = 1
|
|
for w in path[1:]:
|
|
for v in B[w]:
|
|
blocked[v] += 1
|
|
|
|
stack = [iter(F[path[2]])]
|
|
while stack:
|
|
nbrs = stack[-1]
|
|
for w in nbrs:
|
|
if blocked[w] == 1 and (length_bound is None or len(path) < length_bound):
|
|
Fw = F[w]
|
|
if target in Fw:
|
|
yield path + [w]
|
|
else:
|
|
Bw = B[w]
|
|
if target in Bw:
|
|
continue
|
|
for v in Bw:
|
|
blocked[v] += 1
|
|
path.append(w)
|
|
stack.append(iter(Fw))
|
|
break
|
|
else:
|
|
stack.pop()
|
|
for v in B[path.pop()]:
|
|
blocked[v] -= 1
|
|
|
|
|
|
@not_implemented_for("undirected")
|
|
@nx._dispatch
|
|
def recursive_simple_cycles(G):
|
|
"""Find simple cycles (elementary circuits) of a directed graph.
|
|
|
|
A `simple cycle`, or `elementary circuit`, is a closed path where
|
|
no node appears twice. Two elementary circuits are distinct if they
|
|
are not cyclic permutations of each other.
|
|
|
|
This version uses a recursive algorithm to build a list of cycles.
|
|
You should probably use the iterator version called simple_cycles().
|
|
Warning: This recursive version uses lots of RAM!
|
|
It appears in NetworkX for pedagogical value.
|
|
|
|
Parameters
|
|
----------
|
|
G : NetworkX DiGraph
|
|
A directed graph
|
|
|
|
Returns
|
|
-------
|
|
A list of cycles, where each cycle is represented by a list of nodes
|
|
along the cycle.
|
|
|
|
Example:
|
|
|
|
>>> edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)]
|
|
>>> G = nx.DiGraph(edges)
|
|
>>> nx.recursive_simple_cycles(G)
|
|
[[0], [2], [0, 1, 2], [0, 2], [1, 2]]
|
|
|
|
Notes
|
|
-----
|
|
The implementation follows pp. 79-80 in [1]_.
|
|
|
|
The time complexity is $O((n+e)(c+1))$ for $n$ nodes, $e$ edges and $c$
|
|
elementary circuits.
|
|
|
|
References
|
|
----------
|
|
.. [1] Finding all the elementary circuits of a directed graph.
|
|
D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975.
|
|
https://doi.org/10.1137/0204007
|
|
|
|
See Also
|
|
--------
|
|
simple_cycles, cycle_basis
|
|
"""
|
|
|
|
# Jon Olav Vik, 2010-08-09
|
|
def _unblock(thisnode):
|
|
"""Recursively unblock and remove nodes from B[thisnode]."""
|
|
if blocked[thisnode]:
|
|
blocked[thisnode] = False
|
|
while B[thisnode]:
|
|
_unblock(B[thisnode].pop())
|
|
|
|
def circuit(thisnode, startnode, component):
|
|
closed = False # set to True if elementary path is closed
|
|
path.append(thisnode)
|
|
blocked[thisnode] = True
|
|
for nextnode in component[thisnode]: # direct successors of thisnode
|
|
if nextnode == startnode:
|
|
result.append(path[:])
|
|
closed = True
|
|
elif not blocked[nextnode]:
|
|
if circuit(nextnode, startnode, component):
|
|
closed = True
|
|
if closed:
|
|
_unblock(thisnode)
|
|
else:
|
|
for nextnode in component[thisnode]:
|
|
if thisnode not in B[nextnode]: # TODO: use set for speedup?
|
|
B[nextnode].append(thisnode)
|
|
path.pop() # remove thisnode from path
|
|
return closed
|
|
|
|
path = [] # stack of nodes in current path
|
|
blocked = defaultdict(bool) # vertex: blocked from search?
|
|
B = defaultdict(list) # graph portions that yield no elementary circuit
|
|
result = [] # list to accumulate the circuits found
|
|
|
|
# Johnson's algorithm exclude self cycle edges like (v, v)
|
|
# To be backward compatible, we record those cycles in advance
|
|
# and then remove from subG
|
|
for v in G:
|
|
if G.has_edge(v, v):
|
|
result.append([v])
|
|
G.remove_edge(v, v)
|
|
|
|
# Johnson's algorithm requires some ordering of the nodes.
|
|
# They might not be sortable so we assign an arbitrary ordering.
|
|
ordering = dict(zip(G, range(len(G))))
|
|
for s in ordering:
|
|
# Build the subgraph induced by s and following nodes in the ordering
|
|
subgraph = G.subgraph(node for node in G if ordering[node] >= ordering[s])
|
|
# Find the strongly connected component in the subgraph
|
|
# that contains the least node according to the ordering
|
|
strongcomp = nx.strongly_connected_components(subgraph)
|
|
mincomp = min(strongcomp, key=lambda ns: min(ordering[n] for n in ns))
|
|
component = G.subgraph(mincomp)
|
|
if len(component) > 1:
|
|
# smallest node in the component according to the ordering
|
|
startnode = min(component, key=ordering.__getitem__)
|
|
for node in component:
|
|
blocked[node] = False
|
|
B[node][:] = []
|
|
dummy = circuit(startnode, startnode, component)
|
|
return result
|
|
|
|
|
|
@nx._dispatch
|
|
def find_cycle(G, source=None, orientation=None):
|
|
"""Returns a cycle found via depth-first traversal.
|
|
|
|
The cycle is a list of edges indicating the cyclic path.
|
|
Orientation of directed edges is controlled by `orientation`.
|
|
|
|
Parameters
|
|
----------
|
|
G : graph
|
|
A directed/undirected graph/multigraph.
|
|
|
|
source : node, list of nodes
|
|
The node from which the traversal begins. If None, then a source
|
|
is chosen arbitrarily and repeatedly until all edges from each node in
|
|
the graph are searched.
|
|
|
|
orientation : None | 'original' | 'reverse' | 'ignore' (default: None)
|
|
For directed graphs and directed multigraphs, edge traversals need not
|
|
respect the original orientation of the edges.
|
|
When set to 'reverse' every edge is traversed in the reverse direction.
|
|
When set to 'ignore', every edge is treated as undirected.
|
|
When set to 'original', every edge is treated as directed.
|
|
In all three cases, the yielded edge tuples add a last entry to
|
|
indicate the direction in which that edge was traversed.
|
|
If orientation is None, the yielded edge has no direction indicated.
|
|
The direction is respected, but not reported.
|
|
|
|
Returns
|
|
-------
|
|
edges : directed edges
|
|
A list of directed edges indicating the path taken for the loop.
|
|
If no cycle is found, then an exception is raised.
|
|
For graphs, an edge is of the form `(u, v)` where `u` and `v`
|
|
are the tail and head of the edge as determined by the traversal.
|
|
For multigraphs, an edge is of the form `(u, v, key)`, where `key` is
|
|
the key of the edge. When the graph is directed, then `u` and `v`
|
|
are always in the order of the actual directed edge.
|
|
If orientation is not None then the edge tuple is extended to include
|
|
the direction of traversal ('forward' or 'reverse') on that edge.
|
|
|
|
Raises
|
|
------
|
|
NetworkXNoCycle
|
|
If no cycle was found.
|
|
|
|
Examples
|
|
--------
|
|
In this example, we construct a DAG and find, in the first call, that there
|
|
are no directed cycles, and so an exception is raised. In the second call,
|
|
we ignore edge orientations and find that there is an undirected cycle.
|
|
Note that the second call finds a directed cycle while effectively
|
|
traversing an undirected graph, and so, we found an "undirected cycle".
|
|
This means that this DAG structure does not form a directed tree (which
|
|
is also known as a polytree).
|
|
|
|
>>> G = nx.DiGraph([(0, 1), (0, 2), (1, 2)])
|
|
>>> nx.find_cycle(G, orientation="original")
|
|
Traceback (most recent call last):
|
|
...
|
|
networkx.exception.NetworkXNoCycle: No cycle found.
|
|
>>> list(nx.find_cycle(G, orientation="ignore"))
|
|
[(0, 1, 'forward'), (1, 2, 'forward'), (0, 2, 'reverse')]
|
|
|
|
See Also
|
|
--------
|
|
simple_cycles
|
|
"""
|
|
if not G.is_directed() or orientation in (None, "original"):
|
|
|
|
def tailhead(edge):
|
|
return edge[:2]
|
|
|
|
elif orientation == "reverse":
|
|
|
|
def tailhead(edge):
|
|
return edge[1], edge[0]
|
|
|
|
elif orientation == "ignore":
|
|
|
|
def tailhead(edge):
|
|
if edge[-1] == "reverse":
|
|
return edge[1], edge[0]
|
|
return edge[:2]
|
|
|
|
explored = set()
|
|
cycle = []
|
|
final_node = None
|
|
for start_node in G.nbunch_iter(source):
|
|
if start_node in explored:
|
|
# No loop is possible.
|
|
continue
|
|
|
|
edges = []
|
|
# All nodes seen in this iteration of edge_dfs
|
|
seen = {start_node}
|
|
# Nodes in active path.
|
|
active_nodes = {start_node}
|
|
previous_head = None
|
|
|
|
for edge in nx.edge_dfs(G, start_node, orientation):
|
|
# Determine if this edge is a continuation of the active path.
|
|
tail, head = tailhead(edge)
|
|
if head in explored:
|
|
# Then we've already explored it. No loop is possible.
|
|
continue
|
|
if previous_head is not None and tail != previous_head:
|
|
# This edge results from backtracking.
|
|
# Pop until we get a node whose head equals the current tail.
|
|
# So for example, we might have:
|
|
# (0, 1), (1, 2), (2, 3), (1, 4)
|
|
# which must become:
|
|
# (0, 1), (1, 4)
|
|
while True:
|
|
try:
|
|
popped_edge = edges.pop()
|
|
except IndexError:
|
|
edges = []
|
|
active_nodes = {tail}
|
|
break
|
|
else:
|
|
popped_head = tailhead(popped_edge)[1]
|
|
active_nodes.remove(popped_head)
|
|
|
|
if edges:
|
|
last_head = tailhead(edges[-1])[1]
|
|
if tail == last_head:
|
|
break
|
|
edges.append(edge)
|
|
|
|
if head in active_nodes:
|
|
# We have a loop!
|
|
cycle.extend(edges)
|
|
final_node = head
|
|
break
|
|
else:
|
|
seen.add(head)
|
|
active_nodes.add(head)
|
|
previous_head = head
|
|
|
|
if cycle:
|
|
break
|
|
else:
|
|
explored.update(seen)
|
|
|
|
else:
|
|
assert len(cycle) == 0
|
|
raise nx.exception.NetworkXNoCycle("No cycle found.")
|
|
|
|
# We now have a list of edges which ends on a cycle.
|
|
# So we need to remove from the beginning edges that are not relevant.
|
|
|
|
for i, edge in enumerate(cycle):
|
|
tail, head = tailhead(edge)
|
|
if tail == final_node:
|
|
break
|
|
|
|
return cycle[i:]
|
|
|
|
|
|
@not_implemented_for("directed")
|
|
@not_implemented_for("multigraph")
|
|
@nx._dispatch(edge_attrs="weight")
|
|
def minimum_cycle_basis(G, weight=None):
|
|
"""Returns a minimum weight cycle basis for G
|
|
|
|
Minimum weight means a cycle basis for which the total weight
|
|
(length for unweighted graphs) of all the cycles is minimum.
|
|
|
|
Parameters
|
|
----------
|
|
G : NetworkX Graph
|
|
weight: string
|
|
name of the edge attribute to use for edge weights
|
|
|
|
Returns
|
|
-------
|
|
A list of cycle lists. Each cycle list is a list of nodes
|
|
which forms a cycle (loop) in G. Note that the nodes are not
|
|
necessarily returned in a order by which they appear in the cycle
|
|
|
|
Examples
|
|
--------
|
|
>>> G = nx.Graph()
|
|
>>> nx.add_cycle(G, [0, 1, 2, 3])
|
|
>>> nx.add_cycle(G, [0, 3, 4, 5])
|
|
>>> nx.minimum_cycle_basis(G)
|
|
[[5, 4, 3, 0], [3, 2, 1, 0]]
|
|
|
|
References:
|
|
[1] Kavitha, Telikepalli, et al. "An O(m^2n) Algorithm for
|
|
Minimum Cycle Basis of Graphs."
|
|
http://link.springer.com/article/10.1007/s00453-007-9064-z
|
|
[2] de Pina, J. 1995. Applications of shortest path methods.
|
|
Ph.D. thesis, University of Amsterdam, Netherlands
|
|
|
|
See Also
|
|
--------
|
|
simple_cycles, cycle_basis
|
|
"""
|
|
# We first split the graph in connected subgraphs
|
|
return sum(
|
|
(_min_cycle_basis(G.subgraph(c), weight) for c in nx.connected_components(G)),
|
|
[],
|
|
)
|
|
|
|
|
|
def _min_cycle_basis(G, weight):
|
|
cb = []
|
|
# We extract the edges not in a spanning tree. We do not really need a
|
|
# *minimum* spanning tree. That is why we call the next function with
|
|
# weight=None. Depending on implementation, it may be faster as well
|
|
tree_edges = list(nx.minimum_spanning_edges(G, weight=None, data=False))
|
|
chords = G.edges - tree_edges - {(v, u) for u, v in tree_edges}
|
|
|
|
# We maintain a set of vectors orthogonal to sofar found cycles
|
|
set_orth = [{edge} for edge in chords]
|
|
while set_orth:
|
|
base = set_orth.pop()
|
|
# kth cycle is "parallel" to kth vector in set_orth
|
|
cycle_edges = _min_cycle(G, base, weight)
|
|
cb.append([v for u, v in cycle_edges])
|
|
|
|
# now update set_orth so that k+1,k+2... th elements are
|
|
# orthogonal to the newly found cycle, as per [p. 336, 1]
|
|
set_orth = [
|
|
(
|
|
{e for e in orth if e not in base if e[::-1] not in base}
|
|
| {e for e in base if e not in orth if e[::-1] not in orth}
|
|
)
|
|
if sum((e in orth or e[::-1] in orth) for e in cycle_edges) % 2
|
|
else orth
|
|
for orth in set_orth
|
|
]
|
|
return cb
|
|
|
|
|
|
def _min_cycle(G, orth, weight):
|
|
"""
|
|
Computes the minimum weight cycle in G,
|
|
orthogonal to the vector orth as per [p. 338, 1]
|
|
Use (u, 1) to indicate the lifted copy of u (denoted u' in paper).
|
|
"""
|
|
Gi = nx.Graph()
|
|
|
|
# Add 2 copies of each edge in G to Gi.
|
|
# If edge is in orth, add cross edge; otherwise in-plane edge
|
|
for u, v, wt in G.edges(data=weight, default=1):
|
|
if (u, v) in orth or (v, u) in orth:
|
|
Gi.add_edges_from([(u, (v, 1)), ((u, 1), v)], Gi_weight=wt)
|
|
else:
|
|
Gi.add_edges_from([(u, v), ((u, 1), (v, 1))], Gi_weight=wt)
|
|
|
|
# find the shortest length in Gi between n and (n, 1) for each n
|
|
# Note: Use "Gi_weight" for name of weight attribute
|
|
spl = nx.shortest_path_length
|
|
lift = {n: spl(Gi, source=n, target=(n, 1), weight="Gi_weight") for n in G}
|
|
|
|
# Now compute that short path in Gi, which translates to a cycle in G
|
|
start = min(lift, key=lift.get)
|
|
end = (start, 1)
|
|
min_path_i = nx.shortest_path(Gi, source=start, target=end, weight="Gi_weight")
|
|
|
|
# Now we obtain the actual path, re-map nodes in Gi to those in G
|
|
min_path = [n if n in G else n[0] for n in min_path_i]
|
|
|
|
# Now remove the edges that occur two times
|
|
# two passes: flag which edges get kept, then build it
|
|
edgelist = list(pairwise(min_path))
|
|
edgeset = set()
|
|
for e in edgelist:
|
|
if e in edgeset:
|
|
edgeset.remove(e)
|
|
elif e[::-1] in edgeset:
|
|
edgeset.remove(e[::-1])
|
|
else:
|
|
edgeset.add(e)
|
|
|
|
min_edgelist = []
|
|
for e in edgelist:
|
|
if e in edgeset:
|
|
min_edgelist.append(e)
|
|
edgeset.remove(e)
|
|
elif e[::-1] in edgeset:
|
|
min_edgelist.append(e[::-1])
|
|
edgeset.remove(e[::-1])
|
|
|
|
return min_edgelist
|
|
|
|
|
|
@not_implemented_for("directed")
|
|
@not_implemented_for("multigraph")
|
|
@nx._dispatch
|
|
def girth(G):
|
|
"""Returns the girth of the graph.
|
|
|
|
The girth of a graph is the length of its shortest cycle, or infinity if
|
|
the graph is acyclic. The algorithm follows the description given on the
|
|
Wikipedia page [1]_, and runs in time O(mn) on a graph with m edges and n
|
|
nodes.
|
|
|
|
Parameters
|
|
----------
|
|
G : NetworkX Graph
|
|
|
|
Returns
|
|
-------
|
|
int or math.inf
|
|
|
|
Examples
|
|
--------
|
|
All examples below (except P_5) can easily be checked using Wikipedia,
|
|
which has a page for each of these famous graphs.
|
|
|
|
>>> nx.girth(nx.chvatal_graph())
|
|
4
|
|
>>> nx.girth(nx.tutte_graph())
|
|
4
|
|
>>> nx.girth(nx.petersen_graph())
|
|
5
|
|
>>> nx.girth(nx.heawood_graph())
|
|
6
|
|
>>> nx.girth(nx.pappus_graph())
|
|
6
|
|
>>> nx.girth(nx.path_graph(5))
|
|
inf
|
|
|
|
References
|
|
----------
|
|
.. [1] https://en.wikipedia.org/wiki/Girth_(graph_theory)
|
|
|
|
"""
|
|
girth = depth_limit = inf
|
|
tree_edge = nx.algorithms.traversal.breadth_first_search.TREE_EDGE
|
|
level_edge = nx.algorithms.traversal.breadth_first_search.LEVEL_EDGE
|
|
for n in G:
|
|
# run a BFS from source n, keeping track of distances; since we want
|
|
# the shortest cycle, no need to explore beyond the current minimum length
|
|
depth = {n: 0}
|
|
for u, v, label in nx.bfs_labeled_edges(G, n):
|
|
du = depth[u]
|
|
if du > depth_limit:
|
|
break
|
|
if label is tree_edge:
|
|
depth[v] = du + 1
|
|
else:
|
|
# if (u, v) is a level edge, the length is du + du + 1 (odd)
|
|
# otherwise, it's a forward edge; length is du + (du + 1) + 1 (even)
|
|
delta = label is level_edge
|
|
length = du + du + 2 - delta
|
|
if length < girth:
|
|
girth = length
|
|
depth_limit = du - delta
|
|
|
|
return girth
|