Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/networkx/algorithms/cycles.py: 9%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

438 statements  

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