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.
- passagemath_graphs-10.6.1rc1.dist-info/METADATA +292 -0
- passagemath_graphs-10.6.1rc1.dist-info/RECORD +260 -0
- passagemath_graphs-10.6.1rc1.dist-info/WHEEL +5 -0
- passagemath_graphs-10.6.1rc1.dist-info/top_level.txt +2 -0
- passagemath_graphs.libs/libgcc_s-69c45f16.so.1 +0 -0
- passagemath_graphs.libs/libgmp-8e78bd9b.so.10.5.0 +0 -0
- passagemath_graphs.libs/libstdc++-1f1a71be.so.6.0.33 +0 -0
- sage/all__sagemath_graphs.py +39 -0
- sage/combinat/abstract_tree.py +2723 -0
- sage/combinat/all__sagemath_graphs.py +34 -0
- sage/combinat/binary_tree.py +5306 -0
- sage/combinat/cluster_algebra_quiver/all.py +22 -0
- sage/combinat/cluster_algebra_quiver/cluster_seed.py +5208 -0
- sage/combinat/cluster_algebra_quiver/interact.py +124 -0
- sage/combinat/cluster_algebra_quiver/mutation_class.py +625 -0
- sage/combinat/cluster_algebra_quiver/mutation_type.py +1555 -0
- sage/combinat/cluster_algebra_quiver/quiver.py +2290 -0
- sage/combinat/cluster_algebra_quiver/quiver_mutation_type.py +2468 -0
- sage/combinat/designs/MOLS_handbook_data.py +570 -0
- sage/combinat/designs/all.py +58 -0
- sage/combinat/designs/bibd.py +1655 -0
- sage/combinat/designs/block_design.py +1071 -0
- sage/combinat/designs/covering_array.py +269 -0
- sage/combinat/designs/covering_design.py +530 -0
- sage/combinat/designs/database.py +5615 -0
- sage/combinat/designs/design_catalog.py +122 -0
- sage/combinat/designs/designs_pyx.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/designs/designs_pyx.pxd +21 -0
- sage/combinat/designs/designs_pyx.pyx +993 -0
- sage/combinat/designs/difference_family.py +3951 -0
- sage/combinat/designs/difference_matrices.py +279 -0
- sage/combinat/designs/evenly_distributed_sets.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/designs/evenly_distributed_sets.pyx +661 -0
- sage/combinat/designs/ext_rep.py +1064 -0
- sage/combinat/designs/gen_quadrangles_with_spread.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/designs/gen_quadrangles_with_spread.pyx +339 -0
- sage/combinat/designs/group_divisible_designs.py +361 -0
- sage/combinat/designs/incidence_structures.py +2357 -0
- sage/combinat/designs/latin_squares.py +581 -0
- sage/combinat/designs/orthogonal_arrays.py +2244 -0
- sage/combinat/designs/orthogonal_arrays_build_recursive.py +1780 -0
- sage/combinat/designs/orthogonal_arrays_find_recursive.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/designs/orthogonal_arrays_find_recursive.pyx +967 -0
- sage/combinat/designs/resolvable_bibd.py +815 -0
- sage/combinat/designs/steiner_quadruple_systems.py +1306 -0
- sage/combinat/designs/subhypergraph_search.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/designs/subhypergraph_search.pyx +530 -0
- sage/combinat/designs/twographs.py +306 -0
- sage/combinat/finite_state_machine.py +14874 -0
- sage/combinat/finite_state_machine_generators.py +2006 -0
- sage/combinat/graph_path.py +448 -0
- sage/combinat/interval_posets.py +3908 -0
- sage/combinat/nu_tamari_lattice.py +269 -0
- sage/combinat/ordered_tree.py +1446 -0
- sage/combinat/posets/all.py +46 -0
- sage/combinat/posets/bubble_shuffle.py +247 -0
- sage/combinat/posets/cartesian_product.py +493 -0
- sage/combinat/posets/d_complete.py +182 -0
- sage/combinat/posets/elements.py +273 -0
- sage/combinat/posets/forest.py +30 -0
- sage/combinat/posets/hasse_cython.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/posets/hasse_cython.pyx +174 -0
- sage/combinat/posets/hasse_diagram.py +3672 -0
- sage/combinat/posets/hochschild_lattice.py +158 -0
- sage/combinat/posets/incidence_algebras.py +794 -0
- sage/combinat/posets/lattices.py +5117 -0
- sage/combinat/posets/linear_extension_iterator.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/posets/linear_extension_iterator.pyx +292 -0
- sage/combinat/posets/linear_extensions.py +1037 -0
- sage/combinat/posets/mobile.py +275 -0
- sage/combinat/posets/moebius_algebra.py +776 -0
- sage/combinat/posets/poset_examples.py +2178 -0
- sage/combinat/posets/posets.py +9360 -0
- sage/combinat/rooted_tree.py +1070 -0
- sage/combinat/shard_order.py +239 -0
- sage/combinat/tamari_lattices.py +384 -0
- sage/combinat/yang_baxter_graph.py +923 -0
- sage/databases/all__sagemath_graphs.py +1 -0
- sage/databases/knotinfo_db.py +1231 -0
- sage/ext_data/all__sagemath_graphs.py +1 -0
- sage/ext_data/graphs/graph_plot_js.html +330 -0
- sage/ext_data/kenzo/CP2.txt +45 -0
- sage/ext_data/kenzo/CP3.txt +349 -0
- sage/ext_data/kenzo/CP4.txt +4774 -0
- sage/ext_data/kenzo/README.txt +49 -0
- sage/ext_data/kenzo/S4.txt +20 -0
- sage/graphs/all.py +42 -0
- sage/graphs/asteroidal_triples.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/asteroidal_triples.pyx +320 -0
- sage/graphs/base/all.py +1 -0
- sage/graphs/base/boost_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/boost_graph.pxd +106 -0
- sage/graphs/base/boost_graph.pyx +3045 -0
- sage/graphs/base/c_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/c_graph.pxd +106 -0
- sage/graphs/base/c_graph.pyx +5096 -0
- sage/graphs/base/dense_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/dense_graph.pxd +28 -0
- sage/graphs/base/dense_graph.pyx +801 -0
- sage/graphs/base/graph_backends.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/graph_backends.pxd +5 -0
- sage/graphs/base/graph_backends.pyx +797 -0
- sage/graphs/base/overview.py +85 -0
- sage/graphs/base/sparse_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/sparse_graph.pxd +90 -0
- sage/graphs/base/sparse_graph.pyx +1653 -0
- sage/graphs/base/static_dense_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/static_dense_graph.pxd +5 -0
- sage/graphs/base/static_dense_graph.pyx +1032 -0
- sage/graphs/base/static_sparse_backend.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/static_sparse_backend.pxd +27 -0
- sage/graphs/base/static_sparse_backend.pyx +1583 -0
- sage/graphs/base/static_sparse_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/static_sparse_graph.pxd +37 -0
- sage/graphs/base/static_sparse_graph.pyx +1375 -0
- sage/graphs/bipartite_graph.py +2732 -0
- sage/graphs/centrality.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/centrality.pyx +1038 -0
- sage/graphs/cographs.py +519 -0
- sage/graphs/comparability.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/comparability.pyx +851 -0
- sage/graphs/connectivity.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/connectivity.pxd +157 -0
- sage/graphs/connectivity.pyx +4813 -0
- sage/graphs/convexity_properties.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/convexity_properties.pxd +16 -0
- sage/graphs/convexity_properties.pyx +870 -0
- sage/graphs/digraph.py +4754 -0
- sage/graphs/digraph_generators.py +1993 -0
- sage/graphs/distances_all_pairs.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/distances_all_pairs.pxd +12 -0
- sage/graphs/distances_all_pairs.pyx +2938 -0
- sage/graphs/domination.py +1363 -0
- sage/graphs/dot2tex_utils.py +100 -0
- sage/graphs/edge_connectivity.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/edge_connectivity.pyx +1215 -0
- sage/graphs/generators/all.py +1 -0
- sage/graphs/generators/basic.py +1769 -0
- sage/graphs/generators/chessboard.py +538 -0
- sage/graphs/generators/classical_geometries.py +1611 -0
- sage/graphs/generators/degree_sequence.py +235 -0
- sage/graphs/generators/distance_regular.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/generators/distance_regular.pyx +2846 -0
- sage/graphs/generators/families.py +4759 -0
- sage/graphs/generators/intersection.py +565 -0
- sage/graphs/generators/platonic_solids.py +262 -0
- sage/graphs/generators/random.py +2623 -0
- sage/graphs/generators/smallgraphs.py +5741 -0
- sage/graphs/generators/world_map.py +724 -0
- sage/graphs/generic_graph.py +26867 -0
- sage/graphs/generic_graph_pyx.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/generic_graph_pyx.pxd +34 -0
- sage/graphs/generic_graph_pyx.pyx +1673 -0
- sage/graphs/genus.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/genus.pyx +622 -0
- sage/graphs/graph.py +9645 -0
- sage/graphs/graph_coloring.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_coloring.pyx +2284 -0
- sage/graphs/graph_database.py +1177 -0
- sage/graphs/graph_decompositions/all.py +1 -0
- sage/graphs/graph_decompositions/bandwidth.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/bandwidth.pyx +428 -0
- sage/graphs/graph_decompositions/clique_separators.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/clique_separators.pyx +616 -0
- sage/graphs/graph_decompositions/cutwidth.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/cutwidth.pyx +753 -0
- sage/graphs/graph_decompositions/fast_digraph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/fast_digraph.pxd +13 -0
- sage/graphs/graph_decompositions/fast_digraph.pyx +212 -0
- sage/graphs/graph_decompositions/graph_products.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/graph_products.pyx +508 -0
- sage/graphs/graph_decompositions/modular_decomposition.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/modular_decomposition.pxd +27 -0
- sage/graphs/graph_decompositions/modular_decomposition.pyx +1536 -0
- sage/graphs/graph_decompositions/slice_decomposition.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/slice_decomposition.pxd +18 -0
- sage/graphs/graph_decompositions/slice_decomposition.pyx +1106 -0
- sage/graphs/graph_decompositions/tree_decomposition.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/tree_decomposition.pxd +17 -0
- sage/graphs/graph_decompositions/tree_decomposition.pyx +1996 -0
- sage/graphs/graph_decompositions/vertex_separation.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/vertex_separation.pxd +5 -0
- sage/graphs/graph_decompositions/vertex_separation.pyx +1963 -0
- sage/graphs/graph_editor.py +82 -0
- sage/graphs/graph_generators.py +3314 -0
- sage/graphs/graph_generators_pyx.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_generators_pyx.pyx +95 -0
- sage/graphs/graph_input.py +812 -0
- sage/graphs/graph_latex.py +2064 -0
- sage/graphs/graph_list.py +410 -0
- sage/graphs/graph_plot.py +1756 -0
- sage/graphs/graph_plot_js.py +338 -0
- sage/graphs/hyperbolicity.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/hyperbolicity.pyx +1704 -0
- sage/graphs/hypergraph_generators.py +364 -0
- sage/graphs/independent_sets.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/independent_sets.pxd +13 -0
- sage/graphs/independent_sets.pyx +402 -0
- sage/graphs/isgci.py +1033 -0
- sage/graphs/isoperimetric_inequalities.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/isoperimetric_inequalities.pyx +489 -0
- sage/graphs/line_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/line_graph.pyx +743 -0
- sage/graphs/lovasz_theta.py +77 -0
- sage/graphs/matching.py +1633 -0
- sage/graphs/matching_covered_graph.py +3590 -0
- sage/graphs/orientations.py +1489 -0
- sage/graphs/partial_cube.py +459 -0
- sage/graphs/path_enumeration.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/path_enumeration.pyx +2040 -0
- sage/graphs/pq_trees.py +1129 -0
- sage/graphs/print_graphs.py +201 -0
- sage/graphs/schnyder.py +865 -0
- sage/graphs/spanning_tree.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/spanning_tree.pyx +1457 -0
- sage/graphs/strongly_regular_db.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/strongly_regular_db.pyx +3340 -0
- sage/graphs/traversals.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/traversals.pxd +9 -0
- sage/graphs/traversals.pyx +1872 -0
- sage/graphs/trees.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/trees.pxd +15 -0
- sage/graphs/trees.pyx +310 -0
- sage/graphs/tutte_polynomial.py +713 -0
- sage/graphs/views.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/views.pyx +794 -0
- sage/graphs/weakly_chordal.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/weakly_chordal.pyx +604 -0
- sage/groups/all__sagemath_graphs.py +1 -0
- sage/groups/perm_gps/all__sagemath_graphs.py +1 -0
- sage/groups/perm_gps/partn_ref/all__sagemath_graphs.py +1 -0
- sage/groups/perm_gps/partn_ref/refinement_graphs.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/groups/perm_gps/partn_ref/refinement_graphs.pxd +38 -0
- sage/groups/perm_gps/partn_ref/refinement_graphs.pyx +1666 -0
- sage/knots/all.py +6 -0
- sage/knots/free_knotinfo_monoid.py +507 -0
- sage/knots/gauss_code.py +291 -0
- sage/knots/knot.py +682 -0
- sage/knots/knot_table.py +284 -0
- sage/knots/knotinfo.py +2900 -0
- sage/knots/link.py +4715 -0
- sage/sandpiles/all.py +13 -0
- sage/sandpiles/examples.py +225 -0
- sage/sandpiles/sandpile.py +6365 -0
- sage/topology/all.py +22 -0
- sage/topology/cell_complex.py +1214 -0
- sage/topology/cubical_complex.py +1976 -0
- sage/topology/delta_complex.py +1806 -0
- sage/topology/filtered_simplicial_complex.py +744 -0
- sage/topology/moment_angle_complex.py +823 -0
- sage/topology/simplicial_complex.py +5160 -0
- sage/topology/simplicial_complex_catalog.py +92 -0
- sage/topology/simplicial_complex_examples.py +1680 -0
- sage/topology/simplicial_complex_homset.py +205 -0
- sage/topology/simplicial_complex_morphism.py +836 -0
- sage/topology/simplicial_set.py +4102 -0
- sage/topology/simplicial_set_catalog.py +55 -0
- sage/topology/simplicial_set_constructions.py +2954 -0
- sage/topology/simplicial_set_examples.py +865 -0
- 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
|