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

432 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-20 07:00 +0000

1""" 

2======================== 

3Cycle finding algorithms 

4======================== 

5""" 

6 

7from collections import Counter, 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._dispatch 

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 """ 

70 gnodes = dict.fromkeys(G) # set-like object that maintains node order 

71 cycles = [] 

72 while gnodes: # loop over connected components 

73 if root is None: 

74 root = gnodes.popitem()[0] 

75 stack = [root] 

76 pred = {root: root} 

77 used = {root: set()} 

78 while stack: # walk the spanning tree finding cycles 

79 z = stack.pop() # use last-in so cycles easier to find 

80 zused = used[z] 

81 for nbr in G[z]: 

82 if nbr not in used: # new node 

83 pred[nbr] = z 

84 stack.append(nbr) 

85 used[nbr] = {z} 

86 elif nbr == z: # self loops 

87 cycles.append([z]) 

88 elif nbr not in zused: # found a cycle 

89 pn = used[nbr] 

90 cycle = [nbr, z] 

91 p = pred[z] 

92 while p not in pn: 

93 cycle.append(p) 

94 p = pred[p] 

95 cycle.append(p) 

96 cycles.append(cycle) 

97 used[nbr].add(z) 

98 for node in pred: 

99 gnodes.pop(node, None) 

100 root = None 

101 return cycles 

102 

103 

104@nx._dispatch 

105def simple_cycles(G, length_bound=None): 

106 """Find simple cycles (elementary circuits) of a graph. 

107 

108 A `simple cycle`, or `elementary circuit`, is a closed path where 

109 no node appears twice. In a directed graph, two simple cycles are distinct 

110 if they are not cyclic permutations of each other. In an undirected graph, 

111 two simple cycles are distinct if they are not cyclic permutations of each 

112 other nor of the other's reversal. 

113 

114 Optionally, the cycles are bounded in length. In the unbounded case, we use 

115 a nonrecursive, iterator/generator version of Johnson's algorithm [1]_. In 

116 the bounded case, we use a version of the algorithm of Gupta and 

117 Suzumura[2]_. There may be better algorithms for some cases [3]_ [4]_ [5]_. 

118 

119 The algorithms of Johnson, and Gupta and Suzumura, are enhanced by some 

120 well-known preprocessing techniques. When G is directed, we restrict our 

121 attention to strongly connected components of G, generate all simple cycles 

122 containing a certain node, remove that node, and further decompose the 

123 remainder into strongly connected components. When G is undirected, we 

124 restrict our attention to biconnected components, generate all simple cycles 

125 containing a particular edge, remove that edge, and further decompose the 

126 remainder into biconnected components. 

127 

128 Note that multigraphs are supported by this function -- and in undirected 

129 multigraphs, a pair of parallel edges is considered a cycle of length 2. 

130 Likewise, self-loops are considered to be cycles of length 1. We define 

131 cycles as sequences of nodes; so the presence of loops and parallel edges 

132 does not change the number of simple cycles in a graph. 

133 

134 Parameters 

135 ---------- 

136 G : NetworkX DiGraph 

137 A directed graph 

138 

139 length_bound : int or None, optional (default=None) 

140 If length_bound is an int, generate all simple cycles of G with length at 

141 most length_bound. Otherwise, generate all simple cycles of G. 

142 

143 Yields 

144 ------ 

145 list of nodes 

146 Each cycle is represented by a list of nodes along the cycle. 

147 

148 Examples 

149 -------- 

150 >>> edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)] 

151 >>> G = nx.DiGraph(edges) 

152 >>> sorted(nx.simple_cycles(G)) 

153 [[0], [0, 1, 2], [0, 2], [1, 2], [2]] 

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._dispatch 

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 if directed: 

589 F = nx.DiGraph((u, v) for u, Gu in G.adj.items() if u not in Gu for v in Gu) 

590 B = F.to_undirected(as_view=False) 

591 else: 

592 F = nx.Graph((u, v) for u, Gu in G.adj.items() if u not in Gu for v in Gu) 

593 B = None 

594 

595 # If we're given a multigraph, we have a few cases to consider with parallel 

596 # edges. 

597 # 

