| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234 |
- """
- ========================
- Cycle finding algorithms
- ========================
- """
- from collections import 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._dispatchable
- 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
- minimum_cycle_basis
- """
- 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._dispatchable
- 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 Graph
- A networkx graph. Undirected, directed, and multigraphs are all supported.
- 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
- --------
- >>> G = nx.DiGraph([(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)])
- >>> 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._dispatchable
- 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.
- loops = set(nx.nodes_with_selfloops(G))
- edges = ((u, v) for u in G if u not in loops for v in G._adj[u] if v not in loops)
- if directed:
- F = nx.DiGraph(edges)
- B = F.to_undirected(as_view=False)
- else:
- F = nx.Graph(edges)
- 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 u in loops:
- continue
- 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._dispatchable(mutates_input=True)
- 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._dispatchable
- 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._dispatchable(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._dispatchable
- 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] `Wikipedia: Girth <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
|