passagemath-graphs 10.6.1rc1__cp310-cp310-musllinux_1_2_aarch64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. passagemath_graphs-10.6.1rc1.dist-info/METADATA +292 -0
  2. passagemath_graphs-10.6.1rc1.dist-info/RECORD +260 -0
  3. passagemath_graphs-10.6.1rc1.dist-info/WHEEL +5 -0
  4. passagemath_graphs-10.6.1rc1.dist-info/top_level.txt +2 -0
  5. passagemath_graphs.libs/libgcc_s-69c45f16.so.1 +0 -0
  6. passagemath_graphs.libs/libgmp-8e78bd9b.so.10.5.0 +0 -0
  7. passagemath_graphs.libs/libstdc++-1f1a71be.so.6.0.33 +0 -0
  8. sage/all__sagemath_graphs.py +39 -0
  9. sage/combinat/abstract_tree.py +2723 -0
  10. sage/combinat/all__sagemath_graphs.py +34 -0
  11. sage/combinat/binary_tree.py +5306 -0
  12. sage/combinat/cluster_algebra_quiver/all.py +22 -0
  13. sage/combinat/cluster_algebra_quiver/cluster_seed.py +5208 -0
  14. sage/combinat/cluster_algebra_quiver/interact.py +124 -0
  15. sage/combinat/cluster_algebra_quiver/mutation_class.py +625 -0
  16. sage/combinat/cluster_algebra_quiver/mutation_type.py +1555 -0
  17. sage/combinat/cluster_algebra_quiver/quiver.py +2290 -0
  18. sage/combinat/cluster_algebra_quiver/quiver_mutation_type.py +2468 -0
  19. sage/combinat/designs/MOLS_handbook_data.py +570 -0
  20. sage/combinat/designs/all.py +58 -0
  21. sage/combinat/designs/bibd.py +1655 -0
  22. sage/combinat/designs/block_design.py +1071 -0
  23. sage/combinat/designs/covering_array.py +269 -0
  24. sage/combinat/designs/covering_design.py +530 -0
  25. sage/combinat/designs/database.py +5615 -0
  26. sage/combinat/designs/design_catalog.py +122 -0
  27. sage/combinat/designs/designs_pyx.cpython-310-aarch64-linux-gnu.so +0 -0
  28. sage/combinat/designs/designs_pyx.pxd +21 -0
  29. sage/combinat/designs/designs_pyx.pyx +993 -0
  30. sage/combinat/designs/difference_family.py +3951 -0
  31. sage/combinat/designs/difference_matrices.py +279 -0
  32. sage/combinat/designs/evenly_distributed_sets.cpython-310-aarch64-linux-gnu.so +0 -0
  33. sage/combinat/designs/evenly_distributed_sets.pyx +661 -0
  34. sage/combinat/designs/ext_rep.py +1064 -0
  35. sage/combinat/designs/gen_quadrangles_with_spread.cpython-310-aarch64-linux-gnu.so +0 -0
  36. sage/combinat/designs/gen_quadrangles_with_spread.pyx +339 -0
  37. sage/combinat/designs/group_divisible_designs.py +361 -0
  38. sage/combinat/designs/incidence_structures.py +2357 -0
  39. sage/combinat/designs/latin_squares.py +581 -0
  40. sage/combinat/designs/orthogonal_arrays.py +2244 -0
  41. sage/combinat/designs/orthogonal_arrays_build_recursive.py +1780 -0
  42. sage/combinat/designs/orthogonal_arrays_find_recursive.cpython-310-aarch64-linux-gnu.so +0 -0
  43. sage/combinat/designs/orthogonal_arrays_find_recursive.pyx +967 -0
  44. sage/combinat/designs/resolvable_bibd.py +815 -0
  45. sage/combinat/designs/steiner_quadruple_systems.py +1306 -0
  46. sage/combinat/designs/subhypergraph_search.cpython-310-aarch64-linux-gnu.so +0 -0
  47. sage/combinat/designs/subhypergraph_search.pyx +530 -0
  48. sage/combinat/designs/twographs.py +306 -0
  49. sage/combinat/finite_state_machine.py +14874 -0
  50. sage/combinat/finite_state_machine_generators.py +2006 -0
  51. sage/combinat/graph_path.py +448 -0
  52. sage/combinat/interval_posets.py +3908 -0
  53. sage/combinat/nu_tamari_lattice.py +269 -0
  54. sage/combinat/ordered_tree.py +1446 -0
  55. sage/combinat/posets/all.py +46 -0
  56. sage/combinat/posets/bubble_shuffle.py +247 -0
  57. sage/combinat/posets/cartesian_product.py +493 -0
  58. sage/combinat/posets/d_complete.py +182 -0
  59. sage/combinat/posets/elements.py +273 -0
  60. sage/combinat/posets/forest.py +30 -0
  61. sage/combinat/posets/hasse_cython.cpython-310-aarch64-linux-gnu.so +0 -0
  62. sage/combinat/posets/hasse_cython.pyx +174 -0
  63. sage/combinat/posets/hasse_diagram.py +3672 -0
  64. sage/combinat/posets/hochschild_lattice.py +158 -0
  65. sage/combinat/posets/incidence_algebras.py +794 -0
  66. sage/combinat/posets/lattices.py +5117 -0
  67. sage/combinat/posets/linear_extension_iterator.cpython-310-aarch64-linux-gnu.so +0 -0
  68. sage/combinat/posets/linear_extension_iterator.pyx +292 -0
  69. sage/combinat/posets/linear_extensions.py +1037 -0
  70. sage/combinat/posets/mobile.py +275 -0
  71. sage/combinat/posets/moebius_algebra.py +776 -0
  72. sage/combinat/posets/poset_examples.py +2178 -0
  73. sage/combinat/posets/posets.py +9360 -0
  74. sage/combinat/rooted_tree.py +1070 -0
  75. sage/combinat/shard_order.py +239 -0
  76. sage/combinat/tamari_lattices.py +384 -0
  77. sage/combinat/yang_baxter_graph.py +923 -0
  78. sage/databases/all__sagemath_graphs.py +1 -0
  79. sage/databases/knotinfo_db.py +1231 -0
  80. sage/ext_data/all__sagemath_graphs.py +1 -0
  81. sage/ext_data/graphs/graph_plot_js.html +330 -0
  82. sage/ext_data/kenzo/CP2.txt +45 -0
  83. sage/ext_data/kenzo/CP3.txt +349 -0
  84. sage/ext_data/kenzo/CP4.txt +4774 -0
  85. sage/ext_data/kenzo/README.txt +49 -0
  86. sage/ext_data/kenzo/S4.txt +20 -0
  87. sage/graphs/all.py +42 -0
  88. sage/graphs/asteroidal_triples.cpython-310-aarch64-linux-gnu.so +0 -0
  89. sage/graphs/asteroidal_triples.pyx +320 -0
  90. sage/graphs/base/all.py +1 -0
  91. sage/graphs/base/boost_graph.cpython-310-aarch64-linux-gnu.so +0 -0
  92. sage/graphs/base/boost_graph.pxd +106 -0
  93. sage/graphs/base/boost_graph.pyx +3045 -0
  94. sage/graphs/base/c_graph.cpython-310-aarch64-linux-gnu.so +0 -0
  95. sage/graphs/base/c_graph.pxd +106 -0
  96. sage/graphs/base/c_graph.pyx +5096 -0
  97. sage/graphs/base/dense_graph.cpython-310-aarch64-linux-gnu.so +0 -0
  98. sage/graphs/base/dense_graph.pxd +28 -0
  99. sage/graphs/base/dense_graph.pyx +801 -0
  100. sage/graphs/base/graph_backends.cpython-310-aarch64-linux-gnu.so +0 -0
  101. sage/graphs/base/graph_backends.pxd +5 -0
  102. sage/graphs/base/graph_backends.pyx +797 -0
  103. sage/graphs/base/overview.py +85 -0
  104. sage/graphs/base/sparse_graph.cpython-310-aarch64-linux-gnu.so +0 -0
  105. sage/graphs/base/sparse_graph.pxd +90 -0
  106. sage/graphs/base/sparse_graph.pyx +1653 -0
  107. sage/graphs/base/static_dense_graph.cpython-310-aarch64-linux-gnu.so +0 -0
  108. sage/graphs/base/static_dense_graph.pxd +5 -0
  109. sage/graphs/base/static_dense_graph.pyx +1032 -0
  110. sage/graphs/base/static_sparse_backend.cpython-310-aarch64-linux-gnu.so +0 -0
  111. sage/graphs/base/static_sparse_backend.pxd +27 -0
  112. sage/graphs/base/static_sparse_backend.pyx +1583 -0
  113. sage/graphs/base/static_sparse_graph.cpython-310-aarch64-linux-gnu.so +0 -0
  114. sage/graphs/base/static_sparse_graph.pxd +37 -0
  115. sage/graphs/base/static_sparse_graph.pyx +1375 -0
  116. sage/graphs/bipartite_graph.py +2732 -0
  117. sage/graphs/centrality.cpython-310-aarch64-linux-gnu.so +0 -0
  118. sage/graphs/centrality.pyx +1038 -0
  119. sage/graphs/cographs.py +519 -0
  120. sage/graphs/comparability.cpython-310-aarch64-linux-gnu.so +0 -0
  121. sage/graphs/comparability.pyx +851 -0
  122. sage/graphs/connectivity.cpython-310-aarch64-linux-gnu.so +0 -0
  123. sage/graphs/connectivity.pxd +157 -0
  124. sage/graphs/connectivity.pyx +4813 -0
  125. sage/graphs/convexity_properties.cpython-310-aarch64-linux-gnu.so +0 -0
  126. sage/graphs/convexity_properties.pxd +16 -0
  127. sage/graphs/convexity_properties.pyx +870 -0
  128. sage/graphs/digraph.py +4754 -0
  129. sage/graphs/digraph_generators.py +1993 -0
  130. sage/graphs/distances_all_pairs.cpython-310-aarch64-linux-gnu.so +0 -0
  131. sage/graphs/distances_all_pairs.pxd +12 -0
  132. sage/graphs/distances_all_pairs.pyx +2938 -0
  133. sage/graphs/domination.py +1363 -0
  134. sage/graphs/dot2tex_utils.py +100 -0
  135. sage/graphs/edge_connectivity.cpython-310-aarch64-linux-gnu.so +0 -0
  136. sage/graphs/edge_connectivity.pyx +1215 -0
  137. sage/graphs/generators/all.py +1 -0
  138. sage/graphs/generators/basic.py +1769 -0
  139. sage/graphs/generators/chessboard.py +538 -0
  140. sage/graphs/generators/classical_geometries.py +1611 -0
  141. sage/graphs/generators/degree_sequence.py +235 -0
  142. sage/graphs/generators/distance_regular.cpython-310-aarch64-linux-gnu.so +0 -0
  143. sage/graphs/generators/distance_regular.pyx +2846 -0
  144. sage/graphs/generators/families.py +4759 -0
  145. sage/graphs/generators/intersection.py +565 -0
  146. sage/graphs/generators/platonic_solids.py +262 -0
  147. sage/graphs/generators/random.py +2623 -0
  148. sage/graphs/generators/smallgraphs.py +5741 -0
  149. sage/graphs/generators/world_map.py +724 -0
  150. sage/graphs/generic_graph.py +26867 -0
  151. sage/graphs/generic_graph_pyx.cpython-310-aarch64-linux-gnu.so +0 -0
  152. sage/graphs/generic_graph_pyx.pxd +34 -0
  153. sage/graphs/generic_graph_pyx.pyx +1673 -0
  154. sage/graphs/genus.cpython-310-aarch64-linux-gnu.so +0 -0
  155. sage/graphs/genus.pyx +622 -0
  156. sage/graphs/graph.py +9645 -0
  157. sage/graphs/graph_coloring.cpython-310-aarch64-linux-gnu.so +0 -0
  158. sage/graphs/graph_coloring.pyx +2284 -0
  159. sage/graphs/graph_database.py +1177 -0
  160. sage/graphs/graph_decompositions/all.py +1 -0
  161. sage/graphs/graph_decompositions/bandwidth.cpython-310-aarch64-linux-gnu.so +0 -0
  162. sage/graphs/graph_decompositions/bandwidth.pyx +428 -0
  163. sage/graphs/graph_decompositions/clique_separators.cpython-310-aarch64-linux-gnu.so +0 -0
  164. sage/graphs/graph_decompositions/clique_separators.pyx +616 -0
  165. sage/graphs/graph_decompositions/cutwidth.cpython-310-aarch64-linux-gnu.so +0 -0
  166. sage/graphs/graph_decompositions/cutwidth.pyx +753 -0
  167. sage/graphs/graph_decompositions/fast_digraph.cpython-310-aarch64-linux-gnu.so +0 -0
  168. sage/graphs/graph_decompositions/fast_digraph.pxd +13 -0
  169. sage/graphs/graph_decompositions/fast_digraph.pyx +212 -0
  170. sage/graphs/graph_decompositions/graph_products.cpython-310-aarch64-linux-gnu.so +0 -0
  171. sage/graphs/graph_decompositions/graph_products.pyx +508 -0
  172. sage/graphs/graph_decompositions/modular_decomposition.cpython-310-aarch64-linux-gnu.so +0 -0
  173. sage/graphs/graph_decompositions/modular_decomposition.pxd +27 -0
  174. sage/graphs/graph_decompositions/modular_decomposition.pyx +1536 -0
  175. sage/graphs/graph_decompositions/slice_decomposition.cpython-310-aarch64-linux-gnu.so +0 -0
  176. sage/graphs/graph_decompositions/slice_decomposition.pxd +18 -0
  177. sage/graphs/graph_decompositions/slice_decomposition.pyx +1106 -0
  178. sage/graphs/graph_decompositions/tree_decomposition.cpython-310-aarch64-linux-gnu.so +0 -0
  179. sage/graphs/graph_decompositions/tree_decomposition.pxd +17 -0
  180. sage/graphs/graph_decompositions/tree_decomposition.pyx +1996 -0
  181. sage/graphs/graph_decompositions/vertex_separation.cpython-310-aarch64-linux-gnu.so +0 -0
  182. sage/graphs/graph_decompositions/vertex_separation.pxd +5 -0
  183. sage/graphs/graph_decompositions/vertex_separation.pyx +1963 -0
  184. sage/graphs/graph_editor.py +82 -0
  185. sage/graphs/graph_generators.py +3314 -0
  186. sage/graphs/graph_generators_pyx.cpython-310-aarch64-linux-gnu.so +0 -0
  187. sage/graphs/graph_generators_pyx.pyx +95 -0
  188. sage/graphs/graph_input.py +812 -0
  189. sage/graphs/graph_latex.py +2064 -0
  190. sage/graphs/graph_list.py +410 -0
  191. sage/graphs/graph_plot.py +1756 -0
  192. sage/graphs/graph_plot_js.py +338 -0
  193. sage/graphs/hyperbolicity.cpython-310-aarch64-linux-gnu.so +0 -0
  194. sage/graphs/hyperbolicity.pyx +1704 -0
  195. sage/graphs/hypergraph_generators.py +364 -0
  196. sage/graphs/independent_sets.cpython-310-aarch64-linux-gnu.so +0 -0
  197. sage/graphs/independent_sets.pxd +13 -0
  198. sage/graphs/independent_sets.pyx +402 -0
  199. sage/graphs/isgci.py +1033 -0
  200. sage/graphs/isoperimetric_inequalities.cpython-310-aarch64-linux-gnu.so +0 -0
  201. sage/graphs/isoperimetric_inequalities.pyx +489 -0
  202. sage/graphs/line_graph.cpython-310-aarch64-linux-gnu.so +0 -0
  203. sage/graphs/line_graph.pyx +743 -0
  204. sage/graphs/lovasz_theta.py +77 -0
  205. sage/graphs/matching.py +1633 -0
  206. sage/graphs/matching_covered_graph.py +3590 -0
  207. sage/graphs/orientations.py +1489 -0
  208. sage/graphs/partial_cube.py +459 -0
  209. sage/graphs/path_enumeration.cpython-310-aarch64-linux-gnu.so +0 -0
  210. sage/graphs/path_enumeration.pyx +2040 -0
  211. sage/graphs/pq_trees.py +1129 -0
  212. sage/graphs/print_graphs.py +201 -0
  213. sage/graphs/schnyder.py +865 -0
  214. sage/graphs/spanning_tree.cpython-310-aarch64-linux-gnu.so +0 -0
  215. sage/graphs/spanning_tree.pyx +1457 -0
  216. sage/graphs/strongly_regular_db.cpython-310-aarch64-linux-gnu.so +0 -0
  217. sage/graphs/strongly_regular_db.pyx +3340 -0
  218. sage/graphs/traversals.cpython-310-aarch64-linux-gnu.so +0 -0
  219. sage/graphs/traversals.pxd +9 -0
  220. sage/graphs/traversals.pyx +1872 -0
  221. sage/graphs/trees.cpython-310-aarch64-linux-gnu.so +0 -0
  222. sage/graphs/trees.pxd +15 -0
  223. sage/graphs/trees.pyx +310 -0
  224. sage/graphs/tutte_polynomial.py +713 -0
  225. sage/graphs/views.cpython-310-aarch64-linux-gnu.so +0 -0
  226. sage/graphs/views.pyx +794 -0
  227. sage/graphs/weakly_chordal.cpython-310-aarch64-linux-gnu.so +0 -0
  228. sage/graphs/weakly_chordal.pyx +604 -0
  229. sage/groups/all__sagemath_graphs.py +1 -0
  230. sage/groups/perm_gps/all__sagemath_graphs.py +1 -0
  231. sage/groups/perm_gps/partn_ref/all__sagemath_graphs.py +1 -0
  232. sage/groups/perm_gps/partn_ref/refinement_graphs.cpython-310-aarch64-linux-gnu.so +0 -0
  233. sage/groups/perm_gps/partn_ref/refinement_graphs.pxd +38 -0
  234. sage/groups/perm_gps/partn_ref/refinement_graphs.pyx +1666 -0
  235. sage/knots/all.py +6 -0
  236. sage/knots/free_knotinfo_monoid.py +507 -0
  237. sage/knots/gauss_code.py +291 -0
  238. sage/knots/knot.py +682 -0
  239. sage/knots/knot_table.py +284 -0
  240. sage/knots/knotinfo.py +2900 -0
  241. sage/knots/link.py +4715 -0
  242. sage/sandpiles/all.py +13 -0
  243. sage/sandpiles/examples.py +225 -0
  244. sage/sandpiles/sandpile.py +6365 -0
  245. sage/topology/all.py +22 -0
  246. sage/topology/cell_complex.py +1214 -0
  247. sage/topology/cubical_complex.py +1976 -0
  248. sage/topology/delta_complex.py +1806 -0
  249. sage/topology/filtered_simplicial_complex.py +744 -0
  250. sage/topology/moment_angle_complex.py +823 -0
  251. sage/topology/simplicial_complex.py +5160 -0
  252. sage/topology/simplicial_complex_catalog.py +92 -0
  253. sage/topology/simplicial_complex_examples.py +1680 -0
  254. sage/topology/simplicial_complex_homset.py +205 -0
  255. sage/topology/simplicial_complex_morphism.py +836 -0
  256. sage/topology/simplicial_set.py +4102 -0
  257. sage/topology/simplicial_set_catalog.py +55 -0
  258. sage/topology/simplicial_set_constructions.py +2954 -0
  259. sage/topology/simplicial_set_examples.py +865 -0
  260. sage/topology/simplicial_set_morphism.py +1464 -0