598 # 1. If we have 2 or more edges in parallel between the nodes (u, v), we 

599 # must not construct longer cycles along (u, v). 

600 # 2. If G is not directed, then a pair of parallel edges between (u, v) is a 

601 # chordless cycle unless there exists a third (or more) parallel edge. 

602 # 3. If G is directed, then parallel edges do not form cycles, but do 

603 # preclude back-edges from forming cycles (handled in the next section), 

604 # Thus, if an edge (u, v) is duplicated and the reverse (v, u) is also 

605 # present, then we remove both from F. 

606 # 

607 # In directed graphs, we need to consider both directions that edges can 

608 # take, so iterate over all edges (u, v) and possibly (v, u). In undirected 

609 # graphs, we need to be a little careful to only consider every edge once, 

610 # so we use a "visited" set to emulate node-order comparisons. 

611 

612 if multigraph: 

613 if not directed: 

614 B = F.copy() 

615 visited = set() 

616 for u, Gu in G.adj.items(): 

617 if directed: 

618 multiplicity = ((v, len(Guv)) for v, Guv in Gu.items()) 

619 for v, m in multiplicity: 

620 if m > 1: 

621 F.remove_edges_from(((u, v), (v, u))) 

622 else: 

623 multiplicity = ((v, len(Guv)) for v, Guv in Gu.items() if v in visited) 

624 for v, m in multiplicity: 

625 if m == 2: 

626 yield [u, v] 

627 if m > 1: 

628 F.remove_edge(u, v) 

629 visited.add(u) 

630 

631 # If we're given a directed graphs, we need to think about digons. If we 

632 # have two edges (u, v) and (v, u), then that's a two-cycle. If either edge 

633 # was duplicated above, then we removed both from F. So, any digons we find 

634 # here are chordless. After finding digons, we remove their edges from F 

635 # to avoid traversing them in the search for chordless cycles. 

636 if directed: 

637 for u, Fu in F.adj.items(): 

638 digons = [[u, v] for v in Fu if F.has_edge(v, u)] 

639 yield from digons 

640 F.remove_edges_from(digons) 

641 F.remove_edges_from(e[::-1] for e in digons) 

642 

643 if length_bound is not None and length_bound == 2: 

644 return 

645 

646 # Now, we prepare to search for cycles. We have removed all cycles of 

647 # lengths 1 and 2, so F is a simple graph or simple digraph. We repeatedly 

648 # separate digraphs into their strongly connected components, and undirected 

649 # graphs into their biconnected components. For each component, we pick a 

650 # node v, search for chordless cycles based at each "stem" (u, v, w), and 

651 # then remove v from that component before separating the graph again. 

652 if directed: 

653 separate = nx.strongly_connected_components 

654 

655 # Directed stems look like (u -> v -> w), so we use the product of 

656 # predecessors of v with successors of v. 

657 def stems(C, v): 

658 for u, w in product(C.pred[v], C.succ[v]): 

659 if not G.has_edge(u, w): # omit stems with acyclic chords 

660 yield [u, v, w], F.has_edge(w, u) 

661 

662 else: 

663 separate = nx.biconnected_components 

664 

665 # Undirected stems look like (u ~ v ~ w), but we must not also search 

666 # (w ~ v ~ u), so we use combinations of v's neighbors of length 2. 

667 def stems(C, v): 

668 yield from (([u, v, w], F.has_edge(w, u)) for u, w in combinations(C[v], 2)) 

669 

670 components = [c for c in separate(F) if len(c) > 2] 

671 while components: 

672 c = components.pop() 

673 v = next(iter(c)) 

674 Fc = F.subgraph(c) 

675 Fcc = Bcc = None 

676 for S, is_triangle in stems(Fc, v): 

677 if is_triangle: 

678 yield S 

679 else: 

680 if Fcc is None: 

681 Fcc = _NeighborhoodCache(Fc) 

682 Bcc = Fcc if B is None else _NeighborhoodCache(B.subgraph(c)) 

683 yield from _chordless_cycle_search(Fcc, Bcc, S, length_bound) 

684 

685 components.extend(c for c in separate(F.subgraph(c - {v})) if len(c) > 2) 

