1"""
2========================
3Cycle finding algorithms
4========================
5"""
6
7from collections import defaultdict
8from itertools import combinations, product
9from math import inf
10
11import networkx as nx
12from networkx.utils import not_implemented_for, pairwise
13
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]
23
24
25@not_implemented_for("directed")
26@not_implemented_for("multigraph")
27@nx._dispatchable
28def cycle_basis(G, root=None):
29 """Returns a list of cycles which form a basis for cycles of G.
30
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.
37
38 Parameters
39 ----------
40 G : NetworkX Graph
41 root : node, optional
42 Specify starting node for basis.
43
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.
48
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]]
56
57 Notes
58 -----
59 This is adapted from algorithm CACM 491 [1]_.
60
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.
65
66 See Also
67 --------
68 simple_cycles
69 minimum_cycle_basis
70 """
71 gnodes = dict.fromkeys(G) # set-like object that maintains node order
72 cycles = []
73 while gnodes: # loop over connected components
74 if root is None:
75 root = gnodes.popitem()[0]
76 stack = [root]
77 pred = {root: root}
78 used = {root: set()}
79 while stack: # walk the spanning tree finding cycles
80 z = stack.pop() # use last-in so cycles easier to find
81 zused = used[z]
82 for nbr in G[z]:
83 if nbr not in used: # new node
84 pred[nbr] = z
85 stack.append(nbr)
86 used[nbr] = {z}
87 elif nbr == z: # self loops
88 cycles.append([z])
89 elif nbr not in zused: # found a cycle
90 pn = used[nbr]
91 cycle = [nbr, z]
92 p = pred[z]
93 while p not in pn:
94 cycle.append(p)
95 p = pred[p]
96 cycle.append(p)
97 cycles.append(cycle)
98 used[nbr].add(z)
99 for node in pred:
100 gnodes.pop(node, None)
101 root = None
102 return cycles
103
104
105@nx._dispatchable
106def simple_cycles(G, length_bound=None):
107 """Find simple cycles (elementary circuits) of a graph.
108
109 A "simple cycle", or "elementary circuit", is a closed path where
110 no node appears twice. In a directed graph, two simple cycles are distinct
111 if they are not cyclic permutations of each other. In an undirected graph,
112 two simple cycles are distinct if they are not cyclic permutations of each
113 other nor of the other's reversal.
114
115 Optionally, the cycles are bounded in length. In the unbounded case, we use
116 a nonrecursive, iterator/generator version of Johnson's algorithm [1]_. In
117 the bounded case, we use a version of the algorithm of Gupta and
118 Suzumura [2]_. There may be better algorithms for some cases [3]_ [4]_ [5]_.
119
120 The algorithms of Johnson, and Gupta and Suzumura, are enhanced by some
121 well-known preprocessing techniques. When `G` is directed, we restrict our
122 attention to strongly connected components of `G`, generate all simple cycles
123 containing a certain node, remove that node, and further decompose the
124 remainder into strongly connected components. When `G` is undirected, we
125 restrict our attention to biconnected components, generate all simple cycles
126 containing a particular edge, remove that edge, and further decompose the
127 remainder into biconnected components.
128
129 Note that multigraphs are supported by this function -- and in undirected
130 multigraphs, a pair of parallel edges is considered a cycle of length 2.
131 Likewise, self-loops are considered to be cycles of length 1. We define
132 cycles as sequences of nodes; so the presence of loops and parallel edges
133 does not change the number of simple cycles in a graph.
134
135 Parameters
136 ----------
137 G : NetworkX Graph
138 A networkx graph. Undirected, directed, and multigraphs are all supported.
139
140 length_bound : int or None, optional (default=None)
141 If `length_bound` is an int, generate all simple cycles of `G` with length at
142 most `length_bound`. Otherwise, generate all simple cycles of `G`.
143
144 Yields
145 ------
146 list of nodes
147 Each cycle is represented by a list of nodes along the cycle.
148
149 Examples
150 --------
151 >>> G = nx.DiGraph([(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)])
152 >>> sorted(nx.simple_cycles(G))
153 [[0], [0, 1, 2], [0, 2], [1, 2], [2]]
154
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:
158
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]]
163
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`.
170
171 Raises
172 ------
173 ValueError
174 when ``length_bound < 0``.
175
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
191
192 See Also
193 --------
194 cycle_basis
195 chordless_cycles
196 """
197
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")
203
204 directed = G.is_directed()
205 yield from ([v] for v, Gv in G.adj.items() if v in Gv)
206
207 if length_bound is not None and length_bound == 1:
208 return
209
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)
216
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)
222
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
233
234 if directed:
235 yield from _directed_cycle_search(G, length_bound)
236 else:
237 yield from _undirected_cycle_search(G, length_bound)
238
239
240def _directed_cycle_search(G, length_bound):
241 """A dispatch function for `simple_cycles` for directed graphs.
242
243 We generate all cycles of G through binary partition.
244
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.
248
249 This is accomplished through the following:
250
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.
256
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.
259
260 Parameters
261 ----------
262 G : NetworkX DiGraph
263 A directed graph
264
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.
268
269 Yields
270 ------
271 list of nodes
272 Each cycle is represented by a list of nodes along the cycle.
273 """
274
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)
288
289
290def _undirected_cycle_search(G, length_bound):
291 """A dispatch function for `simple_cycles` for undirected graphs.
292
293 We generate all cycles of G through binary partition.
294
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)
298
299 This is accomplished through the following:
300
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.
306
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.
309
310 Parameters
311 ----------
312 G : NetworkX Graph
313 An undirected graph
314
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.
318
319 Yields
320 ------
321 list of nodes
322 Each cycle is represented by a list of nodes along the cycle.
323 """
324
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)
338
339
340class _NeighborhoodCache(dict):
341 """Very lightweight graph wrapper which caches neighborhoods as list.
342
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 """
347
348 def __init__(self, G):
349 self.G = G
350
351 def __missing__(self, v):
352 Gv = self[v] = list(self.G[v])
353 return Gv
354
355
356def _johnson_cycle_search(G, path):
357 """The main loop of the cycle-enumeration algorithm of Johnson.
358
359 Parameters
360 ----------
361 G : NetworkX Graph or DiGraph
362 A graph
363
364 path : list
365 A cycle prefix. All cycles generated will begin with this prefix.
366
367 Yields
368 ------
369 list of nodes
370 Each cycle is represented by a list of nodes along the cycle.
371
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
377
378 """
379
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)
414
415
416def _bounded_cycle_search(G, path, length_bound):
417 """The main loop of the cycle-enumeration algorithm of Gupta and Suzumura.
418
419 Parameters
420 ----------
421 G : NetworkX Graph or DiGraph
422 A graph
423
424 path : list
425 A cycle prefix. All cycles generated will begin with this prefix.
426
427 length_bound: int
428 A length bound. All cycles generated will have length at most length_bound.
429
430 Yields
431 ------
432 list of nodes
433 Each cycle is represented by a list of nodes along the cycle.
434
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
439
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)
475
476
477@nx._dispatchable
478def chordless_cycles(G, length_bound=None):
479 """Find simple chordless cycles of a graph.
480
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`.
486
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.
490
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.
494
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.
497
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.
500
501 4. Generalizing the above, edges with parallel clones may not occur in
502 chordless cycles.
503
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.
508
509 Optionally, the cycles are bounded in length.
510
511 We use an algorithm strongly inspired by that of Dias et al [1]_. It has
512 been modified in the following ways:
513
514 1. Recursion is avoided, per Python's limitations.
515
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.
519
520 3. The search is optionally bounded at a specified length.
521
522 4. Support for directed graphs is provided by extending cycles along
523 forward edges, and blocking nodes along forward and reverse edges.
524
525 5. Support for multigraphs is provided by omitting digons from the set
526 of forward edges.
527
528 Parameters
529 ----------
530 G : NetworkX DiGraph
531 A directed graph
532
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.
536
537 Yields
538 ------
539 list of nodes
540 Each cycle is represented by a list of nodes along the cycle.
541
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]]
546
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.
551
552 Raises
553 ------
554 ValueError
555 when length_bound < 0.
556
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
562
563 See Also
564 --------
565 simple_cycles
566 """
567
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")
573
574 directed = G.is_directed()
575 multigraph = G.is_multigraph()
576
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)
581
582 if length_bound is not None and length_bound == 1:
583 return
584
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 loops = set(nx.nodes_with_selfloops(G))
589 edges = ((u, v) for u in G if u not in loops for v in G._adj[u] if v not in loops)
590 if directed:
591 F = nx.DiGraph(edges)
592 B = F.to_undirected(as_view=False)
593 else:
594 F = nx.Graph(edges)
595 B = None
596
597 # If we're given a multigraph, we have a few cases to consider with parallel
598 # edges.
599 #
600 # 1. If we have 2 or more edges in parallel between the nodes (u, v), we
601 # must not construct longer cycles along (u, v).
602 # 2. If G is not directed, then a pair of parallel edges between (u, v) is a
603 # chordless cycle unless there exists a third (or more) parallel edge.
604 # 3. If G is directed, then parallel edges do not form cycles, but do
605 # preclude back-edges from forming cycles (handled in the next section),
606 # Thus, if an edge (u, v) is duplicated and the reverse (v, u) is also
607 # present, then we remove both from F.
608 #
609 # In directed graphs, we need to consider both directions that edges can
610 # take, so iterate over all edges (u, v) and possibly (v, u). In undirected
611 # graphs, we need to be a little careful to only consider every edge once,
612 # so we use a "visited" set to emulate node-order comparisons.
613
614 if multigraph:
615 if not directed:
616 B = F.copy()
617 visited = set()
618 for u, Gu in G.adj.items():
619 if u in loops:
620 continue
621 if directed:
622 multiplicity = ((v, len(Guv)) for v, Guv in Gu.items())
623 for v, m in multiplicity:
624 if m > 1:
625 F.remove_edges_from(((u, v), (v, u)))
626 else:
627 multiplicity = ((v, len(Guv)) for v, Guv in Gu.items() if v in visited)
628 for v, m in multiplicity:
629 if m == 2:
630 yield [u, v]
631 if m > 1:
632 F.remove_edge(u, v)
633 visited.add(u)
634
635 # If we're given a directed graphs, we need to think about digons. If we
636 # have two edges (u, v) and (v, u), then that's a two-cycle. If either edge
637 # was duplicated above, then we removed both from F. So, any digons we find
638 # here are chordless. After finding digons, we remove their edges from F
639 # to avoid traversing them in the search for chordless cycles.
640 if directed:
641 for u, Fu in F.adj.items():
642 digons = [[u, v] for v in Fu if F.has_edge(v, u)]
643 yield from digons
644 F.remove_edges_from(digons)
645 F.remove_edges_from(e[::-1] for e in digons)
646
647 if length_bound is not None and length_bound == 2:
648 return
649
650 # Now, we prepare to search for cycles. We have removed all cycles of
651 # lengths 1 and 2, so F is a simple graph or simple digraph. We repeatedly
652 # separate digraphs into their strongly connected components, and undirected
653 # graphs into their biconnected components. For each component, we pick a
654 # node v, search for chordless cycles based at each "stem" (u, v, w), and
655 # then remove v from that component before separating the graph again.
656 if directed:
657 separate = nx.strongly_connected_components
658
659 # Directed stems look like (u -> v -> w), so we use the product of
660 # predecessors of v with successors of v.
661 def stems(C, v):
662 for u, w in product(C.pred[v], C.succ[v]):
663 if not G.has_edge(u, w): # omit stems with acyclic chords
664 yield [u, v, w], F.has_edge(w, u)
665
666 else:
667 separate = nx.biconnected_components
668
669 # Undirected stems look like (u ~ v ~ w), but we must not also search
670 # (w ~ v ~ u), so we use combinations of v's neighbors of length 2.
671 def stems(C, v):
672 yield from (([u, v, w], F.has_edge(w, u)) for u, w in combinations(C[v], 2))
673
674 components = [c for c in separate(F) if len(c) > 2]
675 while components:
676 c = components.pop()
677 v = next(iter(c))
678 Fc = F.subgraph(c)
679 Fcc = Bcc = None
680 for S, is_triangle in stems(Fc, v):
681 if is_triangle:
682 yield S
683 else:
684 if Fcc is None:
685 Fcc = _NeighborhoodCache(Fc)
686 Bcc = Fcc if B is None else _NeighborhoodCache(B.subgraph(c))
687 yield from _chordless_cycle_search(Fcc, Bcc, S, length_bound)
688
689 components.extend(c for c in separate(F.subgraph(c - {v})) if len(c) > 2)
690
691
692def _chordless_cycle_search(F, B, path, length_bound):
693 """The main loop for chordless cycle enumeration.
694
695 This algorithm is strongly inspired by that of Dias et al [1]_. It has been
696 modified in the following ways:
697
698 1. Recursion is avoided, per Python's limitations
699
700 2. The labeling function is not necessary, because the starting paths
701 are chosen (and deleted from the host graph) to prevent multiple
702 occurrences of the same path
703
704 3. The search is optionally bounded at a specified length
705
706 4. Support for directed graphs is provided by extending cycles along
707 forward edges, and blocking nodes along forward and reverse edges
708
709 5. Support for multigraphs is provided by omitting digons from the set
710 of forward edges
711
712 Parameters
713 ----------
714 F : _NeighborhoodCache
715 A graph of forward edges to follow in constructing cycles
716
717 B : _NeighborhoodCache
718 A graph of blocking edges to prevent the production of chordless cycles
719
720 path : list
721 A cycle prefix. All cycles generated will begin with this prefix.
722
723 length_bound : int
724 A length bound. All cycles generated will have length at most length_bound.
725
726
727 Yields
728 ------
729 list of nodes
730 Each cycle is represented by a list of nodes along the cycle.
731
732 References
733 ----------
734 .. [1] Efficient enumeration of chordless cycles
735 E. Dias and D. Castonguay and H. Longo and W.A.R. Jradi
736 https://arxiv.org/abs/1309.1051
737
738 """
739 blocked = defaultdict(int)
740 target = path[0]
741 blocked[path[1]] = 1
742 for w in path[1:]:
743 for v in B[w]:
744 blocked[v] += 1
745
746 stack = [iter(F[path[2]])]
747 while stack:
748 nbrs = stack[-1]
749 for w in nbrs:
750 if blocked[w] == 1 and (length_bound is None or len(path) < length_bound):
751 Fw = F[w]
752 if target in Fw:
753 yield path + [w]
754 else:
755 Bw = B[w]
756 if target in Bw:
757 continue
758 for v in Bw:
759 blocked[v] += 1
760 path.append(w)
761 stack.append(iter(Fw))
762 break
763 else:
764 stack.pop()
765 for v in B[path.pop()]:
766 blocked[v] -= 1
767
768
769@not_implemented_for("undirected")
770@nx._dispatchable(mutates_input=True)
771def recursive_simple_cycles(G):
772 """Find simple cycles (elementary circuits) of a directed graph.
773
774 A `simple cycle`, or `elementary circuit`, is a closed path where
775 no node appears twice. Two elementary circuits are distinct if they
776 are not cyclic permutations of each other.
777
778 This version uses a recursive algorithm to build a list of cycles.
779 You should probably use the iterator version called simple_cycles().
780 Warning: This recursive version uses lots of RAM!
781 It appears in NetworkX for pedagogical value.
782
783 Parameters
784 ----------
785 G : NetworkX DiGraph
786 A directed graph
787
788 Returns
789 -------
790 A list of cycles, where each cycle is represented by a list of nodes
791 along the cycle.
792
793 Example:
794
795 >>> edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)]
796 >>> G = nx.DiGraph(edges)
797 >>> nx.recursive_simple_cycles(G)
798 [[0], [2], [0, 1, 2], [0, 2], [1, 2]]
799
800 Notes
801 -----
802 The implementation follows pp. 79-80 in [1]_.
803
804 The time complexity is $O((n+e)(c+1))$ for $n$ nodes, $e$ edges and $c$
805 elementary circuits.
806
807 References
808 ----------
809 .. [1] Finding all the elementary circuits of a directed graph.
810 D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975.
811 https://doi.org/10.1137/0204007
812
813 See Also
814 --------
815 simple_cycles, cycle_basis
816 """
817
818 # Jon Olav Vik, 2010-08-09
819 def _unblock(thisnode):
820 """Recursively unblock and remove nodes from B[thisnode]."""
821 if blocked[thisnode]:
822 blocked[thisnode] = False
823 while B[thisnode]:
824 _unblock(B[thisnode].pop())
825
826 def circuit(thisnode, startnode, component):
827 closed = False # set to True if elementary path is closed
828 path.append(thisnode)
829 blocked[thisnode] = True
830 for nextnode in component[thisnode]: # direct successors of thisnode
831 if nextnode == startnode:
832 result.append(path[:])
833 closed = True
834 elif not blocked[nextnode]:
835 if circuit(nextnode, startnode, component):
836 closed = True
837 if closed:
838 _unblock(thisnode)
839 else:
840 for nextnode in component[thisnode]:
841 if thisnode not in B[nextnode]: # TODO: use set for speedup?
842 B[nextnode].append(thisnode)
843 path.pop() # remove thisnode from path
844 return closed
845
846 path = [] # stack of nodes in current path
847 blocked = defaultdict(bool) # vertex: blocked from search?
848 B = defaultdict(list) # graph portions that yield no elementary circuit
849 result = [] # list to accumulate the circuits found
850
851 # Johnson's algorithm exclude self cycle edges like (v, v)
852 # To be backward compatible, we record those cycles in advance
853 # and then remove from subG
854 for v in G:
855 if G.has_edge(v, v):
856 result.append([v])
857 G.remove_edge(v, v)
858
859 # Johnson's algorithm requires some ordering of the nodes.
860 # They might not be sortable so we assign an arbitrary ordering.
861 ordering = dict(zip(G, range(len(G))))
862 for s in ordering:
863 # Build the subgraph induced by s and following nodes in the ordering
864 subgraph = G.subgraph(node for node in G if ordering[node] >= ordering[s])
865 # Find the strongly connected component in the subgraph
866 # that contains the least node according to the ordering
867 strongcomp = nx.strongly_connected_components(subgraph)
868 mincomp = min(strongcomp, key=lambda ns: min(ordering[n] for n in ns))
869 component = G.subgraph(mincomp)
870 if len(component) > 1:
871 # smallest node in the component according to the ordering
872 startnode = min(component, key=ordering.__getitem__)
873 for node in component:
874 blocked[node] = False
875 B[node][:] = []
876 dummy = circuit(startnode, startnode, component)
877 return result
878
879
880@nx._dispatchable
881def find_cycle(G, source=None, orientation=None):
882 """Returns a cycle found via depth-first traversal.
883
884 The cycle is a list of edges indicating the cyclic path.
885 Orientation of directed edges is controlled by `orientation`.
886
887 Parameters
888 ----------
889 G : graph
890 A directed/undirected graph/multigraph.
891
892 source : node, list of nodes
893 The node from which the traversal begins. If None, then a source
894 is chosen arbitrarily and repeatedly until all edges from each node in
895 the graph are searched.
896
897 orientation : None | 'original' | 'reverse' | 'ignore' (default: None)
898 For directed graphs and directed multigraphs, edge traversals need not
899 respect the original orientation of the edges.
900 When set to 'reverse' every edge is traversed in the reverse direction.
901 When set to 'ignore', every edge is treated as undirected.
902 When set to 'original', every edge is treated as directed.
903 In all three cases, the yielded edge tuples add a last entry to
904 indicate the direction in which that edge was traversed.
905 If orientation is None, the yielded edge has no direction indicated.
906 The direction is respected, but not reported.
907
908 Returns
909 -------
910 edges : directed edges
911 A list of directed edges indicating the path taken for the loop.
912 If no cycle is found, then an exception is raised.
913 For graphs, an edge is of the form `(u, v)` where `u` and `v`
914 are the tail and head of the edge as determined by the traversal.
915 For multigraphs, an edge is of the form `(u, v, key)`, where `key` is
916 the key of the edge. When the graph is directed, then `u` and `v`
917 are always in the order of the actual directed edge.
918 If orientation is not None then the edge tuple is extended to include
919 the direction of traversal ('forward' or 'reverse') on that edge.
920
921 Raises
922 ------
923 NetworkXNoCycle
924 If no cycle was found.
925
926 Examples
927 --------
928 In this example, we construct a DAG and find, in the first call, that there
929 are no directed cycles, and so an exception is raised. In the second call,
930 we ignore edge orientations and find that there is an undirected cycle.
931 Note that the second call finds a directed cycle while effectively
932 traversing an undirected graph, and so, we found an "undirected cycle".
933 This means that this DAG structure does not form a directed tree (which
934 is also known as a polytree).
935
936 >>> G = nx.DiGraph([(0, 1), (0, 2), (1, 2)])
937 >>> nx.find_cycle(G, orientation="original")
938 Traceback (most recent call last):
939 ...
940 networkx.exception.NetworkXNoCycle: No cycle found.
941 >>> list(nx.find_cycle(G, orientation="ignore"))
942 [(0, 1, 'forward'), (1, 2, 'forward'), (0, 2, 'reverse')]
943
944 See Also
945 --------
946 simple_cycles
947 """
948 if not G.is_directed() or orientation in (None, "original"):
949
950 def tailhead(edge):
951 return edge[:2]
952
953 elif orientation == "reverse":
954
955 def tailhead(edge):
956 return edge[1], edge[0]
957
958 elif orientation == "ignore":
959
960 def tailhead(edge):
961 if edge[-1] == "reverse":
962 return edge[1], edge[0]
963 return edge[:2]
964
965 explored = set()
966 cycle = []
967 final_node = None
968 for start_node in G.nbunch_iter(source):
969 if start_node in explored:
970 # No loop is possible.
971 continue
972
973 edges = []
974 # All nodes seen in this iteration of edge_dfs
975 seen = {start_node}
976 # Nodes in active path.
977 active_nodes = {start_node}
978 previous_head = None
979
980 for edge in nx.edge_dfs(G, start_node, orientation):
981 # Determine if this edge is a continuation of the active path.
982 tail, head = tailhead(edge)
983 if head in explored:
984 # Then we've already explored it. No loop is possible.
985 continue
986 if previous_head is not None and tail != previous_head:
987 # This edge results from backtracking.
988 # Pop until we get a node whose head equals the current tail.
989 # So for example, we might have:
990 # (0, 1), (1, 2), (2, 3), (1, 4)
991 # which must become:
992 # (0, 1), (1, 4)
993 while True:
994 try:
995 popped_edge = edges.pop()
996 except IndexError:
997 edges = []
998 active_nodes = {tail}
999 break
1000 else:
1001 popped_head = tailhead(popped_edge)[1]
1002 active_nodes.remove(popped_head)
1003
1004 if edges:
1005 last_head = tailhead(edges[-1])[1]
1006 if tail == last_head:
1007 break
1008 edges.append(edge)
1009
1010 if head in active_nodes:
1011 # We have a loop!
1012 cycle.extend(edges)
1013 final_node = head
1014 break
1015 else:
1016 seen.add(head)
1017 active_nodes.add(head)
1018 previous_head = head
1019
1020 if cycle:
1021 break
1022 else:
1023 explored.update(seen)
1024
1025 else:
1026 assert len(cycle) == 0
1027 raise nx.exception.NetworkXNoCycle("No cycle found.")
1028
1029 # We now have a list of edges which ends on a cycle.
1030 # So we need to remove from the beginning edges that are not relevant.
1031
1032 for i, edge in enumerate(cycle):
1033 tail, head = tailhead(edge)
1034 if tail == final_node:
1035 break
1036
1037 return cycle[i:]
1038
1039
1040@not_implemented_for("directed")
1041@not_implemented_for("multigraph")
1042@nx._dispatchable(edge_attrs="weight")
1043def minimum_cycle_basis(G, weight=None):
1044 """Returns a minimum weight cycle basis for G
1045
1046 Minimum weight means a cycle basis for which the total weight
1047 (length for unweighted graphs) of all the cycles is minimum.
1048
1049 Parameters
1050 ----------
1051 G : NetworkX Graph
1052 weight: string
1053 name of the edge attribute to use for edge weights
1054
1055 Returns
1056 -------
1057 A list of cycle lists. Each cycle list is a list of nodes
1058 which forms a cycle (loop) in G. Note that the nodes are not
1059 necessarily returned in a order by which they appear in the cycle
1060
1061 Examples
1062 --------
1063 >>> G = nx.Graph()
1064 >>> nx.add_cycle(G, [0, 1, 2, 3])
1065 >>> nx.add_cycle(G, [0, 3, 4, 5])
1066 >>> nx.minimum_cycle_basis(G)
1067 [[5, 4, 3, 0], [3, 2, 1, 0]]
1068
1069 References:
1070 [1] Kavitha, Telikepalli, et al. "An O(m^2n) Algorithm for
1071 Minimum Cycle Basis of Graphs."
1072 http://link.springer.com/article/10.1007/s00453-007-9064-z
1073 [2] de Pina, J. 1995. Applications of shortest path methods.
1074 Ph.D. thesis, University of Amsterdam, Netherlands
1075
1076 See Also
1077 --------
1078 simple_cycles, cycle_basis
1079 """
1080 # We first split the graph in connected subgraphs
1081 return sum(
1082 (_min_cycle_basis(G.subgraph(c), weight) for c in nx.connected_components(G)),
1083 [],
1084 )
1085
1086
1087def _min_cycle_basis(G, weight):
1088 cb = []
1089 # We extract the edges not in a spanning tree. We do not really need a
1090 # *minimum* spanning tree. That is why we call the next function with
1091 # weight=None. Depending on implementation, it may be faster as well
1092 tree_edges = list(nx.minimum_spanning_edges(G, weight=None, data=False))
1093 chords = G.edges - tree_edges - {(v, u) for u, v in tree_edges}
1094
1095 # We maintain a set of vectors orthogonal to sofar found cycles
1096 set_orth = [{edge} for edge in chords]
1097 while set_orth:
1098 base = set_orth.pop()
1099 # kth cycle is "parallel" to kth vector in set_orth
1100 cycle_edges = _min_cycle(G, base, weight)
1101 cb.append([v for u, v in cycle_edges])
1102
1103 # now update set_orth so that k+1,k+2... th elements are
1104 # orthogonal to the newly found cycle, as per [p. 336, 1]
1105 set_orth = [
1106 (
1107 {e for e in orth if e not in base if e[::-1] not in base}
1108 | {e for e in base if e not in orth if e[::-1] not in orth}
1109 )
1110 if sum((e in orth or e[::-1] in orth) for e in cycle_edges) % 2
1111 else orth
1112 for orth in set_orth
1113 ]
1114 return cb
1115
1116
1117def _min_cycle(G, orth, weight):
1118 """
1119 Computes the minimum weight cycle in G,
1120 orthogonal to the vector orth as per [p. 338, 1]
1121 Use (u, 1) to indicate the lifted copy of u (denoted u' in paper).
1122 """
1123 Gi = nx.Graph()
1124
1125 # Add 2 copies of each edge in G to Gi.
1126 # If edge is in orth, add cross edge; otherwise in-plane edge
1127 for u, v, wt in G.edges(data=weight, default=1):
1128 if (u, v) in orth or (v, u) in orth:
1129 Gi.add_edges_from([(u, (v, 1)), ((u, 1), v)], Gi_weight=wt)
1130 else:
1131 Gi.add_edges_from([(u, v), ((u, 1), (v, 1))], Gi_weight=wt)
1132
1133 # find the shortest length in Gi between n and (n, 1) for each n
1134 # Note: Use "Gi_weight" for name of weight attribute
1135 spl = nx.shortest_path_length
1136 lift = {n: spl(Gi, source=n, target=(n, 1), weight="Gi_weight") for n in G}
1137
1138 # Now compute that short path in Gi, which translates to a cycle in G
1139 start = min(lift, key=lift.get)
1140 end = (start, 1)
1141 min_path_i = nx.shortest_path(Gi, source=start, target=end, weight="Gi_weight")
1142
1143 # Now we obtain the actual path, re-map nodes in Gi to those in G
1144 min_path = [n if n in G else n[0] for n in min_path_i]
1145
1146 # Now remove the edges that occur two times
1147 # two passes: flag which edges get kept, then build it
1148 edgelist = list(pairwise(min_path))
1149 edgeset = set()
1150 for e in edgelist:
1151 if e in edgeset:
1152 edgeset.remove(e)
1153 elif e[::-1] in edgeset:
1154 edgeset.remove(e[::-1])
1155 else:
1156 edgeset.add(e)
1157
1158 min_edgelist = []
1159 for e in edgelist:
1160 if e in edgeset:
1161 min_edgelist.append(e)
1162 edgeset.remove(e)
1163 elif e[::-1] in edgeset:
1164 min_edgelist.append(e[::-1])
1165 edgeset.remove(e[::-1])
1166
1167 return min_edgelist
1168
1169
1170@not_implemented_for("directed")
1171@not_implemented_for("multigraph")
1172@nx._dispatchable
1173def girth(G):
1174 """Returns the girth of the graph.
1175
1176 The girth of a graph is the length of its shortest cycle, or infinity if
1177 the graph is acyclic. The algorithm follows the description given on the
1178 Wikipedia page [1]_, and runs in time O(mn) on a graph with m edges and n
1179 nodes.
1180
1181 Parameters
1182 ----------
1183 G : NetworkX Graph
1184
1185 Returns
1186 -------
1187 int or math.inf
1188
1189 Examples
1190 --------
1191 All examples below (except P_5) can easily be checked using Wikipedia,
1192 which has a page for each of these famous graphs.
1193
1194 >>> nx.girth(nx.chvatal_graph())
1195 4
1196 >>> nx.girth(nx.tutte_graph())
1197 4
1198 >>> nx.girth(nx.petersen_graph())
1199 5
1200 >>> nx.girth(nx.heawood_graph())
1201 6
1202 >>> nx.girth(nx.pappus_graph())
1203 6
1204 >>> nx.girth(nx.path_graph(5))
1205 inf
1206
1207 References
1208 ----------
1209 .. [1] `Wikipedia: Girth <https://en.wikipedia.org/wiki/Girth_(graph_theory)>`_
1210
1211 """
1212 girth = depth_limit = inf
1213 tree_edge = nx.algorithms.traversal.breadth_first_search.TREE_EDGE
1214 level_edge = nx.algorithms.traversal.breadth_first_search.LEVEL_EDGE
1215 for n in G:
1216 # run a BFS from source n, keeping track of distances; since we want
1217 # the shortest cycle, no need to explore beyond the current minimum length
1218 depth = {n: 0}
1219 for u, v, label in nx.bfs_labeled_edges(G, n):
1220 du = depth[u]
1221 if du > depth_limit:
1222 break
1223 if label is tree_edge:
1224 depth[v] = du + 1
1225 else:
1226 # if (u, v) is a level edge, the length is du + du + 1 (odd)
1227 # otherwise, it's a forward edge; length is du + (du + 1) + 1 (even)
1228 delta = label is level_edge
1229 length = du + du + 2 - delta
1230 if length < girth:
1231 girth = length
1232 depth_limit = du - delta
1233
1234 return girth