@@ -0,0 +1,4813 @@
1
+ # sage_setup: distribution = sagemath-graphs
2
+ # cython: binding=True
3
+ r"""
4
+ Connectivity related functions
5
+
6
+ This module implements the connectivity based functions for graphs and digraphs.
7
+ The methods in this module are also available as part of GenericGraph, DiGraph
8
+ or Graph classes as aliases, and these methods can be accessed through this
9
+ module or as class methods.
10
+ Here is what the module can do:
11
+
12
+ **For both directed and undirected graphs:**
13
+
14
+ .. csv-table::
15
+ :class: contentstable
16
+ :widths: 30, 70
17
+ :delim: |
18
+
19
+ :meth:`is_connected` | Check whether the (di)graph is connected.
20
+ :meth:`connected_components` | Return the list of connected components
21
+ :meth:`connected_components_number` | Return the number of connected components.
22
+ :meth:`connected_components_subgraphs` | Return a list of connected components as graph objects.
23
+ :meth:`connected_component_containing_vertex` | Return a list of the vertices connected to vertex.
24
+ :meth:`connected_components_sizes` | Return the sizes of the connected components as a list.
25
+ :meth:`blocks_and_cut_vertices` | Return the blocks and cut vertices of the graph.
26
+ :meth:`blocks_and_cuts_tree` | Return the blocks-and-cuts tree of the graph.
27
+ :meth:`is_cut_edge` | Check whether the input edge is a cut-edge or a bridge.
28
+ :meth:`is_edge_cut` | Check whether the input edges form an edge cut.
29
+ :meth:`is_cut_vertex` | Check whether the input vertex is a cut-vertex.
30
+ :meth:`is_vertex_cut` | Check whether the input vertices form a vertex cut.
31
+ :meth:`edge_connectivity` | Return the edge connectivity of the graph.
32
+ :meth:`vertex_connectivity` | Return the vertex connectivity of the graph.
33
+
34
+ **For DiGraph:**
35
+
36
+ .. csv-table::
37
+ :class: contentstable
38
+ :widths: 30, 70
39
+ :delim: |
40
+
41
+ :meth:`is_strongly_connected` | Check whether the current ``DiGraph`` is strongly connected.
42
+ :meth:`strongly_connected_components_digraph` | Return the digraph of the strongly connected components
43
+ :meth:`strongly_connected_components_subgraphs` | Return the strongly connected components as a list of subgraphs.
44
+ :meth:`strongly_connected_component_containing_vertex` | Return the strongly connected component containing a given vertex.
45
+ :meth:`strong_articulation_points` | Return the strong articulation points of this digraph.
46
+
47
+ **For undirected graphs:**
48
+
49
+ .. csv-table::
50
+ :class: contentstable
51
+ :widths: 30, 70
52
+ :delim: |
53
+
54
+ :meth:`bridges` | Return an iterator over the bridges (or cut edges) of given undirected graph.
55
+ :meth:`cleave` | Return the connected subgraphs separated by the input vertex cut.
56
+ :meth:`is_triconnected` | Check whether the graph is triconnected.
57
+ :meth:`spqr_tree` | Return a SPQR-tree representing the triconnected components of the graph.
58
+ :meth:`spqr_tree_to_graph` | Return the graph represented by the SPQR-tree `T`.
59
+ :meth:`minimal_separators` | Return an iterator over the minimal separators of ``G``.
60
+
61
+ Methods
62
+ -------
63
+ """
64
+
65
+ # ****************************************************************************
66
+ #
67
+ # Copyright (C) 2023 David Coudert <david.coudert@inria.fr>
68
+ #
69
+ # This program is free software: you can redistribute it and/or modify
70
+ # it under the terms of the GNU General Public License as published by
71
+ # the Free Software Foundation, either version 2 of the License, or
72
+ # (at your option) any later version.
73
+ # https://www.gnu.org/licenses/
74
+ # ****************************************************************************
75
+
76
+ from sage.misc.superseded import deprecation
77
+ from sage.sets.disjoint_set cimport DisjointSet
78
+
79
+
80
+ def is_connected(G, forbidden_vertices=None):
81
+ """
82
+ Check whether the (di)graph is connected.
83
+
84
+ Note that in a graph, path connected is equivalent to connected.
85
+
86
+ INPUT:
87
+
88
+ - ``G`` -- the input graph
89
+
90
+ - ``forbidden_vertices`` -- list (default: ``None``); set of vertices to
91
+ avoid during the search
92
+
93
+ .. SEEALSO::
94
+
95
+ - :meth:`~Graph.is_biconnected`
96
+
97
+ EXAMPLES::
98
+
99
+ sage: from sage.graphs.connectivity import is_connected
100
+ sage: G = Graph({0: [1, 2], 1: [2], 3: [4, 5], 4: [5]})
101
+ sage: is_connected(G)
102
+ False
103
+ sage: G.is_connected()
104
+ False
105
+ sage: G.add_edge(0,3)
106
+ sage: is_connected(G)
107
+ True
108
+ sage: is_connected(G, forbidden_vertices=[3])
109
+ False
110
+ sage: is_connected(G, forbidden_vertices=[1])
111
+ True
112
+ sage: D = DiGraph({0: [1, 2], 1: [2], 3: [4, 5], 4: [5]})
113
+ sage: is_connected(D)
114
+ False
115
+ sage: D.add_edge(0, 3)
116
+ sage: is_connected(D)
117
+ True
118
+ sage: D = DiGraph({1: [0], 2: [0]})
119
+ sage: is_connected(D)
120
+ True
121
+
122
+ TESTS:
123
+
124
+ If ``G`` is not a Sage graph, an error is raised::
125
+
126
+ sage: from sage.graphs.connectivity import is_connected
127
+ sage: is_connected('I am not a graph')
128
+ Traceback (most recent call last):
129
+ ...
130
+ TypeError: the input must be a Sage graph
131
+ """
132
+ from sage.graphs.generic_graph import GenericGraph
133
+ if not isinstance(G, GenericGraph):
134
+ raise TypeError("the input must be a Sage graph")
135
+
136
+ if not G.order():
137
+ return True
138
+
139
+ forbidden = None if forbidden_vertices is None else set(forbidden_vertices)
140
+
141
+ try:
142
+ return G._backend.is_connected(forbidden_vertices=forbidden)
143
+ except AttributeError:
144
+ # Search for a vertex in G that is not forbidden
145
+ if forbidden:
146
+ for v in G:
147
+ if v not in forbidden:
148
+ break
149
+ else:
150
+ # The empty graph is connected, so the graph with only forbidden
151
+ # vertices is also connected
152
+ return True
153
+ else:
154
+ v = next(G.vertex_iterator())
155
+ n = len(forbidden)
156
+ for _ in G.depth_first_search(v, ignore_direction=True,
157
+ forbidden_vertices=forbidden):
158
+ n += 1
159
+ return n == G.num_verts()
160
+
161
+
162
+ def connected_components(G, sort=None, key=None, forbidden_vertices=None):
163
+ """
164
+ Return the list of connected components.
165
+
166
+ This returns a list of lists of vertices, each list representing a connected
167
+ component. The list is ordered from largest to smallest component.
168
+
169
+ INPUT:
170
+
171
+ - ``G`` -- the input graph
172
+
173
+ - ``sort`` -- boolean (default: ``None``); if ``True``, vertices inside each
174
+ component are sorted according to the default ordering
175
+
176
+ As of :issue:`35889`, this argument must be explicitly specified (unless a
177
+ ``key`` is given); otherwise a warning is printed and ``sort=True`` is
178
+ used. The default will eventually be changed to ``False``.
179
+
180
+ - ``key`` -- a function (default: ``None``); a function that takes a
181
+ vertex as its one argument and returns a value that can be used for
182
+ comparisons in the sorting algorithm (we must have ``sort=True``)
183
+
184
+ - ``forbidden_vertices`` -- list (default: ``None``); set of vertices to
185
+ avoid during the search
186
+
187
+ EXAMPLES::
188
+
189
+ sage: from sage.graphs.connectivity import connected_components
190
+ sage: G = Graph({0: [1, 3], 1: [2], 2: [3], 4: [5, 6], 5: [6]})
191
+ sage: connected_components(G, sort=True)
192
+ [[0, 1, 2, 3], [4, 5, 6]]
193
+ sage: G.connected_components(sort=True)
194
+ [[0, 1, 2, 3], [4, 5, 6]]
195
+ sage: D = DiGraph({0: [1, 3], 1: [2], 2: [3], 4: [5, 6], 5: [6]})
196
+ sage: connected_components(D, sort=True)
197
+ [[0, 1, 2, 3], [4, 5, 6]]
198
+ sage: connected_components(D, sort=True, key=lambda x: -x)
199
+ [[3, 2, 1, 0], [6, 5, 4]]
200
+
201
+ Connected components in a graph with forbidden vertices::
202
+
203
+ sage: G = graphs.PathGraph(5)
204
+ sage: connected_components(G, sort=True, forbidden_vertices=[2])
205
+ [[0, 1], [3, 4]]
206
+ sage: connected_components(G, sort=True,
207
+ ....: forbidden_vertices=G.neighbor_iterator(2, closed=True))
208
+ [[0], [4]]
209
+
210
+ TESTS:
211
+
212
+ If ``G`` is not a Sage graph, an error is raised::
213
+
214
+ sage: from sage.graphs.connectivity import connected_components
215
+ sage: connected_components('I am not a graph')
216
+ Traceback (most recent call last):
217
+ ...
218
+ TypeError: the input must be a Sage graph
219
+
220
+ When parameter ``key`` is set, parameter ``sort`` must be ``True``::
221
+
222
+ sage: G = Graph(2)
223
+ sage: G.connected_components(sort=False, key=lambda x: x)
224
+ Traceback (most recent call last):
225
+ ...
226
+ ValueError: sort keyword is False, yet a key function is given
227
+
228
+ Deprecation warning for ``sort=None`` (:issue:`35889`)::
229
+
230
+ sage: G = graphs.HouseGraph()
231
+ sage: G.connected_components()
232
+ doctest:...: DeprecationWarning: parameter 'sort' will be set to False by default in the future
233
+ See https://github.com/sagemath/sage/issues/35889 for details.
234
+ [[0, 1, 2, 3, 4]]
235
+ """
236
+ from sage.graphs.generic_graph import GenericGraph
237
+ if not isinstance(G, GenericGraph):
238
+ raise TypeError("the input must be a Sage graph")
239
+
240
+ if sort is None:
241
+ if key is None:
242
+ deprecation(35889, "parameter 'sort' will be set to False by default in the future")
243
+ sort = True
244
+
245
+ if (not sort) and key:
246
+ raise ValueError('sort keyword is False, yet a key function is given')
247
+
248
+ cdef set seen = set(forbidden_vertices) if forbidden_vertices else set()
249
+ cdef list components = []
250
+ for v in G:
251
+ if v not in seen:
252
+ c = connected_component_containing_vertex(G, v, sort=sort, key=key,
253
+ forbidden_vertices=seen)
254
+ seen.update(c)
255
+ components.append(c)
256
+ components.sort(key=lambda comp: -len(comp))
257
+ return components
258
+
259
+
260
+ def connected_components_number(G, forbidden_vertices=None):
261
+ """
262
+ Return the number of connected components.
263
+
264
+ INPUT:
265
+
266
+ - ``G`` -- the input graph
267
+
268
+ - ``forbidden_vertices`` -- list (default: ``None``); set of vertices to
269
+ avoid during the search
270
+
271
+ EXAMPLES::
272
+
273
+ sage: from sage.graphs.connectivity import connected_components_number
274
+ sage: G = Graph({0: [1, 3], 1: [2], 2: [3], 4: [5, 6], 5: [6]})
275
+ sage: connected_components_number(G)
276
+ 2
277
+ sage: G.connected_components_number()
278
+ 2
279
+ sage: D = DiGraph({0: [1, 3], 1: [2], 2: [3], 4: [5, 6], 5: [6]})
280
+ sage: connected_components_number(D)
281
+ 2
282
+ sage: connected_components_number(D, forbidden_vertices=[1, 3])
283
+ 3
284
+
285
+ TESTS:
286
+
287
+ If ``G`` is not a Sage graph, an error is raised::
288
+
289
+ sage: from sage.graphs.connectivity import connected_components_number
290
+ sage: connected_components_number('I am not a graph')
291
+ Traceback (most recent call last):
292
+ ...
293
+ TypeError: the input must be a Sage graph
294
+ """
295
+ return len(connected_components(G, sort=False,
296
+ forbidden_vertices=forbidden_vertices))
297
+
298
+
299
+ def connected_components_subgraphs(G, forbidden_vertices=None):
300
+ """
301
+ Return a list of connected components as graph objects.
302
+
303
+ INPUT:
304
+
305
+ - ``G`` -- the input graph
306
+
307
+ - ``forbidden_vertices`` -- list (default: ``None``); set of vertices to
308
+ avoid during the search
309
+
310
+ EXAMPLES::
311
+
312
+ sage: from sage.graphs.connectivity import connected_components_subgraphs
313
+ sage: G = Graph({0: [1, 3], 1: [2], 2: [3], 4: [5, 6], 5: [6]})
314
+ sage: L = connected_components_subgraphs(G)
315
+ sage: graphs_list.show_graphs(L) # needs sage.plot
316
+ sage: L = connected_components_subgraphs(G, forbidden_vertices=[1, 3])
317
+ sage: graphs_list.show_graphs(L) # needs sage.plot
318
+ sage: D = DiGraph({0: [1, 3], 1: [2], 2: [3], 4: [5, 6], 5: [6]})
319
+ sage: L = connected_components_subgraphs(D)
320
+ sage: graphs_list.show_graphs(L) # needs sage.plot
321
+ sage: L = D.connected_components_subgraphs()
322
+ sage: graphs_list.show_graphs(L) # needs sage.plot
323
+
324
+ TESTS:
325
+
326
+ If ``G`` is not a Sage graph, an error is raised::
327
+
328
+ sage: from sage.graphs.connectivity import connected_components_subgraphs
329
+ sage: connected_components_subgraphs('I am not a graph')
330
+ Traceback (most recent call last):
331
+ ...
332
+ TypeError: the input must be a Sage graph
333
+ """
334
+ from sage.graphs.generic_graph import GenericGraph
335
+ if not isinstance(G, GenericGraph):
336
+ raise TypeError("the input must be a Sage graph")
337
+
338
+ return [G.subgraph(c, inplace=False)
339
+ for c in connected_components(G, sort=False,
340
+ forbidden_vertices=forbidden_vertices)]
341
+
342
+
343
+ def connected_component_containing_vertex(G, vertex, sort=None, key=None,
344
+ forbidden_vertices=None):
345
+ """
346
+ Return a list of the vertices connected to vertex.
347
+
348
+ INPUT:
349
+
350
+ - ``G`` -- the input graph
351
+
352
+ - ``vertex`` -- the vertex to search for
353
+
354
+ - ``sort`` -- boolean (default: ``None``); if ``True``, vertices inside the
355
+ component are sorted according to the default ordering
356
+
357
+ As of :issue:`35889`, this argument must be explicitly specified (unless a
358
+ ``key`` is given); otherwise a warning is printed and ``sort=True`` is
359
+ used. The default will eventually be changed to ``False``.
360
+
361
+ - ``key`` -- a function (default: ``None``); a function that takes a
362
+ vertex as its one argument and returns a value that can be used for
363
+ comparisons in the sorting algorithm (we must have ``sort=True``)
364
+
365
+ - ``forbidden_vertices`` -- list (default: ``None``); set of vertices to
366
+ avoid during the search. The start ``vertex`` cannot be in this set.
367
+
368
+ EXAMPLES::
369
+
370
+ sage: from sage.graphs.connectivity import connected_component_containing_vertex
371
+ sage: G = Graph({0: [1, 3], 1: [2], 2: [3], 4: [5, 6], 5: [6]})
372
+ sage: connected_component_containing_vertex(G, 0, sort=True)
373
+ [0, 1, 2, 3]
374
+ sage: G.connected_component_containing_vertex(0, sort=True)
375
+ [0, 1, 2, 3]
376
+ sage: G.connected_component_containing_vertex(0, sort=True, forbidden_vertices=[1, 3])
377
+ [0]
378
+ sage: D = DiGraph({0: [1, 3], 1: [2], 2: [3], 4: [5, 6], 5: [6]})
379
+ sage: connected_component_containing_vertex(D, 0, sort=True)
380
+ [0, 1, 2, 3]
381
+ sage: connected_component_containing_vertex(D, 0, sort=True, key=lambda x: -x)
382
+ [3, 2, 1, 0]
383
+
384
+ TESTS:
385
+
386
+ If ``G`` is not a Sage graph, an error is raised::
387
+
388
+ sage: from sage.graphs.connectivity import connected_component_containing_vertex
389
+ sage: connected_component_containing_vertex('I am not a graph', 0)
390
+ Traceback (most recent call last):
391
+ ...
392
+ TypeError: the input must be a Sage graph
393
+
394
+ :issue:`35889` is fixed::
395
+
396
+ sage: G = Graph([('A', 1)])
397
+ sage: G.connected_component_containing_vertex(1, sort=False)
398
+ [1, 'A']
399
+ sage: G.connected_component_containing_vertex(1, sort=True)
400
+ Traceback (most recent call last):
401
+ ...
402
+ TypeError: '<' not supported between instances of 'str' and 'int'
403
+
404
+ When parameter ``key`` is set, parameter ``sort`` must be ``True``::
405
+
406
+ sage: G = Graph(2)
407
+ sage: G.connected_component_containing_vertex(1, sort=False, key=lambda x: x)
408
+ Traceback (most recent call last):
409
+ ...
410
+ ValueError: sort keyword is False, yet a key function is given
411
+
412
+ Deprecation warning for ``sort=None`` (:issue:`35889`)::
413
+
414
+ sage: G = graphs.HouseGraph()
415
+ sage: G.connected_component_containing_vertex(1)
416
+ doctest:...: DeprecationWarning: parameter 'sort' will be set to False by default in the future
417
+ See https://github.com/sagemath/sage/issues/35889 for details.
418
+ [0, 1, 2, 3, 4]
419
+ """
420
+ from sage.graphs.generic_graph import GenericGraph
421
+ if not isinstance(G, GenericGraph):
422
+ raise TypeError("the input must be a Sage graph")
423
+
424
+ if sort is None:
425
+ if key is None:
426
+ deprecation(35889, "parameter 'sort' will be set to False by default in the future")
427
+ sort = True
428
+
429
+ if (not sort) and key:
430
+ raise ValueError('sort keyword is False, yet a key function is given')
431
+
432
+ forbidden = None if forbidden_vertices is None else list(forbidden_vertices)
433
+
434
+ try:
435
+ c = list(G._backend.depth_first_search(vertex, ignore_direction=True,
436
+ forbidden_vertices=forbidden))
437
+ except AttributeError:
438
+ c = list(G.depth_first_search(vertex, ignore_direction=True,
439
+ forbidden_vertices=forbidden))
440
+
441
+ if sort:
442
+ return sorted(c, key=key)
443
+ return c
444
+
445
+
446
+ def connected_components_sizes(G, forbidden_vertices=None):
447
+ """
448
+ Return the sizes of the connected components as a list.
449
+
450
+ The list is sorted from largest to lower values.
451
+
452
+ INPUT:
453
+
454
+ - ``G`` -- the input graph
455
+
456
+ - ``forbidden_vertices`` -- list (default: ``None``); set of vertices to
457
+ avoid during the search
458
+
459
+ EXAMPLES::
460
+
461
+ sage: from sage.graphs.connectivity import connected_components_sizes
462
+ sage: for x in graphs(3): # needs nauty
463
+ ....: print(connected_components_sizes(x))
464
+ [1, 1, 1]
465
+ [2, 1]
466
+ [3]
467
+ [3]
468
+ sage: for x in graphs(3): # needs nauty
469
+ ....: print(x.connected_components_sizes())
470
+ [1, 1, 1]
471
+ [2, 1]
472
+ [3]
473
+ [3]
474
+ sage: G = graphs.PathGraph(5)
475
+ sage: G.connected_components_sizes()
476
+ [5]
477
+ sage: G.connected_components_sizes(forbidden_vertices=[1])
478
+ [3, 1]
479
+ sage: G.connected_components_sizes(forbidden_vertices=[1, 3])
480
+ [1, 1, 1]
481
+
482
+ TESTS:
483
+
484
+ If ``G`` is not a Sage graph, an error is raised::
485
+
486
+ sage: from sage.graphs.connectivity import connected_components_sizes
487
+ sage: connected_components_sizes('I am not a graph')
488
+ Traceback (most recent call last):
489
+ ...
490
+ TypeError: the input must be a Sage graph
491
+ """
492
+ from sage.graphs.generic_graph import GenericGraph
493
+ if not isinstance(G, GenericGraph):
494
+ raise TypeError("the input must be a Sage graph")
495
+
496
+ # connected components are sorted from largest to smallest
497
+ return [len(cc) for cc in connected_components(G, sort=False,
498
+ forbidden_vertices=forbidden_vertices)]
499
+
500
+
501
+ def blocks_and_cut_vertices(G, algorithm='Tarjan_Boost', sort=False, key=None):
502
+ """
503
+ Return the blocks and cut vertices of the graph.
504
+
505
+ In the case of a digraph, this computation is done on the underlying
506
+ graph.
507
+
508
+ A cut vertex is one whose deletion increases the number of connected
509
+ components. A block is a maximal induced subgraph which itself has no
510
+ cut vertices. Two distinct blocks cannot overlap in more than a single
511
+ cut vertex.
512
+
513
+ INPUT:
514
+
515
+ - ``algorithm`` -- string (default: ``'Tarjan_Boost'``); the algorithm to
516
+ use among:
517
+
518
+ - ``'Tarjan_Boost'`` -- default; Tarjan's algorithm (Boost
519
+ implementation)
520
+
521
+ - ``'Tarjan_Sage'`` -- Tarjan's algorithm (Sage implementation)
522
+
523
+ - ``sort`` -- boolean (default: ``False``); whether to sort vertices inside
524
+ the components and the list of cut vertices
525
+ **currently only available for ``'Tarjan_Sage'``**
526
+
527
+ - ``key`` -- a function (default: ``None``); a function that takes a
528
+ vertex as its one argument and returns a value that can be used for
529
+ comparisons in the sorting algorithm (we must have ``sort=True``)
530
+
531
+ OUTPUT: ``(B, C)``, where ``B`` is a list of blocks - each is a list of
532
+ vertices and the blocks are the corresponding induced subgraphs - and
533
+ ``C`` is a list of cut vertices.
534
+
535
+ ALGORITHM:
536
+
537
+ We implement the algorithm proposed by Tarjan in [Tarjan72]_. The
538
+ original version is recursive. We emulate the recursion using a stack.
539
+
540
+ .. SEEALSO::
541
+
542
+ - :meth:`blocks_and_cuts_tree`
543
+ - :func:`sage.graphs.base.boost_graph.blocks_and_cut_vertices`
544
+ - :meth:`~Graph.is_biconnected`
545
+ - :meth:`~Graph.bridges`
546
+
547
+ EXAMPLES:
548
+
549
+ We construct a trivial example of a graph with one cut vertex::
550
+
551
+ sage: from sage.graphs.connectivity import blocks_and_cut_vertices
552
+ sage: rings = graphs.CycleGraph(10)
553
+ sage: rings.merge_vertices([0, 5])
554
+ sage: blocks_and_cut_vertices(rings)
555
+ ([[0, 1, 4, 2, 3], [0, 6, 9, 7, 8]], [0])
556
+ sage: rings.blocks_and_cut_vertices()
557
+ ([[0, 1, 4, 2, 3], [0, 6, 9, 7, 8]], [0])
558
+ sage: B, C = blocks_and_cut_vertices(rings, algorithm='Tarjan_Sage', sort=True)
559
+ sage: B, C
560
+ ([[0, 1, 2, 3, 4], [0, 6, 7, 8, 9]], [0])
561
+ sage: B2, C2 = blocks_and_cut_vertices(rings, algorithm='Tarjan_Sage', sort=False)
562
+ sage: Set(map(Set, B)) == Set(map(Set, B2)) and set(C) == set(C2)
563
+ True
564
+
565
+ The Petersen graph is biconnected, hence has no cut vertices::
566
+
567
+ sage: blocks_and_cut_vertices(graphs.PetersenGraph())
568
+ ([[0, 1, 4, 5, 2, 6, 3, 7, 8, 9]], [])
569
+
570
+ Decomposing paths to pairs::
571
+
572
+ sage: g = graphs.PathGraph(4) + graphs.PathGraph(5)
573
+ sage: blocks_and_cut_vertices(g)
574
+ ([[2, 3], [1, 2], [0, 1], [7, 8], [6, 7], [5, 6], [4, 5]], [1, 2, 5, 6, 7])
575
+
576
+ A disconnected graph::
577
+
578
+ sage: g = Graph({1: {2: 28, 3: 10}, 2: {1: 10, 3: 16}, 4: {}, 5: {6: 3, 7: 10, 8: 4}})
579
+ sage: blocks_and_cut_vertices(g)
580
+ ([[1, 2, 3], [5, 6], [5, 7], [5, 8], [4]], [5])
581
+
582
+ A directed graph with Boost's algorithm (:issue:`25994`)::
583
+
584
+ sage: rings = graphs.CycleGraph(10)
585
+ sage: rings.merge_vertices([0, 5])
586
+ sage: rings = rings.to_directed()
587
+ sage: blocks_and_cut_vertices(rings, algorithm='Tarjan_Boost')
588
+ ([[0, 1, 4, 2, 3], [0, 6, 9, 7, 8]], [0])
589
+
590
+ TESTS::
591
+
592
+ sage: blocks_and_cut_vertices(Graph(0))
593
+ ([], [])
594
+ sage: blocks_and_cut_vertices(Graph(1))
595
+ ([[0]], [])
596
+ sage: blocks_and_cut_vertices(Graph(2))
597
+ ([[0], [1]], [])
598
+
599
+ If ``G`` is not a Sage graph, an error is raised::
600
+
601
+ sage: from sage.graphs.connectivity import connected_components_sizes
602
+ sage: connected_components_sizes('I am not a graph')
603
+ Traceback (most recent call last):
604
+ ...
605
+ TypeError: the input must be a Sage graph
606
+ """
607
+ from sage.graphs.generic_graph import GenericGraph
608
+ if not isinstance(G, GenericGraph):
609
+ raise TypeError("the input must be a Sage graph")
610
+
611
+ if algorithm == "Tarjan_Boost":
612
+ from sage.graphs.base.boost_graph import blocks_and_cut_vertices
613
+ return blocks_and_cut_vertices(G)
614
+
615
+ if algorithm != "Tarjan_Sage":
616
+ raise NotImplementedError("blocks and cut vertices algorithm '%s' is not implemented" % algorithm)
617
+
618
+ # If algorithm is "Tarjan_Sage"
619
+ if (not sort) and key:
620
+ raise ValueError('sort keyword is False, yet a key function is given')
621
+
622
+ blocks = []
623
+ cut_vertices = set()
624
+
625
+ # We iterate over all vertices to ensure that we visit each connected
626
+ # component of the graph
627
+ seen = set()
628
+ for start in G.vertex_iterator():
629
+ if start in seen:
630
+ continue
631
+
632
+ # Special case of an isolated vertex
633
+ if not G.degree(start):
634
+ blocks.append([start])
635
+ seen.add(start)
636
+ continue
637
+
638
+ # Each vertex is numbered with an integer from 1...|V(G)|,
639
+ # corresponding to the order in which it is discovered during the
640
+ # DFS.
641
+ number = {}
642
+ num = 1
643
+
644
+ # Associates to each vertex v the smallest number of a vertex that
645
+ # can be reached from v in the orientation of the graph that the
646
+ # algorithm creates.
647
+ low_point = {}
648
+
649
+ # Associates to each vertex an iterator over its neighbors
650
+ neighbors = {}
651
+
652
+ stack = [start]
653
+ edge_stack = []
654
+ start_already_seen = False
655
+
656
+ while stack:
657
+ v = stack[-1]
658
+ seen.add(v)
659
+
660
+ # The first time we meet v
661
+ if v not in number:
662
+ # We number the vertices in the order they are reached
663
+ # during DFS
664
+ number[v] = num
665
+ neighbors[v] = G.neighbor_iterator(v)
666
+ low_point[v] = num
667
+ num += 1
668
+
669
+ try:
670
+ # We consider the next of its neighbors
671
+ w = next(neighbors[v])
672
+
673
+ # If we never met w before, we remember the direction of
674
+ # edge vw, and add w to the stack.
675
+ if w not in number:
676
+ edge_stack.append((v, w))
677
+ stack.append(w)
678
+
679
+ # If w is an ancestor of v in the DFS tree, we remember the
680
+ # direction of edge vw
681
+ elif number[w] < number[v]:
682
+ edge_stack.append((v, w))
683
+ low_point[v] = min(low_point[v], number[w])
684
+
685
+ # We went through all of v's neighbors
686
+ except StopIteration:
687
+ # We trackback, so w takes the value of v and we pop the
688
+ # stack
689
+ w = stack.pop()
690
+
691
+ # Test termination of the algorithm
692
+ if not stack:
693
+ break
694
+
695
+ v = stack[-1]
696
+
697
+ # Propagating the information : low_point[v] indicates the
698
+ # smallest vertex (the vertex x with smallest number[x])
699
+ # that can be reached from v
700
+ low_point[v] = min(low_point[v], low_point[w])
701
+
702
+ # The situation in which there is no path from w to an
703
+ # ancestor of v : we have identified a new biconnected
704
+ # component
705
+ if low_point[w] >= number[v]:
706
+ new_block = set()
707
+ nw = number[w]
708
+ u1, u2 = edge_stack.pop()
709
+ while number[u1] >= nw:
710
+ new_block.add(u1)
711
+ u1, u2 = edge_stack.pop()
712
+ new_block.add(u1)
713
+ if sort:
714
+ this_block = sorted(new_block, key=key)
715
+ else:
716
+ this_block = list(new_block)
717
+ blocks.append(this_block)
718
+
719
+ # We update the set of cut vertices.
720
+ #
721
+ # If v is start, then we add it only if it belongs to
722
+ # several blocks.
723
+ if (v is not start) or start_already_seen:
724
+ cut_vertices.add(v)
725
+ else:
726
+ start_already_seen = True
727
+
728
+ if sort:
729
+ return blocks, sorted(cut_vertices, key=key)
730
+ return blocks, list(cut_vertices)
731
+
732
+
733
+ def blocks_and_cuts_tree(G):
734
+ """
735
+ Return the blocks-and-cuts tree of ``self``.
736
+
737
+ This new graph has two different kinds of vertices, some representing the
738
+ blocks (type B) and some other the cut vertices of the graph (type C).
739
+
740
+ There is an edge between a vertex `u` of type B and a vertex `v` of type C
741
+ if the cut-vertex corresponding to `v` is in the block corresponding to `u`.
742
+
743
+ The resulting graph is a tree, with the additional characteristic property
744
+ that the distance between two leaves is even. When ``self`` is not
745
+ connected, the resulting graph is a forest.
746
+
747
+ When ``self`` is biconnected, the tree is reduced to a single node of
748
+ type `B`.
749
+
750
+ We referred to [HarPri]_ and [Gallai]_ for blocks and cuts tree.
751
+
752
+ .. SEEALSO::
753
+
754
+ - :meth:`~sage.graphs.generic_graph.GenericGraph.blocks_and_cut_vertices`
755
+ - :meth:`~Graph.is_biconnected`
756
+
757
+ EXAMPLES::
758
+
759
+ sage: from sage.graphs.connectivity import blocks_and_cuts_tree
760
+ sage: T = blocks_and_cuts_tree(graphs.KrackhardtKiteGraph()); T
761
+ Graph on 5 vertices
762
+ sage: T.is_isomorphic(graphs.PathGraph(5))
763
+ True
764
+ sage: from sage.graphs.connectivity import blocks_and_cuts_tree
765
+ sage: T = graphs.KrackhardtKiteGraph().blocks_and_cuts_tree(); T
766
+ Graph on 5 vertices
767
+
768
+ The distance between two leaves is even::
769
+
770
+ sage: T = blocks_and_cuts_tree(graphs.RandomTree(40))
771
+ sage: T.is_tree()
772
+ True
773
+ sage: leaves = [v for v in T if T.degree(v) == 1]
774
+ sage: all(T.distance(u,v) % 2 == 0 for u in leaves for v in leaves)
775
+ True
776
+
777
+ The tree of a biconnected graph has a single vertex, of type `B`::
778
+
779
+ sage: T = blocks_and_cuts_tree(graphs.PetersenGraph())
780
+ sage: T.vertices(sort=True)
781
+ [('B', (0, 1, 4, 5, 2, 6, 3, 7, 8, 9))]
782
+
783
+ TESTS:
784
+
785
+ When ``self`` is not connected, the resulting graph is a forest
786
+ (:issue:`24163`)::
787
+
788
+ sage: from sage.graphs.connectivity import blocks_and_cuts_tree
789
+ sage: T = blocks_and_cuts_tree(Graph(2))
790
+ sage: T.is_forest()
791
+ True
792
+
793
+ If ``G`` is not a Sage graph, an error is raised::
794
+
795
+ sage: blocks_and_cuts_tree('I am not a graph')
796
+ Traceback (most recent call last):
797
+ ...
798
+ TypeError: the input must be a Sage graph
799
+ """
800
+ from sage.graphs.generic_graph import GenericGraph
801
+ if not isinstance(G, GenericGraph):
802
+ raise TypeError("the input must be a Sage graph")
803
+
804
+ from sage.graphs.graph import Graph
805
+ B, C = G.blocks_and_cut_vertices()
806
+ B = map(tuple, B)
807
+ set_C = set(C)
808
+ g = Graph()
809
+ for bloc in B:
810
+ g.add_vertex(('B', bloc))
811
+ for c in bloc:
812
+ if c in set_C:
813
+ g.add_edge(('B', bloc), ('C', c))
814
+ return g
815
+
816
+
817
+ def is_edge_cut(G, edges):
818
+ """
819
+ Check whether ``edges`` form an edge cut.
820
+
821
+ A set of edges is an edge cut of a graph if its removal increases the number
822
+ of connected components. In a digraph, we consider the number of (weakly)
823
+ connected components.
824
+
825
+ This method is not working for (di)graphs with multiple edges. Furthermore,
826
+ edge labels are ignored.
827
+
828
+ INPUT:
829
+
830
+ - ``G`` -- a (di)graph
831
+
832
+ - ``edges`` -- a set of edges
833
+
834
+ EXAMPLES:
835
+
836
+ A cycle graph of order 4::
837
+
838
+ sage: from sage.graphs.connectivity import is_edge_cut
839
+ sage: G = graphs.CycleGraph(4)
840
+ sage: is_edge_cut(G, [(1, 2)])
841
+ False
842
+ sage: is_edge_cut(G, [(1, 2), (2, 3)])
843
+ True
844
+ sage: is_edge_cut(G, [(1, 2), (3, 0)])
845
+ True
846
+
847
+ A pending edge is a cut-edge::
848
+
849
+ sage: G.add_edge((0, 5, 'silly'))
850
+ sage: is_edge_cut(G, [(0, 5, 'silly')])
851
+ True
852
+
853
+ Edge labels are ignored, even if specified::
854
+
855
+ sage: G.add_edge((2, 5, 'xyz'))
856
+ sage: is_edge_cut(G, [(0, 5), (2, 5)])
857
+ True
858
+ sage: is_edge_cut(G, [(0, 5), (2, 5, 'xyz')])
859
+ True
860
+ sage: is_edge_cut(G, [(0, 5, 'silly'), (2, 5)])
861
+ True
862
+ sage: is_edge_cut(G, [(0, 5, 'aa'), (2, 5, 'bb')])
863
+ True
864
+
865
+ The graph can have loops::
866
+
867
+ sage: G.allow_loops(True)
868
+ sage: G.add_edge(0, 0)
869
+ sage: is_edge_cut(G, [(0, 5), (2, 5)])
870
+ True
871
+ sage: is_edge_cut(G, [(0, 0), (0, 5), (2, 5)])
872
+ True
873
+
874
+ Multiple edges are not allowed::
875
+
876
+ sage: G.allow_multiple_edges(True)
877
+ sage: is_edge_cut(G, [(0, 5), (2, 5)])
878
+ Traceback (most recent call last):
879
+ ...
880
+ ValueError: This method is not known to work on graphs with
881
+ multiedges. Perhaps this method can be updated to handle them, but in
882
+ the meantime if you want to use it please disallow multiedges using
883
+ allow_multiple_edges().
884
+
885
+ An error is raised if an element of ``edges`` is not an edge of `G`::
886
+
887
+ sage: G = graphs.CycleGraph(4)
888
+ sage: is_edge_cut(G, [(0, 2)])
889
+ Traceback (most recent call last):
890
+ ...
891
+ ValueError: edge (0, 2) is not an edge of the graph
892
+
893
+ For digraphs, this method considers the number of (weakly) connected
894
+ components::
895
+
896
+ sage: G = digraphs.Circuit(4)
897
+ sage: is_edge_cut(G, [(0, 1)])
898
+ False
899
+ sage: G = digraphs.Circuit(4)
900
+ sage: is_edge_cut(G, [(0, 1), (1, 2)])
901
+ True
902
+
903
+ For disconnected (di)graphs, the method checks if the number of (weakly)
904
+ connected components increases::
905
+
906
+ sage: G = graphs.CycleGraph(4) * 2
907
+ sage: is_edge_cut(G, [(1, 2), (2, 3)])
908
+ True
909
+ sage: G = digraphs.Circuit(4) * 2
910
+ sage: is_edge_cut(G, [(0, 1), (1, 2)])
911
+ True
912
+ """
913
+ G._scream_if_not_simple(allow_loops=True)
914
+
915
+ cdef set C = set() # set of edges of the potential cut
916
+ cdef set S = set() # set of incident vertices
917
+ for e in edges:
918
+ u, v = e[0], e[1]
919
+ if not G.has_edge(u, v):
920
+ raise ValueError("edge {0} is not an edge of the graph".format(repr(e)))
921
+ if u == v:
922
+ # We ignore loops
923
+ continue
924
+ if G.degree(u) == 1 or G.degree(v) == 1:
925
+ # e is a pending edge and so a cut-edge
926
+ return True
927
+ S.add(u)
928
+ S.add(v)
929
+ C.add((u, v))
930
+ if not G.is_directed():
931
+ C.add((v, u))
932
+
933
+ cdef list queue
934
+ cdef set seen
935
+ DS = DisjointSet(G)
936
+
937
+ for comp in G.connected_components():
938
+ if not S.intersection(comp):
939
+ # This component is not involved in the cut
940
+ continue
941
+
942
+ # We run a DFS in comp from any vertex and avoid edges in C
943
+ start = comp[0]
944
+ queue = [start]
945
+ seen = set(queue)
946
+ while queue:
947
+ v = queue.pop()
948
+ for e in G.edge_iterator(vertices=[v], labels=False, ignore_direction=True, sort_vertices=False):
949
+ if e in C:
950
+ continue
951
+ w = e[1] if e[0] == v else e[0]
952
+ if w not in seen:
953
+ seen.add(w)
954
+ DS.union(v, w)
955
+ queue.append(w)
956
+
957
+ # We now check if some vertices of comp have not been reached
958
+ if len(set(DS.find(v) for v in comp)) > 1:
959
+ return True
960
+
961
+ return False
962
+
963
+
964
+ def is_cut_edge(G, u, v=None, label=None):
965
+ """
966
+ Check whether the edge ``(u, v)`` is a cut-edge or a bridge of graph ``G``.
967
+
968
+ A cut edge (or bridge) is an edge that when removed increases
969
+ the number of connected components. This function works with
970
+ simple graphs as well as graphs with loops and multiedges. In
971
+ a digraph, a cut edge is an edge that when removed increases
972
+ the number of (weakly) connected components.
973
+
974
+ INPUT: The following forms are accepted
975
+
976
+ - is_cut_edge(G, 1, 2 )
977
+
978
+ - is_cut_edge(G, (1, 2) )
979
+
980
+ - is_cut_edge(G, 1, 2, 'label' )
981
+
982
+ - is_cut_edge(G, (1, 2, 'label') )
983
+
984
+ OUTPUT:
985
+
986
+ - Returns ``True`` if (u,v) is a cut edge, False otherwise
987
+
988
+ EXAMPLES::
989
+
990
+ sage: from sage.graphs.connectivity import is_cut_edge
991
+ sage: G = graphs.CompleteGraph(4)
992
+ sage: is_cut_edge(G,0,2)
993
+ False
994
+ sage: G.is_cut_edge(0,2)
995
+ False
996
+
997
+ sage: G = graphs.CompleteGraph(4)
998
+ sage: G.add_edge((0,5,'silly'))
999
+ sage: is_cut_edge(G,(0,5,'silly'))
1000
+ True
1001
+
1002
+ sage: G = Graph([[0,1],[0,2],[3,4],[4,5],[3,5]])
1003
+ sage: is_cut_edge(G,(0,1))
1004
+ True
1005
+
1006
+ sage: G = Graph([[0,1],[0,2],[1,1]], loops = True)
1007
+ sage: is_cut_edge(G,(1,1))
1008
+ False
1009
+
1010
+ sage: G = digraphs.Circuit(5)
1011
+ sage: is_cut_edge(G,(0,1))
1012
+ False
1013
+
1014
+ sage: G = graphs.CompleteGraph(6)
1015
+ sage: is_cut_edge(G,(0,7))
1016
+ Traceback (most recent call last):
1017
+ ...
1018
+ ValueError: edge not in graph
1019
+ """
1020
+ if label is None:
1021
+ if v is None:
1022
+ try:
1023
+ u, v, label = u
1024
+ except ValueError:
1025
+ u, v = u
1026
+ label = None
1027
+
1028
+ if not G.has_edge(u, v):
1029
+ raise ValueError('edge not in graph')
1030
+
1031
+ # If edge (u,v) is a pending edge, it is also a cut-edge
1032
+ if G.degree(u) == 1 or G.degree(v) == 1:
1033
+ return True
1034
+ elif G.allows_multiple_edges():
1035
+ # If we have two or more edges between u and v, it is not a cut-edge
1036
+ if len([(uu, vv) for uu, vv, ll in G.edges_incident(u) if uu == v or vv == v]) > 1:
1037
+ return False
1038
+
1039
+ g = G.copy(immutable=False) if G.is_immutable() else G
1040
+ g.delete_edge(u, v, label)
1041
+ if g.is_directed():
1042
+ # (u,v) is a cut-edge if u is not in the connected
1043
+ # component containing v of self-(u,v)
1044
+ sol = u not in connected_component_containing_vertex(g, v)
1045
+ else:
1046
+ # (u,v) is a cut-edge if there is no path from u to v in
1047
+ # self-(u,v)
1048
+ sol = not g.distance(u, v) < g.order()
1049
+
1050
+ g.add_edge(u, v, label)
1051
+ return sol
1052
+
1053
+
1054
+ def is_vertex_cut(G, cut, weak=False):
1055
+ r"""
1056
+ Check whether the input vertices form a vertex cut.
1057
+
1058
+ A set of vertices is a vertex cut if its removal from the (di)graph
1059
+ increases the number of (strongly) connected components. This function works
1060
+ with simple graphs as well as graphs with loops and multiple edges.
1061
+
1062
+ INPUT:
1063
+
1064
+ - ``G`` -- a Sage (Di)Graph
1065
+
1066
+ - ``cut`` -- a set of vertices
1067
+
1068
+ - ``weak`` -- boolean (default: ``False``); whether the connectivity of
1069
+ directed graphs is to be taken in the weak sense, that is ignoring edges
1070
+ orientations
1071
+
1072
+ EXAMPLES:
1073
+
1074
+ Giving a cycle graph of order 4::
1075
+
1076
+ sage: from sage.graphs.connectivity import is_vertex_cut
1077
+ sage: G = graphs.CycleGraph(4)
1078
+ sage: is_vertex_cut(G, [0, 1])
1079
+ False
1080
+ sage: is_vertex_cut(G, [0, 2])
1081
+ True
1082
+
1083
+ Giving a disconnected graph::
1084
+
1085
+ sage: from sage.graphs.connectivity import is_vertex_cut
1086
+ sage: G = graphs.CycleGraph(4) * 2
1087
+ sage: G.connected_components()
1088
+ [[0, 1, 2, 3], [4, 5, 6, 7]]
1089
+ sage: is_vertex_cut(G, [0, 2])
1090
+ True
1091
+ sage: is_vertex_cut(G, [4, 6])
1092
+ True
1093
+ sage: is_vertex_cut(G, [0, 6])
1094
+ False
1095
+ sage: is_vertex_cut(G, [0, 4, 6])
1096
+ True
1097
+
1098
+ Comparing the weak and strong connectivity of a digraph::
1099
+
1100
+ sage: D = digraphs.Circuit(6)
1101
+ sage: D.is_strongly_connected()
1102
+ True
1103
+ sage: is_vertex_cut(D, [2])
1104
+ True
1105
+ sage: is_vertex_cut(D, [2], weak=True)
1106
+ False
1107
+
1108
+ Giving a vertex that is not in the graph::
1109
+
1110
+ sage: G = graphs.CompleteGraph(4)
1111
+ sage: is_vertex_cut(G, [7])
1112
+ Traceback (most recent call last):
1113
+ ...
1114
+ ValueError: vertex (7) is not a vertex of the graph
1115
+
1116
+ TESTS:
1117
+
1118
+ If ``G`` is not a Sage graph, an error is raised::
1119
+
1120
+ sage: is_vertex_cut('I am not a graph', [0])
1121
+ Traceback (most recent call last):
1122
+ ...
1123
+ TypeError: the input must be a Sage graph
1124
+ """
1125
+ from sage.graphs.generic_graph import GenericGraph
1126
+ if not isinstance(G, GenericGraph):
1127
+ raise TypeError("the input must be a Sage graph")
1128
+
1129
+ cdef set cutset = set(cut)
1130
+ for u in cutset:
1131
+ if u not in G:
1132
+ raise ValueError("vertex ({0}) is not a vertex of the graph".format(repr(u)))
1133
+
1134
+ if len(cutset) >= G.order() - 1:
1135
+ # A vertex cut must be of size at most n - 2
1136
+ return False
1137
+
1138
+ # We deal with graphs with multiple (strongly) connected components
1139
+ cdef list CC
1140
+ if G.is_directed() and not weak:
1141
+ CC = G.strongly_connected_components()
1142
+ else:
1143
+ CC = G.connected_components(sort=False)
1144
+ if len(CC) > 1:
1145
+ for comp in CC:
1146
+ subcut = cutset.intersection(comp)
1147
+ if subcut and is_vertex_cut(G.subgraph(comp), subcut, weak=weak):
1148
+ return True
1149
+ return False
1150
+
1151
+ cdef list boundary = G.vertex_boundary(cutset)
1152
+ if not boundary:
1153
+ # We need at least 1 vertex in the boundary of the cut
1154
+ return False
1155
+
1156
+ cdef list cases = [(G.neighbor_iterator, boundary)]
1157
+ if not weak and G.is_directed():
1158
+ # Strong connectivity for digraphs.
1159
+ # We perform two DFS starting from an out neighbor of cut and avoiding
1160
+ # cut. The first DFS follows the edges directions, and the second is
1161
+ # in the reverse order. If both allow to reach all neighbors of cut,
1162
+ # then it is not a vertex cut.
1163
+ # We set data for the reverse order
1164
+ in_boundary = set()
1165
+ for u in cutset:
1166
+ in_boundary.update(G.neighbor_in_iterator(u))
1167
+ in_boundary.difference_update(cutset)
1168
+ if not in_boundary:
1169
+ return False
1170
+ cases.append((G.neighbor_in_iterator, list(in_boundary)))
1171
+
1172
+ cdef list queue
1173
+ cdef set seen
1174
+ cdef set targets
1175
+ start = boundary[0]
1176
+
1177
+ for neighbors, this_boundary in cases:
1178
+
1179
+ # We perform a DFS starting from start and avoiding cut
1180
+ queue = [start]
1181
+ seen = set(cutset)
1182
+ seen.add(start)
1183
+ targets = set(this_boundary)
1184
+ targets.discard(start)
1185
+ while queue:
1186
+ v = queue.pop()
1187
+ for w in neighbors(v):
1188
+ if w not in seen:
1189
+ seen.add(w)
1190
+ queue.append(w)
1191
+ targets.discard(w)
1192
+
1193
+ # If some neighbors cannot be reached, we have a vertex cut
1194
+ if targets:
1195
+ return True
1196
+
1197
+ return False
1198
+
1199
+
1200
+ def is_cut_vertex(G, u, weak=False):
1201
+ r"""
1202
+ Check whether the input vertex is a cut-vertex.
1203
+
1204
+ A vertex is a cut-vertex if its removal from the (di)graph increases the
1205
+ number of (strongly) connected components. Isolated vertices or leaves are
1206
+ not cut-vertices. This function works with simple graphs as well as graphs
1207
+ with loops and multiple edges.
1208
+
1209
+ INPUT:
1210
+
1211
+ - ``G`` -- a Sage (Di)Graph
1212
+
1213
+ - ``u`` -- a vertex
1214
+
1215
+ - ``weak`` -- boolean (default: ``False``); whether the connectivity of
1216
+ directed graphs is to be taken in the weak sense, that is ignoring edges
1217
+ orientations
1218
+
1219
+ OUTPUT:
1220
+
1221
+ Return ``True`` if ``u`` is a cut-vertex, and ``False`` otherwise.
1222
+
1223
+ EXAMPLES:
1224
+
1225
+ Giving a LollipopGraph(4,2), that is a complete graph with 4 vertices with a
1226
+ pending edge::
1227
+
1228
+ sage: from sage.graphs.connectivity import is_cut_vertex
1229
+ sage: G = graphs.LollipopGraph(4, 2)
1230
+ sage: is_cut_vertex(G, 0)
1231
+ False
1232
+ sage: is_cut_vertex(G, 3)
1233
+ True
1234
+ sage: G.is_cut_vertex(3)
1235
+ True
1236
+
1237
+ Comparing the weak and strong connectivity of a digraph::
1238
+
1239
+ sage: D = digraphs.Circuit(6)
1240
+ sage: D.is_strongly_connected()
1241
+ True
1242
+ sage: is_cut_vertex(D, 2)
1243
+ True
1244
+ sage: is_cut_vertex(D, 2, weak=True)
1245
+ False
1246
+
1247
+ Giving a vertex that is not in the graph::
1248
+
1249
+ sage: G = graphs.CompleteGraph(4)
1250
+ sage: is_cut_vertex(G, 7)
1251
+ Traceback (most recent call last):
1252
+ ...
1253
+ ValueError: vertex (7) is not a vertex of the graph
1254
+
1255
+ TESTS:
1256
+
1257
+ If ``G`` is not a Sage graph, an error is raised::
1258
+
1259
+ sage: is_cut_vertex('I am not a graph', 0)
1260
+ Traceback (most recent call last):
1261
+ ...
1262
+ TypeError: the input must be a Sage graph
1263
+ """
1264
+ return is_vertex_cut(G, [u], weak=weak)
1265
+
1266
+
1267
+ def minimal_separators(G, forbidden_vertices=None):
1268
+ r"""
1269
+ Return an iterator over the minimal separators of ``G``.
1270
+
1271
+ A separator in a graph is a set of vertices whose removal increases the
1272
+ number of connected components. In other words, a separator is a vertex
1273
+ cut. This method implements the algorithm proposed in [BBC2000]_.
1274
+ It computes the set `S` of minimal separators of a graph in `O(n^3)` time
1275
+ per separator, and so overall in `O(n^3 |S|)` time.
1276
+
1277
+ .. WARNING::
1278
+
1279
+ Note that all separators are recorded during the execution of the
1280
+ algorithm and so the memory consumption of this method might be huge.
1281
+
1282
+ INPUT:
1283
+
1284
+ - ``G`` -- an undirected graph
1285
+
1286
+ - ``forbidden_vertices`` -- list (default: ``None``); set of vertices to
1287
+ avoid during the search
1288
+
1289
+ EXAMPLES::
1290
+
1291
+ sage: P = graphs.PathGraph(5)
1292
+ sage: sorted(sorted(sep) for sep in P.minimal_separators())
1293
+ [[1], [2], [3]]
1294
+ sage: C = graphs.CycleGraph(6)
1295
+ sage: sorted(sorted(sep) for sep in C.minimal_separators())
1296
+ [[0, 2], [0, 3], [0, 4], [1, 3], [1, 4], [1, 5], [2, 4], [2, 5], [3, 5]]
1297
+ sage: sorted(sorted(sep) for sep in C.minimal_separators(forbidden_vertices=[0]))
1298
+ [[2], [3], [4]]
1299
+ sage: sorted(sorted(sep) for sep in (P + C).minimal_separators())
1300
+ [[1], [2], [3], [5, 7], [5, 8], [5, 9], [6, 8],
1301
+ [6, 9], [6, 10], [7, 9], [7, 10], [8, 10]]
1302
+ sage: sorted(sorted(sep) for sep in (P + C).minimal_separators(forbidden_vertices=[10]))
1303
+ [[1], [2], [3], [6], [7], [8]]
1304
+
1305
+ sage: G = graphs.RandomGNP(10, .3)
1306
+ sage: all(G.is_vertex_cut(sep) for sep in G.minimal_separators())
1307
+ True
1308
+
1309
+ TESTS::
1310
+
1311
+ sage: list(Graph().minimal_separators())
1312
+ []
1313
+ sage: list(Graph(1).minimal_separators())
1314
+ []
1315
+ sage: list(Graph(2).minimal_separators())
1316
+ []
1317
+ sage: from sage.graphs.connectivity import minimal_separators
1318
+ sage: list(minimal_separators(DiGraph()))
1319
+ Traceback (most recent call last):
1320
+ ...
1321
+ ValueError: the input must be an undirected graph
1322
+ """
1323
+ from sage.graphs.graph import Graph
1324
+ if not isinstance(G, Graph):
1325
+ raise ValueError("the input must be an undirected graph")
1326
+
1327
+ if forbidden_vertices is not None and G.order() >= 3:
1328
+ # Build the subgraph with active vertices
1329
+ G = G.subgraph(set(G).difference(forbidden_vertices), immutable=True)
1330
+
1331
+ if G.order() < 3:
1332
+ return
1333
+ if not G.is_connected():
1334
+ for cc in G.connected_components(sort=False):
1335
+ if len(cc) > 2:
1336
+ yield from minimal_separators(G.subgraph(cc))
1337
+ return
1338
+
1339
+ # Initialization - identify separators needing further inspection
1340
+ cdef list to_explore = []
1341
+ for v in G:
1342
+ # iterate over the connected components of G \ N[v]
1343
+ for comp in G.connected_components(sort=False, forbidden_vertices=G.neighbor_iterator(v, closed=True)):
1344
+ # The vertex boundary of comp in G is a separator
1345
+ nh = G.vertex_boundary(comp)
1346
+ if nh:
1347
+ to_explore.append(frozenset(nh))
1348
+
1349
+ # Generation of all minimal separators
1350
+ cdef set separators = set()
1351
+ while to_explore:
1352
+ sep = to_explore.pop()
1353
+ if sep in separators:
1354
+ continue
1355
+ yield set(sep)
1356
+ separators.add(sep)
1357
+ for v in sep:
1358
+ # iterate over the connected components of G \ sep \ N(v)
1359
+ for comp in G.connected_components(sort=False, forbidden_vertices=sep.union(G.neighbor_iterator(v))):
1360
+ nh = G.vertex_boundary(comp)
1361
+ if nh:
1362
+ to_explore.append(frozenset(nh))
1363
+
1364
+
1365
+ def edge_connectivity(G,
1366
+ value_only=True,
1367
+ implementation=None,
1368
+ use_edge_labels=False,
1369
+ vertices=False,
1370
+ solver=None,
1371
+ verbose=0,
1372
+ *, integrality_tolerance=1e-3):
1373
+ r"""
1374
+ Return the edge connectivity of the graph.
1375
+
1376
+ For more information, see the :wikipedia:`Connectivity_(graph_theory)`.
1377
+
1378
+ .. NOTE::
1379
+
1380
+ When the graph is a directed graph, this method actually computes the
1381
+ *strong* connectivity, (i.e. a directed graph is strongly `k`-connected
1382
+ if there are `k` disjoint paths between any two vertices `u, v`). If you
1383
+ do not want to consider strong connectivity, the best is probably to
1384
+ convert your ``DiGraph`` object to a ``Graph`` object, and compute the
1385
+ connectivity of this other graph.
1386
+
1387
+ INPUT:
1388
+
1389
+ - ``G`` -- the input Sage (Di)Graph
1390
+
1391
+ - ``value_only`` -- boolean (default: ``True``)
1392
+
1393
+ - When set to ``True`` (default), only the value is returned.
1394
+
1395
+ - When set to ``False``, both the value and a minimum vertex cut are
1396
+ returned.
1397
+
1398
+ - ``implementation`` -- string (default: ``None``); selects an
1399
+ implementation:
1400
+
1401
+ - ``None`` -- default; selects the best implementation available
1402
+
1403
+ - ``'boost'`` -- use the Boost graph library (which is much more
1404
+ efficient). It is not available when ``edge_labels=True``, and it is
1405
+ unreliable for directed graphs (see :issue:`18753`).
1406
+
1407
+ - ``'Sage'`` -- use Sage's implementation based on integer linear
1408
+ programming
1409
+
1410
+ - ``use_edge_labels`` -- boolean (default: ``False``)
1411
+
1412
+ - When set to ``True``, computes a weighted minimum cut where each edge
1413
+ has a weight defined by its label. (If an edge has no label, `1` is
1414
+ assumed.). Implies ``boost`` = ``False``.
1415
+
1416
+ - When set to ``False``, each edge has weight `1`.
1417
+
1418
+ - ``vertices`` -- boolean (default: ``False``)
1419
+
1420
+ - When set to ``True``, also returns the two sets of vertices that are
1421
+ disconnected by the cut. Implies ``value_only=False``.
1422
+
1423
+ - ``solver`` -- string (default: ``None``); specifies a Mixed Integer Linear
1424
+ Programming (MILP) solver to be used. If set to ``None``, the default one
1425
+ is used. For more information on MILP solvers and which default solver is
1426
+ used, see the method :meth:`solve
1427
+ <sage.numerical.mip.MixedIntegerLinearProgram.solve>` of the class
1428
+ :class:`MixedIntegerLinearProgram
1429
+ <sage.numerical.mip.MixedIntegerLinearProgram>`.
1430
+
1431
+ - ``verbose`` -- integer (default: 0); sets the level of verbosity. Set
1432
+ to 0 by default, which means quiet.
1433
+
1434
+ - ``integrality_tolerance`` -- float; parameter for use with MILP solvers
1435
+ over an inexact base ring; see
1436
+ :meth:`MixedIntegerLinearProgram.get_values`.
1437
+
1438
+ EXAMPLES:
1439
+
1440
+ A basic application on the PappusGraph::
1441
+
1442
+ sage: from sage.graphs.connectivity import edge_connectivity
1443
+ sage: g = graphs.PappusGraph()
1444
+ sage: edge_connectivity(g)
1445
+ 3
1446
+ sage: g.edge_connectivity()
1447
+ 3
1448
+
1449
+ The edge connectivity of a complete graph is its minimum degree, and one of
1450
+ the two parts of the bipartition is reduced to only one vertex. The graph of
1451
+ the cut edges is isomorphic to a Star graph::
1452
+
1453
+ sage: g = graphs.CompleteGraph(5)
1454
+ sage: [ value, edges, [ setA, setB ]] = edge_connectivity(g,vertices=True)
1455
+ sage: value
1456
+ 4
1457
+ sage: len(setA) == 1 or len(setB) == 1
1458
+ True
1459
+ sage: cut = Graph()
1460
+ sage: cut.add_edges(edges)
1461
+ sage: cut.is_isomorphic(graphs.StarGraph(4))
1462
+ True
1463
+
1464
+ Even if obviously in any graph we know that the edge connectivity is less
1465
+ than the minimum degree of the graph::
1466
+
1467
+ sage: g = graphs.RandomGNP(10,.3)
1468
+ sage: min(g.degree()) >= edge_connectivity(g)
1469
+ True
1470
+
1471
+ If we build a tree then assign to its edges a random value, the minimum cut
1472
+ will be the edge with minimum value::
1473
+
1474
+ sage: tree = graphs.RandomTree(10)
1475
+ sage: for u,v in tree.edge_iterator(labels=None):
1476
+ ....: tree.set_edge_label(u, v, random())
1477
+ sage: minimum = min(tree.edge_labels())
1478
+ sage: [_, [(_, _, l)]] = edge_connectivity(tree, value_only=False, # needs sage.numerical.mip
1479
+ ....: use_edge_labels=True)
1480
+ sage: l == minimum # needs sage.numerical.mip
1481
+ True
1482
+
1483
+ When ``value_only=True`` and ``implementation="sage"``, this function is
1484
+ optimized for small connectivity values and does not need to build a linear
1485
+ program.
1486
+
1487
+ It is the case for graphs which are not connected ::
1488
+
1489
+ sage: g = 2 * graphs.PetersenGraph()
1490
+ sage: edge_connectivity(g, implementation='sage')
1491
+ 0.0
1492
+
1493
+ For directed graphs, the strong connectivity is tested through the dedicated
1494
+ function::
1495
+
1496
+ sage: g = digraphs.ButterflyGraph(3)
1497
+ sage: edge_connectivity(g, implementation='sage')
1498
+ 0.0
1499
+
1500
+ We check that the result with Boost is the same as the result without Boost::
1501
+
1502
+ sage: g = graphs.RandomGNP(15, .3)
1503
+ sage: (edge_connectivity(g, implementation='boost') # needs sage.numerical.mip
1504
+ ....: == edge_connectivity(g, implementation='sage'))
1505
+ True
1506
+
1507
+ Boost interface also works with directed graphs::
1508
+
1509
+ sage: edge_connectivity(digraphs.Circuit(10), implementation='boost',
1510
+ ....: vertices=True)
1511
+ [1, [(0, 1)], [{0}, {1, 2, 3, 4, 5, 6, 7, 8, 9}]]
1512
+
1513
+ However, the Boost algorithm is not reliable if the input is directed
1514
+ (see :issue:`18753`)::
1515
+
1516
+ sage: g = digraphs.Path(3)
1517
+ sage: edge_connectivity(g)
1518
+ 0.0
1519
+ sage: edge_connectivity(g, implementation='boost')
1520
+ 1
1521
+ sage: g.add_edge(1, 0)
1522
+ sage: edge_connectivity(g)
1523
+ 0.0
1524
+ sage: edge_connectivity(g, implementation='boost')
1525
+ 0
1526
+
1527
+ TESTS:
1528
+
1529
+ Checking that the two implementations agree::
1530
+
1531
+ sage: for i in range(10): # needs sage.numerical.mip
1532
+ ....: g = graphs.RandomGNP(30, 0.3)
1533
+ ....: e1 = edge_connectivity(g, implementation='boost')
1534
+ ....: e2 = edge_connectivity(g, implementation='sage')
1535
+ ....: assert (e1 == e2)
1536
+
1537
+ Disconnected graphs and ``vertices=True``::
1538
+
1539
+ sage: g = graphs.PetersenGraph()
1540
+ sage: edge_connectivity((2 * g), vertices=True)
1541
+ [0, [], [{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, {10, 11, 12, 13, 14, 15, 16, 17, 18, 19}]]
1542
+ sage: edge_connectivity(Graph(), vertices=True)
1543
+ [0, [], [{}, {}]]
1544
+
1545
+ If ``G`` is not a Sage graph, an error is raised::
1546
+
1547
+ sage: edge_connectivity('I am not a graph')
1548
+ Traceback (most recent call last):
1549
+ ...
1550
+ TypeError: the input must be a Sage graph
1551
+ """
1552
+ from sage.graphs.generic_graph import GenericGraph, _weight_if_real, _weight_1
1553
+
1554
+ if not isinstance(G, GenericGraph):
1555
+ raise TypeError("the input must be a Sage graph")
1556
+
1557
+ G._scream_if_not_simple(allow_loops=True)
1558
+ g = G
1559
+
1560
+ if vertices:
1561
+ value_only = False
1562
+
1563
+ if implementation is None:
1564
+ if use_edge_labels or g.is_directed():
1565
+ implementation = "sage"
1566
+ else:
1567
+ implementation = "boost"
1568
+
1569
+ implementation = implementation.lower()
1570
+ if implementation not in ["boost", "sage"]:
1571
+ raise ValueError("'implementation' must be set to 'boost', 'sage' or None.")
1572
+ elif implementation == "boost" and use_edge_labels:
1573
+ raise ValueError("the Boost implementation is currently not able to handle edge labels")
1574
+
1575
+ # Otherwise, an error is created
1576
+ if not g.num_edges() or not g.num_verts():
1577
+ if value_only:
1578
+ return 0
1579
+ elif vertices:
1580
+ return [0, [], [{}, {}]]
1581
+ return [0, []]
1582
+
1583
+ if implementation == "boost":
1584
+ from sage.graphs.base.boost_graph import edge_connectivity
1585
+
1586
+ [obj, edges] = edge_connectivity(g)
1587
+
1588
+ if value_only:
1589
+ return obj
1590
+
1591
+ val = [obj, edges]
1592
+
1593
+ if vertices and obj:
1594
+ H = G.copy()
1595
+ H.delete_edges(edges)
1596
+
1597
+ if H.is_directed():
1598
+ a = set(H.breadth_first_search([x for x, y in edges]))
1599
+ b = set(H).difference(a)
1600
+ val.append([a, b])
1601
+ else:
1602
+ val.append([set(c) for c in connected_components(H, sort=False)])
1603
+ elif vertices:
1604
+ val.append([set(c) for c in connected_components(G, sort=False)])
1605
+
1606
+ return val
1607
+
1608
+ if use_edge_labels:
1609
+ weight = _weight_if_real
1610
+ else:
1611
+ weight = _weight_1
1612
+
1613
+ # Better methods for small connectivity tests, when one is not interested in
1614
+ # cuts...
1615
+ if value_only and not use_edge_labels:
1616
+
1617
+ if G.is_directed():
1618
+ if not is_strongly_connected(G):
1619
+ return 0.0
1620
+
1621
+ else:
1622
+ if not is_connected(G):
1623
+ return 0.0
1624
+
1625
+ h = G.strong_orientation()
1626
+ if not is_strongly_connected(h):
1627
+ return 1.0
1628
+
1629
+ from sage.numerical.mip import MixedIntegerLinearProgram
1630
+
1631
+ p = MixedIntegerLinearProgram(maximization=False, solver=solver)
1632
+
1633
+ in_set = p.new_variable(binary=True)
1634
+ in_cut = p.new_variable(binary=True)
1635
+
1636
+ # A vertex has to be in some set
1637
+ for v in g:
1638
+ p.add_constraint(in_set[0, v] + in_set[1, v], max=1, min=1)
1639
+
1640
+ # There is no empty set
1641
+ p.add_constraint(p.sum(in_set[1, v] for v in g), min=1)
1642
+ p.add_constraint(p.sum(in_set[0, v] for v in g), min=1)
1643
+
1644
+ if g.is_directed():
1645
+ # There is no edge from set 0 to set 1 which is not in the cut
1646
+ for u, v in g.edge_iterator(labels=None):
1647
+ p.add_constraint(in_set[0, u] + in_set[1, v] - in_cut[u, v], max=1)
1648
+
1649
+ p.set_objective(p.sum(weight(l) * in_cut[u, v] for u, v, l in g.edge_iterator()))
1650
+
1651
+ else:
1652
+
1653
+ # Two adjacent vertices are in different sets if and only if
1654
+ # the edge between them is in the cut
1655
+ for u, v in g.edge_iterator(labels=None):
1656
+ p.add_constraint(in_set[0, u] + in_set[1, v] - in_cut[frozenset((u, v))], max=1)
1657
+ p.add_constraint(in_set[1, u] + in_set[0, v] - in_cut[frozenset((u, v))], max=1)
1658
+
1659
+ p.set_objective(p.sum(weight(l) * in_cut[frozenset((u, v))] for u, v, l in g.edge_iterator()))
1660
+
1661
+ obj = p.solve(log=verbose)
1662
+
1663
+ in_cut = p.get_values(in_cut, convert=bool, tolerance=integrality_tolerance)
1664
+
1665
+ if use_edge_labels is False:
1666
+ if g.is_directed():
1667
+ obj = sum(1 for u, v in g.edge_iterator(labels=False) if in_cut[u, v])
1668
+ else:
1669
+ obj = sum(1 for u, v in g.edge_iterator(labels=False) if in_cut[frozenset((u, v))])
1670
+
1671
+ if value_only:
1672
+ return obj
1673
+
1674
+ val = [obj]
1675
+ in_set = p.get_values(in_set, convert=bool, tolerance=integrality_tolerance)
1676
+
1677
+ if g.is_directed():
1678
+ edges = [(u, v, l) for u, v, l in g.edge_iterator() if in_cut[u, v]]
1679
+ else:
1680
+ edges = [(u, v, l) for u, v, l in g.edge_iterator() if in_cut[frozenset((u, v))]]
1681
+
1682
+ val.append(edges)
1683
+
1684
+ if vertices:
1685
+ a = {}
1686
+ b = {}
1687
+ for v in g:
1688
+ if in_set[0, v]:
1689
+ a.add(v)
1690
+ else:
1691
+ b.add(v)
1692
+ val.append([a, b])
1693
+
1694
+ return val
1695
+
1696
+
1697
+ def vertex_connectivity(G, value_only=True, sets=False, k=None, solver=None, verbose=0,
1698
+ *, integrality_tolerance=1e-3):
1699
+ r"""
1700
+ Return the vertex connectivity of the graph.
1701
+
1702
+ For more information, see the :wikipedia:`Connectivity_(graph_theory)` and
1703
+ the :wikipedia:`K-vertex-connected_graph`.
1704
+
1705
+ .. NOTE::
1706
+
1707
+ * When the graph is directed, this method actually computes the *strong*
1708
+ connectivity, (i.e. a directed graph is strongly `k`-connected if
1709
+ there are `k` vertex disjoint paths between any two vertices `u,
1710
+ v`). If you do not want to consider strong connectivity, the best is
1711
+ probably to convert your ``DiGraph`` object to a ``Graph`` object, and
1712
+ compute the connectivity of this other graph.
1713
+
1714
+ * By convention, a complete graph on `n` vertices is `n-1` connected. In
1715
+ this case, no certificate can be given as there is no pair of vertices
1716
+ split by a cut of order `k-1`. For this reason, the certificates
1717
+ returned in this situation are empty.
1718
+
1719
+ INPUT:
1720
+
1721
+ - ``G`` -- the input Sage (Di)Graph
1722
+
1723
+ - ``value_only`` -- boolean (default: ``True``)
1724
+
1725
+ - When set to ``True`` (default), only the value is returned.
1726
+
1727
+ - When set to ``False``, both the value and a minimum vertex cut are
1728
+ returned.
1729
+
1730
+ - ``sets`` -- boolean (default: ``False``); whether to also return the two
1731
+ sets of vertices that are disconnected by the cut (implies
1732
+ ``value_only=False``)
1733
+
1734
+ - ``k`` -- integer (default: ``None``); when specified, check if the vertex
1735
+ connectivity of the (di)graph is larger or equal to `k`. The method thus
1736
+ outputs a boolean only.
1737
+
1738
+ - ``solver`` -- string (default: ``None``); specifies a Mixed Integer Linear
1739
+ Programming (MILP) solver to be used. If set to ``None``, the default one
1740
+ is used. For more information on MILP solvers and which default solver is
1741
+ used, see the method :meth:`solve
1742
+ <sage.numerical.mip.MixedIntegerLinearProgram.solve>` of the class
1743
+ :class:`MixedIntegerLinearProgram
1744
+ <sage.numerical.mip.MixedIntegerLinearProgram>`.
1745
+
1746
+ - ``verbose`` -- integer (default: 0); sets the level of verbosity. Set
1747
+ to 0 by default, which means quiet.
1748
+
1749
+ - ``integrality_tolerance`` -- float; parameter for use with MILP solvers
1750
+ over an inexact base ring; see
1751
+ :meth:`MixedIntegerLinearProgram.get_values`.
1752
+
1753
+ EXAMPLES:
1754
+
1755
+ A basic application on a ``PappusGraph``::
1756
+
1757
+ sage: from sage.graphs.connectivity import vertex_connectivity
1758
+ sage: g = graphs.PappusGraph()
1759
+ sage: vertex_connectivity(g) # needs sage.numerical.mip
1760
+ 3
1761
+ sage: g.vertex_connectivity() # needs sage.numerical.mip
1762
+ 3
1763
+
1764
+ In a grid, the vertex connectivity is equal to the minimum degree, in which
1765
+ case one of the two sets is of cardinality `1`::
1766
+
1767
+ sage: g = graphs.GridGraph([ 3,3 ])
1768
+ sage: [value, cut, [ setA, setB ]] = vertex_connectivity(g, sets=True) # needs sage.numerical.mip
1769
+ sage: len(setA) == 1 or len(setB) == 1 # needs sage.numerical.mip
1770
+ True
1771
+
1772
+ A vertex cut in a tree is any internal vertex::
1773
+
1774
+ sage: tree = graphs.RandomTree(15)
1775
+ sage: val, [cut_vertex] = vertex_connectivity(tree, value_only=False) # needs sage.numerical.mip
1776
+ sage: tree.degree(cut_vertex) > 1 # needs sage.numerical.mip
1777
+ True
1778
+
1779
+ When ``value_only = True``, this function is optimized for small
1780
+ connectivity values and does not need to build a linear program.
1781
+
1782
+ It is the case for connected graphs which are not connected::
1783
+
1784
+ sage: g = 2 * graphs.PetersenGraph()
1785
+ sage: vertex_connectivity(g) # needs sage.numerical.mip
1786
+ 0
1787
+
1788
+ Or if they are just 1-connected::
1789
+
1790
+ sage: g = graphs.PathGraph(10)
1791
+ sage: vertex_connectivity(g) # needs sage.numerical.mip
1792
+ 1
1793
+
1794
+ For directed graphs, the strong connectivity is tested through the dedicated
1795
+ function::
1796
+
1797
+ sage: g = digraphs.ButterflyGraph(3)
1798
+ sage: vertex_connectivity(g) # needs sage.numerical.mip
1799
+ 0
1800
+
1801
+ A complete graph on `10` vertices is `9`-connected::
1802
+
1803
+ sage: g = graphs.CompleteGraph(10)
1804
+ sage: vertex_connectivity(g) # needs sage.numerical.mip
1805
+ 9
1806
+
1807
+ A complete digraph on `10` vertices is `9`-connected::
1808
+
1809
+ sage: g = DiGraph(graphs.CompleteGraph(10))
1810
+ sage: vertex_connectivity(g) # needs sage.numerical.mip
1811
+ 9
1812
+
1813
+ When parameter ``k`` is set, we only check for the existence of a vertex cut
1814
+ of order at least ``k``::
1815
+
1816
+ sage: g = graphs.PappusGraph()
1817
+ sage: vertex_connectivity(g, k=3) # needs sage.numerical.mip
1818
+ True
1819
+ sage: vertex_connectivity(g, k=4) # needs sage.numerical.mip
1820
+ False
1821
+
1822
+ TESTS:
1823
+
1824
+ Giving negative value to parameter ``k``::
1825
+
1826
+ sage: g = graphs.PappusGraph()
1827
+ sage: vertex_connectivity(g, k=-1)
1828
+ Traceback (most recent call last):
1829
+ ...
1830
+ ValueError: parameter k must be strictly positive
1831
+
1832
+ The empty graph has vertex connectivity 0, is considered connected but not
1833
+ biconnected. The empty digraph is considered strongly connected::
1834
+
1835
+ sage: from sage.graphs.connectivity import is_strongly_connected
1836
+ sage: from sage.graphs.connectivity import is_connected
1837
+ sage: empty = Graph()
1838
+ sage: vertex_connectivity(empty) # needs sage.numerical.mip
1839
+ 0
1840
+ sage: vertex_connectivity(empty, k=1) == is_connected(empty) # needs sage.numerical.mip
1841
+ True
1842
+ sage: vertex_connectivity(Graph(), k=2) == empty.is_biconnected() # needs sage.numerical.mip
1843
+ True
1844
+ sage: vertex_connectivity(DiGraph(), k=1) == is_strongly_connected(DiGraph()) # needs sage.numerical.mip
1845
+ True
1846
+
1847
+ If ``G`` is not a Sage (Di)Graph, an error is raised::
1848
+
1849
+ sage: vertex_connectivity('I am not a graph')
1850
+ Traceback (most recent call last):
1851
+ ...
1852
+ TypeError: the input must be a Sage graph
1853
+
1854
+ Complete Graph with loops or multiple edges (:issue:`25589`)::
1855
+
1856
+ sage: G = Graph([(0, 1), (0, 1)], multiedges=True)
1857
+ sage: G.vertex_connectivity() # needs sage.numerical.mip
1858
+ 1
1859
+ sage: G = graphs.CompleteGraph(4)
1860
+ sage: G.allow_loops(True)
1861
+ sage: G.add_edge(0, 0)
1862
+ sage: G.vertex_connectivity(value_only=False, verbose=1) # needs sage.numerical.mip
1863
+ (3, [])
1864
+ sage: G.allow_multiple_edges(True)
1865
+ sage: G.add_edge(0, 1)
1866
+ sage: G.vertex_connectivity(value_only=False, verbose=1) # needs sage.numerical.mip
1867
+ (3, [])
1868
+
1869
+ Check that :issue:`38723` is fixed::
1870
+
1871
+ sage: # needs sage.modules
1872
+ sage: G = graphs.SierpinskiGasketGraph(3)
1873
+ sage: G.vertex_connectivity(k=1) # needs sage.numerical.mip
1874
+ True
1875
+ sage: G.vertex_connectivity(k=2) # needs sage.numerical.mip
1876
+ True
1877
+ sage: G.vertex_connectivity(k=3) # needs sage.numerical.mip
1878
+ False
1879
+ """
1880
+ from sage.graphs.generic_graph import GenericGraph
1881
+ if not isinstance(G, GenericGraph):
1882
+ raise TypeError("the input must be a Sage graph")
1883
+
1884
+ g = G
1885
+
1886
+ if k is not None:
1887
+ if k < 1:
1888
+ raise ValueError("parameter k must be strictly positive")
1889
+ if not g.order():
1890
+ # We follow the convention of is_connected, is_biconnected and
1891
+ # is_strongly_connected
1892
+ return k == 1
1893
+ if ((g.is_directed() and k > min(min(g.in_degree()), min(g.out_degree())))
1894
+ or (not g.is_directed() and (k > min(g.degree())))):
1895
+ return False
1896
+ value_only = True
1897
+ sets = False
1898
+
1899
+ elif sets:
1900
+ value_only = False
1901
+
1902
+ # When the graph is complete, the MILP below is infeasible.
1903
+ if (g.is_clique(directed_clique=g.is_directed())
1904
+ or (not g.is_directed() and g.to_simple().is_clique())):
1905
+ if k is not None:
1906
+ return g.order() > k
1907
+ if value_only:
1908
+ return max(g.order() - 1, 0)
1909
+ elif not sets:
1910
+ return max(g.order() - 1, 0), []
1911
+ return max(g.order() - 1, 0), [], [[], []]
1912
+
1913
+ if value_only:
1914
+ if G.is_directed():
1915
+ if not is_strongly_connected(G):
1916
+ return 0 if k is None else False
1917
+
1918
+ else:
1919
+ if not is_connected(G):
1920
+ return 0 if k is None else False
1921
+
1922
+ if G.blocks_and_cut_vertices()[1]:
1923
+ return 1 if k is None else (k == 1)
1924
+
1925
+ if not G.is_triconnected():
1926
+ return 2 if k is None else (k <= 2)
1927
+ elif k == 3:
1928
+ return True
1929
+
1930
+ if k == 1:
1931
+ # We know that the (di)graph is (strongly) connected
1932
+ return True
1933
+
1934
+ from sage.numerical.mip import MixedIntegerLinearProgram, MIPSolverException
1935
+
1936
+ p = MixedIntegerLinearProgram(maximization=False, solver=solver)
1937
+
1938
+ # Sets 0 and 2 are "real" sets while set 1 represents the cut
1939
+ in_set = p.new_variable(binary=True)
1940
+
1941
+ # A vertex has to be in some set
1942
+ for v in g:
1943
+ p.add_constraint(in_set[0, v] + in_set[1, v] + in_set[2, v], max=1, min=1)
1944
+
1945
+ # There is no empty set
1946
+ p.add_constraint(p.sum(in_set[0, v] for v in g), min=1)
1947
+ p.add_constraint(p.sum(in_set[2, v] for v in g), min=1)
1948
+
1949
+ if g.is_directed():
1950
+ # There is no edge from set 0 to set 1 which is not in the cut
1951
+ for u, v in g.edge_iterator(labels=None):
1952
+ p.add_constraint(in_set[0, u] + in_set[2, v], max=1)
1953
+ else:
1954
+ # Two adjacent vertices are in different sets if and only if
1955
+ # the edge between them is in the cut
1956
+ for u, v in g.edge_iterator(labels=None):
1957
+ p.add_constraint(in_set[0, u] + in_set[2, v], max=1)
1958
+ p.add_constraint(in_set[2, u] + in_set[0, v], max=1)
1959
+
1960
+ if k is not None:
1961
+ # To check if the vertex connectivity is at least k, we check if
1962
+ # there exists a cut of order at most k-1. If the ILP is infeasible,
1963
+ # the vertex connectivity is >= k.
1964
+ p.add_constraint(p.sum(in_set[1, v] for v in g) <= k-1)
1965
+ try:
1966
+ p.solve(log=verbose)
1967
+ return False
1968
+ except MIPSolverException:
1969
+ return True
1970
+
1971
+ p.set_objective(p.sum(in_set[1, v] for v in g))
1972
+
1973
+ val = p.solve(log=verbose)
1974
+
1975
+ in_set = p.get_values(in_set, convert=bool, tolerance=integrality_tolerance)
1976
+
1977
+ if value_only:
1978
+ return sum(1 for v in g if in_set[1, v])
1979
+
1980
+ cut = []
1981
+ a = []
1982
+ b = []
1983
+
1984
+ for v in g:
1985
+ if in_set[0, v]:
1986
+ a.append(v)
1987
+ elif in_set[1, v]:
1988
+ cut.append(v)
1989
+ else:
1990
+ b.append(v)
1991
+
1992
+ if sets:
1993
+ return val, cut, [a, b]
1994
+
1995
+ return val, cut
1996
+
1997
+
1998
+ def is_strongly_connected(G):
1999
+ r"""
2000
+ Check whether the current ``DiGraph`` is strongly connected.
2001
+
2002
+ EXAMPLES:
2003
+
2004
+ The circuit is obviously strongly connected::
2005
+
2006
+ sage: from sage.graphs.connectivity import is_strongly_connected
2007
+ sage: g = digraphs.Circuit(5)
2008
+ sage: is_strongly_connected(g)
2009
+ True
2010
+ sage: g.is_strongly_connected()
2011
+ True
2012
+
2013
+ But a transitive triangle is not::
2014
+
2015
+ sage: g = DiGraph({0: [1, 2], 1: [2]})
2016
+ sage: is_strongly_connected(g)
2017
+ False
2018
+
2019
+ TESTS:
2020
+
2021
+ If ``G`` is not a Sage DiGraph, an error is raised::
2022
+
2023
+ sage: is_strongly_connected('I am not a graph')
2024
+ Traceback (most recent call last):
2025
+ ...
2026
+ TypeError: the input must be a Sage DiGraph
2027
+ """
2028
+ from sage.graphs.digraph import DiGraph
2029
+ if not isinstance(G, DiGraph):
2030
+ raise TypeError("the input must be a Sage DiGraph")
2031
+
2032
+ if G.order() <= 1:
2033
+ return True
2034
+
2035
+ try:
2036
+ return G._backend.is_strongly_connected()
2037
+
2038
+ except AttributeError:
2039
+ return len(G.strongly_connected_components()) == 1
2040
+
2041
+
2042
+ def strongly_connected_components_digraph(G, keep_labels=False):
2043
+ r"""
2044
+ Return the digraph of the strongly connected components.
2045
+
2046
+ The digraph of the strongly connected components of a graph `G` has a vertex
2047
+ per strongly connected component included in `G`. There is an edge from a
2048
+ component `C_1` to a component `C_2` if there is an edge in `G` from a
2049
+ vertex `u_1 \in C_1` to a vertex `u_2 \in C_2`.
2050
+
2051
+ INPUT:
2052
+
2053
+ - ``G`` -- the input DiGraph
2054
+
2055
+ - ``keep_labels`` -- boolean (default: ``False``); when
2056
+ ``keep_labels=True``, the resulting digraph has an edge from a component
2057
+ `C_i` to a component `C_j` for each edge in `G` from a vertex `u_i \in
2058
+ C_i` to a vertex `u_j \in C_j`. Hence the resulting digraph may have loops
2059
+ and multiple edges. However, edges in the result with same source, target,
2060
+ and label are not duplicated (see examples below). When
2061
+ ``keep_labels=False``, the return digraph is simple, so without loops nor
2062
+ multiple edges, and edges are unlabelled.
2063
+
2064
+ EXAMPLES:
2065
+
2066
+ Such a digraph is always acyclic::
2067
+
2068
+ sage: from sage.graphs.connectivity import strongly_connected_components_digraph
2069
+ sage: g = digraphs.RandomDirectedGNP(15, .1)
2070
+ sage: scc_digraph = strongly_connected_components_digraph(g)
2071
+ sage: scc_digraph.is_directed_acyclic()
2072
+ True
2073
+ sage: scc_digraph = g.strongly_connected_components_digraph()
2074
+ sage: scc_digraph.is_directed_acyclic()
2075
+ True
2076
+
2077
+ The vertices of the digraph of strongly connected components are exactly the
2078
+ strongly connected components::
2079
+
2080
+ sage: g = digraphs.ButterflyGraph(2)
2081
+ sage: scc_digraph = strongly_connected_components_digraph(g)
2082
+ sage: g.is_directed_acyclic()
2083
+ True
2084
+ sage: V_scc = list(scc_digraph)
2085
+ sage: all(Set(scc) in V_scc for scc in g.strongly_connected_components())
2086
+ True
2087
+
2088
+ The following digraph has three strongly connected components, and the
2089
+ digraph of those is a
2090
+ :meth:`~sage.graphs.digraph_generators.TransitiveTournament`::
2091
+
2092
+ sage: g = DiGraph({0: {1: "01", 2: "02", 3: "03"}, 1: {2: "12"}, 2:{1: "21", 3: "23"}})
2093
+ sage: scc_digraph = strongly_connected_components_digraph(g)
2094
+ sage: scc_digraph.is_isomorphic(digraphs.TransitiveTournament(3))
2095
+ True
2096
+
2097
+ By default, the labels are discarded, and the result has no loops nor
2098
+ multiple edges. If ``keep_labels`` is ``True``, then the labels are kept,
2099
+ and the result is a multi digraph, possibly with multiple edges and
2100
+ loops. However, edges in the result with same source, target, and label are
2101
+ not duplicated (see the edges from 0 to the strongly connected component
2102
+ `\{1,2\}` below)::
2103
+
2104
+ sage: g = DiGraph({0: {1: "0-12", 2: "0-12", 3: "0-3"}, 1: {2: "1-2", 3: "1-3"}, 2: {1: "2-1", 3: "2-3"}})
2105
+ sage: g.order(), g.size()
2106
+ (4, 7)
2107
+ sage: scc_digraph = strongly_connected_components_digraph(g, keep_labels=True)
2108
+ sage: (scc_digraph.order(), scc_digraph.size())
2109
+ (3, 6)
2110
+ sage: set(g.edge_labels()) == set(scc_digraph.edge_labels())
2111
+ True
2112
+
2113
+ TESTS:
2114
+
2115
+ If ``G`` is not a Sage DiGraph, an error is raised::
2116
+
2117
+ sage: strongly_connected_components_digraph('I am not a graph')
2118
+ Traceback (most recent call last):
2119
+ ...
2120
+ TypeError: the input must be a Sage DiGraph
2121
+ """
2122
+ from sage.graphs.digraph import DiGraph
2123
+ if not isinstance(G, DiGraph):
2124
+ raise TypeError("the input must be a Sage DiGraph")
2125
+
2126
+ from sage.sets.set import Set
2127
+
2128
+ cdef list scc = G.strongly_connected_components()
2129
+ cdef list scc_set = [Set(_) for _ in scc]
2130
+ cdef dict d = {v: i for i, c in enumerate(scc) for v in c}
2131
+
2132
+ if keep_labels:
2133
+ g = DiGraph(len(scc), multiedges=True, loops=True)
2134
+ g.add_edges(set((d[u], d[v], label) for u, v, label in G.edge_iterator()))
2135
+
2136
+ else:
2137
+ g = DiGraph(len(scc), multiedges=False, loops=False)
2138
+ g.add_edges(((d[u], d[v]) for u, v in G.edge_iterator(labels=False)), loops=False)
2139
+
2140
+ g.relabel(scc_set, inplace=True)
2141
+ return g
2142
+
2143
+
2144
+ def strongly_connected_components_subgraphs(G):
2145
+ r"""
2146
+ Return the strongly connected components as a list of subgraphs.
2147
+
2148
+ EXAMPLES:
2149
+
2150
+ In the symmetric digraph of a graph, the strongly connected components are
2151
+ the connected components::
2152
+
2153
+ sage: from sage.graphs.connectivity import strongly_connected_components_subgraphs
2154
+ sage: g = graphs.PetersenGraph()
2155
+ sage: d = DiGraph(g)
2156
+ sage: strongly_connected_components_subgraphs(d)
2157
+ [Subgraph of (Petersen graph): Digraph on 10 vertices]
2158
+ sage: d.strongly_connected_components_subgraphs()
2159
+ [Subgraph of (Petersen graph): Digraph on 10 vertices]
2160
+
2161
+ ::
2162
+
2163
+ sage: g = DiGraph([(0, 1), (1, 0), (1, 2), (2, 3), (3, 2)])
2164
+ sage: strongly_connected_components_subgraphs(g)
2165
+ [Subgraph of (): Digraph on 2 vertices, Subgraph of (): Digraph on 2 vertices]
2166
+
2167
+ TESTS:
2168
+
2169
+ If ``G`` is not a Sage DiGraph, an error is raised::
2170
+
2171
+ sage: strongly_connected_components_subgraphs('I am not a graph')
2172
+ Traceback (most recent call last):
2173
+ ...
2174
+ TypeError: the input must be a Sage DiGraph
2175
+ """
2176
+ from sage.graphs.digraph import DiGraph
2177
+ if not isinstance(G, DiGraph):
2178
+ raise TypeError("the input must be a Sage DiGraph")
2179
+
2180
+ return [G.subgraph(_) for _ in G.strongly_connected_components()]
2181
+
2182
+
2183
+ def strongly_connected_component_containing_vertex(G, v):
2184
+ """
2185
+ Return the strongly connected component containing a given vertex.
2186
+
2187
+ INPUT:
2188
+
2189
+ - ``G`` -- the input DiGraph
2190
+
2191
+ - ``v`` -- a vertex
2192
+
2193
+ EXAMPLES:
2194
+
2195
+ In the symmetric digraph of a graph, the strongly connected components are
2196
+ the connected components::
2197
+
2198
+ sage: from sage.graphs.connectivity import strongly_connected_component_containing_vertex
2199
+ sage: g = graphs.PetersenGraph()
2200
+ sage: d = DiGraph(g)
2201
+ sage: strongly_connected_component_containing_vertex(d, 0)
2202
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
2203
+ sage: d.strongly_connected_component_containing_vertex(0)
2204
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
2205
+
2206
+ ::
2207
+
2208
+ sage: g = DiGraph([(0, 1), (1, 0), (1, 2), (2, 3), (3, 2)])
2209
+ sage: strongly_connected_component_containing_vertex(g, 0)
2210
+ [0, 1]
2211
+
2212
+ TESTS:
2213
+
2214
+ If ``G`` is not a Sage DiGraph, an error is raised::
2215
+
2216
+ sage: strongly_connected_component_containing_vertex('I am not a graph', 0)
2217
+ Traceback (most recent call last):
2218
+ ...
2219
+ TypeError: the input must be a Sage DiGraph
2220
+
2221
+ If the vertex is not in the DiGraph::
2222
+
2223
+ sage: strongly_connected_component_containing_vertex(DiGraph(1), 'z')
2224
+ Traceback (most recent call last):
2225
+ ...
2226
+ ValueError: vertex ('z') is not a vertex of the DiGraph
2227
+ """
2228
+ from sage.graphs.digraph import DiGraph
2229
+ if not isinstance(G, DiGraph):
2230
+ raise TypeError("the input must be a Sage DiGraph")
2231
+
2232
+ if v not in G:
2233
+ raise ValueError("vertex ({0}) is not a vertex of the DiGraph".format(repr(v)))
2234
+
2235
+ if G.order() == 1:
2236
+ return [v]
2237
+
2238
+ try:
2239
+ return G._backend.strongly_connected_component_containing_vertex(v)
2240
+
2241
+ except AttributeError:
2242
+ raise AttributeError("this function is only defined for C graphs")
2243
+
2244
+
2245
+ def strong_articulation_points(G):
2246
+ r"""
2247
+ Return the strong articulation points of this digraph.
2248
+
2249
+ A vertex is a strong articulation point if its deletion increases the
2250
+ number of strongly connected components. This method implements the
2251
+ algorithm described in [ILS2012]_. The time complexity is dominated by
2252
+ the time complexity of the immediate dominators finding algorithm.
2253
+
2254
+ OUTPUT: the list of strong articulation points
2255
+
2256
+ EXAMPLES:
2257
+
2258
+ Two cliques sharing a vertex::
2259
+
2260
+ sage: from sage.graphs.connectivity import strong_articulation_points
2261
+ sage: D = digraphs.Complete(4)
2262
+ sage: D.add_clique([3, 4, 5, 6])
2263
+ sage: strong_articulation_points(D)
2264
+ [3]
2265
+ sage: D.strong_articulation_points()
2266
+ [3]
2267
+
2268
+ Two cliques connected by some arcs::
2269
+
2270
+ sage: D = digraphs.Complete(4) * 2
2271
+ sage: D.add_edges([(0, 4), (7, 3)])
2272
+ sage: sorted(strong_articulation_points(D))
2273
+ [0, 3, 4, 7]
2274
+ sage: D.add_edge(1, 5)
2275
+ sage: sorted(strong_articulation_points(D))
2276
+ [3, 7]
2277
+ sage: D.add_edge(6, 2)
2278
+ sage: strong_articulation_points(D)
2279
+ []
2280
+
2281
+ .. SEEALSO::
2282
+
2283
+ - :meth:`~sage.graphs.digraph.DiGraph.strongly_connected_components`
2284
+ - :meth:`~sage.graphs.base.boost_graph.dominator_tree`
2285
+
2286
+ TESTS:
2287
+
2288
+ All strong articulation points are found::
2289
+
2290
+ sage: from sage.graphs.connectivity import strong_articulation_points
2291
+ sage: def sap_naive(G):
2292
+ ....: nscc = len(G.strongly_connected_components())
2293
+ ....: S = []
2294
+ ....: for u in G:
2295
+ ....: H = copy(G)
2296
+ ....: H.delete_vertex(u)
2297
+ ....: if len(H.strongly_connected_components()) > nscc:
2298
+ ....: S.append(u)
2299
+ ....: return S
2300
+ sage: D = digraphs.RandomDirectedGNP(20, 0.1)
2301
+ sage: X = sap_naive(D)
2302
+ sage: SAP = strong_articulation_points(D)
2303
+ sage: set(X) == set(SAP)
2304
+ True
2305
+
2306
+ Trivial cases::
2307
+
2308
+ sage: strong_articulation_points(DiGraph())
2309
+ []
2310
+ sage: strong_articulation_points(DiGraph(1))
2311
+ []
2312
+ sage: strong_articulation_points(DiGraph(2))
2313
+ []
2314
+
2315
+ If ``G`` is not a Sage DiGraph, an error is raised::
2316
+
2317
+ sage: strong_articulation_points('I am not a graph')
2318
+ Traceback (most recent call last):
2319
+ ...
2320
+ TypeError: the input must be a Sage DiGraph
2321
+
2322
+ Issue :issue:`29958` is fixed::
2323
+
2324
+ sage: D = DiGraph('SA?GA??_??a???@?@OH_?@?I??b??G?AgGGCO??AC????a?????A@????AOCOQ?d??I?')
2325
+ sage: SAP = strong_articulation_points(D)
2326
+ sage: set(SAP) == {1, 2, 4, 17, 18}
2327
+ True
2328
+ """
2329
+ from sage.graphs.digraph import DiGraph
2330
+ if not isinstance(G, DiGraph):
2331
+ raise TypeError("the input must be a Sage DiGraph")
2332
+
2333
+ # The method is applied on each strongly connected component
2334
+ if is_strongly_connected(G):
2335
+ # Make a mutable copy of self
2336
+ L = [DiGraph([(u, v) for u, v in G.edge_iterator(labels=0) if u != v],
2337
+ data_structure='sparse', immutable=False)]
2338
+ else:
2339
+ # Get the list of strongly connected components of self as mutable
2340
+ # subgraphs
2341
+ L = [G.subgraph(scc, immutable=False) for scc in G.strongly_connected_components()]
2342
+
2343
+ SAP = []
2344
+ for g in L:
2345
+ n = g.order()
2346
+ if n <= 2:
2347
+ continue
2348
+
2349
+ # 1. Choose arbitrarily a vertex r, and test whether r is a strong
2350
+ # articulation point.
2351
+ r = next(g.vertex_iterator())
2352
+ E = g.incoming_edges(r) + g.outgoing_edges(r)
2353
+ g.delete_vertex(r)
2354
+ if not is_strongly_connected(g):
2355
+ SAP.append(r)
2356
+ g.add_edges(E)
2357
+
2358
+ # 2. Compute the set of non-trivial immediate dominators in g
2359
+ Dr = set(g.dominator_tree(r, return_dict=True).values())
2360
+
2361
+ # 3. Compute the set of non-trivial immediate dominators in the
2362
+ # reverse digraph
2363
+ DRr = set(g.dominator_tree(r, return_dict=True, reverse=True).values())
2364
+
2365
+ # 4. Store D(r) + DR(r) - r
2366
+ SAP.extend(Dr.union(DRr).difference([r, None]))
2367
+
2368
+ return SAP
2369
+
2370
+
2371
+ def bridges(G, labels=True):
2372
+ r"""
2373
+ Return an iterator over the bridges (or cut edges).
2374
+
2375
+ A bridge is an edge whose deletion disconnects the undirected graph.
2376
+ A disconnected graph has no bridge.
2377
+
2378
+ INPUT:
2379
+
2380
+ - ``labels`` -- boolean (default: ``True``); if ``False``, each bridge is a
2381
+ tuple `(u, v)` of vertices
2382
+
2383
+ EXAMPLES::
2384
+
2385
+ sage: from sage.graphs.connectivity import bridges
2386
+ sage: from sage.graphs.connectivity import is_connected
2387
+ sage: g = 2 * graphs.PetersenGraph()
2388
+ sage: g.add_edge(1, 10)
2389
+ sage: is_connected(g)
2390
+ True
2391
+ sage: list(bridges(g))
2392
+ [(1, 10, None)]
2393
+ sage: list(g.bridges())
2394
+ [(1, 10, None)]
2395
+
2396
+ Every edge of a tree is a bridge::
2397
+
2398
+ sage: g = graphs.RandomTree(100)
2399
+ sage: sum(1 for _ in g.bridges()) == 99
2400
+ True
2401
+
2402
+ TESTS:
2403
+
2404
+ Disconnected graphs have no bridges::
2405
+
2406
+ sage: g = 2*graphs.PetersenGraph()
2407
+ sage: next(g.bridges())
2408
+ Traceback (most recent call last):
2409
+ ...
2410
+ StopIteration
2411
+
2412
+ Graph with multiple edges and edge labels::
2413
+
2414
+ sage: g = 2 * graphs.CycleGraph(3)
2415
+ sage: g.allow_multiple_edges(True)
2416
+ sage: g.add_edges(g.edges(sort=False))
2417
+ sage: g.add_edge(2, 3, "label")
2418
+ sage: list(bridges(g, labels=True))
2419
+ [(2, 3, 'label')]
2420
+
2421
+ Issue :issue:`23817` is solved::
2422
+
2423
+ sage: G = Graph()
2424
+ sage: G.add_edge(0, 1)
2425
+ sage: list(bridges(G))
2426
+ [(0, 1, None)]
2427
+ sage: G.allow_loops(True)
2428
+ sage: G.add_edge(0, 0)
2429
+ sage: G.add_edge(1, 1)
2430
+ sage: list(bridges(G))
2431
+ [(0, 1, None)]
2432
+
2433
+ If ``G`` is not a Sage Graph, an error is raised::
2434
+
2435
+ sage: next(bridges('I am not a graph'))
2436
+ Traceback (most recent call last):
2437
+ ...
2438
+ TypeError: the input must be an undirected Sage graph
2439
+ """
2440
+ from sage.graphs.graph import Graph
2441
+ if not isinstance(G, Graph):
2442
+ raise TypeError("the input must be an undirected Sage graph")
2443
+
2444
+ # Small graphs and disconnected graphs have no bridge
2445
+ if G.order() < 2 or not is_connected(G):
2446
+ return
2447
+
2448
+ B, _ = G.blocks_and_cut_vertices()
2449
+
2450
+ # A block of size 2 is a bridge, unless the vertices are connected with
2451
+ # multiple edges.
2452
+ cdef bint multiple_edges = G.allows_multiple_edges()
2453
+ cdef set ME = set(G.multiple_edges(labels=False)) if multiple_edges else set()
2454
+ for b in B:
2455
+ if len(b) == 2 and not tuple(b) in ME:
2456
+ if labels:
2457
+ if multiple_edges:
2458
+ [label] = G.edge_label(b[0], b[1])
2459
+ else:
2460
+ label = G.edge_label(b[0], b[1])
2461
+ yield (b[0], b[1], label)
2462
+ else:
2463
+ yield tuple(b)
2464
+
2465
+
2466
+ # ==============================================================================
2467
+ # Methods for finding 3-vertex-connected components and building SPQR-tree
2468
+ # ==============================================================================
2469
+
2470
+ def cleave(G, cut_vertices=None, virtual_edges=True, solver=None, verbose=0,
2471
+ *, integrality_tolerance=1e-3):
2472
+ r"""
2473
+ Return the connected subgraphs separated by the input vertex cut.
2474
+
2475
+ Given a connected (multi)graph `G` and a vertex cut `X`, this method
2476
+ computes the list of subgraphs of `G` induced by each connected component
2477
+ `c` of `G\setminus X` plus `X`, i.e., `G[c\cup X]`.
2478
+
2479
+ INPUT:
2480
+
2481
+ - ``G`` -- a Graph
2482
+
2483
+ - ``cut_vertices`` -- iterable container of vertices (default: ``None``); a
2484
+ set of vertices representing a vertex cut of ``G``. If no vertex cut is
2485
+ given, the method will compute one via a call to
2486
+ :meth:`~sage.graphs.connectivity.vertex_connectivity`.
2487
+
2488
+ - ``virtual_edges`` -- boolean (default: ``True``); whether to add virtual
2489
+ edges to the sides of the cut or not. A virtual edge is an edge between a
2490
+ pair of vertices of the cut that are not connected by an edge in ``G``.
2491
+
2492
+ - ``solver`` -- string (default: ``None``); specifies a Mixed Integer Linear
2493
+ Programming (MILP) solver to be used. If set to ``None``, the default one
2494
+ is used. For more information on MILP solvers and which default solver is
2495
+ used, see the method :meth:`solve
2496
+ <sage.numerical.mip.MixedIntegerLinearProgram.solve>` of the class
2497
+ :class:`MixedIntegerLinearProgram
2498
+ <sage.numerical.mip.MixedIntegerLinearProgram>`.
2499
+
2500
+ - ``verbose`` -- integer (default: 0); sets the level of verbosity. Set
2501
+ to 0 by default, which means quiet.
2502
+
2503
+ - ``integrality_tolerance`` -- float; parameter for use with MILP solvers
2504
+ over an inexact base ring; see
2505
+ :meth:`MixedIntegerLinearProgram.get_values`.
2506
+
2507
+ OUTPUT: a triple `(S, C, f)`, where
2508
+
2509
+ - `S` is a list of the graphs that are sides of the vertex cut.
2510
+
2511
+ - `C` is the graph of the cocycles. For each pair of vertices of the cut,
2512
+ if there exists an edge between them, `C` has one copy of each edge
2513
+ connecting them in ``G`` per sides of the cut plus one extra copy.
2514
+ Furthermore, when ``virtual_edges == True``, if a pair of vertices of the
2515
+ cut is not connected by an edge in ``G``, then it has one virtual edge
2516
+ between them per sides of the cut.
2517
+
2518
+ - `f` is the complement of the subgraph of ``G`` induced by the vertex
2519
+ cut. Hence, its vertex set is the vertex cut, and its edge set is the set
2520
+ of virtual edges (i.e., edges between pairs of vertices of the cut that
2521
+ are not connected by an edge in ``G``). When ``virtual_edges == False``,
2522
+ the edge set is empty.
2523
+
2524
+ EXAMPLES:
2525
+
2526
+ If there is an edge between cut vertices::
2527
+
2528
+ sage: from sage.graphs.connectivity import cleave
2529
+ sage: G = Graph(2)
2530
+ sage: for _ in range(3):
2531
+ ....: G.add_clique([0, 1, G.add_vertex(), G.add_vertex()])
2532
+ sage: S1,C1,f1 = cleave(G, cut_vertices=[0, 1])
2533
+ sage: [g.order() for g in S1]
2534
+ [4, 4, 4]
2535
+ sage: C1.order(), C1.size()
2536
+ (2, 4)
2537
+ sage: f1.vertices(sort=True), f1.edges(sort=True)
2538
+ ([0, 1], [])
2539
+
2540
+ If ``virtual_edges == False`` and there is an edge between cut vertices::
2541
+
2542
+ sage: G.subgraph([0, 1]).complement() == Graph([[0, 1], []])
2543
+ True
2544
+ sage: S2,C2,f2 = cleave(G, cut_vertices=[0, 1], virtual_edges=False)
2545
+ sage: (S1 == S2, C1 == C2, f1 == f2)
2546
+ (True, True, True)
2547
+
2548
+ If cut vertices doesn't have edge between them::
2549
+
2550
+ sage: G.delete_edge(0, 1)
2551
+ sage: S1,C1,f1 = cleave(G, cut_vertices=[0, 1])
2552
+ sage: [g.order() for g in S1]
2553
+ [4, 4, 4]
2554
+ sage: C1.order(), C1.size()
2555
+ (2, 3)
2556
+ sage: f1.vertices(sort=True), f1.edges(sort=True)
2557
+ ([0, 1], [(0, 1, None)])
2558
+
2559
+ If ``virtual_edges == False`` and the cut vertices are not connected by an
2560
+ edge::
2561
+
2562
+ sage: G.subgraph([0, 1]).complement() == Graph([[0, 1], []])
2563
+ False
2564
+ sage: S2,C2,f2 = cleave(G, cut_vertices=[0, 1], virtual_edges=False)
2565
+ sage: [g.order() for g in S2]
2566
+ [4, 4, 4]
2567
+ sage: C2.order(), C2.size()
2568
+ (2, 0)
2569
+ sage: f2.vertices(sort=True), f2.edges(sort=True)
2570
+ ([0, 1], [])
2571
+ sage: (S1 == S2, C1 == C2, f1 == f2)
2572
+ (False, False, False)
2573
+
2574
+ If `G` is a biconnected multigraph::
2575
+
2576
+ sage: G = graphs.CompleteBipartiteGraph(2, 3)
2577
+ sage: G.add_edge(2, 3)
2578
+ sage: G.allow_multiple_edges(True)
2579
+ sage: G.add_edges(G.edge_iterator())
2580
+ sage: G.add_edges([(0, 1), (0, 1), (0, 1)])
2581
+ sage: S,C,f = cleave(G, cut_vertices=[0, 1])
2582
+ sage: for g in S:
2583
+ ....: print(g.edges(sort=True, labels=0))
2584
+ [(0, 1), (0, 1), (0, 1), (0, 2), (0, 2), (0, 3), (0, 3), (1, 2), (1, 2), (1, 3), (1, 3), (2, 3), (2, 3)]
2585
+ [(0, 1), (0, 1), (0, 1), (0, 4), (0, 4), (1, 4), (1, 4)]
2586
+
2587
+ TESTS::
2588
+
2589
+ sage: cleave(Graph(2))
2590
+ Traceback (most recent call last):
2591
+ ...
2592
+ ValueError: this method is designed for connected graphs only
2593
+ sage: cleave(graphs.PathGraph(3), cut_vertices=[5])
2594
+ Traceback (most recent call last):
2595
+ ...
2596
+ ValueError: vertex 5 is not a vertex of the input graph
2597
+ sage: cleave(Graph())
2598
+ Traceback (most recent call last):
2599
+ ...
2600
+ ValueError: the input graph has no vertex cut
2601
+ sage: cleave(graphs.CompleteGraph(5))
2602
+ Traceback (most recent call last):
2603
+ ...
2604
+ ValueError: the input graph has no vertex cut
2605
+ sage: cleave(graphs.CompleteGraph(5), cut_vertices=[3, 4])
2606
+ Traceback (most recent call last):
2607
+ ...
2608
+ ValueError: the set cut_vertices is not a vertex cut of the graph
2609
+ """
2610
+ if not G.is_connected():
2611
+ raise ValueError("this method is designed for connected graphs only")
2612
+
2613
+ # If a vertex cut is given, we check that it is valid. Otherwise, we compute
2614
+ # a small vertex cut
2615
+ if cut_vertices is None:
2616
+ _, cut_vertices = G.vertex_connectivity(value_only=False,
2617
+ solver=solver, verbose=verbose,
2618
+ integrality_tolerance=integrality_tolerance)
2619
+ if not cut_vertices:
2620
+ # Typical example is a clique
2621
+ raise ValueError("the input graph has no vertex cut")
2622
+ else:
2623
+ cut_vertices = list(cut_vertices)
2624
+ for u in cut_vertices:
2625
+ if u not in G:
2626
+ raise ValueError(f"vertex {u} is not a vertex of the input graph")
2627
+
2628
+ H = G.copy(immutable=False)
2629
+ H.delete_vertices(cut_vertices)
2630
+ CC = H.connected_components(sort=False)
2631
+ if len(CC) == 1:
2632
+ raise ValueError("the set cut_vertices is not a vertex cut of the graph")
2633
+
2634
+ # We identify the virtual edges, i.e., pairs of vertices of the vertex cut
2635
+ # that are not connected by an edge in G
2636
+ from sage.graphs.graph import Graph
2637
+ K = G.subgraph(cut_vertices)
2638
+ if virtual_edges:
2639
+ if K.allows_multiple_edges():
2640
+ virtual_cut_graph = K.to_simple().complement()
2641
+ else:
2642
+ virtual_cut_graph = K.complement()
2643
+ else:
2644
+ virtual_cut_graph = Graph([cut_vertices, []])
2645
+
2646
+ # We now build the graphs in each side of the cut, including the vertices
2647
+ # from the vertex cut
2648
+ cut_sides = []
2649
+ for comp in CC:
2650
+ h = G.subgraph(comp + cut_vertices)
2651
+ if virtual_edges:
2652
+ h.add_edges(virtual_cut_graph.edge_iterator())
2653
+ cut_sides.append(h)
2654
+
2655
+ # We build the cocycles for re-assembly. For each edge between a pair of
2656
+ # vertices of the cut in the original graph G, a bond with one edge more
2657
+ # than the number of cut sides is needed. For pairs of vertices of the cut
2658
+ # that are not connected by an edge in G, a bond with one edge per cut side
2659
+ # is needed.
2660
+ cocycles = Graph([cut_vertices, []], multiedges=True)
2661
+ if K.size():
2662
+ cocycles.add_edges(K.edges(sort=False) * (len(cut_sides) + 1))
2663
+ if virtual_edges and virtual_cut_graph:
2664
+ cocycles.add_edges(virtual_cut_graph.edges(sort=False) * len(cut_sides))
2665
+
2666
+ return cut_sides, cocycles, virtual_cut_graph
2667
+
2668
+
2669
+ def spqr_tree(G, algorithm='Hopcroft_Tarjan', solver=None, verbose=0,
2670
+ *, integrality_tolerance=1e-3):
2671
+ r"""
2672
+ Return an SPQR-tree representing the triconnected components of the graph.
2673
+
2674
+ An SPQR-tree is a tree data structure used to represent the triconnected
2675
+ components of a biconnected (multi)graph and the 2-vertex cuts separating
2676
+ them. A node of a SPQR-tree, and the graph associated with it, can be one of
2677
+ the following four types:
2678
+
2679
+ - ``'S'`` -- the associated graph is a cycle with at least three vertices
2680
+ ``'S'`` stands for ``series``
2681
+
2682
+ - ``'P'`` -- the associated graph is a dipole graph, a multigraph with two
2683
+ vertices and three or more edges. ``'P'`` stands for ``parallel``
2684
+
2685
+ - ``'Q'`` -- the associated graph has a single real edge. This trivial case
2686
+ is necessary to handle the graph that has only one edge
2687
+
2688
+ - ``'R'`` -- the associated graph is a 3-connected graph that is not a cycle
2689
+ or dipole. ``'R'`` stands for ``rigid``
2690
+
2691
+ This method decomposes a biconnected graph into cycles, cocycles, and
2692
+ 3-connected blocks summed over cocycles, and arranges them as a SPQR-tree.
2693
+ More precisely, it splits the graph at each of its 2-vertex cuts, giving a
2694
+ unique decomposition into 3-connected blocks, cycles and cocycles. The
2695
+ cocycles are dipole graphs with one edge per real edge between the included
2696
+ vertices and one additional (virtual) edge per connected component resulting
2697
+ from deletion of the vertices in the cut. See the :wikipedia:`SPQR_tree`.
2698
+
2699
+ INPUT:
2700
+
2701
+ - ``G`` -- the input graph
2702
+
2703
+ - ``algorithm`` -- string (default: ``'Hopcroft_Tarjan'``); the algorithm to
2704
+ use among:
2705
+
2706
+ - ``'Hopcroft_Tarjan'`` -- default; use the algorithm proposed by
2707
+ Hopcroft and Tarjan in [Hopcroft1973]_ and later corrected by Gutwenger
2708
+ and Mutzel in [Gut2001]_. See
2709
+ :class:`~sage.graphs.connectivity.TriconnectivitySPQR`.
2710
+
2711
+ - ``'cleave'`` -- using method :meth:`~sage.graphs.connectivity.cleave`
2712
+
2713
+ - ``solver`` -- string (default: ``None``); specifies a Mixed Integer Linear
2714
+ Programming (MILP) solver to be used. If set to ``None``, the default one
2715
+ is used. For more information on MILP solvers and which default solver is
2716
+ used, see the method :meth:`solve
2717
+ <sage.numerical.mip.MixedIntegerLinearProgram.solve>` of the class
2718
+ :class:`MixedIntegerLinearProgram
2719
+ <sage.numerical.mip.MixedIntegerLinearProgram>`.
2720
+
2721
+ - ``verbose`` -- integer (default: 0); sets the level of verbosity. Set
2722
+ to 0 by default, which means quiet.
2723
+
2724
+ - ``integrality_tolerance`` -- float; parameter for use with MILP solvers
2725
+ over an inexact base ring; see
2726
+ :meth:`MixedIntegerLinearProgram.get_values`.
2727
+
2728
+ OUTPUT: ``SPQR-tree`` a tree whose vertices are labeled with the block's type
2729
+ and the subgraph of three-blocks in the decomposition.
2730
+
2731
+ EXAMPLES::
2732
+
2733
+ sage: from sage.graphs.connectivity import spqr_tree
2734
+ sage: G = Graph(2)
2735
+ sage: for i in range(3):
2736
+ ....: G.add_clique([0, 1, G.add_vertex(), G.add_vertex()])
2737
+ sage: Tree = spqr_tree(G)
2738
+ sage: Tree.order()
2739
+ 4
2740
+ sage: K4 = graphs.CompleteGraph(4)
2741
+ sage: all(u[1].is_isomorphic(K4) for u in Tree if u[0] == 'R')
2742
+ True
2743
+ sage: from sage.graphs.connectivity import spqr_tree_to_graph
2744
+ sage: G.is_isomorphic(spqr_tree_to_graph(Tree))
2745
+ True
2746
+
2747
+ sage: G = Graph(2)
2748
+ sage: for i in range(3):
2749
+ ....: G.add_path([0, G.add_vertex(), G.add_vertex(), 1])
2750
+ sage: Tree = spqr_tree(G)
2751
+ sage: Tree.order()
2752
+ 4
2753
+ sage: C4 = graphs.CycleGraph(4)
2754
+ sage: all(u[1].is_isomorphic(C4) for u in Tree if u[0] == 'S')
2755
+ True
2756
+ sage: G.is_isomorphic(spqr_tree_to_graph(Tree))
2757
+ True
2758
+
2759
+ sage: G.allow_multiple_edges(True)
2760
+ sage: G.add_edges(G.edge_iterator())
2761
+ sage: Tree = spqr_tree(G)
2762
+ sage: Tree.order()
2763
+ 13
2764
+ sage: all(u[1].is_isomorphic(C4) for u in Tree if u[0] == 'S')
2765
+ True
2766
+ sage: G.is_isomorphic(spqr_tree_to_graph(Tree))
2767
+ True
2768
+
2769
+ sage: G = graphs.CycleGraph(6)
2770
+ sage: Tree = spqr_tree(G)
2771
+ sage: Tree.order()
2772
+ 1
2773
+ sage: G.is_isomorphic(spqr_tree_to_graph(Tree))
2774
+ True
2775
+ sage: G.add_edge(0, 3)
2776
+ sage: Tree = spqr_tree(G)
2777
+ sage: Tree.order()
2778
+ 3
2779
+ sage: G.is_isomorphic(spqr_tree_to_graph(Tree))
2780
+ True
2781
+
2782
+ sage: G = Graph('LlCG{O@?GBoMw?')
2783
+ sage: T = spqr_tree(G, algorithm='Hopcroft_Tarjan')
2784
+ sage: G.is_isomorphic(spqr_tree_to_graph(T))
2785
+ True
2786
+ sage: T2 = spqr_tree(G, algorithm='cleave') # needs sage.numerical.mip
2787
+ sage: G.is_isomorphic(spqr_tree_to_graph(T2)) # needs sage.numerical.mip
2788
+ True
2789
+
2790
+ sage: G = Graph([(0, 1)], multiedges=True)
2791
+ sage: T = spqr_tree(G, algorithm='cleave') # needs sage.numerical.mip
2792
+ sage: T.vertices(sort=True) # needs sage.numerical.mip
2793
+ [('Q', Multi-graph on 2 vertices)]
2794
+ sage: G.is_isomorphic(spqr_tree_to_graph(T)) # needs sage.numerical.mip
2795
+ True
2796
+ sage: T = spqr_tree(G, algorithm='Hopcroft_Tarjan')
2797
+ sage: T.vertices(sort=True)
2798
+ [('Q', Multi-graph on 2 vertices)]
2799
+ sage: G.add_edge(0, 1)
2800
+ sage: spqr_tree(G, algorithm='cleave').vertices(sort=True) # needs sage.numerical.mip
2801
+ [('P', Multi-graph on 2 vertices)]
2802
+
2803
+ sage: from collections import Counter
2804
+ sage: G = graphs.PetersenGraph()
2805
+ sage: T = G.spqr_tree(algorithm='Hopcroft_Tarjan')
2806
+ sage: Counter(u[0] for u in T)
2807
+ Counter({'R': 1})
2808
+ sage: T = G.spqr_tree(algorithm='cleave') # needs sage.numerical.mip
2809
+ sage: Counter(u[0] for u in T) # needs sage.numerical.mip
2810
+ Counter({'R': 1})
2811
+ sage: for u,v in list(G.edges(labels=False, sort=False)):
2812
+ ....: G.add_path([u, G.add_vertex(), G.add_vertex(), v])
2813
+ sage: T = G.spqr_tree(algorithm='Hopcroft_Tarjan')
2814
+ sage: sorted(Counter(u[0] for u in T).items())
2815
+ [('P', 15), ('R', 1), ('S', 15)]
2816
+ sage: T = G.spqr_tree(algorithm='cleave') # needs sage.numerical.mip
2817
+ sage: sorted(Counter(u[0] for u in T).items()) # needs sage.numerical.mip
2818
+ [('P', 15), ('R', 1), ('S', 15)]
2819
+ sage: for u,v in list(G.edges(labels=False, sort=False)):
2820
+ ....: G.add_path([u, G.add_vertex(), G.add_vertex(), v])
2821
+ sage: T = G.spqr_tree(algorithm='Hopcroft_Tarjan')
2822
+ sage: sorted(Counter(u[0] for u in T).items())
2823
+ [('P', 60), ('R', 1), ('S', 75)]
2824
+ sage: T = G.spqr_tree(algorithm='cleave') # long time # needs sage.numerical.mip
2825
+ sage: sorted(Counter(u[0] for u in T).items()) # long time # needs sage.numerical.mip
2826
+ [('P', 60), ('R', 1), ('S', 75)]
2827
+
2828
+ TESTS::
2829
+
2830
+ sage: G = graphs.PathGraph(4)
2831
+ sage: spqr_tree(G)
2832
+ Traceback (most recent call last):
2833
+ ...
2834
+ ValueError: graph is not biconnected
2835
+
2836
+ sage: G = Graph([(0, 0)], loops=True)
2837
+ sage: spqr_tree(G)
2838
+ Traceback (most recent call last):
2839
+ ...
2840
+ ValueError: graph is not biconnected
2841
+
2842
+ sage: spqr_tree(Graph(), algorithm='easy')
2843
+ Traceback (most recent call last):
2844
+ ...
2845
+ NotImplementedError: SPQR tree algorithm 'easy' is not implemented
2846
+ """
2847
+ from sage.graphs.generic_graph import GenericGraph
2848
+ if not isinstance(G, GenericGraph):
2849
+ raise TypeError("the input must be a Sage graph")
2850
+
2851
+ if algorithm == "Hopcroft_Tarjan":
2852
+ tric = TriconnectivitySPQR(G)
2853
+ return tric.get_spqr_tree()
2854
+
2855
+ if algorithm != "cleave":
2856
+ raise NotImplementedError("SPQR tree algorithm '{}' is not implemented".format(algorithm))
2857
+
2858
+ from sage.graphs.graph import Graph
2859
+ from collections import Counter
2860
+
2861
+ if G.has_loops():
2862
+ raise ValueError("generation of SPQR-trees is only implemented for graphs without loops")
2863
+
2864
+ if G.order() == 2 and G.size():
2865
+ return Graph({('Q' if G.size() == 1 else 'P', Graph(G, immutable=True, multiedges=True)): []},
2866
+ name='SPQR-tree of {}'.format(G.name()))
2867
+
2868
+ cut_size, cut_vertices = G.vertex_connectivity(value_only=False, solver=solver, verbose=verbose,
2869
+ integrality_tolerance=integrality_tolerance)
2870
+
2871
+ if cut_size < 2:
2872
+ raise ValueError("generation of SPQR-trees is only implemented for 2-connected graphs")
2873
+ elif cut_size > 2:
2874
+ return Graph({('R', Graph(G, immutable=True)): []}, name='SPQR-tree of {}'.format(G.name()))
2875
+
2876
+ # Split_multiple_edge Algorithm. If the input graph has multiple edges, we
2877
+ # make SG a simple graph while recording virtual edges that will be needed
2878
+ # to 2-sum with cocycles during reconstruction. After this step, next steps
2879
+ # will be finding S and R blocks.
2880
+ if G.has_multiple_edges():
2881
+ SG = G.to_simple()
2882
+ counter_multiedges = Counter([frozenset(e) for e in G.multiple_edges(labels=False)])
2883
+ else:
2884
+ SG = G
2885
+ counter_multiedges = Counter()
2886
+
2887
+ # If SG simplifies to a cycle, we return the corresponding SPQR tree
2888
+ if SG.is_cycle():
2889
+ T = Graph(name='SPQR-tree of {}'.format(G.name()))
2890
+ S_node = ('S', Graph(SG, immutable=True))
2891
+ T.add_vertex(S_node)
2892
+ for e, num in counter_multiedges.items():
2893
+ P_node = ('P', Graph([e] * (num + 1), multiedges=True, immutable=True))
2894
+ T.add_edge(S_node, P_node)
2895
+ return T
2896
+
2897
+ # We now search for 2-vertex cuts. If a cut is found, we split the graph and
2898
+ # check each side for S or R blocks or another 2-vertex cut
2899
+ R_blocks = []
2900
+ two_blocks = [(SG, cut_vertices)]
2901
+ cocycles_count = Counter()
2902
+ cycles_list = []
2903
+ virtual_edge_to_cycles = dict()
2904
+
2905
+ while two_blocks:
2906
+ B, B_cut = two_blocks.pop()
2907
+ # B will be always simple graph.
2908
+ S, C, f = cleave(B, cut_vertices=B_cut)
2909
+ # Store the number of edges of the cocycle (P block)
2910
+ fe = frozenset(B_cut)
2911
+ cocycles_count[fe] += C.size()
2912
+ if f.size():
2913
+ virtual_edge_to_cycles[fe] = []
2914
+ # Check the sides of the cut
2915
+ for K in S:
2916
+ if K.is_cycle():
2917
+ # Add this cycle to the list of cycles
2918
+ cycles_list.append(K)
2919
+ else:
2920
+ K_cut_size, K_cut_vertices = K.vertex_connectivity(value_only=False, solver=solver, verbose=verbose,
2921
+ integrality_tolerance=integrality_tolerance)
2922
+ if K_cut_size == 2:
2923
+ # The graph has a 2-vertex cut. We add it to the stack
2924
+ two_blocks.append((K, K_cut_vertices))
2925
+ else:
2926
+ # The graph is 3-vertex connected
2927
+ R_blocks.append(('R', Graph(K, immutable=True)))
2928
+
2929
+ # Cycles of order > 3 may have been triangulated; We undo this to reduce the
2930
+ # number of S-blocks. Two cycles can be merged if they share a virtual edge
2931
+ # that is not shared by any other block, i.e., cocycles_count[e] == 2. We
2932
+ # first associate cycles to virtual edges. Then, we use a DisjointSet to
2933
+ # form the groups of cycles to be merged.
2934
+ for K_index, K in enumerate(cycles_list):
2935
+ for e in K.edge_iterator(labels=False):
2936
+ fe = frozenset(e)
2937
+ if fe in virtual_edge_to_cycles:
2938
+ virtual_edge_to_cycles[fe].append(K_index)
2939
+ from sage.sets.disjoint_set import DisjointSet
2940
+ DS = DisjointSet(range(len(cycles_list)))
2941
+ for fe in virtual_edge_to_cycles:
2942
+ if cocycles_count[fe] == 2 and len(virtual_edge_to_cycles[fe]) == 2:
2943
+ # This virtual edge is only between 2 cycles
2944
+ C1, C2 = virtual_edge_to_cycles[fe]
2945
+ DS.union(C1, C2)
2946
+ cycles_list[C1].delete_edge(fe)
2947
+ cycles_list[C2].delete_edge(fe)
2948
+ cocycles_count[fe] -= 2
2949
+
2950
+ # We finalize the creation of S_blocks.
2951
+ S_blocks = []
2952
+ for root, indexes in DS.root_to_elements_dict().items():
2953
+ E = []
2954
+ for i in indexes:
2955
+ E.extend(cycles_list[i].edge_iterator(labels=False))
2956
+ S_blocks.append(('S', Graph(E, immutable=True)))
2957
+
2958
+ # We now build the SPQR tree
2959
+ Tree = Graph(name='SPQR tree of {}'.format(G.name()))
2960
+ SR_blocks = S_blocks + R_blocks
2961
+ Tree.add_vertices(SR_blocks)
2962
+ P2 = []
2963
+ for e, num in cocycles_count.items():
2964
+ if num:
2965
+ P_block = ('P', Graph([e] * (num + max(0, counter_multiedges[e] - 1)), multiedges=True, immutable=True))
2966
+ for block in SR_blocks:
2967
+ # Note: here we use a try...except statement since the immutable
2968
+ # graph backend raises an error if an end vertex of the edge is
2969
+ # not in the graph.
2970
+ try:
2971
+ if block[1].has_edge(e):
2972
+ Tree.add_edge(block, P_block)
2973
+ except LookupError:
2974
+ continue
2975
+ if num == 2:
2976
+ # When 2 S or R blocks are separated by a 2-cut without edge, we
2977
+ # have added a P block with only 2 edges. We must remove them
2978
+ # and connect neighbors by an edge. So we record these blocks
2979
+ P2.append(P_block)
2980
+
2981
+ # We now remove the P blocks with only 2 edges.
2982
+ for P_block in P2:
2983
+ u, v = Tree.neighbors(P_block)
2984
+ Tree.add_edge(u, v)
2985
+ Tree.delete_vertex(P_block)
2986
+
2987
+ # We finally add P blocks to account for multiple edges of the input graph
2988
+ # that are not involved in any separator of the graph
2989
+ for e, num in counter_multiedges.items():
2990
+ if not cocycles_count[e]:
2991
+ P_block = ('P', Graph([e] * (num + 1), multiedges=True, immutable=True))
2992
+ for block in SR_blocks:
2993
+ try:
2994
+ if block[1].has_edge(e):
2995
+ Tree.add_edge(block, P_block)
2996
+ break
2997
+ except LookupError:
2998
+ continue
2999
+
3000
+ return Tree
3001
+
3002
+
3003
+ def spqr_tree_to_graph(T):
3004
+ r"""
3005
+ Return the graph represented by the SPQR-tree `T`.
3006
+
3007
+ The main purpose of this method is to test :meth:`spqr_tree`.
3008
+
3009
+ INPUT:
3010
+
3011
+ - ``T`` -- a SPQR tree as returned by :meth:`spqr_tree`
3012
+
3013
+ OUTPUT: a (multi) graph
3014
+
3015
+ EXAMPLES:
3016
+
3017
+ :wikipedia:`SPQR_tree` reference paper example::
3018
+
3019
+ sage: from sage.graphs.connectivity import spqr_tree
3020
+ sage: from sage.graphs.connectivity import spqr_tree_to_graph
3021
+ sage: G = Graph([(1, 2), (1, 4), (1, 8), (1, 12), (3, 4), (2, 3),
3022
+ ....: (2, 13), (3, 13), (4, 5), (4, 7), (5, 6), (5, 8), (5, 7), (6, 7),
3023
+ ....: (8, 11), (8, 9), (8, 12), (9, 10), (9, 11), (9, 12), (10, 12)])
3024
+ sage: T = spqr_tree(G)
3025
+ sage: H = spqr_tree_to_graph(T)
3026
+ sage: H.is_isomorphic(G)
3027
+ True
3028
+
3029
+ A small multigraph ::
3030
+
3031
+ sage: G = Graph([(0, 2), (0, 2), (1, 3), (2, 3)], multiedges=True)
3032
+ sage: for i in range(3):
3033
+ ....: G.add_clique([0, 1, G.add_vertex(), G.add_vertex()])
3034
+ sage: for i in range(3):
3035
+ ....: G.add_clique([2, 3, G.add_vertex(), G.add_vertex()])
3036
+ sage: T = spqr_tree(G)
3037
+ sage: H = spqr_tree_to_graph(T)
3038
+ sage: H.is_isomorphic(G)
3039
+ True
3040
+
3041
+ TESTS:
3042
+
3043
+ Check that the method is working for the empty SPQR tree::
3044
+
3045
+ sage: H = spqr_tree_to_graph(Graph())
3046
+ sage: H.is_isomorphic(Graph())
3047
+ True
3048
+
3049
+ Check that :issue:`38527` is fixed::
3050
+
3051
+ sage: # needs sage.numerical.mip
3052
+ sage: from sage.graphs.connectivity import spqr_tree, spqr_tree_to_graph
3053
+ sage: G = Graph('LlCG{O@?GBoMw?')
3054
+ sage: T1 = spqr_tree(G, algorithm="Hopcroft_Tarjan")
3055
+ sage: T2 = spqr_tree(G, algorithm="cleave")
3056
+ sage: T1.is_isomorphic(T2)
3057
+ True
3058
+ sage: G1 = spqr_tree_to_graph(T1)
3059
+ sage: G2 = spqr_tree_to_graph(T2)
3060
+ sage: G.is_isomorphic(G1)
3061
+ True
3062
+ sage: G.is_isomorphic(G2)
3063
+ True
3064
+ """
3065
+ from sage.graphs.graph import Graph
3066
+ from collections import Counter
3067
+
3068
+ count_G = Counter()
3069
+ count_P = Counter()
3070
+ vertex_to_int = dict()
3071
+ for t, g in T:
3072
+ for u in g:
3073
+ if u not in vertex_to_int:
3074
+ vertex_to_int[u] = len(vertex_to_int)
3075
+ if t in ['P', 'Q']:
3076
+ for u, v, label in g.edge_iterator():
3077
+ if vertex_to_int[u] < vertex_to_int[v]:
3078
+ count_P[u, v, label] += 1
3079
+ else:
3080
+ count_P[v, u, label] += 1
3081
+ else:
3082
+ for u, v, label in g.edge_iterator():
3083
+ if vertex_to_int[u] < vertex_to_int[v]:
3084
+ count_G[u, v, label] += 1
3085
+ else:
3086
+ count_G[v, u, label] += 1
3087
+
3088
+ G = Graph(multiedges=True)
3089
+ for e, num in count_G.items():
3090
+ if e in count_P:
3091
+ num = abs(count_P[e] - count_G[e])
3092
+ elif num == 2:
3093
+ # Case of 2 S or R blocks separated by a 2-cut without edge.
3094
+ # No P-block was created as a P-block has at least 3 edges.
3095
+ continue
3096
+ for _ in range(num):
3097
+ G.add_edge(e)
3098
+
3099
+ # Some edges might only be in P_blocks. Such edges are true edges of the
3100
+ # graph. This happen when virtual edges have distinct labels.
3101
+ for e, num in count_P.items():
3102
+ if e not in count_G:
3103
+ for _ in range(num):
3104
+ G.add_edge(e)
3105
+
3106
+ return G
3107
+
3108
+
3109
+ # Helper methods for ``TriconnectivitySPQR``.
3110
+ # Define a doubly linked list
3111
+
3112
+ cdef inline _LinkedListNode_initialize(_LinkedListNode * node, Py_ssize_t data):
3113
+ """
3114
+ Initialize the ``_LinkedListNode`` with value data.
3115
+ """
3116
+ node.prev = NULL
3117
+ node.next = NULL
3118
+ node.data = data
3119
+
3120
+
3121
+ cdef inline _LinkedList_initialize(_LinkedList * ll):
3122
+ """
3123
+ Initialize the ``_LinkedList``.
3124
+ """
3125
+ ll.head = NULL
3126
+ ll.tail = NULL
3127
+ ll.length = 0
3128
+
3129
+ cdef _LinkedList_set_head(_LinkedList * ll, _LinkedListNode * h):
3130
+ """
3131
+ Set the node ``h`` as the head and tail of the linked list ``ll``.
3132
+ """
3133
+ ll.head = h
3134
+ ll.tail = h
3135
+ ll.length = 1
3136
+
3137
+ cdef inline _LinkedListNode * _LinkedList_get_head(_LinkedList * ll) noexcept:
3138
+ """
3139
+ Return the head of the linked list ``ll``.
3140
+ """
3141
+ return ll.head
3142
+
3143
+ cdef inline Py_ssize_t _LinkedList_get_length(_LinkedList * ll) noexcept:
3144
+ """
3145
+ Return the length of the linked list ``ll``.
3146
+ """
3147
+ return ll.length
3148
+
3149
+ cdef _LinkedList_append(_LinkedList * ll, _LinkedListNode * node):
3150
+ """
3151
+ Append the node ``node`` to the linked list ``ll``.
3152
+ """
3153
+ if not ll.head:
3154
+ _LinkedList_set_head(ll, node)
3155
+ else:
3156
+ ll.tail.next = node
3157
+ node.prev = ll.tail
3158
+ ll.tail = node
3159
+ ll.length += 1
3160
+
3161
+ cdef _LinkedList_remove(_LinkedList * ll, _LinkedListNode * node):
3162
+ """
3163
+ Remove the node ``node`` from the linked list ``ll``.
3164
+ """
3165
+ if not node.prev and not node.next:
3166
+ ll.head = NULL
3167
+ ll.tail = NULL
3168
+ elif not node.prev: # node is head
3169
+ ll.head = node.next
3170
+ node.next.prev = NULL
3171
+ elif not node.next: # node is tail
3172
+ node.prev.next = NULL
3173
+ ll.tail = node.prev
3174
+ else:
3175
+ node.prev.next = node.next
3176
+ node.next.prev = node.prev
3177
+ ll.length -= 1
3178
+
3179
+ cdef _LinkedList_push_front(_LinkedList * ll, _LinkedListNode * node):
3180
+ """
3181
+ Add node ``node`` to the beginning of the linked list ``ll``.
3182
+ """
3183
+ if not ll.head:
3184
+ _LinkedList_set_head(ll, node)
3185
+ else:
3186
+ ll.head.prev = node
3187
+ node.next = ll.head
3188
+ ll.head = node
3189
+ ll.length += 1
3190
+
3191
+ cdef _LinkedList_concatenate(_LinkedList * lst1, _LinkedList * lst2):
3192
+ """
3193
+ Concatenate lst2 to lst1.
3194
+
3195
+ Makes lst2 empty.
3196
+ """
3197
+ lst1.tail.next = lst2.head
3198
+ lst2.head.prev = lst1.tail
3199
+ lst1.tail = lst2.tail
3200
+ lst1.length += lst2.length
3201
+ lst2.head = NULL
3202
+ lst2.length = 0
3203
+
3204
+ cdef str _LinkedList_to_string(_LinkedList * ll):
3205
+ """
3206
+ Return a string representation of ``self``.
3207
+ """
3208
+ cdef _LinkedListNode * temp = ll.head
3209
+ cdef list s = []
3210
+ while temp:
3211
+ s.append(str(temp.data))
3212
+ temp = temp.next
3213
+ return " ".join(s)
3214
+
3215
+ cdef class _Component:
3216
+ """
3217
+ Connected component class.
3218
+
3219
+ This is a helper class for ``TriconnectivitySPQR``.
3220
+
3221
+ This class is used to store a connected component. It contains:
3222
+
3223
+ - ``edge_list`` -- list of edges belonging to the component,
3224
+ stored as a :class:`_LinkedList`
3225
+
3226
+ - ``component_type`` -- the type of the component
3227
+
3228
+ - 0 if bond.
3229
+ - 1 if polygon.
3230
+ - 2 is triconnected component.
3231
+ """
3232
+ def __init__(self, list edge_list, int type_c):
3233
+ """
3234
+ Initialize this component.
3235
+
3236
+ INPUT:
3237
+
3238
+ - ``edge_list`` -- list of edges to be added to the component
3239
+
3240
+ - ``type_c`` -- type of the component (0, 1, or 2)
3241
+
3242
+ TESTS::
3243
+
3244
+ sage: cython_code = [
3245
+ ....: 'from sage.graphs.connectivity cimport _Component',
3246
+ ....: 'cdef _Component comp = _Component([], 0)',
3247
+ ....: 'comp.add_edge(2)',
3248
+ ....: 'comp.add_edge(3)',
3249
+ ....: 'comp.finish_tric_or_poly(4)',
3250
+ ....: 'print(comp)']
3251
+ sage: cython(os.linesep.join(cython_code)) # needs sage.misc.cython
3252
+ Polygon: 2 3 4
3253
+ """
3254
+ self.mem = MemoryAllocator()
3255
+ self.edge_list = <_LinkedList *> self.mem.malloc(sizeof(_LinkedList))
3256
+ _LinkedList_initialize(self.edge_list)
3257
+
3258
+ cdef Py_ssize_t e_index
3259
+ for e_index in edge_list:
3260
+ self.add_edge(e_index)
3261
+ self.component_type = type_c
3262
+
3263
+ cdef add_edge(self, Py_ssize_t e_index):
3264
+ """
3265
+ Add edge index ``e_index`` to the component.
3266
+ """
3267
+ cdef _LinkedListNode * node = <_LinkedListNode *> self.mem.malloc(sizeof(_LinkedListNode))
3268
+ _LinkedListNode_initialize(node, e_index)
3269
+ _LinkedList_append(self.edge_list, node)
3270
+
3271
+ cdef finish_tric_or_poly(self, Py_ssize_t e_index):
3272
+ r"""
3273
+ Finalize the component by adding edge ``e``.
3274
+
3275
+ Edge ``e`` is the last edge to be added to the component.
3276
+ Classify the component as a polygon or triconnected component
3277
+ depending on the number of edges belonging to it.
3278
+ """
3279
+ self.add_edge(e_index)
3280
+ if _LinkedList_get_length(self.edge_list) > 3:
3281
+ self.component_type = 2
3282
+ else:
3283
+ self.component_type = 1
3284
+
3285
+ def __str__(self):
3286
+ """
3287
+ Return a string representation of the component.
3288
+
3289
+ TESTS::
3290
+
3291
+ sage: cython_code = [
3292
+ ....: 'from sage.graphs.connectivity cimport _Component',
3293
+ ....: 'cdef _Component comp = _Component([], 0)',
3294
+ ....: 'comp.add_edge(2)',
3295
+ ....: 'comp.add_edge(3)',
3296
+ ....: 'comp.finish_tric_or_poly(4)',
3297
+ ....: 'print(comp)']
3298
+ sage: cython(os.linesep.join(cython_code)) # needs sage.misc.cython
3299
+ Polygon: 2 3 4
3300
+ """
3301
+ if self.component_type == 0:
3302
+ type_str = "Bond: "
3303
+ elif self.component_type == 1:
3304
+ type_str = "Polygon: "
3305
+ else:
3306
+ type_str = "Triconnected: "
3307
+ return type_str + _LinkedList_to_string(self.edge_list)
3308
+
3309
+ cdef list get_edge_list(self):
3310
+ """
3311
+ Return the list of edges belonging to the component.
3312
+ """
3313
+ cdef list e_list = []
3314
+ cdef _LinkedListNode * e_node = _LinkedList_get_head(self.edge_list)
3315
+ while e_node:
3316
+ e_list.append(e_node.data)
3317
+ e_node = e_node.next
3318
+ return e_list
3319
+
3320
+
3321
+ cdef class TriconnectivitySPQR:
3322
+ r"""
3323
+ Decompose a graph into triconnected components and build SPQR-tree.
3324
+
3325
+ This class implements the algorithm proposed by Hopcroft and Tarjan in
3326
+ [Hopcroft1973]_, and later corrected by Gutwenger and Mutzel in [Gut2001]_,
3327
+ for finding the triconnected components of a biconnected graph. It then
3328
+ organizes these components into a SPQR-tree. See the:wikipedia:`SPQR_tree`.
3329
+
3330
+ A SPQR-tree is a tree data structure used to represent the triconnected
3331
+ components of a biconnected (multi)graph and the 2-vertex cuts separating
3332
+ them. A node of a SPQR-tree, and the graph associated with it, can be one of
3333
+ the following four types:
3334
+
3335
+ - ``'S'`` -- the associated graph is a cycle with at least three vertices
3336
+ ``'S'`` stands for ``series`` and is also called a ``polygon``
3337
+
3338
+ - ``'P'`` -- the associated graph is a dipole graph, a multigraph with two
3339
+ vertices and three or more edges. ``'P'`` stands for ``parallel`` and the
3340
+ node is called a ``bond``.
3341
+
3342
+ - ``'Q'`` -- the associated graph has a single real edge. This trivial case
3343
+ is necessary to handle the graph that has only one edge.
3344
+
3345
+ - ``'R'`` -- the associated graph is a 3-vertex-connected graph that is not
3346
+ a cycle or dipole. ``'R'`` stands for ``rigid``.
3347
+
3348
+ The edges of the tree indicate the 2-vertex cuts of the graph.
3349
+
3350
+ INPUT:
3351
+
3352
+ - ``G`` -- graph; if ``G`` is a :class:`DiGraph`, the computation is done on
3353
+ the underlying :class:`Graph` (i.e., ignoring edge orientation)
3354
+
3355
+ - ``check`` -- boolean (default: ``True``); indicates whether ``G`` needs to
3356
+ be tested for biconnectivity
3357
+
3358
+ .. SEEALSO::
3359
+
3360
+ - :meth:`sage.graphs.connectivity.spqr_tree`
3361
+ - :meth:`~Graph.is_biconnected`
3362
+ - :wikipedia:`SPQR_tree`
3363
+
3364
+ EXAMPLES:
3365
+
3366
+ Example from the :wikipedia:`SPQR_tree`::
3367
+
3368
+ sage: from sage.graphs.connectivity import TriconnectivitySPQR
3369
+ sage: from sage.graphs.connectivity import spqr_tree_to_graph
3370
+ sage: G = Graph([(1, 2), (1, 4), (1, 8), (1, 12), (3, 4), (2, 3),
3371
+ ....: (2, 13), (3, 13), (4, 5), (4, 7), (5, 6), (5, 8), (5, 7), (6, 7),
3372
+ ....: (8, 11), (8, 9), (8, 12), (9, 10), (9, 11), (9, 12), (10, 12)])
3373
+ sage: tric = TriconnectivitySPQR(G)
3374
+ sage: T = tric.get_spqr_tree()
3375
+ sage: G.is_isomorphic(spqr_tree_to_graph(T))
3376
+ True
3377
+
3378
+ An example from [Hopcroft1973]_::
3379
+
3380
+ sage: G = Graph([(1, 2), (1, 4), (1, 8), (1, 12), (1, 13), (2, 3),
3381
+ ....: (2, 13), (3, 4), (3, 13), (4, 5), (4, 7), (5, 6), (5, 7), (5, 8),
3382
+ ....: (6, 7), (8, 9), (8, 11), (8, 12), (9, 10), (9, 11), (9, 12),
3383
+ ....: (10, 11), (10, 12)])
3384
+ sage: tric = TriconnectivitySPQR(G)
3385
+ sage: T = tric.get_spqr_tree()
3386
+ sage: G.is_isomorphic(spqr_tree_to_graph(T))
3387
+ True
3388
+ sage: tric.print_triconnected_components()
3389
+ Triconnected: [(8, 9, None), (9, 12, None), (9, 11, None), (8, 11, None), (10, 11, None), (9, 10, None), (10, 12, None), (8, 12, 'newVEdge0')]
3390
+ Bond: [(8, 12, None), (8, 12, 'newVEdge0'), (8, 12, 'newVEdge1')]
3391
+ Polygon: [(6, 7, None), (5, 6, None), (7, 5, 'newVEdge2')]
3392
+ Bond: [(7, 5, 'newVEdge2'), (5, 7, 'newVEdge3'), (5, 7, None)]
3393
+ Polygon: [(5, 7, 'newVEdge3'), (4, 7, None), (5, 4, 'newVEdge4')]
3394
+ Bond: [(5, 4, 'newVEdge4'), (4, 5, 'newVEdge5'), (4, 5, None)]
3395
+ Polygon: [(4, 5, 'newVEdge5'), (5, 8, None), (1, 4, 'newVEdge9'), (1, 8, 'newVEdge10')]
3396
+ Triconnected: [(1, 2, None), (2, 13, None), (1, 13, None), (3, 13, None), (2, 3, None), (1, 3, 'newVEdge7')]
3397
+ Polygon: [(1, 3, 'newVEdge7'), (3, 4, None), (1, 4, 'newVEdge8')]
3398
+ Bond: [(1, 4, None), (1, 4, 'newVEdge8'), (1, 4, 'newVEdge9')]
3399
+ Bond: [(1, 8, None), (1, 8, 'newVEdge10'), (1, 8, 'newVEdge11')]
3400
+ Polygon: [(8, 12, 'newVEdge1'), (1, 8, 'newVEdge11'), (1, 12, None)]
3401
+
3402
+ An example from [Gut2001]_::
3403
+
3404
+ sage: G = Graph([(1, 2), (1, 4), (2, 3), (2, 5), (3, 4), (3, 5), (4, 5),
3405
+ ....: (4, 6), (5, 7), (5, 8), (5, 14), (6, 8), (7, 14), (8, 9), (8, 10),
3406
+ ....: (8, 11), (8, 12), (9, 10), (10, 13), (10, 14), (10, 15), (10, 16),
3407
+ ....: (11, 12), (11, 13), (12, 13), (14, 15), (14, 16), (15, 16)])
3408
+ sage: T = TriconnectivitySPQR(G).get_spqr_tree()
3409
+ sage: G.is_isomorphic(spqr_tree_to_graph(T))
3410
+ True
3411
+
3412
+ An example with multi-edges and accessing the triconnected components::
3413
+
3414
+ sage: G = Graph([(1, 2), (1, 5), (1, 5), (2, 3), (2, 3), (3, 4), (4, 5)], multiedges=True)
3415
+ sage: tric = TriconnectivitySPQR(G)
3416
+ sage: T = tric.get_spqr_tree()
3417
+ sage: G.is_isomorphic(spqr_tree_to_graph(T))
3418
+ True
3419
+ sage: tric.print_triconnected_components()
3420
+ Bond: [(1, 5, None), (1, 5, None), (1, 5, 'newVEdge0')]
3421
+ Bond: [(2, 3, None), (2, 3, None), (2, 3, 'newVEdge1')]
3422
+ Polygon: [(4, 5, None), (1, 5, 'newVEdge0'), (3, 4, None), (2, 3, 'newVEdge1'), (1, 2, None)]
3423
+
3424
+ An example of a triconnected graph::
3425
+
3426
+ sage: G = Graph([('a', 'b'), ('a', 'c'), ('a', 'd'), ('b', 'c'), ('b', 'd'), ('c', 'd')])
3427
+ sage: T = TriconnectivitySPQR(G).get_spqr_tree()
3428
+ sage: print(T.vertices(sort=True))
3429
+ [('R', Multi-graph on 4 vertices)]
3430
+ sage: G.is_isomorphic(spqr_tree_to_graph(T))
3431
+ True
3432
+
3433
+ An example of a directed graph with multi-edges::
3434
+
3435
+ sage: G = DiGraph([(1, 2), (2, 3), (3, 4), (4, 5), (1, 5), (5, 1)])
3436
+ sage: tric = TriconnectivitySPQR(G)
3437
+ sage: tric.print_triconnected_components()
3438
+ Bond: [(1, 5, None), (5, 1, None), (1, 5, 'newVEdge0')]
3439
+ Polygon: [(4, 5, None), (1, 5, 'newVEdge0'), (3, 4, None), (2, 3, None), (1, 2, None)]
3440
+
3441
+ Edge labels are preserved by the construction::
3442
+
3443
+ sage: G = Graph([(0, 1, '01'), (0, 4, '04'), (1, 2, '12'), (1, 5, '15'),
3444
+ ....: (2, 3, '23'), (2, 6, '26'), (3, 7, '37'), (4, 5, '45'),
3445
+ ....: (5, 6, '56'), (6, 7, 67)])
3446
+ sage: T = TriconnectivitySPQR(G).get_spqr_tree()
3447
+ sage: H = spqr_tree_to_graph(T)
3448
+ sage: all(G.has_edge(e) for e in H.edge_iterator())
3449
+ True
3450
+ sage: all(H.has_edge(e) for e in G.edge_iterator())
3451
+ True
3452
+
3453
+ TESTS:
3454
+
3455
+ A disconnected graph::
3456
+
3457
+ sage: from sage.graphs.connectivity import TriconnectivitySPQR
3458
+ sage: G = Graph([(1,2),(3,5)])
3459
+ sage: tric = TriconnectivitySPQR(G)
3460
+ Traceback (most recent call last):
3461
+ ...
3462
+ ValueError: graph is not connected
3463
+
3464
+ A graph with a cut vertex::
3465
+
3466
+ sage: from sage.graphs.connectivity import TriconnectivitySPQR
3467
+ sage: G = Graph([(1,2),(1,3),(2,3),(3,4),(3,5),(4,5)])
3468
+ sage: tric = TriconnectivitySPQR(G)
3469
+ Traceback (most recent call last):
3470
+ ...
3471
+ ValueError: graph has a cut vertex
3472
+ """
3473
+ def __init__(self, G, check=True):
3474
+ """
3475
+ Initialize this object, decompose the graph and build SPQR-tree.
3476
+
3477
+ INPUT:
3478
+
3479
+ - ``G`` -- graph; if ``G`` is a :class:`DiGraph`, the computation is
3480
+ done on the underlying :class:`Graph` (i.e., ignoring edge
3481
+ orientation)
3482
+
3483
+ - ``check`` -- boolean (default: ``True``); indicates whether ``G``
3484
+ needs to be tested for biconnectivity
3485
+
3486
+ EXAMPLES:
3487
+
3488
+ Example from the :wikipedia:`SPQR_tree`::
3489
+
3490
+ sage: from sage.graphs.connectivity import TriconnectivitySPQR
3491
+ sage: from sage.graphs.connectivity import spqr_tree_to_graph
3492
+ sage: G = Graph([(1, 2), (1, 4), (1, 8), (1, 12), (3, 4), (2, 3),
3493
+ ....: (2, 13), (3, 13), (4, 5), (4, 7), (5, 6), (5, 8), (5, 7), (6, 7),
3494
+ ....: (8, 11), (8, 9), (8, 12), (9, 10), (9, 11), (9, 12), (10, 12)])
3495
+ sage: tric = TriconnectivitySPQR(G)
3496
+ sage: T = tric.get_spqr_tree()
3497
+ sage: G.is_isomorphic(spqr_tree_to_graph(T))
3498
+ True
3499
+ """
3500
+ self.n = G.order()
3501
+ self.m = G.size()
3502
+ self.graph_name = G.name()
3503
+ self.mem = MemoryAllocator()
3504
+
3505
+ # We set the largest possible index of an edge to 2 * m + 1
3506
+ # The algorithm creates at most n virtual edges, so this is large enough
3507
+ self.max_number_of_edges = 2 * self.m + 1
3508
+
3509
+ # Trivial cases
3510
+ if self.n < 2:
3511
+ raise ValueError("graph is not biconnected")
3512
+ elif self.n == 2 and self.m:
3513
+ # a P block with at least 1 edge
3514
+ self.comp_final_edge_list = [G.edges(sort=False)]
3515
+ self.comp_type = [0]
3516
+ self.__build_spqr_tree()
3517
+ return
3518
+ elif self.m < self.n - 1:
3519
+ # less edges than a tree
3520
+ raise ValueError("graph is not connected")
3521
+ elif self.m < self.n:
3522
+ # less edges than a cycle
3523
+ raise ValueError("graph is not biconnected")
3524
+
3525
+ cdef Py_ssize_t i, j
3526
+ cdef Py_ssize_t e_index
3527
+
3528
+ # We relabel the graph and store it in different arrays:
3529
+ # - Vertices are relabeled as integers in [0..n-1]
3530
+ # - Edges are relabeled with distinct labels in [0..m-1] to distinguish
3531
+ # between multi-edges
3532
+ # - Virtual edges created by the algorithm have labels >= m
3533
+ # - We use these edge labels as unique edge identifiers. Each of these
3534
+ # edge labels is also the index of the edge extremities and original
3535
+ # edge label in appropriate arrays
3536
+ # - The status of an edge is: unseen=0, tree=1, frond=2, inactive=-1
3537
+ self.int_to_vertex = list(G)
3538
+ self.vertex_to_int = {u: i for i, u in enumerate(self.int_to_vertex)}
3539
+ self.edge_extremity_first = <int * > self.mem.allocarray(self.max_number_of_edges, sizeof(int))
3540
+ self.edge_extremity_second = <int * > self.mem.allocarray(self.max_number_of_edges, sizeof(int))
3541
+ self.int_to_original_edge_label = [] # to associate original edge label
3542
+ self.edge_status = <int *> self.mem.allocarray(self.max_number_of_edges, sizeof(int))
3543
+ for e_index, (u, v, l) in enumerate(G.edge_iterator()):
3544
+ self.int_to_original_edge_label.append(l)
3545
+ self.edge_extremity_first[e_index] = self.vertex_to_int[u]
3546
+ self.edge_extremity_second[e_index] = self.vertex_to_int[v]
3547
+ self.edge_status[e_index] = 0
3548
+
3549
+ # Label used for virtual edges, incremented at every new virtual edge
3550
+ self.virtual_edge_num = 0
3551
+
3552
+ #
3553
+ # Initialize data structures needed for the algorithm
3554
+ #
3555
+
3556
+ # Edges of the graph which are in the reverse direction in palm tree
3557
+ self.reverse_edges = <bint *> self.mem.allocarray(self.max_number_of_edges, sizeof(bint))
3558
+ for i in range(self.max_number_of_edges):
3559
+ self.reverse_edges[i] = False
3560
+
3561
+ # DFS number of vertex i
3562
+ self.dfs_number = <int *> self.mem.allocarray(self.n, sizeof(int))
3563
+ for i in range(self.n):
3564
+ self.dfs_number[i] = 0
3565
+
3566
+ # Linked list of fronds entering vertex i in the order they are visited
3567
+ self.highpt = <_LinkedList **> self.mem.allocarray(self.n, sizeof(_LinkedList *))
3568
+ for i in range(self.n):
3569
+ self.highpt[i] = <_LinkedList *> self.mem.malloc(sizeof(_LinkedList))
3570
+ _LinkedList_initialize(self.highpt[i])
3571
+
3572
+ # A dictionary whose key is an edge e, value is a pointer to element in
3573
+ # self.highpt containing the edge e. Used in the `path_search` function.
3574
+ self.in_high = <_LinkedListNode **> self.mem.allocarray(self.max_number_of_edges, sizeof(_LinkedListNode *))
3575
+ for i in range(self.max_number_of_edges):
3576
+ self.in_high[i] = NULL
3577
+
3578
+ # Translates DFS number of a vertex to its new number
3579
+ self.old_to_new = <int *> self.mem.allocarray(self.n + 1, sizeof(int))
3580
+ self.newnum = <int *> self.mem.allocarray(self.n + 1, sizeof(int))
3581
+ self.node_at = <int *> self.mem.allocarray(self.n + 1, sizeof(int))
3582
+ self.lowpt1 = <int *> self.mem.allocarray(self.n + 1, sizeof(int))
3583
+ self.lowpt2 = <int *> self.mem.allocarray(self.n + 1, sizeof(int))
3584
+ for i in range(self.n + 1):
3585
+ self.old_to_new[i] = 0
3586
+ self.newnum[i] = 0
3587
+ self.node_at[i] = 0
3588
+ self.lowpt1[i] = -1
3589
+ self.lowpt2[i] = -1
3590
+
3591
+ # i^th value contains a LinkedList of incident edges of vertex i
3592
+ self.adj = <_LinkedList **> self.mem.allocarray(self.n, sizeof(_LinkedList *))
3593
+ for i in range(self.n):
3594
+ self.adj[i] = <_LinkedList *> self.mem.malloc(sizeof(_LinkedList))
3595
+ _LinkedList_initialize(self.adj[i])
3596
+
3597
+ # A dictionary whose key is an edge, value is a pointer to element in
3598
+ # self.adj containing the edge. Used in the `path_search` function.
3599
+ self.in_adj = <_LinkedListNode **> self.mem.allocarray(self.max_number_of_edges, sizeof(_LinkedListNode *))
3600
+ for i in range(self.max_number_of_edges):
3601
+ self.in_adj[i] = NULL
3602
+
3603
+ self.nd = <int *> self.mem.allocarray(self.n, sizeof(int))
3604
+ for i in range(self.n):
3605
+ self.nd[i] = 0
3606
+
3607
+ # Parent vertex of vertex i in the palm tree
3608
+ self.parent = <int *> self.mem.allocarray(self.n, sizeof(int))
3609
+ self.degree = <int *> self.mem.allocarray(self.n, sizeof(int))
3610
+ self.tree_arc = <int *> self.mem.allocarray(self.n, sizeof(int))
3611
+ self.vertex_at = <int *> self.mem.allocarray(self.n, sizeof(int))
3612
+ for i in range(self.n):
3613
+ self.parent[i] = -1
3614
+ self.degree[i] = 0
3615
+ self.tree_arc[i] = -1
3616
+ self.vertex_at[i] = 1
3617
+
3618
+ self.dfs_counter = 0
3619
+ self.components_list = [] # list of components of `graph_copy`
3620
+ self.graph_copy_adjacency = [[] for i in range(self.n)] # Stores adjacency list
3621
+
3622
+ # Dictionary of (e, True/False) to denote if edge e starts a path
3623
+ self.starts_path = <bint *> self.mem.allocarray(self.max_number_of_edges, sizeof(bint))
3624
+
3625
+ # Stacks used in `path_search` function
3626
+ self.e_stack = []
3627
+ self.t_stack_h = <int *> self.mem.allocarray(self.max_number_of_edges, sizeof(int))
3628
+ self.t_stack_a = <int *> self.mem.allocarray(self.max_number_of_edges, sizeof(int))
3629
+ self.t_stack_b = <int *> self.mem.allocarray(self.max_number_of_edges, sizeof(int))
3630
+ self.t_stack_top = 0
3631
+ self.t_stack_a[self.t_stack_top] = -1
3632
+
3633
+ # The final triconnected components are stored
3634
+ self.comp_final_edge_list = [] # i^th entry is list of edges in i^th component
3635
+ self.comp_type = [] # i^th entry is type of i^th component
3636
+ # associate final edge e to its internal index
3637
+ self.final_edge_to_edge_index = {}
3638
+ # The final SPQR tree is stored
3639
+ self.spqr_tree = None # Graph
3640
+
3641
+ # Arrays used in different methods. We allocate them only once
3642
+ self.tmp_array_n_int_1 = <int *> self.mem.allocarray(self.n, sizeof(int))
3643
+ self.tmp_array_n_int_2 = <int *> self.mem.allocarray(self.n, sizeof(int))
3644
+ self.tmp_array_n_int_3 = <int *> self.mem.allocarray(self.n, sizeof(int))
3645
+ self.tmp_array_n_bint_1 = <bint *> self.mem.allocarray(self.n, sizeof(bint))
3646
+
3647
+ #
3648
+ # Triconnectivity algorithm
3649
+ #
3650
+
3651
+ # Deal with multiple edges
3652
+ self.__split_multiple_edges()
3653
+
3654
+ # Build adjacency list
3655
+ for e_index in range(self.m + self.virtual_edge_num):
3656
+ if self.edge_status[e_index] == -1:
3657
+ continue
3658
+ i = self.edge_extremity_first[e_index]
3659
+ j = self.edge_extremity_second[e_index]
3660
+ self.graph_copy_adjacency[i].append(e_index)
3661
+ self.graph_copy_adjacency[j].append(e_index)
3662
+ self.degree[i] += 1
3663
+ self.degree[j] += 1
3664
+
3665
+ self.dfs_counter = 0 # Initialisation for dfs1()
3666
+ self.start_vertex = 0 # Initialisation for dfs1()
3667
+ cdef int cut_vertex = self.__dfs1(self.start_vertex, check=check)
3668
+
3669
+ if check:
3670
+ # If graph is disconnected
3671
+ if self.dfs_counter < self.n:
3672
+ raise ValueError("graph is not connected")
3673
+
3674
+ # If graph has a cut vertex
3675
+ if cut_vertex != -1:
3676
+ raise ValueError("graph has a cut vertex")
3677
+
3678
+ # Identify reversed edges to reflect the palm tree arcs and fronds
3679
+ cdef bint up
3680
+ for e_index in range(self.m + self.virtual_edge_num):
3681
+ if self.edge_status[e_index] == -1:
3682
+ continue
3683
+ i = self.edge_extremity_first[e_index]
3684
+ j = self.edge_extremity_second[e_index]
3685
+ up = (self.dfs_number[j] - self.dfs_number[i]) > 0
3686
+ if (up and self.edge_status[e_index] == 2) or (not up and self.edge_status[e_index] == 1):
3687
+ # Add edge to the set reverse_edges
3688
+ self.reverse_edges[e_index] = True
3689
+
3690
+ self.__build_acceptable_adj_struct()
3691
+ self.__dfs2()
3692
+
3693
+ self.__path_search(self.start_vertex)
3694
+
3695
+ # last split component
3696
+ cdef _Component c
3697
+ if self.e_stack:
3698
+ e_index = self.__estack_pop()
3699
+ c = _Component(self.e_stack, 0)
3700
+ c.finish_tric_or_poly(e_index)
3701
+ self.components_list.append(c)
3702
+
3703
+ self.__assemble_triconnected_components()
3704
+
3705
+ self.__build_spqr_tree()
3706
+
3707
+ cdef int __new_virtual_edge(self, int u, int v) noexcept:
3708
+ """
3709
+ Return a new virtual edge between ``u`` and ``v``.
3710
+ """
3711
+ cdef Py_ssize_t e_index = self.m + self.virtual_edge_num
3712
+ self.int_to_original_edge_label.append("newVEdge"+str(self.virtual_edge_num))
3713
+ self.virtual_edge_num += 1
3714
+ self.edge_extremity_first[e_index] = u
3715
+ self.edge_extremity_second[e_index] = v
3716
+ self.edge_status[e_index] = 0
3717
+ return e_index
3718
+
3719
+ cdef _LinkedListNode * __new_LinkedListNode(self, Py_ssize_t e_index) noexcept:
3720
+ """
3721
+ Create a new ``_LinkedListNode`` initialized with value ``e_index``.
3722
+ """
3723
+ cdef _LinkedListNode * node = <_LinkedListNode *> self.mem.malloc(sizeof(_LinkedListNode))
3724
+ _LinkedListNode_initialize(node, e_index)
3725
+ return node
3726
+
3727
+ cdef Py_ssize_t __high(self, Py_ssize_t v) noexcept:
3728
+ """
3729
+ Return the ``high(v)`` value, which is the first value in
3730
+ ``highpt`` list of ``v``.
3731
+ """
3732
+ cdef _LinkedListNode * head = _LinkedList_get_head(self.highpt[v])
3733
+ if head:
3734
+ return head.data
3735
+ return 0
3736
+
3737
+ cdef __del_high(self, int e_index):
3738
+ """
3739
+ Delete edge ``e`` from the ``highpt`` list of the endpoint ``v``
3740
+ it belongs to.
3741
+ """
3742
+ cdef int v
3743
+ cdef _LinkedListNode * it = self.in_high[e_index]
3744
+ if it:
3745
+ if self.reverse_edges[e_index]:
3746
+ v = self.edge_extremity_first[e_index]
3747
+ else:
3748
+ v = self.edge_extremity_second[e_index]
3749
+ _LinkedList_remove(self.highpt[v], it)
3750
+
3751
+ cdef __split_multiple_edges(self):
3752
+ """
3753
+ Make the graph simple and build bonds recording multiple edges.
3754
+
3755
+ If there are `k` multiple edges between `u` and `v`, then a new
3756
+ component (a bond) with `k+1` edges (one of them is a virtual edge) will
3757
+ be created, all the `k` edges are deleted from the graph and the virtual
3758
+ edge between `u` and `v` is added to the graph.
3759
+ """
3760
+ cdef dict sub_bucket
3761
+ cdef list b, sb
3762
+ cdef int u, v, e_index, virtual_e_index
3763
+ cdef list bucket = [[] for u in range(self.n)]
3764
+
3765
+ # We form buckets of edges with same min(e[0], e[1])
3766
+ for e_index in range(self.m):
3767
+ u = min(self.edge_extremity_first[e_index], self.edge_extremity_second[e_index])
3768
+ bucket[u].append(e_index)
3769
+
3770
+ # We split each bucket into sub-buckets with same max(e[0], e[1]) thus
3771
+ # identifying groups of multiple edges
3772
+ for u, b in enumerate(bucket):
3773
+ if not b or len(b) == 1:
3774
+ # Nothing to do
3775
+ continue
3776
+ sub_bucket = {}
3777
+ for e_index in b:
3778
+ v = self.__edge_other_extremity(e_index, u)
3779
+ if v in sub_bucket:
3780
+ sub_bucket[v].append(e_index)
3781
+ else:
3782
+ sub_bucket[v] = [e_index]
3783
+
3784
+ for v, sb in sub_bucket.items():
3785
+ if len(sb) == 1:
3786
+ continue
3787
+
3788
+ # We have multiple edges. We remove them from graph_copy, add a
3789
+ # virtual edge to graph_copy, and create a component containing
3790
+ # all removed multiple edges and the virtual edge.
3791
+ for e_index in sb:
3792
+ self.edge_status[e_index] = -1
3793
+
3794
+ virtual_e_index = self.__new_virtual_edge(u, v)
3795
+ self.edge_status[virtual_e_index] = 0
3796
+
3797
+ sb.append(virtual_e_index)
3798
+ self.__new_component(sb, 0)
3799
+
3800
+ cdef int __dfs1(self, int start, bint check=True) noexcept:
3801
+ """
3802
+ Build the palm-tree of the graph using a dfs traversal.
3803
+
3804
+ Also populates the lists ``lowpt1``, ``lowpt2``, ``nd``, ``parent``,
3805
+ and ``dfs_number``. It updates the dict ``edge_status`` to reflect
3806
+ palm tree arcs and fronds.
3807
+
3808
+ INPUT:
3809
+
3810
+ - ``start`` -- the start vertex for DFS
3811
+
3812
+ - ``check`` -- if ``True``, the graph is tested for biconnectivity; if
3813
+ the graph has a cut vertex, the cut vertex is returned; otherwise
3814
+ the graph is assumed to be biconnected, function returns ``None``
3815
+
3816
+ OUTPUT:
3817
+
3818
+ - If ``check`` is set to ``True`` and a cut vertex is found, the cut
3819
+ vertex is returned. If no cut vertex is found, return ``-1``.
3820
+ - If ``check`` is set to ``False``, ``-1`` is returned.
3821
+ """
3822
+ cdef Py_ssize_t v, w
3823
+ cdef Py_ssize_t e_index
3824
+ cdef int cut_vertex = -1 # Storing the cut vertex, if any
3825
+ cdef int* adjacency = self.tmp_array_n_int_3
3826
+ cdef list cur_adj
3827
+ cdef Py_ssize_t len_cur_adj
3828
+ for v in range(self.n):
3829
+ adjacency[v] = 0
3830
+
3831
+ # Defining a stack. stack_top == -1 means empty stack
3832
+ cdef int* stack = self.tmp_array_n_int_1
3833
+ cdef Py_ssize_t stack_top = 0
3834
+ stack[stack_top] = start
3835
+
3836
+ # Used for testing biconnectivity
3837
+ cdef int* first_son = self.tmp_array_n_int_2
3838
+ for v in range(self.n):
3839
+ first_son[v] = -1
3840
+
3841
+ while stack_top != -1:
3842
+ v = stack[stack_top]
3843
+
3844
+ if not self.dfs_number[v]:
3845
+ self.dfs_counter += 1
3846
+ self.dfs_number[v] = self.dfs_counter
3847
+ self.lowpt1[v] = self.lowpt2[v] = self.dfs_number[v]
3848
+ self.nd[v] = 1
3849
+
3850
+ cur_adj = self.graph_copy_adjacency[v]
3851
+ len_cur_adj = len(cur_adj)
3852
+ # Find the next e_index such that self.edge_status[e_index] is False
3853
+ if adjacency[v] == len_cur_adj:
3854
+ adjacency[v] = -1
3855
+ elif adjacency[v] != -1:
3856
+ e_index = cur_adj[adjacency[v]]
3857
+ adjacency[v] += 1
3858
+ while self.edge_status[e_index] > 0:
3859
+ if adjacency[v] == len_cur_adj:
3860
+ adjacency[v] = -1
3861
+ break
3862
+ e_index = cur_adj[adjacency[v]]
3863
+ adjacency[v] += 1
3864
+
3865
+ if adjacency[v] != -1:
3866
+ # Opposite vertex of edge e
3867
+ w = self.__edge_other_extremity(e_index, v)
3868
+ if not self.dfs_number[w]:
3869
+ self.edge_status[e_index] = 1 # tree edge
3870
+ if first_son[v] == -1:
3871
+ first_son[v] = w
3872
+ self.tree_arc[w] = e_index
3873
+
3874
+ stack_top += 1
3875
+ stack[stack_top] = w
3876
+ self.parent[w] = v
3877
+
3878
+ else:
3879
+ self.edge_status[e_index] = 2 # frond
3880
+ if self.dfs_number[w] < self.lowpt1[v]:
3881
+ self.lowpt2[v] = self.lowpt1[v]
3882
+ self.lowpt1[v] = self.dfs_number[w]
3883
+ elif self.dfs_number[w] > self.lowpt1[v]:
3884
+ self.lowpt2[v] = min(self.lowpt2[v], self.dfs_number[w])
3885
+
3886
+ else:
3887
+ # We trackback, so w takes the value of v and we pop the stack
3888
+ w = stack[stack_top]
3889
+ stack_top -= 1
3890
+
3891
+ # Test termination
3892
+ if stack_top == -1:
3893
+ break
3894
+
3895
+ v = stack[stack_top]
3896
+
3897
+ if check:
3898
+ # Check for cut vertex.
3899
+ # The situation in which there is no path from w to an
3900
+ # ancestor of v : we have identified a cut vertex
3901
+ if ((self.lowpt1[w] >= self.dfs_number[v])
3902
+ and (w != first_son[v] or self.parent[v] != -1)):
3903
+ cut_vertex = v
3904
+
3905
+ # Calculate the `lowpt1` and `lowpt2` values.
3906
+ # `lowpt1` is the smallest vertex (the vertex x with smallest
3907
+ # dfs_number[x]) that can be reached from v.
3908
+ # `lowpt2` is the next smallest vertex that can be reached from v.
3909
+ if self.lowpt1[w] < self.lowpt1[v]:
3910
+ self.lowpt2[v] = min(self.lowpt1[v], self.lowpt2[w])
3911
+ self.lowpt1[v] = self.lowpt1[w]
3912
+
3913
+ elif self.lowpt1[w] == self.lowpt1[v]:
3914
+ self.lowpt2[v] = min(self.lowpt2[v], self.lowpt2[w])
3915
+
3916
+ else:
3917
+ self.lowpt2[v] = min(self.lowpt2[v], self.lowpt1[w])
3918
+
3919
+ self.nd[v] += self.nd[w]
3920
+
3921
+ return cut_vertex # cut_vertex is -1 if graph does not have a cut vertex
3922
+
3923
+ cdef __build_acceptable_adj_struct(self):
3924
+ """
3925
+ Build the adjacency lists for each vertex with certain properties of
3926
+ the ordering, using the ``lowpt1`` and ``lowpt2`` values.
3927
+
3928
+ The list ``adj`` and the dictionary ``in_adj`` are populated.
3929
+
3930
+ ``phi`` values of each edge are calculated using the ``lowpt`` values of
3931
+ incident vertices. The edges are then sorted by the ``phi`` values and
3932
+ added to adjacency list.
3933
+ """
3934
+ cdef Py_ssize_t max_size = 3 * self.n + 2
3935
+ cdef Py_ssize_t i, u, v
3936
+ cdef int e_index, edge_type, phi
3937
+ cdef list bucket = [[] for i in range(max_size + 1)]
3938
+ cdef _LinkedListNode * node
3939
+
3940
+ for e_index in range(self.m + self.virtual_edge_num):
3941
+ edge_type = self.edge_status[e_index]
3942
+ if edge_type == -1:
3943
+ continue
3944
+ u = self.edge_extremity_first[e_index]
3945
+ v = self.edge_extremity_second[e_index]
3946
+
3947
+ # Compute phi value
3948
+ # bucket sort adjacency list by phi values
3949
+ if self.reverse_edges[e_index]:
3950
+ if edge_type == 1: # tree arc
3951
+ if self.lowpt2[u] < self.dfs_number[v]:
3952
+ phi = 3 * self.lowpt1[u]
3953
+ else:
3954
+ phi = 3 * self.lowpt1[u] + 2
3955
+ else: # tree frond
3956
+ phi = 3 * self.dfs_number[u] + 1
3957
+ else:
3958
+ if edge_type == 1: # tree arc
3959
+ if self.lowpt2[v] < self.dfs_number[u]:
3960
+ phi = 3 * self.lowpt1[v]
3961
+ else:
3962
+ phi = 3 * self.lowpt1[v] + 2
3963
+ else: # tree frond
3964
+ phi = 3 * self.dfs_number[v] + 1
3965
+
3966
+ bucket[phi].append(e_index)
3967
+
3968
+ # Populate `adj` and `in_adj` with the sorted edges
3969
+ for i in range(1, max_size + 1):
3970
+ for e_index in bucket[i]:
3971
+ node = self.__new_LinkedListNode(e_index)
3972
+ if self.reverse_edges[e_index]:
3973
+ _LinkedList_append(self.adj[self.edge_extremity_second[e_index]], node)
3974
+ else:
3975
+ _LinkedList_append(self.adj[self.edge_extremity_first[e_index]], node)
3976
+ self.in_adj[e_index] = node
3977
+
3978
+ cdef __path_finder(self, int start):
3979
+ """
3980
+ This function is a helper function for :meth:`__dfs2` function.
3981
+
3982
+ Calculate ``newnum[v]`` and identify the edges which start a new path.
3983
+
3984
+ INPUT:
3985
+
3986
+ - ``start`` -- the start vertex
3987
+ """
3988
+ cdef bint new_path = True
3989
+ cdef Py_ssize_t v, w
3990
+ cdef Py_ssize_t e_index
3991
+ cdef _LinkedListNode * e_node
3992
+ cdef _LinkedListNode * highpt_node
3993
+
3994
+ # Defining a stack. stack_top == -1 means empty stack
3995
+ cdef int* stack = self.tmp_array_n_int_1
3996
+ cdef Py_ssize_t stack_top = 0
3997
+ stack[stack_top] = start
3998
+
3999
+ cdef bint * seen = self.tmp_array_n_bint_1
4000
+ for v in range(self.n):
4001
+ seen[v] = False
4002
+
4003
+ cdef _LinkedListNode ** pointer_e_node = <_LinkedListNode ** > self.mem.allocarray(self.n, sizeof(_LinkedListNode *))
4004
+ for v in range(self.n):
4005
+ pointer_e_node[v] = _LinkedList_get_head(self.adj[v])
4006
+
4007
+ while stack_top != -1:
4008
+ v = stack[stack_top]
4009
+ if not seen[v]:
4010
+ self.newnum[v] = self.dfs_counter - self.nd[v] + 1
4011
+ seen[v] = True
4012
+ e_node = pointer_e_node[v]
4013
+
4014
+ if e_node:
4015
+ e_index = e_node.data
4016
+ pointer_e_node[v] = e_node.next
4017
+ # opposite vertex of e
4018
+ w = self.__edge_other_extremity(e_index, v)
4019
+ if new_path:
4020
+ new_path = False
4021
+ self.starts_path[e_index] = True
4022
+ if self.edge_status[e_index] == 1: # tree arc
4023
+ stack_top += 1
4024
+ stack[stack_top] = w
4025
+ else:
4026
+ # Identified a new frond that enters `w`. Add to `highpt[w]`.
4027
+ highpt_node = self.__new_LinkedListNode(self.newnum[v])
4028
+ _LinkedList_append(self.highpt[w], highpt_node)
4029
+ self.in_high[e_index] = highpt_node
4030
+ new_path = True
4031
+
4032
+ else:
4033
+ # We trackback
4034
+ self.dfs_counter -= 1
4035
+ stack_top -= 1
4036
+
4037
+ cdef __dfs2(self):
4038
+ """
4039
+ Update the values of ``lowpt1`` and ``lowpt2`` lists with the
4040
+ help of new numbering obtained from :meth:`__path_finder`.
4041
+ Populate ``highpt`` values.
4042
+ """
4043
+ cdef Py_ssize_t v
4044
+ cdef Py_ssize_t e_index
4045
+
4046
+ self.dfs_counter = self.n
4047
+ for e_index in range(self.m + self.virtual_edge_num):
4048
+ self.in_high[e_index] = NULL
4049
+ self.starts_path[e_index] = False
4050
+
4051
+ # We call the pathFinder function with the start vertex
4052
+ self.__path_finder(self.start_vertex)
4053
+
4054
+ # Update `old_to_new` values with the calculated `newnum` values
4055
+ for v in range(self.n):
4056
+ self.old_to_new[self.dfs_number[v]] = self.newnum[v]
4057
+
4058
+ # Update lowpt values according to `newnum` values.
4059
+ for v in range(self.n):
4060
+ self.node_at[self.newnum[v]] = v
4061
+ self.lowpt1[v] = self.old_to_new[self.lowpt1[v]]
4062
+ self.lowpt2[v] = self.old_to_new[self.lowpt2[v]]
4063
+
4064
+ cdef int __path_search(self, int start) except -1:
4065
+ """
4066
+ Find the separation pairs and construct the split components.
4067
+
4068
+ Check for type-1 and type-2 separation pairs, and construct the split
4069
+ components while also creating new virtual edges wherever required.
4070
+
4071
+ INPUT:
4072
+
4073
+ - ``start`` -- the start vertex
4074
+ """
4075
+ cdef int e_index, e_virt_index
4076
+ cdef int x, y, h, xx
4077
+ cdef int v, vnum, outv
4078
+ cdef int w, wnum
4079
+ cdef int temp_index, temp_target
4080
+ cdef int a, b, e_ab_index, e_ab_source
4081
+ cdef int e1_index, e2_index, e2_source
4082
+ cdef int xy_index, xy_target
4083
+ cdef int eh_index, eh_source
4084
+ cdef _LinkedListNode * it
4085
+ cdef _LinkedListNode * e_node
4086
+ cdef _LinkedListNode * temp_node
4087
+ cdef _LinkedListNode * vnum_node
4088
+ cdef _LinkedListNode * e_virt_node
4089
+ cdef _Component comp
4090
+
4091
+ # Defining a stack. stack_v_top == -1 means empty stack
4092
+ cdef int* stack_v = self.tmp_array_n_int_1
4093
+ cdef Py_ssize_t stack_v_top = 0
4094
+ stack_v[stack_v_top] = start
4095
+
4096
+ cdef int* y_dict = self.tmp_array_n_int_2
4097
+ y_dict[start] = 0
4098
+
4099
+ cdef int* outv_dict = self.tmp_array_n_int_3
4100
+ outv_dict[start] = _LinkedList_get_length(self.adj[start])
4101
+
4102
+ cdef _LinkedListNode ** e_node_dict = <_LinkedListNode **> self.mem.allocarray(self.n, sizeof(_LinkedListNode *))
4103
+ e_node_dict[start] = _LinkedList_get_head(self.adj[start])
4104
+
4105
+ while stack_v_top != -1:
4106
+ v = stack_v[stack_v_top]
4107
+ e_node = e_node_dict[v]
4108
+
4109
+ if e_node:
4110
+ # Restore values of variables
4111
+ y = y_dict[v]
4112
+ vnum = self.newnum[v]
4113
+ outv = outv_dict[v]
4114
+ e_index = e_node.data
4115
+ it = e_node
4116
+ if self.reverse_edges[e_index]:
4117
+ w = self.edge_extremity_first[e_index] # target
4118
+ else:
4119
+ w = self.edge_extremity_second[e_index]
4120
+ wnum = self.newnum[w]
4121
+
4122
+ if self.edge_status[e_index] == 1: # e is a tree arc
4123
+ if self.starts_path[e_index]: # if a new path starts at edge e
4124
+ # Pop all (h,a,b) from tstack where a > lowpt1[w]
4125
+ if self.t_stack_a[self.t_stack_top] > self.lowpt1[w]:
4126
+ while self.t_stack_a[self.t_stack_top] > self.lowpt1[w]:
4127
+ y = max(y, self.t_stack_h[self.t_stack_top])
4128
+ b = self.t_stack_b[self.t_stack_top]
4129
+ self.t_stack_top -= 1
4130
+ self.__tstack_push(y, self.lowpt1[w], b)
4131
+
4132
+ else:
4133
+ self.__tstack_push(wnum + self.nd[w] - 1, self.lowpt1[w], vnum)
4134
+ self.__tstack_push_eos()
4135
+
4136
+ # We emulate the recursive call on w using a stack
4137
+ stack_v_top += 1
4138
+ stack_v[stack_v_top] = w
4139
+ y_dict[w] = 0
4140
+ outv_dict[w] = _LinkedList_get_length(self.adj[w])
4141
+ e_node_dict[w] = _LinkedList_get_head(self.adj[w])
4142
+ y_dict[v] = y
4143
+ continue
4144
+
4145
+ else: # e is a frond
4146
+ if self.starts_path[e_index]:
4147
+ # pop all (h,a,b) from tstack where a > w
4148
+ if self.t_stack_a[self.t_stack_top] > wnum:
4149
+ while self.t_stack_a[self.t_stack_top] > wnum:
4150
+ y = max(y, self.t_stack_h[self.t_stack_top])
4151
+ b = self.t_stack_b[self.t_stack_top]
4152
+ self.t_stack_top -= 1
4153
+ self.__tstack_push(y, wnum, b)
4154
+
4155
+ else:
4156
+ self.__tstack_push(vnum, wnum, vnum)
4157
+ self.e_stack.append(e_index) # add edge (v,w) to ESTACK
4158
+
4159
+ else:
4160
+ # We are done with v, so we trackback
4161
+ stack_v_top -= 1
4162
+
4163
+ # Test termination
4164
+ if stack_v_top == -1:
4165
+ continue
4166
+
4167
+ # Restore state of variables
4168
+ v = stack_v[stack_v_top]
4169
+ e_node = e_node_dict[v]
4170
+ y = y_dict[v]
4171
+ vnum = self.newnum[v]
4172
+ outv = outv_dict[v]
4173
+ e_index = e_node.data
4174
+ it = e_node
4175
+ if self.reverse_edges[e_index]:
4176
+ w = self.edge_extremity_first[e_index] # target
4177
+ else:
4178
+ w = self.edge_extremity_second[e_index]
4179
+ wnum = self.newnum[w]
4180
+
4181
+ # Continue operations with tree arc e
4182
+
4183
+ self.e_stack.append(self.tree_arc[w])
4184
+ temp_node = _LinkedList_get_head(self.adj[w])
4185
+ temp_index = temp_node.data
4186
+ if self.reverse_edges[temp_index]:
4187
+ temp_target = self.edge_extremity_first[temp_index]
4188
+ else:
4189
+ temp_target = self.edge_extremity_second[temp_index]
4190
+
4191
+ # Type-2 separation pair check
4192
+ # while v is not the start_vertex
4193
+ while vnum != 1 and ((self.t_stack_a[self.t_stack_top] == vnum)
4194
+ or (self.degree[w] == 2 and self.newnum[temp_target] > wnum)):
4195
+ a = self.t_stack_a[self.t_stack_top]
4196
+ b = self.t_stack_b[self.t_stack_top]
4197
+ if a == vnum and self.parent[self.node_at[b]] == self.node_at[a]:
4198
+ self.t_stack_top -= 1
4199
+
4200
+ else:
4201
+ e_ab_index = -1
4202
+ if self.degree[w] == 2 and self.newnum[temp_target] > wnum:
4203
+ # found type-2 separation pair - (v, temp_target)
4204
+ e1_index = self.__estack_pop()
4205
+ e2_index = self.__estack_pop()
4206
+ _LinkedList_remove(self.adj[w], self.in_adj[e2_index])
4207
+
4208
+ if self.reverse_edges[e2_index]:
4209
+ x = self.edge_extremity_first[e2_index] # target
4210
+ else:
4211
+ x = self.edge_extremity_second[e2_index] # target
4212
+
4213
+ e_virt_index = self.__new_virtual_edge(v, x)
4214
+ self.degree[v] -= 1
4215
+ self.degree[x] -= 1
4216
+
4217
+ if self.reverse_edges[e2_index]:
4218
+ e2_source = self.edge_extremity_second[e2_index] # target
4219
+ else:
4220
+ e2_source = self.edge_extremity_first[e2_index]
4221
+ if e2_source != w:
4222
+ raise ValueError("graph is not biconnected")
4223
+
4224
+ self.__new_component([e1_index, e2_index, e_virt_index], 1)
4225
+
4226
+ if self.e_stack:
4227
+ e1_index = self.e_stack[-1]
4228
+ if self.reverse_edges[e1_index]:
4229
+ if (self.edge_extremity_first[e1_index] == v
4230
+ and self.edge_extremity_second[e1_index] == x):
4231
+ e_ab_index = self.__estack_pop()
4232
+ _LinkedList_remove(self.adj[x], self.in_adj[e_ab_index])
4233
+ self.__del_high(e_ab_index)
4234
+ else:
4235
+ if (self.edge_extremity_first[e1_index] == x
4236
+ and self.edge_extremity_second[e1_index] == v):
4237
+ e_ab_index = self.__estack_pop()
4238
+ _LinkedList_remove(self.adj[x], self.in_adj[e_ab_index])
4239
+ self.__del_high(e_ab_index)
4240
+
4241
+ else: # found type-2 separation pair - (self.node_at[a], self.node_at[b])
4242
+ h = self.t_stack_h[self.t_stack_top]
4243
+ self.t_stack_top -= 1
4244
+
4245
+ comp = _Component([], 0)
4246
+ while True:
4247
+ xy_index = self.e_stack[-1]
4248
+ if self.reverse_edges[xy_index]:
4249
+ x = self.edge_extremity_second[xy_index]
4250
+ xy_target = self.edge_extremity_first[xy_index]
4251
+ else:
4252
+ x = self.edge_extremity_first[xy_index]
4253
+ xy_target = self.edge_extremity_second[xy_index]
4254
+ if not (a <= self.newnum[x] and self.newnum[x] <= h
4255
+ and a <= self.newnum[xy_target] and self.newnum[xy_target] <= h):
4256
+ break
4257
+ if ((self.newnum[x] == a and self.newnum[xy_target] == b)
4258
+ or (self.newnum[xy_target] == a and self.newnum[x] == b)):
4259
+ e_ab_index = self.__estack_pop()
4260
+ if self.reverse_edges[e_ab_index]:
4261
+ e_ab_source = self.edge_extremity_second[e_ab_index] # source
4262
+ else:
4263
+ e_ab_source = self.edge_extremity_first[e_ab_index] # source
4264
+ _LinkedList_remove(self.adj[e_ab_source], self.in_adj[e_ab_index])
4265
+ self.__del_high(e_ab_index)
4266
+
4267
+ else:
4268
+ eh_index = self.__estack_pop()
4269
+ if self.reverse_edges[eh_index]:
4270
+ eh_source = self.edge_extremity_second[eh_index]
4271
+ else:
4272
+ eh_source = self.edge_extremity_first[eh_index]
4273
+ if it != self.in_adj[eh_index]:
4274
+ _LinkedList_remove(self.adj[eh_source], self.in_adj[eh_index])
4275
+ self.__del_high(eh_index)
4276
+
4277
+ comp.add_edge(eh_index)
4278
+ self.degree[x] -= 1
4279
+ self.degree[xy_target] -= 1
4280
+
4281
+ e_virt_index = self.__new_virtual_edge(self.node_at[a], self.node_at[b])
4282
+ comp.finish_tric_or_poly(e_virt_index)
4283
+ self.components_list.append(comp)
4284
+ comp = None
4285
+ x = self.node_at[b]
4286
+
4287
+ if e_ab_index != -1:
4288
+ comp = _Component([e_ab_index, e_virt_index], 0)
4289
+ e_virt_index = self.__new_virtual_edge(v, x)
4290
+ comp.add_edge(e_virt_index)
4291
+ self.degree[x] -= 1
4292
+ self.degree[v] -= 1
4293
+ self.components_list.append(comp)
4294
+ comp = None
4295
+
4296
+ self.e_stack.append(e_virt_index)
4297
+ # Replace the edge in `it` with `e_virt`
4298
+ it.data = e_virt_index
4299
+
4300
+ self.in_adj[e_virt_index] = it
4301
+ self.degree[x] += 1
4302
+ self.degree[v] += 1
4303
+ self.parent[x] = v
4304
+ self.tree_arc[x] = e_virt_index
4305
+ self.edge_status[e_virt_index] = 1
4306
+ w = x
4307
+ wnum = self.newnum[w]
4308
+
4309
+ # update the values used in the while loop check
4310
+ temp_node = _LinkedList_get_head(self.adj[w])
4311
+ temp_index = temp_node.data
4312
+ if self.reverse_edges[temp_index]:
4313
+ temp_target = self.edge_extremity_first[temp_index]
4314
+ else:
4315
+ temp_target = self.edge_extremity_second[temp_index]
4316
+
4317
+ # start type-1 check
4318
+ if (self.lowpt2[w] >= vnum and self.lowpt1[w] < vnum
4319
+ and (self.parent[v] != self.start_vertex or outv >= 2)):
4320
+ # type-1 separation pair - (self.node_at[self.lowpt1[w]], v)
4321
+ # Create a new component and add edges to it
4322
+ comp = _Component([], 0)
4323
+ if not self.e_stack:
4324
+ raise ValueError("stack is empty")
4325
+ while self.e_stack:
4326
+ xy_index = self.e_stack[-1]
4327
+ if self.reverse_edges[xy_index]:
4328
+ xx = self.newnum[self.edge_extremity_second[xy_index]] # source
4329
+ y = self.newnum[self.edge_extremity_first[xy_index]] # target
4330
+ else:
4331
+ xx = self.newnum[self.edge_extremity_first[xy_index]] # source
4332
+ y = self.newnum[self.edge_extremity_second[xy_index]] # target
4333
+
4334
+ if not ((wnum <= xx and xx < wnum + self.nd[w])
4335
+ or (wnum <= y and y < wnum + self.nd[w])):
4336
+ break
4337
+
4338
+ comp.add_edge(self.__estack_pop())
4339
+ self.__del_high(xy_index)
4340
+ self.degree[self.node_at[xx]] -= 1
4341
+ self.degree[self.node_at[y]] -= 1
4342
+
4343
+ e_virt_index = self.__new_virtual_edge(v, self.node_at[self.lowpt1[w]])
4344
+ comp.finish_tric_or_poly(e_virt_index) # Add virtual edge to component
4345
+ self.components_list.append(comp)
4346
+ comp = None
4347
+
4348
+ if ((xx == vnum and y == self.lowpt1[w])
4349
+ or (y == vnum and xx == self.lowpt1[w])):
4350
+ comp_bond = _Component([], 0) # new triple bond
4351
+ eh_index = self.__estack_pop()
4352
+ if self.in_adj[eh_index] != it:
4353
+ if self.reverse_edges[eh_index]:
4354
+ _LinkedList_remove(self.adj[self.edge_extremity_second[eh_index]], self.in_adj[eh_index])
4355
+ else:
4356
+ _LinkedList_remove(self.adj[self.edge_extremity_first[eh_index]], self.in_adj[eh_index])
4357
+
4358
+ comp_bond.add_edge(eh_index)
4359
+ comp_bond.add_edge(e_virt_index)
4360
+ e_virt_index = self.__new_virtual_edge(v, self.node_at[self.lowpt1[w]])
4361
+ comp_bond.add_edge(e_virt_index)
4362
+ if self.in_high[eh_index]:
4363
+ self.in_high[e_virt_index] = self.in_high[eh_index]
4364
+ self.degree[v] -= 1
4365
+ self.degree[self.node_at[self.lowpt1[w]]] -= 1
4366
+
4367
+ self.components_list.append(comp_bond)
4368
+ comp_bond = None
4369
+
4370
+ if self.node_at[self.lowpt1[w]] != self.parent[v]:
4371
+ self.e_stack.append(e_virt_index)
4372
+
4373
+ # replace edge in `it` with `e_virt`
4374
+ it.data = e_virt_index
4375
+
4376
+ self.in_adj[e_virt_index] = it
4377
+ if not self.in_high[e_virt_index] and self.__high(self.node_at[self.lowpt1[w]]) < vnum:
4378
+ vnum_node = self.__new_LinkedListNode(vnum)
4379
+ _LinkedList_push_front(self.highpt[self.node_at[self.lowpt1[w]]], vnum_node)
4380
+ self.in_high[e_virt_index] = vnum_node
4381
+
4382
+ self.degree[v] += 1
4383
+ self.degree[self.node_at[self.lowpt1[w]]] += 1
4384
+
4385
+ else:
4386
+ _LinkedList_remove(self.adj[v], it)
4387
+ comp_bond = _Component([e_virt_index], 0)
4388
+ e_virt_index = self.__new_virtual_edge(self.node_at[self.lowpt1[w]], v)
4389
+ comp_bond.add_edge(e_virt_index)
4390
+
4391
+ eh_index = self.tree_arc[v]
4392
+ comp_bond.add_edge(eh_index)
4393
+
4394
+ self.components_list.append(comp_bond)
4395
+ comp_bond = None
4396
+
4397
+ self.tree_arc[v] = e_virt_index
4398
+ self.edge_status[e_virt_index] = 1
4399
+ if self.in_adj[eh_index]:
4400
+ self.in_adj[e_virt_index] = self.in_adj[eh_index]
4401
+ e_virt_node = self.__new_LinkedListNode(e_virt_index)
4402
+ self.in_adj[eh_index] = e_virt_node
4403
+ # end type-1 search
4404
+
4405
+ # if a path starts at edge e, empty the tstack.
4406
+ if self.starts_path[e_index]:
4407
+ while self.__tstack_not_eos():
4408
+ self.t_stack_top -= 1
4409
+ self.t_stack_top -= 1
4410
+
4411
+ while (self.__tstack_not_eos() and self.t_stack_b[self.t_stack_top] != vnum
4412
+ and self.__high(v) > self.t_stack_h[self.t_stack_top]):
4413
+ self.t_stack_top -= 1
4414
+
4415
+ outv_dict[v] -= 1
4416
+
4417
+ # Go to next edge in adjacency list
4418
+ e_node_dict[v] = e_node.next
4419
+
4420
+ cdef __assemble_triconnected_components(self):
4421
+ """
4422
+ Iterate through all the split components built by :meth:`__path_finder`
4423
+ and merges two bonds or two polygons that share an edge for constructing
4424
+ the final triconnected components.
4425
+
4426
+ Subsequently, convert the edges in triconnected components into original
4427
+ vertices and edges. The triconnected components are stored in
4428
+ ``self.comp_final_edge_list`` and ``self.comp_type``.
4429
+ """
4430
+ cdef Py_ssize_t i, j
4431
+ cdef Py_ssize_t e_index
4432
+ cdef _Component c1, c2
4433
+ cdef int c1_type
4434
+ cdef _LinkedListNode * e_node
4435
+ cdef _LinkedListNode * e_node_next
4436
+ cdef _LinkedList * l1
4437
+ cdef _LinkedList * l2
4438
+
4439
+ cdef Py_ssize_t num_components = len(self.components_list)
4440
+ cdef bint* visited = <bint*> self.mem.allocarray(num_components, sizeof(bint))
4441
+ for i in range(num_components):
4442
+ visited[i] = False
4443
+
4444
+ # The index of first (second) component that an edge belongs to
4445
+ cdef int* comp1 = <int*> self.mem.allocarray(self.m + self.virtual_edge_num, sizeof(int))
4446
+ cdef int* comp2 = <int*> self.mem.allocarray(self.m + self.virtual_edge_num, sizeof(int))
4447
+
4448
+ # Pointer to the edge node in first (second) component
4449
+ cdef _LinkedListNode ** item1 = <_LinkedListNode **> self.mem.allocarray(self.m + self.virtual_edge_num, sizeof(_LinkedListNode *))
4450
+ cdef _LinkedListNode ** item2 = <_LinkedListNode **> self.mem.allocarray(self.m + self.virtual_edge_num, sizeof(_LinkedListNode *))
4451
+ for i in range(self.m + self.virtual_edge_num):
4452
+ item1[i] = NULL
4453
+ item2[i] = NULL
4454
+
4455
+ # For each edge, we populate the comp1, comp2, item1 and item2 values
4456
+ for i in range(num_components): # for each component
4457
+ e_node = _LinkedList_get_head((<_Component> self.components_list[i]).edge_list)
4458
+ while e_node: # for each edge
4459
+ e_index = e_node.data
4460
+ if not item1[e_index]:
4461
+ comp1[e_index] = i
4462
+ item1[e_index] = e_node
4463
+ else:
4464
+ comp2[e_index] = i
4465
+ item2[e_index] = e_node
4466
+
4467
+ e_node = e_node.next
4468
+
4469
+ # For each edge in a component, if the edge is a virtual edge, merge
4470
+ # the two components the edge belongs to
4471
+ for i in range(num_components):
4472
+ c1 = <_Component> self.components_list[i]
4473
+ c1_type = c1.component_type
4474
+ l1 = c1.edge_list
4475
+ visited[i] = True
4476
+
4477
+ if not _LinkedList_get_length(l1):
4478
+ continue
4479
+
4480
+ if c1_type == 0 or c1_type == 1:
4481
+ e_node = _LinkedList_get_head((<_Component> self.components_list[i]).edge_list)
4482
+ # Iterate through each edge in the component
4483
+ while e_node:
4484
+ e_index = e_node.data
4485
+ e_node_next = e_node.next
4486
+ if not self.__is_virtual_edge(e_index):
4487
+ e_node = e_node_next
4488
+ continue
4489
+
4490
+ j = comp1[e_index]
4491
+ if visited[j]:
4492
+ j = comp2[e_index]
4493
+ if visited[j]:
4494
+ e_node = e_node_next
4495
+ continue
4496
+ e_node2 = item2[e_index]
4497
+ else:
4498
+ e_node2 = item1[e_index]
4499
+
4500
+ c2 = <_Component> self.components_list[j]
4501
+
4502
+ # If the two components are not the same type, do not merge
4503
+ if c1_type != c2.component_type:
4504
+ e_node = e_node_next # Go to next edge
4505
+ continue
4506
+
4507
+ visited[j] = True
4508
+ l2 = c2.edge_list
4509
+
4510
+ # Remove the corresponding virtual edges in both the components
4511
+ # and merge the components
4512
+ _LinkedList_remove(l2, e_node2)
4513
+ _LinkedList_concatenate(l1, l2)
4514
+
4515
+ # if `e_node_next` was empty, after merging two components,
4516
+ # more edges are added to the component.
4517
+ if not e_node_next:
4518
+ e_node_next = e_node.next # Go to next edge
4519
+
4520
+ _LinkedList_remove(l1, e_node)
4521
+
4522
+ e_node = e_node_next
4523
+
4524
+ # Convert connected components into original graph vertices and edges
4525
+ cdef list e_list_new
4526
+ cdef list e_index_list
4527
+ cdef tuple e_new
4528
+ self.comp_final_edge_list = []
4529
+ self.comp_type = []
4530
+ self.final_edge_to_edge_index = {}
4531
+ for comp in self.components_list:
4532
+ if _LinkedList_get_length((<_Component> comp).edge_list):
4533
+ e_index_list = (<_Component> comp).get_edge_list()
4534
+ e_list_new = []
4535
+ # For each edge, get the original source, target and label
4536
+ for e_index in e_index_list:
4537
+ source = self.int_to_vertex[self.edge_extremity_first[e_index]]
4538
+ target = self.int_to_vertex[self.edge_extremity_second[e_index]]
4539
+ label = self.int_to_original_edge_label[e_index]
4540
+ e_new = (source, target, label)
4541
+ e_list_new.append(e_new)
4542
+ self.final_edge_to_edge_index[e_new] = e_index
4543
+ # Add the component data to `comp_final_edge_list` and `comp_type`
4544
+ self.comp_type.append((<_Component> comp).component_type)
4545
+ self.comp_final_edge_list.append(e_list_new)
4546
+
4547
+ cdef __build_spqr_tree(self):
4548
+ """
4549
+ Build the SPQR-tree of the graph and store it in variable
4550
+ ``self.spqr_tree``. See
4551
+ :meth:`~sage.graphs.connectivity.TriconnectivitySPQR.get_spqr_tree`.
4552
+ """
4553
+ # Types of components 0: "P", 1: "S", 2: "R"
4554
+ cdef list component_type = ["P", "S", "R"]
4555
+
4556
+ from sage.graphs.graph import Graph
4557
+ self.spqr_tree = Graph(multiedges=False, name='SPQR-tree of {}'.format(self.graph_name))
4558
+
4559
+ if len(self.comp_final_edge_list) == 1 and self.comp_type[0] == 0:
4560
+ self.spqr_tree.add_vertex(('Q' if len(self.comp_final_edge_list[0]) == 1 else 'P',
4561
+ Graph(self.comp_final_edge_list[0], immutable=True, multiedges=True)))
4562
+ return
4563
+
4564
+ cdef list int_to_vertex = []
4565
+ cdef dict partner_nodes = {}
4566
+ cdef Py_ssize_t i, j
4567
+ cdef Py_ssize_t e_index
4568
+
4569
+ for i in range(len(self.comp_final_edge_list)):
4570
+ # Create a new tree vertex
4571
+ u = (component_type[self.comp_type[i]],
4572
+ Graph(self.comp_final_edge_list[i], immutable=True, multiedges=True))
4573
+ self.spqr_tree.add_vertex(u)
4574
+ int_to_vertex.append(u)
4575
+
4576
+ # Add an edge to each node containing the same virtual edge
4577
+ for e in self.comp_final_edge_list[i]:
4578
+ e_index = self.final_edge_to_edge_index[e]
4579
+ if self.__is_virtual_edge(e_index):
4580
+ if e_index in partner_nodes:
4581
+ for j in partner_nodes[e_index]:
4582
+ self.spqr_tree.add_edge(int_to_vertex[i], int_to_vertex[j])
4583
+ partner_nodes[e_index].append(i)
4584
+ else:
4585
+ partner_nodes[e_index] = [i]
4586
+
4587
+ def print_triconnected_components(self):
4588
+ """
4589
+ Print the type and list of edges of each component.
4590
+
4591
+ EXAMPLES:
4592
+
4593
+ An example from [Hopcroft1973]_::
4594
+
4595
+ sage: from sage.graphs.connectivity import TriconnectivitySPQR
4596
+ sage: from sage.graphs.connectivity import spqr_tree_to_graph
4597
+ sage: G = Graph([(1, 2), (1, 4), (1, 8), (1, 12), (1, 13), (2, 3),
4598
+ ....: (2, 13), (3, 4), (3, 13), (4, 5), (4, 7), (5, 6), (5, 7), (5, 8),
4599
+ ....: (6, 7), (8, 9), (8, 11), (8, 12), (9, 10), (9, 11), (9, 12),
4600
+ ....: (10, 11), (10, 12)])
4601
+ sage: tric = TriconnectivitySPQR(G)
4602
+ sage: T = tric.get_spqr_tree()
4603
+ sage: G.is_isomorphic(spqr_tree_to_graph(T))
4604
+ True
4605
+ sage: tric.print_triconnected_components()
4606
+ Triconnected: [(8, 9, None), (9, 12, None), (9, 11, None), (8, 11, None), (10, 11, None), (9, 10, None), (10, 12, None), (8, 12, 'newVEdge0')]
4607
+ Bond: [(8, 12, None), (8, 12, 'newVEdge0'), (8, 12, 'newVEdge1')]
4608
+ Polygon: [(6, 7, None), (5, 6, None), (7, 5, 'newVEdge2')]
4609
+ Bond: [(7, 5, 'newVEdge2'), (5, 7, 'newVEdge3'), (5, 7, None)]
4610
+ Polygon: [(5, 7, 'newVEdge3'), (4, 7, None), (5, 4, 'newVEdge4')]
4611
+ Bond: [(5, 4, 'newVEdge4'), (4, 5, 'newVEdge5'), (4, 5, None)]
4612
+ Polygon: [(4, 5, 'newVEdge5'), (5, 8, None), (1, 4, 'newVEdge9'), (1, 8, 'newVEdge10')]
4613
+ Triconnected: [(1, 2, None), (2, 13, None), (1, 13, None), (3, 13, None), (2, 3, None), (1, 3, 'newVEdge7')]
4614
+ Polygon: [(1, 3, 'newVEdge7'), (3, 4, None), (1, 4, 'newVEdge8')]
4615
+ Bond: [(1, 4, None), (1, 4, 'newVEdge8'), (1, 4, 'newVEdge9')]
4616
+ Bond: [(1, 8, None), (1, 8, 'newVEdge10'), (1, 8, 'newVEdge11')]
4617
+ Polygon: [(8, 12, 'newVEdge1'), (1, 8, 'newVEdge11'), (1, 12, None)]
4618
+ """
4619
+ # The types are {0: "Bond", 1: "Polygon", 2: "Triconnected"}
4620
+ cdef list prefix = ["Bond", "Polygon", "Triconnected"]
4621
+ cdef Py_ssize_t i
4622
+ for i in range(len(self.comp_final_edge_list)):
4623
+ print("{}: {}".format(prefix[self.comp_type[i]], self.comp_final_edge_list[i]))
4624
+
4625
+ def get_triconnected_components(self):
4626
+ r"""
4627
+ Return the triconnected components as a list of tuples.
4628
+
4629
+ Each component is represented as a tuple of the type of the component
4630
+ and the list of edges of the component.
4631
+
4632
+ EXAMPLES::
4633
+
4634
+ sage: from sage.graphs.connectivity import TriconnectivitySPQR
4635
+ sage: G = Graph(2)
4636
+ sage: for i in range(3):
4637
+ ....: G.add_path([0, G.add_vertex(), G.add_vertex(), 1])
4638
+ sage: tric = TriconnectivitySPQR(G)
4639
+ sage: tric.get_triconnected_components()
4640
+ [('Polygon', [(4, 5, None), (0, 4, None), (1, 5, None), (1, 0, 'newVEdge1')]),
4641
+ ('Polygon', [(6, 7, None), (0, 6, None), (1, 7, None), (1, 0, 'newVEdge3')]),
4642
+ ('Bond', [(1, 0, 'newVEdge1'), (1, 0, 'newVEdge3'), (1, 0, 'newVEdge4')]),
4643
+ ('Polygon', [(1, 3, None), (1, 0, 'newVEdge4'), (2, 3, None), (0, 2, None)])]
4644
+ """
4645
+ cdef list comps = []
4646
+ cdef Py_ssize_t i
4647
+ # The types are {0: "Bond", 1: "Polygon", 2: "Triconnected"}
4648
+ cdef list prefix = ["Bond", "Polygon", "Triconnected"]
4649
+ for i in range(len(self.comp_final_edge_list)):
4650
+ comps.append((prefix[self.comp_type[i]], self.comp_final_edge_list[i]))
4651
+ return comps
4652
+
4653
+ def get_spqr_tree(self):
4654
+ r"""
4655
+ Return an SPQR-tree representing the triconnected components of the
4656
+ graph.
4657
+
4658
+ An SPQR-tree is a tree data structure used to represent the triconnected
4659
+ components of a biconnected (multi)graph and the 2-vertex cuts
4660
+ separating them. A node of a SPQR-tree, and the graph associated with
4661
+ it, can be one of the following four types:
4662
+
4663
+ - ``'S'`` -- the associated graph is a cycle with at least three vertices.
4664
+ ``'S'`` stands for ``series``.
4665
+
4666
+ - ``'P'`` -- the associated graph is a dipole graph, a multigraph with
4667
+ two vertices and three or more edges. ``'P'`` stands for ``parallel``.
4668
+
4669
+ - ``'Q'`` -- the associated graph has a single real edge. This trivial
4670
+ case is necessary to handle the graph that has only one edge.
4671
+
4672
+ - ``'R'`` -- the associated graph is a 3-connected graph that is not a
4673
+ cycle or dipole. ``'R'`` stands for ``rigid``.
4674
+
4675
+ The edges of the tree indicate the 2-vertex cuts of the graph.
4676
+
4677
+ OUTPUT:
4678
+
4679
+ ``SPQR-tree`` a tree whose vertices are labeled with the block's
4680
+ type and the subgraph of three-blocks in the decomposition.
4681
+
4682
+ EXAMPLES::
4683
+
4684
+ sage: from sage.graphs.connectivity import TriconnectivitySPQR
4685
+ sage: G = Graph(2)
4686
+ sage: for i in range(3):
4687
+ ....: G.add_clique([0, 1, G.add_vertex(), G.add_vertex()])
4688
+ sage: tric = TriconnectivitySPQR(G)
4689
+ sage: Tree = tric.get_spqr_tree()
4690
+ sage: K4 = graphs.CompleteGraph(4)
4691
+ sage: all(u[1].is_isomorphic(K4) for u in Tree if u[0] == 'R')
4692
+ True
4693
+ sage: from sage.graphs.connectivity import spqr_tree_to_graph
4694
+ sage: G.is_isomorphic(spqr_tree_to_graph(Tree))
4695
+ True
4696
+
4697
+ sage: G = Graph(2)
4698
+ sage: for i in range(3):
4699
+ ....: G.add_path([0, G.add_vertex(), G.add_vertex(), 1])
4700
+ sage: tric = TriconnectivitySPQR(G)
4701
+ sage: Tree = tric.get_spqr_tree()
4702
+ sage: C4 = graphs.CycleGraph(4)
4703
+ sage: all(u[1].is_isomorphic(C4) for u in Tree if u[0] == 'S')
4704
+ True
4705
+ sage: G.is_isomorphic(spqr_tree_to_graph(Tree))
4706
+ True
4707
+
4708
+ sage: G.allow_multiple_edges(True)
4709
+ sage: G.add_edges(G.edge_iterator())
4710
+ sage: tric = TriconnectivitySPQR(G)
4711
+ sage: Tree = tric.get_spqr_tree()
4712
+ sage: all(u[1].is_isomorphic(C4) for u in Tree if u[0] == 'S')
4713
+ True
4714
+ sage: G.is_isomorphic(spqr_tree_to_graph(Tree))
4715
+ True
4716
+
4717
+ sage: G = graphs.CycleGraph(6)
4718
+ sage: tric = TriconnectivitySPQR(G)
4719
+ sage: Tree = tric.get_spqr_tree()
4720
+ sage: Tree.order()
4721
+ 1
4722
+ sage: G.is_isomorphic(spqr_tree_to_graph(Tree))
4723
+ True
4724
+ sage: G.add_edge(0, 3)
4725
+ sage: tric = TriconnectivitySPQR(G)
4726
+ sage: Tree = tric.get_spqr_tree()
4727
+ sage: Tree.order()
4728
+ 3
4729
+ sage: G.is_isomorphic(spqr_tree_to_graph(Tree))
4730
+ True
4731
+
4732
+ sage: G = Graph([(0, 1)], multiedges=True)
4733
+ sage: tric = TriconnectivitySPQR(G)
4734
+ sage: Tree = tric.get_spqr_tree()
4735
+ sage: Tree.vertices(sort=True)
4736
+ [('Q', Multi-graph on 2 vertices)]
4737
+ sage: G.add_edge(0, 1)
4738
+ sage: Tree = TriconnectivitySPQR(G).get_spqr_tree()
4739
+ sage: Tree.vertices(sort=True)
4740
+ [('P', Multi-graph on 2 vertices)]
4741
+ """
4742
+ return self.spqr_tree
4743
+
4744
+
4745
+ def is_triconnected(G):
4746
+ r"""
4747
+ Check whether the graph is triconnected.
4748
+
4749
+ A triconnected graph is a connected graph on 3 or more vertices that is not
4750
+ broken into disconnected pieces by deleting any pair of vertices.
4751
+
4752
+ EXAMPLES:
4753
+
4754
+ The Petersen graph is triconnected::
4755
+
4756
+ sage: G = graphs.PetersenGraph()
4757
+ sage: G.is_triconnected()
4758
+ True
4759
+
4760
+ But a 2D grid is not::
4761
+
4762
+ sage: G = graphs.Grid2dGraph(3, 3)
4763
+ sage: G.is_triconnected()
4764
+ False
4765
+
4766
+ By convention, a cycle of order 3 is triconnected::
4767
+
4768
+ sage: G = graphs.CycleGraph(3)
4769
+ sage: G.is_triconnected()
4770
+ True
4771
+
4772
+ But cycles of order 4 and more are not::
4773
+
4774
+ sage: [graphs.CycleGraph(i).is_triconnected() for i in range(4, 8)]
4775
+ [False, False, False, False]
4776
+
4777
+ Comparing different methods on random graphs that are not always
4778
+ triconnected::
4779
+
4780
+ sage: G = graphs.RandomBarabasiAlbert(50, 3) # needs networkx
4781
+ sage: G.is_triconnected() == G.vertex_connectivity(k=3) # needs networkx
4782
+ True
4783
+
4784
+ .. SEEALSO::
4785
+
4786
+ - :meth:`~sage.graphs.generic_graph.GenericGraph.is_connected`
4787
+ - :meth:`~Graph.is_biconnected`
4788
+ - :meth:`~sage.graphs.connectivity.spqr_tree`
4789
+ - :wikipedia:`SPQR_tree`
4790
+
4791
+ TESTS::
4792
+
4793
+ sage: [Graph(i).is_triconnected() for i in range(4)]
4794
+ [False, False, False, False]
4795
+ sage: [graphs.CompleteGraph(i).is_triconnected() for i in range(3, 6)]
4796
+ [True, True, True]
4797
+ """
4798
+ if G.order() < 3:
4799
+ return False
4800
+
4801
+ try:
4802
+ T = G.spqr_tree()
4803
+ except ValueError:
4804
+ # The graph is not biconnected
4805
+ return False
4806
+
4807
+ from collections import Counter
4808
+ C = Counter(v[0] for v in T)
4809
+ if 'S' in C:
4810
+ return G.order() == 3
4811
+ # Since the graph has order >= 3, is biconnected and has no 'S' block, it
4812
+ # has at least one 'R' block. A triconnected graph has only one such block.
4813
+ return C['R'] == 1