686 

687 

688def _chordless_cycle_search(F, B, path, length_bound): 

689 """The main loop for chordless cycle enumeration. 

690 

691 This algorithm is strongly inspired by that of Dias et al [1]_. It has been 

692 modified in the following ways: 

693 

694 1. Recursion is avoided, per Python's limitations 

695 

696 2. The labeling function is not necessary, because the starting paths 

697 are chosen (and deleted from the host graph) to prevent multiple 

698 occurrences of the same path 

699 

700 3. The search is optionally bounded at a specified length 

701 

702 4. Support for directed graphs is provided by extending cycles along 

703 forward edges, and blocking nodes along forward and reverse edges 

704 

705 5. Support for multigraphs is provided by omitting digons from the set 

706 of forward edges 

707 

708 Parameters 

709 ---------- 

710 F : _NeighborhoodCache 

711 A graph of forward edges to follow in constructing cycles 

712 

713 B : _NeighborhoodCache 

714 A graph of blocking edges to prevent the production of chordless cycles 

715 

716 path : list 

717 A cycle prefix. All cycles generated will begin with this prefix. 

718 

719 length_bound : int 

720 A length bound. All cycles generated will have length at most length_bound. 

721 

722 

723 Yields 

724 ------ 

725 list of nodes 

726 Each cycle is represented by a list of nodes along the cycle. 

727 

728 References 

729 ---------- 

730 .. [1] Efficient enumeration of chordless cycles 

731 E. Dias and D. Castonguay and H. Longo and W.A.R. Jradi 

732 https://arxiv.org/abs/1309.1051 

733 

734 """ 

735 blocked = defaultdict(int) 

736 target = path[0] 

737 blocked[path[1]] = 1 

738 for w in path[1:]: 

739 for v in B[w]: 

740 blocked[v] += 1 

741 

742 stack = [iter(F[path[2]])] 

743 while stack: 

744 nbrs = stack[-1] 

745 for w in nbrs: 

746 if blocked[w] == 1 and (length_bound is None or len(path) < length_bound): 

747 Fw = F[w] 

748 if target in Fw: 

749 yield path + [w] 

750 else: 

751 Bw = B[w] 

752 if target in Bw: 

753 continue 

754 for v in Bw: 

755 blocked[v] += 1 

756 path.append(w) 

757 stack.append(iter(Fw)) 

758 break 

759 else: 

760 stack.pop() 

761 for v in B[path.pop()]: 

762 blocked[v] -= 1 

763 

764 

765@not_implemented_for("undirected") 

766@nx._dispatch 

767def recursive_simple_cycles(G): 

768 """Find simple cycles (elementary circuits) of a directed graph. 

769 

770 A `simple cycle`, or `elementary circuit`, is a closed path where 

771 no node appears twice. Two elementary circuits are distinct if they 

772 are not cyclic permutations of each other. 

773 

774 This version uses a recursive algorithm to build a list of cycles. 

775 You should probably use the iterator version called simple_cycles(). 

776 Warning: This recursive version uses lots of RAM! 

777 It appears in NetworkX for pedagogical value. 

778 

779 Parameters 

780 ---------- 

781 G : NetworkX DiGraph 

782 A directed graph 

783 

784 Returns 

785 ------- 

786 A list of cycles, where each cycle is represented by a list of nodes 

787 along the cycle. 

788 

789 Example: 

790 

791 >>> edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)] 

792 >>> G = nx.DiGraph(edges) 

793 >>> nx.recursive_simple_cycles(G) 

794 [[0], [2], [0, 1, 2], [0, 2], [1, 2]] 

795 

796 Notes 

797 ----- 

798 The implementation follows pp. 79-80 in [1]_. 

799 

800 The time complexity is $O((n+e)(c+1))$ for $n$ nodes, $e$ edges and $c$ 

801 elementary circuits. 

802 

803 References 

804 ---------- 

805 .. [1] Finding all the elementary circuits of a directed graph. 

806 D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975. 

807 https://doi.org/10.1137/0204007 

808 

809 See Also 

810 -------- 

811 simple_cycles, cycle_basis 

812 """ 

813 

814 # Jon Olav Vik, 2010-08-09 

