Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/networkx/algorithms/cycles.py: 9%
432 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-10-20 07:00 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-10-20 07:00 +0000
1"""
2========================
3Cycle finding algorithms
4========================
5"""
7from collections import Counter, defaultdict
8from itertools import combinations, product
9from math import inf
11import networkx as nx
12from networkx.utils import not_implemented_for, pairwise
14__all__ = [
15 "cycle_basis",
16 "simple_cycles",
17 "recursive_simple_cycles",
18 "find_cycle",
19 "minimum_cycle_basis",
20 "chordless_cycles",
21 "girth",
22]
25@not_implemented_for("directed")
26@not_implemented_for("multigraph")
27@nx._dispatch
28def cycle_basis(G, root=None):
29 """Returns a list of cycles which form a basis for cycles of G.
31 A basis for cycles of a network is a minimal collection of
32 cycles such that any cycle in the network can be written
33 as a sum of cycles in the basis. Here summation of cycles
34 is defined as "exclusive or" of the edges. Cycle bases are
35 useful, e.g. when deriving equations for electric circuits
36 using Kirchhoff's Laws.
38 Parameters
39 ----------
40 G : NetworkX Graph
41 root : node, optional
42 Specify starting node for basis.
44 Returns
45 -------
46 A list of cycle lists. Each cycle list is a list of nodes
47 which forms a cycle (loop) in G.
49 Examples
50 --------
51 >>> G = nx.Graph()
52 >>> nx.add_cycle(G, [0, 1, 2, 3])
53 >>> nx.add_cycle(G, [0, 3, 4, 5])
54 >>> nx.cycle_basis(G, 0)
55 [[3, 4, 5, 0], [1, 2, 3, 0]]
57 Notes
58 -----
59 This is adapted from algorithm CACM 491 [1]_.
61 References
62 ----------
63 .. [1] Paton, K. An algorithm for finding a fundamental set of
64 cycles of a graph. Comm. ACM 12, 9 (Sept 1969), 514-518.
66 See Also
67 --------
68 simple_cycles
69 """
70 gnodes = dict.fromkeys(G) # set-like object that maintains node order
71 cycles = []
72 while gnodes: # loop over connected components
73 if root is None:
74 root = gnodes.popitem()[0]
75 stack = [root]
76 pred = {root: root}
77 used = {root: set()}
78 while stack: # walk the spanning tree finding cycles
79 z = stack.pop() # use last-in so cycles easier to find
80 zused = used[z]
81 for nbr in G[z]:
82 if nbr not in used: # new node
83 pred[nbr] = z
84 stack.append(nbr)
85 used[nbr] = {z}
86 elif nbr == z: # self loops
87 cycles.append([z])
88 elif nbr not in zused: # found a cycle
89 pn = used[nbr]
90 cycle = [nbr, z]
91 p = pred[z]
92 while p not in pn:
93 cycle.append(p)
94 p = pred[p]
95 cycle.append(p)
96 cycles.append(cycle)
97 used[nbr].add(z)
98 for node in pred:
99 gnodes.pop(node, None)
100 root = None
101 return cycles
104@nx._dispatch
105def simple_cycles(G, length_bound=None):
106 """Find simple cycles (elementary circuits) of a graph.
108 A `simple cycle`, or `elementary circuit`, is a closed path where
109 no node appears twice. In a directed graph, two simple cycles are distinct
110 if they are not cyclic permutations of each other. In an undirected graph,
111 two simple cycles are distinct if they are not cyclic permutations of each
112 other nor of the other's reversal.
114 Optionally, the cycles are bounded in length. In the unbounded case, we use
115 a nonrecursive, iterator/generator version of Johnson's algorithm [1]_. In
116 the bounded case, we use a version of the algorithm of Gupta and
117 Suzumura[2]_. There may be better algorithms for some cases [3]_ [4]_ [5]_.
119 The algorithms of Johnson, and Gupta and Suzumura, are enhanced by some
120 well-known preprocessing techniques. When G is directed, we restrict our
121 attention to strongly connected components of G, generate all simple cycles
122 containing a certain node, remove that node, and further decompose the
123 remainder into strongly connected components. When G is undirected, we
124 restrict our attention to biconnected components, generate all simple cycles
125 containing a particular edge, remove that edge, and further decompose the
126 remainder into biconnected components.
128 Note that multigraphs are supported by this function -- and in undirected
129 multigraphs, a pair of parallel edges is considered a cycle of length 2.
130 Likewise, self-loops are considered to be cycles of length 1. We define
131 cycles as sequences of nodes; so the presence of loops and parallel edges
132 does not change the number of simple cycles in a graph.
134 Parameters
135 ----------
136 G : NetworkX DiGraph
137 A directed graph
139 length_bound : int or None, optional (default=None)
140 If length_bound is an int, generate all simple cycles of G with length at
141 most length_bound. Otherwise, generate all simple cycles of G.
143 Yields
144 ------
145 list of nodes
146 Each cycle is represented by a list of nodes along the cycle.
148 Examples
149 --------
150 >>> edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)]
151 >>> G = nx.DiGraph(edges)
152 >>> sorted(nx.simple_cycles(G))
153 [[0], [0, 1, 2], [0, 2], [1, 2], [2]]
155 To filter the cycles so that they don't include certain nodes or edges,
156 copy your graph and eliminate those nodes or edges before calling.
157 For example, to exclude self-loops from the above example:
159 >>> H = G.copy()
160 >>> H.remove_edges_from(nx.selfloop_edges(G))
161 >>> sorted(nx.simple_cycles(H))
162 [[0, 1, 2], [0, 2], [1, 2]]
164 Notes
165 -----
166 When length_bound is None, the time complexity is $O((n+e)(c+1))$ for $n$
167 nodes, $e$ edges and $c$ simple circuits. Otherwise, when length_bound > 1,
168 the time complexity is $O((c+n)(k-1)d^k)$ where $d$ is the average degree of
169 the nodes of G and $k$ = length_bound.
171 Raises
172 ------
173 ValueError
174 when length_bound < 0.
176 References
177 ----------
178 .. [1] Finding all the elementary circuits of a directed graph.
179 D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975.
180 https://doi.org/10.1137/0204007
181 .. [2] Finding All Bounded-Length Simple Cycles in a Directed Graph
182 A. Gupta and T. Suzumura https://arxiv.org/abs/2105.10094
183 .. [3] Enumerating the cycles of a digraph: a new preprocessing strategy.
184 G. Loizou and P. Thanish, Information Sciences, v. 27, 163-182, 1982.
185 .. [4] A search strategy for the elementary cycles of a directed graph.
186 J.L. Szwarcfiter and P.E. Lauer, BIT NUMERICAL MATHEMATICS,
187 v. 16, no. 2, 192-204, 1976.
188 .. [5] Optimal Listing of Cycles and st-Paths in Undirected Graphs
189 R. Ferreira and R. Grossi and A. Marino and N. Pisanti and R. Rizzi and
190 G. Sacomoto https://arxiv.org/abs/1205.2766
192 See Also
193 --------
194 cycle_basis
195 chordless_cycles
196 """
198 if length_bound is not None:
199 if length_bound == 0:
200 return
201 elif length_bound < 0:
202 raise ValueError("length bound must be non-negative")
204 directed = G.is_directed()
205 yield from ([v] for v, Gv in G.adj.items() if v in Gv)
207 if length_bound is not None and length_bound == 1:
208 return
210 if G.is_multigraph() and not directed:
211 visited = set()
212 for u, Gu in G.adj.items():
213 multiplicity = ((v, len(Guv)) for v, Guv in Gu.items() if v in visited)
214 yield from ([u, v] for v, m in multiplicity if m > 1)
215 visited.add(u)
217 # explicitly filter out loops; implicitly filter out parallel edges
218 if directed:
219 G = nx.DiGraph((u, v) for u, Gu in G.adj.items() for v in Gu if v != u)
220 else:
221 G = nx.Graph((u, v) for u, Gu in G.adj.items() for v in Gu if v != u)
223 # this case is not strictly necessary but improves performance
224 if length_bound is not None and length_bound == 2:
225 if directed:
226 visited = set()
227 for u, Gu in G.adj.items():
228 yield from (
229 [v, u] for v in visited.intersection(Gu) if G.has_edge(v, u)
230 )
231 visited.add(u)
232 return
234 if directed:
235 yield from _directed_cycle_search(G, length_bound)
236 else:
237 yield from _undirected_cycle_search(G, length_bound)
240def _directed_cycle_search(G, length_bound):
241 """A dispatch function for `simple_cycles` for directed graphs.
243 We generate all cycles of G through binary partition.
245 1. Pick a node v in G which belongs to at least one cycle
246 a. Generate all cycles of G which contain the node v.
247 b. Recursively generate all cycles of G \\ v.
249 This is accomplished through the following:
251 1. Compute the strongly connected components SCC of G.
252 2. Select and remove a biconnected component C from BCC. Select a
253 non-tree edge (u, v) of a depth-first search of G[C].
254 3. For each simple cycle P containing v in G[C], yield P.
255 4. Add the biconnected components of G[C \\ v] to BCC.
257 If the parameter length_bound is not None, then step 3 will be limited to
258 simple cycles of length at most length_bound.
260 Parameters
261 ----------
262 G : NetworkX DiGraph
263 A directed graph
265 length_bound : int or None
266 If length_bound is an int, generate all simple cycles of G with length at most length_bound.
267 Otherwise, generate all simple cycles of G.
269 Yields
270 ------
271 list of nodes
272 Each cycle is represented by a list of nodes along the cycle.
273 """
275 scc = nx.strongly_connected_components
276 components = [c for c in scc(G) if len(c) >= 2]
277 while components:
278 c = components.pop()
279 Gc = G.subgraph(c)
280 v = next(iter(c))
281 if length_bound is None:
282 yield from _johnson_cycle_search(Gc, [v])
283 else:
284 yield from _bounded_cycle_search(Gc, [v], length_bound)
285 # delete v after searching G, to make sure we can find v
286 G.remove_node(v)
287 components.extend(c for c in scc(Gc) if len(c) >= 2)
290def _undirected_cycle_search(G, length_bound):
291 """A dispatch function for `simple_cycles` for undirected graphs.
293 We generate all cycles of G through binary partition.
295 1. Pick an edge (u, v) in G which belongs to at least one cycle
296 a. Generate all cycles of G which contain the edge (u, v)
297 b. Recursively generate all cycles of G \\ (u, v)
299 This is accomplished through the following:
301 1. Compute the biconnected components BCC of G.
302 2. Select and remove a biconnected component C from BCC. Select a
303 non-tree edge (u, v) of a depth-first search of G[C].
304 3. For each (v -> u) path P remaining in G[C] \\ (u, v), yield P.
305 4. Add the biconnected components of G[C] \\ (u, v) to BCC.
307 If the parameter length_bound is not None, then step 3 will be limited to simple paths
308 of length at most length_bound.
310 Parameters
311 ----------
312 G : NetworkX Graph
313 An undirected graph
315 length_bound : int or None
316 If length_bound is an int, generate all simple cycles of G with length at most length_bound.
317 Otherwise, generate all simple cycles of G.
319 Yields
320 ------
321 list of nodes
322 Each cycle is represented by a list of nodes along the cycle.
323 """
325 bcc = nx.biconnected_components
326 components = [c for c in bcc(G) if len(c) >= 3]
327 while components:
328 c = components.pop()
329 Gc = G.subgraph(c)
330 uv = list(next(iter(Gc.edges)))
331 G.remove_edge(*uv)
332 # delete (u, v) before searching G, to avoid fake 3-cycles [u, v, u]
333 if length_bound is None:
334 yield from _johnson_cycle_search(Gc, uv)
335 else:
336 yield from _bounded_cycle_search(Gc, uv, length_bound)
337 components.extend(c for c in bcc(Gc) if len(c) >= 3)
340class _NeighborhoodCache(dict):
341 """Very lightweight graph wrapper which caches neighborhoods as list.
343 This dict subclass uses the __missing__ functionality to query graphs for
344 their neighborhoods, and store the result as a list. This is used to avoid
345 the performance penalty incurred by subgraph views.
346 """
348 def __init__(self, G):
349 self.G = G
351 def __missing__(self, v):
352 Gv = self[v] = list(self.G[v])
353 return Gv
356def _johnson_cycle_search(G, path):
357 """The main loop of the cycle-enumeration algorithm of Johnson.
359 Parameters
360 ----------
361 G : NetworkX Graph or DiGraph
362 A graph
364 path : list
365 A cycle prefix. All cycles generated will begin with this prefix.
367 Yields
368 ------
369 list of nodes
370 Each cycle is represented by a list of nodes along the cycle.
372 References
373 ----------
374 .. [1] Finding all the elementary circuits of a directed graph.
375 D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975.
376 https://doi.org/10.1137/0204007
378 """
380 G = _NeighborhoodCache(G)
381 blocked = set(path)
382 B = defaultdict(set) # graph portions that yield no elementary circuit
383 start = path[0]
384 stack = [iter(G[path[-1]])]
385 closed = [False]
386 while stack:
387 nbrs = stack[-1]
388 for w in nbrs:
389 if w == start:
390 yield path[:]
391 closed[-1] = True
392 elif w not in blocked:
393 path.append(w)
394 closed.append(False)
395 stack.append(iter(G[w]))
396 blocked.add(w)
397 break
398 else: # no more nbrs
399 stack.pop()
400 v = path.pop()
401 if closed.pop():
402 if closed:
403 closed[-1] = True
404 unblock_stack = {v}
405 while unblock_stack:
406 u = unblock_stack.pop()
407 if u in blocked:
408 blocked.remove(u)
409 unblock_stack.update(B[u])
410 B[u].clear()
411 else:
412 for w in G[v]:
413 B[w].add(v)
416def _bounded_cycle_search(G, path, length_bound):
417 """The main loop of the cycle-enumeration algorithm of Gupta and Suzumura.
419 Parameters
420 ----------
421 G : NetworkX Graph or DiGraph
422 A graph
424 path : list
425 A cycle prefix. All cycles generated will begin with this prefix.
427 length_bound: int
428 A length bound. All cycles generated will have length at most length_bound.
430 Yields
431 ------
432 list of nodes
433 Each cycle is represented by a list of nodes along the cycle.
435 References
436 ----------
437 .. [1] Finding All Bounded-Length Simple Cycles in a Directed Graph
438 A. Gupta and T. Suzumura https://arxiv.org/abs/2105.10094
440 """
441 G = _NeighborhoodCache(G)
442 lock = {v: 0 for v in path}
443 B = defaultdict(set)
444 start = path[0]
445 stack = [iter(G[path[-1]])]
446 blen = [length_bound]
447 while stack:
448 nbrs = stack[-1]
449 for w in nbrs:
450 if w == start:
451 yield path[:]
452 blen[-1] = 1
453 elif len(path) < lock.get(w, length_bound):
454 path.append(w)
455 blen.append(length_bound)
456 lock[w] = len(path)
457 stack.append(iter(G[w]))
458 break
459 else:
460 stack.pop()
461 v = path.pop()
462 bl = blen.pop()
463 if blen:
464 blen[-1] = min(blen[-1], bl)
465 if bl < length_bound:
466 relax_stack = [(bl, v)]
467 while relax_stack:
468 bl, u = relax_stack.pop()
469 if lock.get(u, length_bound) < length_bound - bl + 1:
470 lock[u] = length_bound - bl + 1
471 relax_stack.extend((bl + 1, w) for w in B[u].difference(path))
472 else:
473 for w in G[v]:
474 B[w].add(v)
477@nx._dispatch
478def chordless_cycles(G, length_bound=None):
479 """Find simple chordless cycles of a graph.
481 A `simple cycle` is a closed path where no node appears twice. In a simple
482 cycle, a `chord` is an additional edge between two nodes in the cycle. A
483 `chordless cycle` is a simple cycle without chords. Said differently, a
484 chordless cycle is a cycle C in a graph G where the number of edges in the
485 induced graph G[C] is equal to the length of `C`.
487 Note that some care must be taken in the case that G is not a simple graph
488 nor a simple digraph. Some authors limit the definition of chordless cycles
489 to have a prescribed minimum length; we do not.
491 1. We interpret self-loops to be chordless cycles, except in multigraphs
492 with multiple loops in parallel. Likewise, in a chordless cycle of
493 length greater than 1, there can be no nodes with self-loops.
495 2. We interpret directed two-cycles to be chordless cycles, except in
496 multi-digraphs when any edge in a two-cycle has a parallel copy.
498 3. We interpret parallel pairs of undirected edges as two-cycles, except
499 when a third (or more) parallel edge exists between the two nodes.
501 4. Generalizing the above, edges with parallel clones may not occur in
502 chordless cycles.
504 In a directed graph, two chordless cycles are distinct if they are not
505 cyclic permutations of each other. In an undirected graph, two chordless
506 cycles are distinct if they are not cyclic permutations of each other nor of
507 the other's reversal.
509 Optionally, the cycles are bounded in length.
511 We use an algorithm strongly inspired by that of Dias et al [1]_. It has
512 been modified in the following ways:
514 1. Recursion is avoided, per Python's limitations
516 2. The labeling function is not necessary, because the starting paths
517 are chosen (and deleted from the host graph) to prevent multiple
518 occurrences of the same path
520 3. The search is optionally bounded at a specified length
522 4. Support for directed graphs is provided by extending cycles along
523 forward edges, and blocking nodes along forward and reverse edges
525 5. Support for multigraphs is provided by omitting digons from the set
526 of forward edges
528 Parameters
529 ----------
530 G : NetworkX DiGraph
531 A directed graph
533 length_bound : int or None, optional (default=None)
534 If length_bound is an int, generate all simple cycles of G with length at
535 most length_bound. Otherwise, generate all simple cycles of G.
537 Yields
538 ------
539 list of nodes
540 Each cycle is represented by a list of nodes along the cycle.
542 Examples
543 --------
544 >>> sorted(list(nx.chordless_cycles(nx.complete_graph(4))))
545 [[1, 0, 2], [1, 0, 3], [2, 0, 3], [2, 1, 3]]
547 Notes
548 -----
549 When length_bound is None, and the graph is simple, the time complexity is
550 $O((n+e)(c+1))$ for $n$ nodes, $e$ edges and $c$ chordless cycles.
552 Raises
553 ------
554 ValueError
555 when length_bound < 0.
557 References
558 ----------
559 .. [1] Efficient enumeration of chordless cycles
560 E. Dias and D. Castonguay and H. Longo and W.A.R. Jradi
561 https://arxiv.org/abs/1309.1051
563 See Also
564 --------
565 simple_cycles
566 """
568 if length_bound is not None:
569 if length_bound == 0:
570 return
571 elif length_bound < 0:
572 raise ValueError("length bound must be non-negative")
574 directed = G.is_directed()
575 multigraph = G.is_multigraph()
577 if multigraph:
578 yield from ([v] for v, Gv in G.adj.items() if len(Gv.get(v, ())) == 1)
579 else:
580 yield from ([v] for v, Gv in G.adj.items() if v in Gv)
582 if length_bound is not None and length_bound == 1:
583 return
585 # Nodes with loops cannot belong to longer cycles. Let's delete them here.
586 # also, we implicitly reduce the multiplicity of edges down to 1 in the case
587 # of multiedges.
588 if directed:
589 F = nx.DiGraph((u, v) for u, Gu in G.adj.items() if u not in Gu for v in Gu)
590 B = F.to_undirected(as_view=False)
591 else:
592 F = nx.Graph((u, v) for u, Gu in G.adj.items() if u not in Gu for v in Gu)
593 B = None
595 # If we're given a multigraph, we have a few cases to consider with parallel
596 # edges.
597 #
598 # 1. If we have 2 or more edges in parallel between the nodes (u, v), we
599 # must not construct longer cycles along (u, v).
600 # 2. If G is not directed, then a pair of parallel edges between (u, v) is a
601 # chordless cycle unless there exists a third (or more) parallel edge.
602 # 3. If G is directed, then parallel edges do not form cycles, but do
603 # preclude back-edges from forming cycles (handled in the next section),
604 # Thus, if an edge (u, v) is duplicated and the reverse (v, u) is also
605 # present, then we remove both from F.
606 #
607 # In directed graphs, we need to consider both directions that edges can
608 # take, so iterate over all edges (u, v) and possibly (v, u). In undirected
609 # graphs, we need to be a little careful to only consider every edge once,
610 # so we use a "visited" set to emulate node-order comparisons.
612 if multigraph:
613 if not directed:
614 B = F.copy()
615 visited = set()
616 for u, Gu in G.adj.items():
617 if directed:
618 multiplicity = ((v, len(Guv)) for v, Guv in Gu.items())
619 for v, m in multiplicity:
620 if m > 1:
621 F.remove_edges_from(((u, v), (v, u)))
622 else:
623 multiplicity = ((v, len(Guv)) for v, Guv in Gu.items() if v in visited)
624 for v, m in multiplicity:
625 if m == 2:
626 yield [u, v]
627 if m > 1:
628 F.remove_edge(u, v)
629 visited.add(u)
631 # If we're given a directed graphs, we need to think about digons. If we
632 # have two edges (u, v) and (v, u), then that's a two-cycle. If either edge
633 # was duplicated above, then we removed both from F. So, any digons we find
634 # here are chordless. After finding digons, we remove their edges from F
635 # to avoid traversing them in the search for chordless cycles.
636 if directed:
637 for u, Fu in F.adj.items():
638 digons = [[u, v] for v in Fu if F.has_edge(v, u)]
639 yield from digons
640 F.remove_edges_from(digons)
641 F.remove_edges_from(e[::-1] for e in digons)
643 if length_bound is not None and length_bound == 2:
644 return
646 # Now, we prepare to search for cycles. We have removed all cycles of
647 # lengths 1 and 2, so F is a simple graph or simple digraph. We repeatedly
648 # separate digraphs into their strongly connected components, and undirected
649 # graphs into their biconnected components. For each component, we pick a
650 # node v, search for chordless cycles based at each "stem" (u, v, w), and
651 # then remove v from that component before separating the graph again.
652 if directed:
653 separate = nx.strongly_connected_components
655 # Directed stems look like (u -> v -> w), so we use the product of
656 # predecessors of v with successors of v.
657 def stems(C, v):
658 for u, w in product(C.pred[v], C.succ[v]):
659 if not G.has_edge(u, w): # omit stems with acyclic chords
660 yield [u, v, w], F.has_edge(w, u)
662 else:
663 separate = nx.biconnected_components
665 # Undirected stems look like (u ~ v ~ w), but we must not also search
666 # (w ~ v ~ u), so we use combinations of v's neighbors of length 2.
667 def stems(C, v):
668 yield from (([u, v, w], F.has_edge(w, u)) for u, w in combinations(C[v], 2))
670 components = [c for c in separate(F) if len(c) > 2]
671 while components:
672 c = components.pop()
673 v = next(iter(c))
674 Fc = F.subgraph(c)
675 Fcc = Bcc = None
676 for S, is_triangle in stems(Fc, v):
677 if is_triangle:
678 yield S
679 else:
680 if Fcc is None:
681 Fcc = _NeighborhoodCache(Fc)
682 Bcc = Fcc if B is None else _NeighborhoodCache(B.subgraph(c))
683 yield from _chordless_cycle_search(Fcc, Bcc, S, length_bound)
685 components.extend(c for c in separate(F.subgraph(c - {v})) if len(c) > 2)
688def _chordless_cycle_search(F, B, path, length_bound):
689 """The main loop for chordless cycle enumeration.
691 This algorithm is strongly inspired by that of Dias et al [1]_. It has been
692 modified in the following ways:
694 1. Recursion is avoided, per Python's limitations
696 2. The labeling function is not necessary, because the starting paths
697 are chosen (and deleted from the host graph) to prevent multiple
698 occurrences of the same path
700 3. The search is optionally bounded at a specified length
702 4. Support for directed graphs is provided by extending cycles along
703 forward edges, and blocking nodes along forward and reverse edges
705 5. Support for multigraphs is provided by omitting digons from the set
706 of forward edges
708 Parameters
709 ----------
710 F : _NeighborhoodCache
711 A graph of forward edges to follow in constructing cycles
713 B : _NeighborhoodCache
714 A graph of blocking edges to prevent the production of chordless cycles
716 path : list
717 A cycle prefix. All cycles generated will begin with this prefix.
719 length_bound : int
720 A length bound. All cycles generated will have length at most length_bound.
723 Yields
724 ------
725 list of nodes
726 Each cycle is represented by a list of nodes along the cycle.
728 References
729 ----------
730 .. [1] Efficient enumeration of chordless cycles
731 E. Dias and D. Castonguay and H. Longo and W.A.R. Jradi
732 https://arxiv.org/abs/1309.1051
734 """
735 blocked = defaultdict(int)
736 target = path[0]
737 blocked[path[1]] = 1
738 for w in path[1:]:
739 for v in B[w]:
740 blocked[v] += 1
742 stack = [iter(F[path[2]])]
743 while stack:
744 nbrs = stack[-1]
745 for w in nbrs:
746 if blocked[w] == 1 and (length_bound is None or len(path) < length_bound):
747 Fw = F[w]
748 if target in Fw:
749 yield path + [w]
750 else:
751 Bw = B[w]
752 if target in Bw:
753 continue
754 for v in Bw:
755 blocked[v] += 1
756 path.append(w)
757 stack.append(iter(Fw))
758 break
759 else:
760 stack.pop()
761 for v in B[path.pop()]:
762 blocked[v] -= 1
765@not_implemented_for("undirected")
766@nx._dispatch
767def recursive_simple_cycles(G):
768 """Find simple cycles (elementary circuits) of a directed graph.
770 A `simple cycle`, or `elementary circuit`, is a closed path where
771 no node appears twice. Two elementary circuits are distinct if they
772 are not cyclic permutations of each other.
774 This version uses a recursive algorithm to build a list of cycles.
775 You should probably use the iterator version called simple_cycles().
776 Warning: This recursive version uses lots of RAM!
777 It appears in NetworkX for pedagogical value.
779 Parameters
780 ----------
781 G : NetworkX DiGraph
782 A directed graph
784 Returns
785 -------
786 A list of cycles, where each cycle is represented by a list of nodes
787 along the cycle.
789 Example:
791 >>> edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)]
792 >>> G = nx.DiGraph(edges)
793 >>> nx.recursive_simple_cycles(G)
794 [[0], [2], [0, 1, 2], [0, 2], [1, 2]]
796 Notes
797 -----
798 The implementation follows pp. 79-80 in [1]_.
800 The time complexity is $O((n+e)(c+1))$ for $n$ nodes, $e$ edges and $c$
801 elementary circuits.
803 References
804 ----------
805 .. [1] Finding all the elementary circuits of a directed graph.
806 D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975.
807 https://doi.org/10.1137/0204007
809 See Also
810 --------
811 simple_cycles, cycle_basis
812 """
814 # Jon Olav Vik, 2010-08-09
815 def _unblock(thisnode):
816 """Recursively unblock and remove nodes from B[thisnode]."""
817 if blocked[thisnode]:
818 blocked[thisnode] = False
819 while B[thisnode]:
820 _unblock(B[thisnode].pop())
822 def circuit(thisnode, startnode, component):
823 closed = False # set to True if elementary path is closed
824 path.append(thisnode)
825 blocked[thisnode] = True
826 for nextnode in component[thisnode]: # direct successors of thisnode
827 if nextnode == startnode:
828 result.append(path[:])
829 closed = True
830 elif not blocked[nextnode]:
831 if circuit(nextnode, startnode, component):
832 closed = True
833 if closed:
834 _unblock(thisnode)
835 else:
836 for nextnode in component[thisnode]:
837 if thisnode not in B[nextnode]: # TODO: use set for speedup?
838 B[nextnode].append(thisnode)
839 path.pop() # remove thisnode from path
840 return closed
842 path = [] # stack of nodes in current path
843 blocked = defaultdict(bool) # vertex: blocked from search?
844 B = defaultdict(list) # graph portions that yield no elementary circuit
845 result = [] # list to accumulate the circuits found
847 # Johnson's algorithm exclude self cycle edges like (v, v)
848 # To be backward compatible, we record those cycles in advance
849 # and then remove from subG
850 for v in G:
851 if G.has_edge(v, v):
852 result.append([v])
853 G.remove_edge(v, v)
855 # Johnson's algorithm requires some ordering of the nodes.
856 # They might not be sortable so we assign an arbitrary ordering.
857 ordering = dict(zip(G, range(len(G))))
858 for s in ordering:
859 # Build the subgraph induced by s and following nodes in the ordering
860 subgraph = G.subgraph(node for node in G if ordering[node] >= ordering[s])
861 # Find the strongly connected component in the subgraph
862 # that contains the least node according to the ordering
863 strongcomp = nx.strongly_connected_components(subgraph)
864 mincomp = min(strongcomp, key=lambda ns: min(ordering[n] for n in ns))
865 component = G.subgraph(mincomp)
866 if len(component) > 1:
867 # smallest node in the component according to the ordering
868 startnode = min(component, key=ordering.__getitem__)
869 for node in component:
870 blocked[node] = False
871 B[node][:] = []
872 dummy = circuit(startnode, startnode, component)
873 return result
876@nx._dispatch
877def find_cycle(G, source=None, orientation=None):
878 """Returns a cycle found via depth-first traversal.
880 The cycle is a list of edges indicating the cyclic path.
881 Orientation of directed edges is controlled by `orientation`.
883 Parameters
884 ----------
885 G : graph
886 A directed/undirected graph/multigraph.
888 source : node, list of nodes
889 The node from which the traversal begins. If None, then a source
890 is chosen arbitrarily and repeatedly until all edges from each node in
891 the graph are searched.
893 orientation : None | 'original' | 'reverse' | 'ignore' (default: None)
894 For directed graphs and directed multigraphs, edge traversals need not
895 respect the original orientation of the edges.
896 When set to 'reverse' every edge is traversed in the reverse direction.
897 When set to 'ignore', every edge is treated as undirected.
898 When set to 'original', every edge is treated as directed.
899 In all three cases, the yielded edge tuples add a last entry to
900 indicate the direction in which that edge was traversed.
901 If orientation is None, the yielded edge has no direction indicated.
902 The direction is respected, but not reported.
904 Returns
905 -------
906 edges : directed edges
907 A list of directed edges indicating the path taken for the loop.
908 If no cycle is found, then an exception is raised.
909 For graphs, an edge is of the form `(u, v)` where `u` and `v`
910 are the tail and head of the edge as determined by the traversal.
911 For multigraphs, an edge is of the form `(u, v, key)`, where `key` is
912 the key of the edge. When the graph is directed, then `u` and `v`
913 are always in the order of the actual directed edge.
914 If orientation is not None then the edge tuple is extended to include
915 the direction of traversal ('forward' or 'reverse') on that edge.
917 Raises
918 ------
919 NetworkXNoCycle
920 If no cycle was found.
922 Examples
923 --------
924 In this example, we construct a DAG and find, in the first call, that there
925 are no directed cycles, and so an exception is raised. In the second call,
926 we ignore edge orientations and find that there is an undirected cycle.
927 Note that the second call finds a directed cycle while effectively
928 traversing an undirected graph, and so, we found an "undirected cycle".
929 This means that this DAG structure does not form a directed tree (which
930 is also known as a polytree).
932 >>> G = nx.DiGraph([(0, 1), (0, 2), (1, 2)])
933 >>> nx.find_cycle(G, orientation="original")
934 Traceback (most recent call last):
935 ...
936 networkx.exception.NetworkXNoCycle: No cycle found.
937 >>> list(nx.find_cycle(G, orientation="ignore"))
938 [(0, 1, 'forward'), (1, 2, 'forward'), (0, 2, 'reverse')]
940 See Also
941 --------
942 simple_cycles
943 """
944 if not G.is_directed() or orientation in (None, "original"):
946 def tailhead(edge):
947 return edge[:2]
949 elif orientation == "reverse":
951 def tailhead(edge):
952 return edge[1], edge[0]
954 elif orientation == "ignore":
956 def tailhead(edge):
957 if edge[-1] == "reverse":
958 return edge[1], edge[0]
959 return edge[:2]
961 explored = set()
962 cycle = []
963 final_node = None
964 for start_node in G.nbunch_iter(source):
965 if start_node in explored:
966 # No loop is possible.
967 continue
969 edges = []
970 # All nodes seen in this iteration of edge_dfs
971 seen = {start_node}
972 # Nodes in active path.
973 active_nodes = {start_node}
974 previous_head = None
976 for edge in nx.edge_dfs(G, start_node, orientation):
977 # Determine if this edge is a continuation of the active path.
978 tail, head = tailhead(edge)
979 if head in explored:
980 # Then we've already explored it. No loop is possible.
981 continue
982 if previous_head is not None and tail != previous_head:
983 # This edge results from backtracking.
984 # Pop until we get a node whose head equals the current tail.
985 # So for example, we might have:
986 # (0, 1), (1, 2), (2, 3), (1, 4)
987 # which must become:
988 # (0, 1), (1, 4)
989 while True:
990 try:
991 popped_edge = edges.pop()
992 except IndexError:
993 edges = []
994 active_nodes = {tail}
995 break
996 else:
997 popped_head = tailhead(popped_edge)[1]
998 active_nodes.remove(popped_head)
1000 if edges:
1001 last_head = tailhead(edges[-1])[1]
1002 if tail == last_head:
1003 break
1004 edges.append(edge)
1006 if head in active_nodes:
1007 # We have a loop!
1008 cycle.extend(edges)
1009 final_node = head
1010 break
1011 else:
1012 seen.add(head)
1013 active_nodes.add(head)
1014 previous_head = head
1016 if cycle:
1017 break
1018 else:
1019 explored.update(seen)
1021 else:
1022 assert len(cycle) == 0
1023 raise nx.exception.NetworkXNoCycle("No cycle found.")
1025 # We now have a list of edges which ends on a cycle.
1026 # So we need to remove from the beginning edges that are not relevant.
1028 for i, edge in enumerate(cycle):
1029 tail, head = tailhead(edge)
1030 if tail == final_node:
1031 break
1033 return cycle[i:]
1036@not_implemented_for("directed")
1037@not_implemented_for("multigraph")
1038@nx._dispatch(edge_attrs="weight")
1039def minimum_cycle_basis(G, weight=None):
1040 """Returns a minimum weight cycle basis for G
1042 Minimum weight means a cycle basis for which the total weight
1043 (length for unweighted graphs) of all the cycles is minimum.
1045 Parameters
1046 ----------
1047 G : NetworkX Graph
1048 weight: string
1049 name of the edge attribute to use for edge weights
1051 Returns
1052 -------
1053 A list of cycle lists. Each cycle list is a list of nodes
1054 which forms a cycle (loop) in G. Note that the nodes are not
1055 necessarily returned in a order by which they appear in the cycle
1057 Examples
1058 --------
1059 >>> G = nx.Graph()
1060 >>> nx.add_cycle(G, [0, 1, 2, 3])
1061 >>> nx.add_cycle(G, [0, 3, 4, 5])
1062 >>> nx.minimum_cycle_basis(G)
1063 [[5, 4, 3, 0], [3, 2, 1, 0]]
1065 References:
1066 [1] Kavitha, Telikepalli, et al. "An O(m^2n) Algorithm for
1067 Minimum Cycle Basis of Graphs."
1068 http://link.springer.com/article/10.1007/s00453-007-9064-z
1069 [2] de Pina, J. 1995. Applications of shortest path methods.
1070 Ph.D. thesis, University of Amsterdam, Netherlands
1072 See Also
1073 --------
1074 simple_cycles, cycle_basis
1075 """
1076 # We first split the graph in connected subgraphs
1077 return sum(
1078 (_min_cycle_basis(G.subgraph(c), weight) for c in nx.connected_components(G)),
1079 [],
1080 )
1083def _min_cycle_basis(G, weight):
1084 cb = []
1085 # We extract the edges not in a spanning tree. We do not really need a
1086 # *minimum* spanning tree. That is why we call the next function with
1087 # weight=None. Depending on implementation, it may be faster as well
1088 tree_edges = list(nx.minimum_spanning_edges(G, weight=None, data=False))
1089 chords = G.edges - tree_edges - {(v, u) for u, v in tree_edges}
1091 # We maintain a set of vectors orthogonal to sofar found cycles
1092 set_orth = [{edge} for edge in chords]
1093 while set_orth:
1094 base = set_orth.pop()
1095 # kth cycle is "parallel" to kth vector in set_orth
1096 cycle_edges = _min_cycle(G, base, weight)
1097 cb.append([v for u, v in cycle_edges])
1099 # now update set_orth so that k+1,k+2... th elements are
1100 # orthogonal to the newly found cycle, as per [p. 336, 1]
1101 set_orth = [
1102 (
1103 {e for e in orth if e not in base if e[::-1] not in base}
1104 | {e for e in base if e not in orth if e[::-1] not in orth}
1105 )
1106 if sum((e in orth or e[::-1] in orth) for e in cycle_edges) % 2
1107 else orth
1108 for orth in set_orth
1109 ]
1110 return cb
1113def _min_cycle(G, orth, weight):
1114 """
1115 Computes the minimum weight cycle in G,
1116 orthogonal to the vector orth as per [p. 338, 1]
1117 Use (u, 1) to indicate the lifted copy of u (denoted u' in paper).
1118 """
1119 Gi = nx.Graph()
1121 # Add 2 copies of each edge in G to Gi.
1122 # If edge is in orth, add cross edge; otherwise in-plane edge
1123 for u, v, wt in G.edges(data=weight, default=1):
1124 if (u, v) in orth or (v, u) in orth:
1125 Gi.add_edges_from([(u, (v, 1)), ((u, 1), v)], Gi_weight=wt)
1126 else:
1127 Gi.add_edges_from([(u, v), ((u, 1), (v, 1))], Gi_weight=wt)
1129 # find the shortest length in Gi between n and (n, 1) for each n
1130 # Note: Use "Gi_weight" for name of weight attribute
1131 spl = nx.shortest_path_length
1132 lift = {n: spl(Gi, source=n, target=(n, 1), weight="Gi_weight") for n in G}
1134 # Now compute that short path in Gi, which translates to a cycle in G
1135 start = min(lift, key=lift.get)
1136 end = (start, 1)
1137 min_path_i = nx.shortest_path(Gi, source=start, target=end, weight="Gi_weight")
1139 # Now we obtain the actual path, re-map nodes in Gi to those in G
1140 min_path = [n if n in G else n[0] for n in min_path_i]
1142 # Now remove the edges that occur two times
1143 # two passes: flag which edges get kept, then build it
1144 edgelist = list(pairwise(min_path))
1145 edgeset = set()
1146 for e in edgelist:
1147 if e in edgeset:
1148 edgeset.remove(e)
1149 elif e[::-1] in edgeset:
1150 edgeset.remove(e[::-1])
1151 else:
1152 edgeset.add(e)
1154 min_edgelist = []
1155 for e in edgelist:
1156 if e in edgeset:
1157 min_edgelist.append(e)
1158 edgeset.remove(e)
1159 elif e[::-1] in edgeset:
1160 min_edgelist.append(e[::-1])
1161 edgeset.remove(e[::-1])
1163 return min_edgelist
1166@not_implemented_for("directed")
1167@not_implemented_for("multigraph")
1168@nx._dispatch
1169def girth(G):
1170 """Returns the girth of the graph.
1172 The girth of a graph is the length of its shortest cycle, or infinity if
1173 the graph is acyclic. The algorithm follows the description given on the
1174 Wikipedia page [1]_, and runs in time O(mn) on a graph with m edges and n
1175 nodes.
1177 Parameters
1178 ----------
1179 G : NetworkX Graph
1181 Returns
1182 -------
1183 int or math.inf
1185 Examples
1186 --------
1187 All examples below (except P_5) can easily be checked using Wikipedia,
1188 which has a page for each of these famous graphs.
1190 >>> nx.girth(nx.chvatal_graph())
1191 4
1192 >>> nx.girth(nx.tutte_graph())
1193 4
1194 >>> nx.girth(nx.petersen_graph())
1195 5
1196 >>> nx.girth(nx.heawood_graph())
1197 6
1198 >>> nx.girth(nx.pappus_graph())
1199 6
1200 >>> nx.girth(nx.path_graph(5))
1201 inf
1203 References
1204 ----------
1205 .. [1] https://en.wikipedia.org/wiki/Girth_(graph_theory)
1207 """
1208 girth = depth_limit = inf
1209 tree_edge = nx.algorithms.traversal.breadth_first_search.TREE_EDGE
1210 level_edge = nx.algorithms.traversal.breadth_first_search.LEVEL_EDGE
1211 for n in G:
1212 # run a BFS from source n, keeping track of distances; since we want
1213 # the shortest cycle, no need to explore beyond the current minimum length
1214 depth = {n: 0}
1215 for u, v, label in nx.bfs_labeled_edges(G, n):
1216 du = depth[u]
1217 if du > depth_limit:
1218 break
1219 if label is tree_edge:
1220 depth[v] = du + 1
1221 else:
1222 # if (u, v) is a level edge, the length is du + du + 1 (odd)
1223 # otherwise, it's a forward edge; length is du + (du + 1) + 1 (even)
1224 delta = label is level_edge
1225 length = du + du + 2 - delta
1226 if length < girth:
1227 girth = length
1228 depth_limit = du - delta
1230 return girth