From d1189913d8a33e0875d63c3e928264db84ce799e Mon Sep 17 00:00:00 2001 From: dcoudert Date: Wed, 1 Oct 2025 16:39:17 +0200 Subject: [PATCH 1/6] add method number_of_biconnected_components --- src/sage/graphs/connectivity.pyx | 65 ++++++++++++++++++++++++++++++++ src/sage/graphs/generic_graph.py | 2 + 2 files changed, 67 insertions(+) diff --git a/src/sage/graphs/connectivity.pyx b/src/sage/graphs/connectivity.pyx index b1f6db2b54f..fe8117d50e6 100644 --- a/src/sage/graphs/connectivity.pyx +++ b/src/sage/graphs/connectivity.pyx @@ -23,6 +23,7 @@ Here is what the module can do: :meth:`blocks_and_cut_vertices` | Return the blocks and cut vertices of the graph. :meth:`blocks_and_cuts_tree` | Return the blocks-and-cuts tree of the graph. :meth:`biconnected_components_subgraphs` | Return a list of biconnected components as graph objects. + :meth:`number_of_biconnected_components` | Return the number of biconnected components. :meth:`is_cut_edge` | Check whether the input edge is a cut-edge or a bridge. :meth:`is_edge_cut` | Check whether the input edges form an edge cut. :meth:`is_cut_vertex` | Check whether the input vertex is a cut-vertex. @@ -818,6 +819,70 @@ def biconnected_components_subgraphs(G): return [G.subgraph(c) for c in blocks_and_cut_vertices(G)[0]] +def number_of_biconnected_components(G): + r""" + Return the number of biconnected components. + + A biconnected component is a maximal subgraph on two or more vertices that + is biconnected, i.e., removing any vertex does not disconnect it. + + .. SEEALSO:: + + - :meth:`~sage.graphs.generic_graph.GenericGraph.blocks_and_cut_vertices` + - :meth:`~Graph.is_biconnected` + + EXAMPLES: + + The disjoint union of cycles has as many biconnected components as the + number of cycles:: + + sage: G = graphs.CycleGraph(5) + sage: G.number_of_biconnected_components() + 1 + sage: (3 * G).number_of_biconnected_components() + 3 + + A block graph is a connected graph in which every biconnected component + (block) is a clique. Hence its number of biconnected components is its + number of blocks:: + + sage: number_of_blocks = randint(4, 10) + sage: G = graphs.RandomBlockGraph(number_of_blocks, 5) + sage: G.number_of_biconnected_components() == number_of_blocks + True + + By definition, an edge is a biconnected component. Hence, the number of + biconnected components of a tree is its number of edges:: + + sage: T = graphs.RandomTree(randint(0, 10)) + sage: T.number_of_biconnected_components() == T.size() + True + + An isolated vertex is a block but not a biconnected component:: + + sage: G = Graph(3) + sage: len(G.blocks_and_cut_vertices()[0]) + 3 + sage: G.number_of_biconnected_components() + 0 + + TESTS: + + An error is raised if the input is not a Sage graph:: + + sage: from sage.graphs.connectivity import number_of_biconnected_components + sage: number_of_biconnected_components('I am not a graph') + Traceback (most recent call last): + ... + TypeError: the input must be a Sage graph + """ + from sage.graphs.generic_graph import GenericGraph + if not isinstance(G, GenericGraph): + raise TypeError("the input must be a Sage graph") + + return len([c for c in G.blocks_and_cut_vertices()[0] if len(c) > 1]) + + def is_edge_cut(G, edges): """ Check whether ``edges`` form an edge cut. diff --git a/src/sage/graphs/generic_graph.py b/src/sage/graphs/generic_graph.py index 847ce71bf05..62cf9b873ae 100644 --- a/src/sage/graphs/generic_graph.py +++ b/src/sage/graphs/generic_graph.py @@ -245,6 +245,7 @@ :meth:`~GenericGraph.blocks_and_cut_vertices` | Compute the blocks and cut vertices of the graph. :meth:`~GenericGraph.blocks_and_cuts_tree` | Compute the blocks-and-cuts tree of the graph. :meth:`~GenericGraph.biconnected_components_subgraphs` | Return a list of biconnected components as graph objects. + :meth:`~GenericGraph.number_of_biconnected_components` | Return the number of biconnected components. :meth:`~GenericGraph.is_cut_edge` | Check whether the input edge is a cut-edge or a bridge. :meth:`~GenericGraph.is_edge_cut` | Check whether the input edges form an edge cut. :meth:`~GenericGraph.is_cut_vertex` | Check whether the input vertex is a cut-vertex. @@ -26005,6 +26006,7 @@ def is_self_complementary(self): is_cut_vertex, is_edge_cut, is_vertex_cut, + number_of_biconnected_components, vertex_connectivity, ) from sage.graphs.distances_all_pairs import distances_distribution, szeged_index From 7d5b2277fe53a87b26a30a8cb4e7d9037ec1e275 Mon Sep 17 00:00:00 2001 From: dcoudert Date: Wed, 1 Oct 2025 17:38:47 +0200 Subject: [PATCH 2/6] add method biconnected_components --- src/sage/graphs/connectivity.pyx | 46 +++++++++++++++++++++++++++++--- src/sage/graphs/generic_graph.py | 2 ++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/sage/graphs/connectivity.pyx b/src/sage/graphs/connectivity.pyx index fe8117d50e6..e14bf7d45cb 100644 --- a/src/sage/graphs/connectivity.pyx +++ b/src/sage/graphs/connectivity.pyx @@ -22,6 +22,7 @@ Here is what the module can do: :meth:`connected_components_sizes` | Return the sizes of the connected components as a list. :meth:`blocks_and_cut_vertices` | Return the blocks and cut vertices of the graph. :meth:`blocks_and_cuts_tree` | Return the blocks-and-cuts tree of the graph. + :meth:`biconnected_components` | Return the list of biconnected components. :meth:`biconnected_components_subgraphs` | Return a list of biconnected components as graph objects. :meth:`number_of_biconnected_components` | Return the number of biconnected components. :meth:`is_cut_edge` | Check whether the input edge is a cut-edge or a bridge. @@ -779,12 +780,49 @@ def blocks_and_cuts_tree(G): return g +def biconnected_components(G): + r""" + Return the list of biconnected components. + + A biconnected component is a maximal subgraph on two or more vertices that + is biconnected, i.e., removing any vertex does not disconnect it. + + INPUT: + + - ``G`` -- the input graph + + EXAMPLES:: + + sage: from sage.graphs.connectivity import biconnected_components + sage: G = Graph({0: [1, 2], 1: [0, 2], 2: [0, 1, 3], 3: [2]}) + sage: sorted(len(b) for b in biconnected_components(G)) + [2, 3] + sage: sorted(len(b) for b in biconnected_components(2 * G)) + [2, 2, 3, 3] + + TESTS: + + If ``G`` is not a Sage graph, an error is raised:: + + sage: from sage.graphs.connectivity import biconnected_components + sage: biconnected_components('I am not a graph') + Traceback (most recent call last): + ... + TypeError: the input must be a Sage graph + """ + from sage.graphs.generic_graph import GenericGraph + if not isinstance(G, GenericGraph): + raise TypeError("the input must be a Sage graph") + + return [b for b in blocks_and_cut_vertices(G)[0] if len(b) > 1] + + def biconnected_components_subgraphs(G): r""" Return a list of biconnected components as graph objects. - A biconnected component is a maximal subgraph that is biconnected, i.e., - removing any vertex does not disconnect it. + A biconnected component is a maximal subgraph on two or more vertices that + is biconnected, i.e., removing any vertex does not disconnect it. INPUT: @@ -816,7 +854,7 @@ def biconnected_components_subgraphs(G): if not isinstance(G, GenericGraph): raise TypeError("the input must be a Sage graph") - return [G.subgraph(c) for c in blocks_and_cut_vertices(G)[0]] + return [G.subgraph(c) for c in G.biconnected_components()] def number_of_biconnected_components(G): @@ -880,7 +918,7 @@ def number_of_biconnected_components(G): if not isinstance(G, GenericGraph): raise TypeError("the input must be a Sage graph") - return len([c for c in G.blocks_and_cut_vertices()[0] if len(c) > 1]) + return len(G.biconnected_components()) def is_edge_cut(G, edges): diff --git a/src/sage/graphs/generic_graph.py b/src/sage/graphs/generic_graph.py index 62cf9b873ae..eda36cda5cf 100644 --- a/src/sage/graphs/generic_graph.py +++ b/src/sage/graphs/generic_graph.py @@ -244,6 +244,7 @@ :meth:`~GenericGraph.connected_components_sizes` | Return the sizes of the connected components as a list. :meth:`~GenericGraph.blocks_and_cut_vertices` | Compute the blocks and cut vertices of the graph. :meth:`~GenericGraph.blocks_and_cuts_tree` | Compute the blocks-and-cuts tree of the graph. + :meth:`~GenericGraph.biconnected_components` | Return the list of biconnected components. :meth:`~GenericGraph.biconnected_components_subgraphs` | Return a list of biconnected components as graph objects. :meth:`~GenericGraph.number_of_biconnected_components` | Return the number of biconnected components. :meth:`~GenericGraph.is_cut_edge` | Check whether the input edge is a cut-edge or a bridge. @@ -25992,6 +25993,7 @@ def is_self_complementary(self): from sage.graphs.base.static_dense_graph import connected_subgraph_iterator from sage.graphs.base.static_sparse_graph import spectral_radius from sage.graphs.connectivity import ( + biconnected_components, biconnected_components_subgraphs, blocks_and_cut_vertices, blocks_and_cuts_tree, From 196151d9ce718d1fe4cdd08cd20625fd706c3293 Mon Sep 17 00:00:00 2001 From: dcoudert Date: Wed, 1 Oct 2025 17:49:13 +0200 Subject: [PATCH 3/6] move method is_biconnected from graph.py to connectivity.pyx --- src/sage/graphs/connectivity.pyx | 53 ++++++++++++++++++++--- src/sage/graphs/generic_graph.py | 4 +- src/sage/graphs/graph.py | 40 ----------------- src/sage/graphs/isgci.py | 2 +- src/sage/graphs/matching_covered_graph.py | 4 +- 5 files changed, 53 insertions(+), 50 deletions(-) diff --git a/src/sage/graphs/connectivity.pyx b/src/sage/graphs/connectivity.pyx index e14bf7d45cb..15b238050ae 100644 --- a/src/sage/graphs/connectivity.pyx +++ b/src/sage/graphs/connectivity.pyx @@ -22,6 +22,7 @@ Here is what the module can do: :meth:`connected_components_sizes` | Return the sizes of the connected components as a list. :meth:`blocks_and_cut_vertices` | Return the blocks and cut vertices of the graph. :meth:`blocks_and_cuts_tree` | Return the blocks-and-cuts tree of the graph. + :meth:`is_biconnected` | Check whether the graph is biconnected. :meth:`biconnected_components` | Return the list of biconnected components. :meth:`biconnected_components_subgraphs` | Return a list of biconnected components as graph objects. :meth:`number_of_biconnected_components` | Return the number of biconnected components. @@ -92,7 +93,7 @@ def is_connected(G, forbidden_vertices=None): .. SEEALSO:: - - :meth:`~Graph.is_biconnected` + - :meth:`~sage.graphs.generic_graph.GenericGraph.is_biconnected` EXAMPLES:: @@ -507,7 +508,7 @@ def blocks_and_cut_vertices(G, algorithm='Tarjan_Boost', sort=False, key=None): - :meth:`blocks_and_cuts_tree` - :func:`sage.graphs.base.boost_graph.blocks_and_cut_vertices` - - :meth:`~Graph.is_biconnected` + - :meth:`~sage.graphs.generic_graph.GenericGraph.is_biconnected` - :meth:`~Graph.bridges` EXAMPLES: @@ -718,7 +719,7 @@ def blocks_and_cuts_tree(G): .. SEEALSO:: - :meth:`~sage.graphs.generic_graph.GenericGraph.blocks_and_cut_vertices` - - :meth:`~Graph.is_biconnected` + - :meth:`~sage.graphs.generic_graph.GenericGraph.is_biconnected` EXAMPLES:: @@ -780,6 +781,46 @@ def blocks_and_cuts_tree(G): return g +def is_biconnected(G): + r""" + Check whether the graph is biconnected. + + A biconnected graph is a connected graph on two or more vertices that is not + broken into disconnected pieces by deleting any single vertex. + + .. SEEALSO:: + + - :meth:`~sage.graphs.generic_graph.GenericGraph.is_connected` + - :meth:`~sage.graphs.generic_graph.GenericGraph.blocks_and_cut_vertices` + - :meth:`~sage.graphs.generic_graph.GenericGraph.blocks_and_cuts_tree` + - :wikipedia:`Biconnected_graph` + + EXAMPLES:: + + sage: G = graphs.PetersenGraph() + sage: G.is_biconnected() + True + sage: G.add_path([0,'a','b']) + sage: G.is_biconnected() + False + sage: G.add_edge('b', 1) + sage: G.is_biconnected() + True + + TESTS:: + + sage: Graph().is_biconnected() + False + sage: Graph(1).is_biconnected() + False + sage: graphs.CompleteGraph(2).is_biconnected() + True + """ + if G.order() < 2 or not G.is_connected(): + return False + return not G.blocks_and_cut_vertices()[1] + + def biconnected_components(G): r""" Return the list of biconnected components. @@ -867,7 +908,7 @@ def number_of_biconnected_components(G): .. SEEALSO:: - :meth:`~sage.graphs.generic_graph.GenericGraph.blocks_and_cut_vertices` - - :meth:`~Graph.is_biconnected` + - :meth:`~sage.graphs.generic_graph.GenericGraph.is_biconnected` EXAMPLES: @@ -3466,7 +3507,7 @@ cdef class TriconnectivitySPQR: .. SEEALSO:: - :meth:`sage.graphs.connectivity.spqr_tree` - - :meth:`~Graph.is_biconnected` + - :meth:`~sage.graphs.generic_graph.GenericGraph.is_biconnected` - :wikipedia:`SPQR_tree` EXAMPLES: @@ -4892,7 +4933,7 @@ def is_triconnected(G): .. SEEALSO:: - :meth:`~sage.graphs.generic_graph.GenericGraph.is_connected` - - :meth:`~Graph.is_biconnected` + - :meth:`~sage.graphs.generic_graph.GenericGraph.is_biconnected` - :meth:`~sage.graphs.connectivity.spqr_tree` - :wikipedia:`SPQR_tree` diff --git a/src/sage/graphs/generic_graph.py b/src/sage/graphs/generic_graph.py index eda36cda5cf..9292b2d30ae 100644 --- a/src/sage/graphs/generic_graph.py +++ b/src/sage/graphs/generic_graph.py @@ -236,7 +236,7 @@ :widths: 30, 70 :delim: | - :meth:`~GenericGraph.is_connected` | Test whether the (di)graph is connected. + :meth:`~GenericGraph.is_connected` | Check whether the (di)graph is connected. :meth:`~GenericGraph.connected_components` | Return the list of connected components :meth:`~GenericGraph.connected_components_number` | Return the number of connected components. :meth:`~GenericGraph.connected_components_subgraphs` | Return a list of connected components as graph objects. @@ -244,6 +244,7 @@ :meth:`~GenericGraph.connected_components_sizes` | Return the sizes of the connected components as a list. :meth:`~GenericGraph.blocks_and_cut_vertices` | Compute the blocks and cut vertices of the graph. :meth:`~GenericGraph.blocks_and_cuts_tree` | Compute the blocks-and-cuts tree of the graph. + :meth:`~GenericGraph.is_biconnected` | Check whether the graph is biconnected. :meth:`~GenericGraph.biconnected_components` | Return the list of biconnected components. :meth:`~GenericGraph.biconnected_components_subgraphs` | Return a list of biconnected components as graph objects. :meth:`~GenericGraph.number_of_biconnected_components` | Return the number of biconnected components. @@ -26003,6 +26004,7 @@ def is_self_complementary(self): connected_components_sizes, connected_components_subgraphs, edge_connectivity, + is_biconnected, is_connected, is_cut_edge, is_cut_vertex, diff --git a/src/sage/graphs/graph.py b/src/sage/graphs/graph.py index 248fd65d12f..c5e36ce9c6d 100644 --- a/src/sage/graphs/graph.py +++ b/src/sage/graphs/graph.py @@ -1754,46 +1754,6 @@ def is_cactus(self): B = self.blocks_and_cut_vertices()[0] return len(self.faces()) == sum(1 for b in B if len(b) > 2) + 1 - @doc_index("Graph properties") - def is_biconnected(self): - """ - Test if the graph is biconnected. - - A biconnected graph is a connected graph on two or more vertices that is - not broken into disconnected pieces by deleting any single vertex. - - .. SEEALSO:: - - - :meth:`~sage.graphs.generic_graph.GenericGraph.is_connected` - - :meth:`~sage.graphs.generic_graph.GenericGraph.blocks_and_cut_vertices` - - :meth:`~sage.graphs.generic_graph.GenericGraph.blocks_and_cuts_tree` - - :wikipedia:`Biconnected_graph` - - EXAMPLES:: - - sage: G = graphs.PetersenGraph() - sage: G.is_biconnected() - True - sage: G.add_path([0,'a','b']) - sage: G.is_biconnected() - False - sage: G.add_edge('b', 1) - sage: G.is_biconnected() - True - - TESTS:: - - sage: Graph().is_biconnected() - False - sage: Graph(1).is_biconnected() - False - sage: graphs.CompleteGraph(2).is_biconnected() - True - """ - if self.order() < 2 or not self.is_connected(): - return False - return not self.blocks_and_cut_vertices()[1] - @doc_index("Graph properties") def is_block_graph(self): r""" diff --git a/src/sage/graphs/isgci.py b/src/sage/graphs/isgci.py index 5efedd77616..d00bb9597e7 100644 --- a/src/sage/graphs/isgci.py +++ b/src/sage/graphs/isgci.py @@ -144,7 +144,7 @@ * - Biconnected - - :meth:`~sage.graphs.graph.Graph.is_biconnected`, + - :meth:`~sage.graphs.generic_graph.GenericGraph.is_biconnected`, :meth:`~sage.graphs.generic_graph.GenericGraph.blocks_and_cut_vertices`, :meth:`~sage.graphs.generic_graph.GenericGraph.blocks_and_cuts_tree` diff --git a/src/sage/graphs/matching_covered_graph.py b/src/sage/graphs/matching_covered_graph.py index 5d7e2ba49b8..48756e1e389 100644 --- a/src/sage/graphs/matching_covered_graph.py +++ b/src/sage/graphs/matching_covered_graph.py @@ -2387,8 +2387,8 @@ def is_biconnected(self): .. NOTE:: This method overwrites the - :meth:`~sage.graphs.graph.Graph.is_biconnected` method - in order to return ``True`` as matching covered graphs are + :meth:`~sage.graphs.generic_graph.GenericGraph.is_biconnected` + method in order to return ``True`` as matching covered graphs are biconnected. EXAMPLES: From 64b9de27da4c14d96412de4fa1a8bd7cde3d042a Mon Sep 17 00:00:00 2001 From: dcoudert Date: Fri, 3 Oct 2025 11:33:42 +0200 Subject: [PATCH 4/6] add parameter forbidden_vertices in blocks_and_cut_vertices --- src/sage/graphs/connectivity.pyx | 37 ++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/src/sage/graphs/connectivity.pyx b/src/sage/graphs/connectivity.pyx index 15b238050ae..c6d1df8b5e7 100644 --- a/src/sage/graphs/connectivity.pyx +++ b/src/sage/graphs/connectivity.pyx @@ -465,7 +465,8 @@ def connected_components_sizes(G, forbidden_vertices=None): forbidden_vertices=forbidden_vertices)] -def blocks_and_cut_vertices(G, algorithm='Tarjan_Boost', sort=False, key=None): +def blocks_and_cut_vertices(G, algorithm='Tarjan_Boost', sort=False, key=None, + forbidden_vertices=None): """ Return the blocks and cut vertices of the graph. @@ -495,6 +496,10 @@ def blocks_and_cut_vertices(G, algorithm='Tarjan_Boost', sort=False, key=None): vertex as its one argument and returns a value that can be used for comparisons in the sorting algorithm (we must have ``sort=True``) + - ``forbidden_vertices`` -- list (default: ``None``); set of vertices to + avoid during the search. This parameter is currently only available when + ``algorithm`` is ``'Tarjan_Sage'`` and so is ignored otherwise. + OUTPUT: ``(B, C)``, where ``B`` is a list of blocks - each is a list of vertices and the blocks are the corresponding induced subgraphs - and ``C`` is a list of cut vertices. @@ -508,7 +513,7 @@ def blocks_and_cut_vertices(G, algorithm='Tarjan_Boost', sort=False, key=None): - :meth:`blocks_and_cuts_tree` - :func:`sage.graphs.base.boost_graph.blocks_and_cut_vertices` - - :meth:`~sage.graphs.generic_graph.GenericGraph.is_biconnected` + - :meth:`~Graph.is_biconnected` - :meth:`~Graph.bridges` EXAMPLES: @@ -554,6 +559,23 @@ def blocks_and_cut_vertices(G, algorithm='Tarjan_Boost', sort=False, key=None): sage: blocks_and_cut_vertices(rings, algorithm='Tarjan_Boost') ([[0, 1, 4, 2, 3], [0, 6, 9, 7, 8]], [0]) + Check the behavior of parameter ``forbidden_vertices``:: + + sage: G = graphs.WindmillGraph(4, 3) + sage: G.blocks_and_cut_vertices(sort=True, algorithm='Tarjan_Sage') + ([[0, 1, 2, 3], [0, 4, 5, 6], [0, 7, 8, 9]], [0]) + sage: G.blocks_and_cut_vertices(sort=True, algorithm='Tarjan_Sage', forbidden_vertices=[0]) + ([[1, 2, 3], [4, 5, 6], [7, 8, 9]], []) + sage: G.blocks_and_cut_vertices(sort=True, algorithm='Tarjan_Sage', forbidden_vertices=[1]) + ([[0, 2, 3], [0, 4, 5, 6], [0, 7, 8, 9]], [0]) + sage: G = graphs.PathGraph(3) + sage: G.blocks_and_cut_vertices(sort=True, algorithm='Tarjan_Sage') + ([[1, 2], [0, 1]], [1]) + sage: G.blocks_and_cut_vertices(sort=True, algorithm='Tarjan_Sage', forbidden_vertices=[0]) + ([[1, 2]], []) + sage: G.blocks_and_cut_vertices(sort=True, algorithm='Tarjan_Sage', forbidden_vertices=[1]) + ([[0], [2]], []) + TESTS:: sage: blocks_and_cut_vertices(Graph(0)) @@ -586,18 +608,22 @@ def blocks_and_cut_vertices(G, algorithm='Tarjan_Boost', sort=False, key=None): if (not sort) and key: raise ValueError('sort keyword is False, yet a key function is given') + forbidden = set(forbidden_vertices) if forbidden_vertices is not None else set() + blocks = [] cut_vertices = set() # We iterate over all vertices to ensure that we visit each connected # component of the graph - seen = set() + seen = set(forbidden) for start in G.vertex_iterator(): if start in seen: continue # Special case of an isolated vertex - if not G.degree(start): + if (not G.degree(start) or + (forbidden and + len(set(G.neighbors(start, closed=True)) - forbidden) == 1)): blocks.append([start]) seen.add(start) continue @@ -637,6 +663,9 @@ def blocks_and_cut_vertices(G, algorithm='Tarjan_Boost', sort=False, key=None): # We consider the next of its neighbors w = next(neighbors[v]) + if w in forbidden: + continue + # If we never met w before, we remember the direction of # edge vw, and add w to the stack. if w not in number: From fddaf9ec14be6bcf23cfed7ee5cc223db7f137e6 Mon Sep 17 00:00:00 2001 From: dcoudert Date: Fri, 3 Oct 2025 14:03:37 +0200 Subject: [PATCH 5/6] add parameter forbidden_vertices to boost blocks_and_cute_vertices --- src/sage/graphs/base/boost_graph.pyx | 40 ++++++++++++++++++++++++---- src/sage/graphs/connectivity.pyx | 20 +++++++++----- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/sage/graphs/base/boost_graph.pyx b/src/sage/graphs/base/boost_graph.pyx index 41a41a2eaac..d51a8832d29 100644 --- a/src/sage/graphs/base/boost_graph.pyx +++ b/src/sage/graphs/base/boost_graph.pyx @@ -49,7 +49,6 @@ Functions # http://www.gnu.org/licenses/ # **************************************************************************** -cimport cython from cysignals.signals cimport sig_check, sig_on, sig_off from libcpp.set cimport set as cset from libcpp.pair cimport pair @@ -744,7 +743,7 @@ cpdef min_spanning_tree(g, return [(u, v, g.edge_label(u, v)) for u, v in edges] -cpdef blocks_and_cut_vertices(g): +cpdef blocks_and_cut_vertices(g, forbidden_vertices=None): r""" Compute the blocks and cut vertices of the graph. @@ -755,6 +754,9 @@ cpdef blocks_and_cut_vertices(g): - ``g`` -- the input Sage graph + - ``forbidden_vertices`` -- list (default: ``None``); set of vertices to + avoid during the search + OUTPUT: A 2-dimensional vector with m+1 rows (m is the number of biconnected @@ -772,10 +774,29 @@ cpdef blocks_and_cut_vertices(g): sage: blocks_and_cut_vertices(g) ([[8, 9], [7, 8], [0, 1, 2, 3, 5, 4, 6, 7]], [8, 7]) - sage: G = Graph([(0,1,{'name':'a','weight':1}), (0,2,{'name':'b','weight':3}), (1,2,{'name':'b','weight':1})]) + sage: G = Graph([(0,1,{'name':'a','weight':1}), + ....: (0,2,{'name':'b','weight':3}), + ....: (1,2,{'name':'b','weight':1})]) sage: blocks_and_cut_vertices(G) ([[0, 1, 2]], []) + Check the behavior of parameter ``forbidden_vertices``:: + + sage: G = graphs.WindmillGraph(4, 3) + sage: blocks_and_cut_vertices(G) + ([[0, 1, 2, 3], [0, 4, 5, 6], [0, 7, 8, 9]], [0]) + sage: blocks_and_cut_vertices(G, forbidden_vertices=[0]) + ([[1, 2, 3], [4, 5, 6], [7, 8, 9]], []) + sage: blocks_and_cut_vertices(G, forbidden_vertices=[1]) + ([[0, 2, 3], [0, 4, 5, 6], [0, 7, 8, 9]], [0]) + sage: G = graphs.PathGraph(3) + sage: blocks_and_cut_vertices(G) + ([[1, 2], [0, 1]], [1]) + sage: blocks_and_cut_vertices(G, forbidden_vertices=[0]) + ([[1, 2]], []) + sage: blocks_and_cut_vertices(G, forbidden_vertices=[1]) + ([[0], [2]], []) + TESTS: Given an input which is not a graph:: @@ -790,8 +811,17 @@ cpdef blocks_and_cut_vertices(g): if not isinstance(g, GenericGraph): raise TypeError("the input must be a Sage graph") - if g.allows_loops() or g.allows_multiple_edges() or g.is_directed(): - g = g.to_simple() + cdef set forbidden = set() if forbidden_vertices is None else set(forbidden_vertices) + + if (g.allows_loops() or g.allows_multiple_edges() + or g.is_directed() or forbidden): + # Build the underlying undirected graph without loops or multiple edges, + # and without the forbidden vertices + V = [v for v in g if v not in forbidden] + E = [(u, v) for u, v in g.edge_iterator(vertices=V, labels=False, sort_vertices=False) + if u != v and u not in forbidden and v not in forbidden] + from sage.graphs.graph import Graph + g = Graph([V, E], format='vertices_and_edges') cdef BoostVecGraph g_boost cdef vector[vector[v_index]] result diff --git a/src/sage/graphs/connectivity.pyx b/src/sage/graphs/connectivity.pyx index c6d1df8b5e7..48627541e37 100644 --- a/src/sage/graphs/connectivity.pyx +++ b/src/sage/graphs/connectivity.pyx @@ -497,8 +497,7 @@ def blocks_and_cut_vertices(G, algorithm='Tarjan_Boost', sort=False, key=None, comparisons in the sorting algorithm (we must have ``sort=True``) - ``forbidden_vertices`` -- list (default: ``None``); set of vertices to - avoid during the search. This parameter is currently only available when - ``algorithm`` is ``'Tarjan_Sage'`` and so is ignored otherwise. + avoid during the search OUTPUT: ``(B, C)``, where ``B`` is a list of blocks - each is a list of vertices and the blocks are the corresponding induced subgraphs - and @@ -562,10 +561,14 @@ def blocks_and_cut_vertices(G, algorithm='Tarjan_Boost', sort=False, key=None, Check the behavior of parameter ``forbidden_vertices``:: sage: G = graphs.WindmillGraph(4, 3) - sage: G.blocks_and_cut_vertices(sort=True, algorithm='Tarjan_Sage') + sage: G.blocks_and_cut_vertices(sort=True) ([[0, 1, 2, 3], [0, 4, 5, 6], [0, 7, 8, 9]], [0]) + sage: G.blocks_and_cut_vertices(sort=True, algorithm='Tarjan_Boost', forbidden_vertices=[0]) + ([[1, 2, 3], [4, 5, 6], [7, 8, 9]], []) sage: G.blocks_and_cut_vertices(sort=True, algorithm='Tarjan_Sage', forbidden_vertices=[0]) ([[1, 2, 3], [4, 5, 6], [7, 8, 9]], []) + sage: G.blocks_and_cut_vertices(sort=True, algorithm='Tarjan_Boost', forbidden_vertices=[1]) + ([[0, 2, 3], [0, 4, 5, 6], [0, 7, 8, 9]], [0]) sage: G.blocks_and_cut_vertices(sort=True, algorithm='Tarjan_Sage', forbidden_vertices=[1]) ([[0, 2, 3], [0, 4, 5, 6], [0, 7, 8, 9]], [0]) sage: G = graphs.PathGraph(3) @@ -575,6 +578,8 @@ def blocks_and_cut_vertices(G, algorithm='Tarjan_Boost', sort=False, key=None, ([[1, 2]], []) sage: G.blocks_and_cut_vertices(sort=True, algorithm='Tarjan_Sage', forbidden_vertices=[1]) ([[0], [2]], []) + sage: G.blocks_and_cut_vertices(sort=True, algorithm='Tarjan_Boost', forbidden_vertices=[1]) + ([[0], [2]], []) TESTS:: @@ -597,9 +602,10 @@ def blocks_and_cut_vertices(G, algorithm='Tarjan_Boost', sort=False, key=None, if not isinstance(G, GenericGraph): raise TypeError("the input must be a Sage graph") + if algorithm == "Tarjan_Boost": from sage.graphs.base.boost_graph import blocks_and_cut_vertices - return blocks_and_cut_vertices(G) + return blocks_and_cut_vertices(G, forbidden_vertices=forbidden_vertices) if algorithm != "Tarjan_Sage": raise NotImplementedError("blocks and cut vertices algorithm '%s' is not implemented" % algorithm) @@ -608,14 +614,13 @@ def blocks_and_cut_vertices(G, algorithm='Tarjan_Boost', sort=False, key=None, if (not sort) and key: raise ValueError('sort keyword is False, yet a key function is given') - forbidden = set(forbidden_vertices) if forbidden_vertices is not None else set() - blocks = [] cut_vertices = set() # We iterate over all vertices to ensure that we visit each connected # component of the graph - seen = set(forbidden) + cdef set forbidden = set() if forbidden_vertices is None else set(forbidden_vertices) + cdef set seen = set(forbidden) for start in G.vertex_iterator(): if start in seen: continue @@ -664,6 +669,7 @@ def blocks_and_cut_vertices(G, algorithm='Tarjan_Boost', sort=False, key=None, w = next(neighbors[v]) if w in forbidden: + # We skip that neighbor continue # If we never met w before, we remember the direction of From 2ce54738df1da65afa8dd68168ee2120ea5c263d Mon Sep 17 00:00:00 2001 From: dcoudert Date: Fri, 3 Oct 2025 14:42:23 +0200 Subject: [PATCH 6/6] add parameter forbidden_vertices to methods related to biconnected components --- src/sage/graphs/connectivity.pyx | 100 +++++++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 10 deletions(-) diff --git a/src/sage/graphs/connectivity.pyx b/src/sage/graphs/connectivity.pyx index 48627541e37..ca895a27f60 100644 --- a/src/sage/graphs/connectivity.pyx +++ b/src/sage/graphs/connectivity.pyx @@ -732,7 +732,7 @@ def blocks_and_cut_vertices(G, algorithm='Tarjan_Boost', sort=False, key=None, return blocks, list(cut_vertices) -def blocks_and_cuts_tree(G): +def blocks_and_cuts_tree(G, forbidden_vertices=None): """ Return the blocks-and-cuts tree of ``self``. @@ -751,6 +751,14 @@ def blocks_and_cuts_tree(G): We referred to [HarPri]_ and [Gallai]_ for blocks and cuts tree. + INPUT: + + - ``G`` -- a Sage graph + + - ``forbidden_vertices`` -- list (default: ``None``); set of vertices to + avoid during the search. This is equilavent to get the blocks-and-cut tree + of a graph in which the forbidden vertices have been removed. + .. SEEALSO:: - :meth:`~sage.graphs.generic_graph.GenericGraph.blocks_and_cut_vertices` @@ -782,6 +790,18 @@ def blocks_and_cuts_tree(G): sage: T.vertices(sort=True) [('B', (0, 1, 4, 5, 2, 6, 3, 7, 8, 9))] + Check the behavior of parameter ``forbidden_vertices``:: + + sage: G = graphs.CycleGraph(5) + sage: G.blocks_and_cut_vertices() + ([[0, 1, 4, 2, 3]], []) + sage: G.blocks_and_cuts_tree() + Graph on 1 vertex + sage: G.blocks_and_cut_vertices(forbidden_vertices=[0]) + ([[3, 4], [2, 3], [1, 2]], [2, 3]) + sage: G.blocks_and_cuts_tree(forbidden_vertices=[0]) + Graph on 5 vertices + TESTS: When ``self`` is not connected, the resulting graph is a forest @@ -804,7 +824,7 @@ def blocks_and_cuts_tree(G): raise TypeError("the input must be a Sage graph") from sage.graphs.graph import Graph - B, C = G.blocks_and_cut_vertices() + B, C = G.blocks_and_cut_vertices(forbidden_vertices=forbidden_vertices) B = map(tuple, B) set_C = set(C) g = Graph() @@ -816,13 +836,21 @@ def blocks_and_cuts_tree(G): return g -def is_biconnected(G): +def is_biconnected(G, forbidden_vertices=None): r""" Check whether the graph is biconnected. A biconnected graph is a connected graph on two or more vertices that is not broken into disconnected pieces by deleting any single vertex. + INPUT: + + - ``G`` -- a Sage graph + + - ``forbidden_vertices`` -- list (default: ``None``); set of vertices to + avoid during the search. This is equilavent to check whether the graph in + which the forbidden vertices have been removed is biconnected. + .. SEEALSO:: - :meth:`~sage.graphs.generic_graph.GenericGraph.is_connected` @@ -842,6 +870,14 @@ def is_biconnected(G): sage: G.is_biconnected() True + Check the behavior of parameter ``forbidden_vertices``:: + + sage: G = graphs.CycleGraph(5) + sage: G.is_biconnected() + True + sage: G.is_biconnected(forbidden_vertices=[0]) + False + TESTS:: sage: Graph().is_biconnected() @@ -853,10 +889,10 @@ def is_biconnected(G): """ if G.order() < 2 or not G.is_connected(): return False - return not G.blocks_and_cut_vertices()[1] + return not G.blocks_and_cut_vertices(forbidden_vertices=forbidden_vertices)[1] -def biconnected_components(G): +def biconnected_components(G, forbidden_vertices=None): r""" Return the list of biconnected components. @@ -867,6 +903,10 @@ def biconnected_components(G): - ``G`` -- the input graph + - ``forbidden_vertices`` -- list (default: ``None``); set of vertices to + avoid during the search. This is equilavent to get the biconnected + components of the graph after the removal of the forbidden vertices. + EXAMPLES:: sage: from sage.graphs.connectivity import biconnected_components @@ -876,6 +916,14 @@ def biconnected_components(G): sage: sorted(len(b) for b in biconnected_components(2 * G)) [2, 2, 3, 3] + Check the behavior of parameter ``forbidden_vertices``:: + + sage: G = graphs.CycleGraph(5) + sage: len(G.biconnected_components()) + 1 + sage: len(G.biconnected_components(forbidden_vertices=[0])) + 3 + TESTS: If ``G`` is not a Sage graph, an error is raised:: @@ -890,10 +938,11 @@ def biconnected_components(G): if not isinstance(G, GenericGraph): raise TypeError("the input must be a Sage graph") - return [b for b in blocks_and_cut_vertices(G)[0] if len(b) > 1] + B = blocks_and_cut_vertices(G, forbidden_vertices=forbidden_vertices)[0] + return [b for b in B if len(b) > 1] -def biconnected_components_subgraphs(G): +def biconnected_components_subgraphs(G, forbidden_vertices=None): r""" Return a list of biconnected components as graph objects. @@ -904,6 +953,11 @@ def biconnected_components_subgraphs(G): - ``G`` -- the input graph + - ``forbidden_vertices`` -- list (default: ``None``); set of vertices to + avoid during the search. This is equilavent to get the biconnected + components subgraphs of the graph after the removal of the forbidden + vertices. + EXAMPLES:: sage: from sage.graphs.connectivity import biconnected_components_subgraphs @@ -916,6 +970,14 @@ def biconnected_components_subgraphs(G): sage: L[1].edges() [(0, 1, None), (0, 2, None), (1, 2, None)] + Check the behavior of parameter ``forbidden_vertices``:: + + sage: G = graphs.CycleGraph(5) + sage: len(G.biconnected_components_subgraphs()) + 1 + sage: len(G.biconnected_components_subgraphs(forbidden_vertices=[0])) + 3 + TESTS: If ``G`` is not a Sage graph, an error is raised:: @@ -930,16 +992,26 @@ def biconnected_components_subgraphs(G): if not isinstance(G, GenericGraph): raise TypeError("the input must be a Sage graph") - return [G.subgraph(c) for c in G.biconnected_components()] + B = G.biconnected_components(forbidden_vertices=forbidden_vertices) + return [G.subgraph(c) for c in B] -def number_of_biconnected_components(G): +def number_of_biconnected_components(G, forbidden_vertices=None): r""" Return the number of biconnected components. A biconnected component is a maximal subgraph on two or more vertices that is biconnected, i.e., removing any vertex does not disconnect it. + INPUT: + + - ``G`` -- the input graph + + - ``forbidden_vertices`` -- list (default: ``None``); set of vertices to + avoid during the search. This is equilavent to get the number of + biconnected components of the graph after the removal of the forbidden + vertices. + .. SEEALSO:: - :meth:`~sage.graphs.generic_graph.GenericGraph.blocks_and_cut_vertices` @@ -980,6 +1052,14 @@ def number_of_biconnected_components(G): sage: G.number_of_biconnected_components() 0 + Check the behavior of parameter ``forbidden_vertices``:: + + sage: G = graphs.CycleGraph(5) + sage: G.number_of_biconnected_components() + 1 + sage: G.number_of_biconnected_components(forbidden_vertices=[0]) + 3 + TESTS: An error is raised if the input is not a Sage graph:: @@ -994,7 +1074,7 @@ def number_of_biconnected_components(G): if not isinstance(G, GenericGraph): raise TypeError("the input must be a Sage graph") - return len(G.biconnected_components()) + return len(G.biconnected_components(forbidden_vertices=forbidden_vertices)) def is_edge_cut(G, edges):