815 def _unblock(thisnode): 

816 """Recursively unblock and remove nodes from B[thisnode].""" 

817 if blocked[thisnode]: 

818 blocked[thisnode] = False 

819 while B[thisnode]: 

820 _unblock(B[thisnode].pop()) 

821 

822 def circuit(thisnode, startnode, component): 

823 closed = False # set to True if elementary path is closed 

824 path.append(thisnode) 

825 blocked[thisnode] = True 

826 for nextnode in component[thisnode]: # direct successors of thisnode 

827 if nextnode == startnode: 

828 result.append(path[:]) 

829 closed = True 

830 elif not blocked[nextnode]: 

831 if circuit(nextnode, startnode, component): 

832 closed = True 

833 if closed: 

834 _unblock(thisnode) 

835 else: 

836 for nextnode in component[thisnode]: 

837 if thisnode not in B[nextnode]: # TODO: use set for speedup? 

838 B[nextnode].append(thisnode) 

839 path.pop() # remove thisnode from path 

840 return closed 

841 

842 path = [] # stack of nodes in current path 

843 blocked = defaultdict(bool) # vertex: blocked from search? 

844 B = defaultdict(list) # graph portions that yield no elementary circuit 

845 result = [] # list to accumulate the circuits found 

846 

847 # Johnson's algorithm exclude self cycle edges like (v, v) 

848 # To be backward compatible, we record those cycles in advance 

849 # and then remove from subG 

850 for v in G: 

851 if G.has_edge(v, v): 

852 result.append([v]) 

853 G.remove_edge(v, v) 

854 

855 # Johnson's algorithm requires some ordering of the nodes. 

856 # They might not be sortable so we assign an arbitrary ordering. 

857 ordering = dict(zip(G, range(len(G)))) 

858 for s in ordering: 

859 # Build the subgraph induced by s and following nodes in the ordering 

860 subgraph = G.subgraph(node for node in G if ordering[node] >= ordering[s]) 

861 # Find the strongly connected component in the subgraph 

862 # that contains the least node according to the ordering 

863 strongcomp = nx.strongly_connected_components(subgraph) 

864 mincomp = min(strongcomp, key=lambda ns: min(ordering[n] for n in ns)) 

865 component = G.subgraph(mincomp) 

866 if len(component) > 1: 

867 # smallest node in the component according to the ordering 

868 startnode = min(component, key=ordering.__getitem__) 

869 for node in component: 

870 blocked[node] = False 

871 B[node][:] = [] 

872 dummy = circuit(startnode, startnode, component) 

873 return result 

874 

875 

876@nx._dispatch 

877def find_cycle(G, source=None, orientation=None): 

878 """Returns a cycle found via depth-first traversal. 

879 

880 The cycle is a list of edges indicating the cyclic path. 

881 Orientation of directed edges is controlled by `orientation`. 

882 

883 Parameters 

884 ---------- 

885 G : graph 

886 A directed/undirected graph/multigraph. 

887 

888 source : node, list of nodes 

889 The node from which the traversal begins. If None, then a source 

890 is chosen arbitrarily and repeatedly until all edges from each node in 

891 the graph are searched. 

892 

893 orientation : None | 'original' | 'reverse' | 'ignore' (default: None) 

894 For directed graphs and directed multigraphs, edge traversals need not 

895 respect the original orientation of the edges. 

896 When set to 'reverse' every edge is traversed in the reverse direction. 

897 When set to 'ignore', every edge is treated as undirected. 

898 When set to 'original', every edge is treated as directed. 

899 In all three cases, the yielded edge tuples add a last entry to 

900 indicate the direction in which that edge was traversed. 

901 If orientation is None, the yielded edge has no direction indicated. 

902 The direction is respected, but not reported. 

903 

904 Returns 

905 ------- 

906 edges : directed edges 

907 A list of directed edges indicating the path taken for the loop. 

908 If no cycle is found, then an exception is raised. 

909 For graphs, an edge is of the form `(u, v)` where `u` and `v` 

910 are the tail and head of the edge as determined by the traversal. 

911 For multigraphs, an edge is of the form `(u, v, key)`, where `key` is 

912 the key of the edge. When the graph is directed, then `u` and `v` 

913 are always in the order of the actual directed edge. 

914 If orientation is not None then the edge tuple is extended to include 

915 the direction of traversal ('forward' or 'reverse') on that edge. 

916 

917 Raises 

918 ------ 

919 NetworkXNoCycle 

920 If no cycle was found. 

921 

922 Examples 

923 -------- 

924 In this example, we construct a DAG and find, in the first call, that there 

925 are no directed cycles, and so an exception is raised. In the second call, 

926 we ignore edge orientations and find that there is an undirected cycle. 

927 Note that the second call finds a directed cycle while effectively 

928 traversing an undirected graph, and so, we found an "undirected cycle". 

929 This means that this DAG structure does not form a directed tree (which 

930 is also known as a polytree). 

931 

932 >>> G = nx.DiGraph([(0, 1), (0, 2), (1, 2)]) 

933 >>> nx.find_cycle(G, orientation="original") 

934 Traceback (most recent call last): 

935 ... 

936 networkx.exception.NetworkXNoCycle: No cycle found. 

937 >>> list(nx.find_cycle(G, orientation="ignore")) 

938 [(0, 1, 'forward'), (1, 2, 'forward'), (0, 2, 'reverse')] 

939 

940 See Also 

941 -------- 

942 simple_cycles 

943 """ 

944 if not G.is_directed() or orientation in (None, "original"): 

945 

946 def tailhead(edge): 

947 return edge[:2] 

948 

949 elif orientation == "reverse": 

950 

951 def tailhead(edge): 

952 return edge[1], edge[0] 

953 

954 elif orientation == "ignore": 

955 

956 def tailhead(edge): 

957 if edge[-1] == "reverse": 

958 return edge[1], edge[0] 

959 return edge[:2] 

960 

961 explored = set() 

962 cycle = [] 

963 final_node = None 

964 for start_node in G.nbunch_iter(source): 

965 if start_node in explored: 

966 # No loop is possible. 

967 continue 

968 

969 edges = [] 

970 # All nodes seen in this iteration of edge_dfs 

971 seen = {start_node} 

972 # Nodes in active path. 

973 active_nodes = {start_node} 

974 previous_head = None 

975 

976 for edge in nx.edge_dfs(G, start_node, orientation): 

977 # Determine if this edge is a continuation of the active path. 

978 tail, head = tailhead(edge) 

979 if head in explored: 

980 # Then we've already explored it. No loop is possible. 

981 continue 

982 if previous_head is not None and tail != previous_head: 

983 # This edge results from backtracking. 

984 # Pop until we get a node whose head equals the current tail. 

985 # So for example, we might have: 

986 # (0, 1), (1, 2), (2, 3), (1, 4) 

987 # which must become: 

988 # (0, 1), (1, 4) 

989 while True: 

990 try: 

991 popped_edge = edges.pop() 

992 except IndexError: 

993 edges = [] 

994 active_nodes = {tail} 

995 break 

996 else: 

997 popped_head = tailhead(popped_edge)[1] 

998 active_nodes.remove(popped_head) 

999 

1000 if edges: 

1001 last_head = tailhead(edges[-1])[1] 

1002 if tail == last_head: 

1003 break 

1004 edges.append(edge) 

1005 

1006 if head in active_nodes: 

1007 # We have a loop! 

1008 cycle.extend(edges) 

1009 final_node = head 

1010 break 

1011 else: 

1012 seen.add(head) 

1013 active_nodes.add(head) 

1014 previous_head = head 

1015 

1016 if cycle: 

1017 break 

1018 else: 

1019 explored.update(seen) 

1020 

1021 else: 

1022 assert len(cycle) == 0 

1023 raise nx.exception.NetworkXNoCycle("No cycle found.") 

1024 

1025 # We now have a list of edges which ends on a cycle. 

1026 # So we need to remove from the beginning edges that are not relevant. 

1027 

1028 for i, edge in enumerate(cycle): 

1029 tail, head = tailhead(edge) 

1030 if tail == final_node: 

1031 break 

1032 

1033 return cycle[i:] 

1034 

1035 

1036@not_implemented_for("directed") 

1037@not_implemented_for("multigraph") 

1038@nx._dispatch(edge_attrs="weight") 

1039def minimum_cycle_basis(G, weight=None): 

1040 """Returns a minimum weight cycle basis for G 

1041 

1042 Minimum weight means a cycle basis for which the total weight 

1043 (length for unweighted graphs) of all the cycles is minimum. 

1044 

1045 Parameters 

1046 ---------- 

1047 G : NetworkX Graph 

1048 weight: string 

1049 name of the edge attribute to use for edge weights 

1050 

1051 Returns 

1052 ------- 

1053 A list of cycle lists. Each cycle list is a list of nodes 

1054 which forms a cycle (loop) in G. Note that the nodes are not 

1055 necessarily returned in a order by which they appear in the cycle 

1056 

1057 Examples 

1058 -------- 

1059 >>> G = nx.Graph() 

1060 >>> nx.add_cycle(G, [0, 1, 2, 3]) 

1061 >>> nx.add_cycle(G, [0, 3, 4, 5]) 

1062 >>> nx.minimum_cycle_basis(G) 

1063 [[5, 4, 3, 0], [3, 2, 1, 0]] 

1064 

1065 References: 

1066 [1] Kavitha, Telikepalli, et al. "An O(m^2n) Algorithm for 

1067 Minimum Cycle Basis of Graphs." 

1068 http://link.springer.com/article/10.1007/s00453-007-9064-z 

1069 [2] de Pina, J. 1995. Applications of shortest path methods. 

1070 Ph.D. thesis, University of Amsterdam, Netherlands 

1071 

1072 See Also 

1073 -------- 

1074 simple_cycles, cycle_basis 

1075 """ 

1076 # We first split the graph in connected subgraphs 

1077 return sum( 

1078 (_min_cycle_basis(G.subgraph(c), weight) for c in nx.connected_components(G)), 

1079 [], 

1080 ) 

1081 

1082 

1083def _min_cycle_basis(G, weight): 

1084 cb = [] 

1085 # We extract the edges not in a spanning tree. We do not really need a 

1086 # *minimum* spanning tree. That is why we call the next function with 

1087 # weight=None. Depending on implementation, it may be faster as well 

1088 tree_edges = list(nx.minimum_spanning_edges(G, weight=None, data=False)) 

1089 chords = G.edges - tree_edges - {(v, u) for u, v in tree_edges} 

1090 

1091 # We maintain a set of vectors orthogonal to sofar found cycles 

1092 set_orth = [{edge} for edge in chords] 

1093 while set_orth: 

1094 base = set_orth.pop() 

1095 # kth cycle is "parallel" to kth vector in set_orth 

1096 cycle_edges = _min_cycle(G, base, weight) 

1097 cb.append([v for u, v in cycle_edges]) 

1098 

1099 # now update set_orth so that k+1,k+2... th elements are 

1100 # orthogonal to the newly found cycle, as per [p. 336, 1] 

1101 set_orth = [ 

1102 ( 

1103 {e for e in orth if e not in base if e[::-1] not in base} 

1104 | {e for e in base if e not in orth if e[::-1] not in orth} 

1105 ) 

1106 if sum((e in orth or e[::-1] in orth) for e in cycle_edges) % 2 

1107 else orth 

1108 for orth in set_orth 

1109 ] 

1110 return cb 

1111 

1112 

1113def _min_cycle(G, orth, weight): 

1114 """ 

1115 Computes the minimum weight cycle in G, 

1116 orthogonal to the vector orth as per [p. 338, 1] 

1117 Use (u, 1) to indicate the lifted copy of u (denoted u' in paper). 

1118 """ 

1119 Gi = nx.Graph() 

1120 

1121 # Add 2 copies of each edge in G to Gi. 

1122 # If edge is in orth, add cross edge; otherwise in-plane edge 

1123 for u, v, wt in G.edges(data=weight, default=1): 

1124 if (u, v) in orth or (v, u) in orth: 

1125 Gi.add_edges_from([(u, (v, 1)), ((u, 1), v)], Gi_weight=wt) 

1126 else: 

1127 Gi.add_edges_from([(u, v), ((u, 1), (v, 1))], Gi_weight=wt) 

1128 

1129 # find the shortest length in Gi between n and (n, 1) for each n 

1130 # Note: Use "Gi_weight" for name of weight attribute 

1131 spl = nx.shortest_path_length 

1132 lift = {n: spl(Gi, source=n, target=(n, 1), weight="Gi_weight") for n in G} 

1133 

1134 # Now compute that short path in Gi, which translates to a cycle in G 

1135 start = min(lift, key=lift.get) 

1136 end = (start, 1) 

1137 min_path_i = nx.shortest_path(Gi, source=start, target=end, weight="Gi_weight") 

1138 

1139 # Now we obtain the actual path, re-map nodes in Gi to those in G 

1140 min_path = [n if n in G else n[0] for n in min_path_i] 

1141 

1142 # Now remove the edges that occur two times 

1143 # two passes: flag which edges get kept, then build it 

1144 edgelist = list(pairwise(min_path)) 

1145 edgeset = set() 

1146 for e in edgelist: 

1147 if e in edgeset: 

1148 edgeset.remove(e) 

1149 elif e[::-1] in edgeset: 

1150 edgeset.remove(e[::-1]) 

1151 else: 

1152 edgeset.add(e) 

1153 

1154 min_edgelist = [] 

1155 for e in edgelist: 

1156 if e in edgeset: 

1157 min_edgelist.append(e) 

1158 edgeset.remove(e) 

1159 elif e[::-1] in edgeset: 

1160 min_edgelist.append(e[::-1]) 

1161 edgeset.remove(e[::-1]) 

1162 

1163 return min_edgelist 

1164 

1165 

1166@not_implemented_for("directed") 

1167@not_implemented_for("multigraph") 

1168@nx._dispatch 

1169def girth(G): 

1170 """Returns the girth of the graph. 

1171 

1172 The girth of a graph is the length of its shortest cycle, or infinity if 

1173 the graph is acyclic. The algorithm follows the description given on the 

1174 Wikipedia page [1]_, and runs in time O(mn) on a graph with m edges and n 

1175 nodes. 

1176 

1177 Parameters 

1178 ---------- 

1179 G : NetworkX Graph 

1180 

1181 Returns 

1182 ------- 

1183 int or math.inf 

1184 

1185 Examples 

1186 -------- 

1187 All examples below (except P_5) can easily be checked using Wikipedia, 

1188 which has a page for each of these famous graphs. 

1189 

1190 >>> nx.girth(nx.chvatal_graph()) 

1191 4 

1192 >>> nx.girth(nx.tutte_graph()) 

1193 4 

1194 >>> nx.girth(nx.petersen_graph()) 

1195 5 

1196 >>> nx.girth(nx.heawood_graph()) 

1197 6 

1198 >>> nx.girth(nx.pappus_graph()) 

1199 6 

1200 >>> nx.girth(nx.path_graph(5)) 

1201 inf 

1202 

1203 References 

1204 ---------- 

1205 .. [1] https://en.wikipedia.org/wiki/Girth_(graph_theory) 

1206 

1207 """ 

1208 girth = depth_limit = inf 

1209 tree_edge = nx.algorithms.traversal.breadth_first_search.TREE_EDGE 

1210 level_edge = nx.algorithms.traversal.breadth_first_search.LEVEL_EDGE 

1211 for n in G: 

1212 # run a BFS from source n, keeping track of distances; since we want 

1213 # the shortest cycle, no need to explore beyond the current minimum length 

1214 depth = {n: 0} 

1215 for u, v, label in nx.bfs_labeled_edges(G, n): 

1216 du = depth[u] 

1217 if du > depth_limit: 

1218 break 

1219 if label is tree_edge: 

1220 depth[v] = du + 1 

1221 else: 

1222 # if (u, v) is a level edge, the length is du + du + 1 (odd) 

1223 # otherwise, it's a forward edge; length is du + (du + 1) + 1 (even) 

1224 delta = label is level_edge 

1225 length = du + du + 2 - delta 

1226 if length < girth: 

1227 girth = length 

1228 depth_limit = du - delta 

1229 

1230 return girth