Skip to content

Commit a30eb04

Browse files
author
Release Manager
committed
gh-38418: add method is_vertex_cut to (di)graphs This PR answers a query from https://ask.sagemath.org/question/78391/does-sage-have-a-function-to- determine-vertex-cuts/ It adds to (di)graphs a method to check if a set of vertices is a vertex cut of the (di)graph, that is if its removal from the (di)graph increases the number of (strongly) connected components. This generalizes method `is_cut_vertex`. ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> - [x] The title is concise and informative. - [x] The description explains in detail what this PR is about. - [x] I have linked a relevant issue or discussion. - [x] I have created tests covering the changes. - [x] I have updated the documentation and checked the documentation preview. ### ⌛ Dependencies <!-- List all open PRs that this PR logically depends on. For example, --> <!-- - #12345: short description why this is a dependency --> <!-- - #34567: ... --> URL: #38418 Reported by: David Coudert Reviewer(s): David Coudert, Kwankyu Lee, Matthias Köppe
2 parents 6b02856 + 361ec53 commit a30eb04

File tree

2 files changed

+147
-62
lines changed

2 files changed

+147
-62
lines changed

src/sage/graphs/connectivity.pyx

Lines changed: 144 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Here is what the module can do:
2626
:meth:`is_cut_edge` | Check whether the input edge is a cut-edge or a bridge.
2727
:meth:`is_edge_cut` | Check whether the input edges form an edge cut.
2828
:meth:`is_cut_vertex` | Check whether the input vertex is a cut-vertex.
29+
:meth:`is_vertex_cut` | Check whether the input vertices form a vertex cut.
2930
:meth:`edge_connectivity` | Return the edge connectivity of the graph.
3031
:meth:`vertex_connectivity` | Return the vertex connectivity of the graph.
3132
@@ -971,58 +972,64 @@ def is_cut_edge(G, u, v=None, label=None):
971972
return sol
972973

973974

974-
def is_cut_vertex(G, u, weak=False):
975+
def is_vertex_cut(G, cut, weak=False):
975976
r"""
976-
Check whether the input vertex is a cut-vertex.
977+
Check whether the input vertices form a vertex cut.
977978
978-
A vertex is a cut-vertex if its removal from the (di)graph increases the
979-
number of (strongly) connected components. Isolated vertices or leafs are
980-
not cut-vertices. This function works with simple graphs as well as graphs
981-
with loops and multiple edges.
979+
A set of vertices is a vertex cut if its removal from the (di)graph
980+
increases the number of (strongly) connected components. This function works
981+
with simple graphs as well as graphs with loops and multiple edges.
982982
983983
INPUT:
984984
985985
- ``G`` -- a Sage (Di)Graph
986986
987-
- ``u`` -- a vertex
987+
- ``cut`` -- a set of vertices
988988
989989
- ``weak`` -- boolean (default: ``False``); whether the connectivity of
990990
directed graphs is to be taken in the weak sense, that is ignoring edges
991991
orientations
992992
993-
OUTPUT:
994-
995-
Return ``True`` if ``u`` is a cut-vertex, and ``False`` otherwise.
996-
997993
EXAMPLES:
998994
999-
Giving a LollipopGraph(4,2), that is a complete graph with 4 vertices with a
1000-
pending edge::
995+
Giving a cycle graph of order 4::
1001996
1002-
sage: from sage.graphs.connectivity import is_cut_vertex
1003-
sage: G = graphs.LollipopGraph(4, 2)
1004-
sage: is_cut_vertex(G, 0)
997+
sage: from sage.graphs.connectivity import is_vertex_cut
998+
sage: G = graphs.CycleGraph(4)
999+
sage: is_vertex_cut(G, [0, 1])
10051000
False
1006-
sage: is_cut_vertex(G, 3)
1001+
sage: is_vertex_cut(G, [0, 2])
10071002
True
1008-
sage: G.is_cut_vertex(3)
1003+
1004+
Giving a disconnected graph::
1005+
1006+
sage: from sage.graphs.connectivity import is_vertex_cut
1007+
sage: G = graphs.CycleGraph(4) * 2
1008+
sage: G.connected_components()
1009+
[[0, 1, 2, 3], [4, 5, 6, 7]]
1010+
sage: is_vertex_cut(G, [0, 2])
1011+
True
1012+
sage: is_vertex_cut(G, [4, 6])
1013+
True
1014+
sage: is_vertex_cut(G, [0, 6])
1015+
False
1016+
sage: is_vertex_cut(G, [0, 4, 6])
10091017
True
10101018
10111019
Comparing the weak and strong connectivity of a digraph::
10121020
1013-
sage: from sage.graphs.connectivity import is_strongly_connected
10141021
sage: D = digraphs.Circuit(6)
1015-
sage: is_strongly_connected(D)
1022+
sage: D.is_strongly_connected()
10161023
True
1017-
sage: is_cut_vertex(D, 2)
1024+
sage: is_vertex_cut(D, [2])
10181025
True
1019-
sage: is_cut_vertex(D, 2, weak=True)
1026+
sage: is_vertex_cut(D, [2], weak=True)
10201027
False
10211028
10221029
Giving a vertex that is not in the graph::
10231030
10241031
sage: G = graphs.CompleteGraph(4)
1025-
sage: is_cut_vertex(G, 7)
1032+
sage: is_vertex_cut(G, [7])
10261033
Traceback (most recent call last):
10271034
...
10281035
ValueError: vertex (7) is not a vertex of the graph
@@ -1031,7 +1038,7 @@ def is_cut_vertex(G, u, weak=False):
10311038
10321039
If ``G`` is not a Sage graph, an error is raised::
10331040
1034-
sage: is_cut_vertex('I am not a graph', 0)
1041+
sage: is_vertex_cut('I am not a graph', [0])
10351042
Traceback (most recent call last):
10361043
...
10371044
TypeError: the input must be a Sage graph
@@ -1040,68 +1047,144 @@ def is_cut_vertex(G, u, weak=False):
10401047
if not isinstance(G, GenericGraph):
10411048
raise TypeError("the input must be a Sage graph")
10421049

1043-
if u not in G:
1044-
raise ValueError("vertex ({0}) is not a vertex of the graph".format(repr(u)))
1045-
1046-
# Initialization
1047-
cdef set CC
1048-
cdef list neighbors_func
1049-
if not G.is_directed() or weak:
1050-
# Weak connectivity
1050+
cdef set cutset = set(cut)
1051+
for u in cutset:
1052+
if u not in G:
1053+
raise ValueError("vertex ({0}) is not a vertex of the graph".format(repr(u)))
10511054

1052-
if G.degree(u) < 2:
1053-
# An isolated or a leaf vertex is not a cut vertex
1054-
return False
1055-
1056-
neighbors_func = [G.neighbor_iterator]
1057-
start = next(G.neighbor_iterator(u))
1058-
CC = set(G)
1055+
if len(cutset) >= G.order() - 1:
1056+
# A vertex cut must be of size at most n - 2
1057+
return False
10591058

1059+
# We deal with graphs with multiple (strongly) connected components
1060+
cdef list CC
1061+
if G.is_directed() and not weak:
1062+
CC = G.strongly_connected_components()
10601063
else:
1061-
# Strong connectivity for digraphs
1062-
1063-
if not G.out_degree(u) or not G.in_degree(u):
1064-
# A vertex without in or out neighbors is not a cut vertex
1065-
return False
1064+
CC = G.connected_components(sort=False)
1065+
if len(CC) > 1:
1066+
for comp in CC:
1067+
subcut = cutset.intersection(comp)
1068+
if subcut and is_vertex_cut(G.subgraph(comp), subcut, weak=weak):
1069+
return True
1070+
return False
10661071

1067-
# We consider only the strongly connected component containing u
1068-
CC = set(strongly_connected_component_containing_vertex(G, u))
1072+
cdef list boundary = G.vertex_boundary(cutset)
1073+
if not boundary:
1074+
# We need at least 1 vertex in the boundary of the cut
1075+
return False
10691076

1070-
# We perform two DFS starting from an out neighbor of u and avoiding
1071-
# u. The first DFS follows the edges directions, and the second is
1072-
# in the reverse order. If both allow to reach all neighbors of u,
1073-
# then u is not a cut vertex
1074-
neighbors_func = [G.neighbor_out_iterator, G.neighbor_in_iterator]
1075-
start = next(G.neighbor_out_iterator(u))
1077+
cdef list cases = [(G.neighbor_iterator, boundary)]
1078+
if not weak and G.is_directed():
1079+
# Strong connectivity for digraphs.
1080+
# We perform two DFS starting from an out neighbor of cut and avoiding
1081+
# cut. The first DFS follows the edges directions, and the second is
1082+
# in the reverse order. If both allow to reach all neighbors of cut,
1083+
# then it is not a vertex cut.
1084+
# We set data for the reverse order
1085+
in_boundary = set()
1086+
for u in cutset:
1087+
in_boundary.update(G.neighbor_in_iterator(u))
1088+
in_boundary.difference_update(cutset)
1089+
if not in_boundary:
1090+
return False
1091+
cases.append((G.neighbor_in_iterator, list(in_boundary)))
10761092

1077-
CC.discard(u)
1078-
CC.discard(start)
10791093
cdef list queue
10801094
cdef set seen
10811095
cdef set targets
1096+
start = boundary[0]
10821097

1083-
for neighbors in neighbors_func:
1098+
for neighbors, this_boundary in cases:
10841099

1085-
# We perform a DFS starting from a neighbor of u and avoiding u
1100+
# We perform a DFS starting from start and avoiding cut
10861101
queue = [start]
1087-
seen = set(queue)
1088-
targets = CC.intersection(G.neighbor_iterator(u))
1102+
seen = set(cutset)
1103+
seen.add(start)
1104+
targets = set(this_boundary)
10891105
targets.discard(start)
1090-
while queue and targets:
1106+
while queue:
10911107
v = queue.pop()
10921108
for w in neighbors(v):
1093-
if w not in seen and w in CC:
1109+
if w not in seen:
10941110
seen.add(w)
10951111
queue.append(w)
10961112
targets.discard(w)
10971113

1098-
# If some neighbors cannot be reached, u is a cut vertex.
1114+
# If some neighbors cannot be reached, we have a vertex cut
10991115
if targets:
11001116
return True
11011117

11021118
return False
11031119

11041120

1121+
def is_cut_vertex(G, u, weak=False):
1122+
r"""
1123+
Check whether the input vertex is a cut-vertex.
1124+
1125+
A vertex is a cut-vertex if its removal from the (di)graph increases the
1126+
number of (strongly) connected components. Isolated vertices or leaves are
1127+
not cut-vertices. This function works with simple graphs as well as graphs
1128+
with loops and multiple edges.
1129+
1130+
INPUT:
1131+
1132+
- ``G`` -- a Sage (Di)Graph
1133+
1134+
- ``u`` -- a vertex
1135+
1136+
- ``weak`` -- boolean (default: ``False``); whether the connectivity of
1137+
directed graphs is to be taken in the weak sense, that is ignoring edges
1138+
orientations
1139+
1140+
OUTPUT:
1141+
1142+
Return ``True`` if ``u`` is a cut-vertex, and ``False`` otherwise.
1143+
1144+
EXAMPLES:
1145+
1146+
Giving a LollipopGraph(4,2), that is a complete graph with 4 vertices with a
1147+
pending edge::
1148+
1149+
sage: from sage.graphs.connectivity import is_cut_vertex
1150+
sage: G = graphs.LollipopGraph(4, 2)
1151+
sage: is_cut_vertex(G, 0)
1152+
False
1153+
sage: is_cut_vertex(G, 3)
1154+
True
1155+
sage: G.is_cut_vertex(3)
1156+
True
1157+
1158+
Comparing the weak and strong connectivity of a digraph::
1159+
1160+
sage: D = digraphs.Circuit(6)
1161+
sage: D.is_strongly_connected()
1162+
True
1163+
sage: is_cut_vertex(D, 2)
1164+
True
1165+
sage: is_cut_vertex(D, 2, weak=True)
1166+
False
1167+
1168+
Giving a vertex that is not in the graph::
1169+
1170+
sage: G = graphs.CompleteGraph(4)
1171+
sage: is_cut_vertex(G, 7)
1172+
Traceback (most recent call last):
1173+
...
1174+
ValueError: vertex (7) is not a vertex of the graph
1175+
1176+
TESTS:
1177+
1178+
If ``G`` is not a Sage graph, an error is raised::
1179+
1180+
sage: is_cut_vertex('I am not a graph', 0)
1181+
Traceback (most recent call last):
1182+
...
1183+
TypeError: the input must be a Sage graph
1184+
"""
1185+
return is_vertex_cut(G, [u], weak=weak)
1186+
1187+
11051188
def edge_connectivity(G,
11061189
value_only=True,
11071190
implementation=None,

src/sage/graphs/generic_graph.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,8 @@
245245
:meth:`~GenericGraph.blocks_and_cuts_tree` | Compute the blocks-and-cuts tree of the graph.
246246
:meth:`~GenericGraph.is_cut_edge` | Check whether the input edge is a cut-edge or a bridge.
247247
:meth:`~GenericGraph.`is_edge_cut` | Check whether the input edges form an edge cut.
248-
:meth:`~GenericGraph.is_cut_vertex` | Return ``True`` if the input vertex is a cut-vertex.
248+
:meth:`~GenericGraph.is_cut_vertex` | Check whether the input vertex is a cut-vertex.
249+
:meth:`~GenericGraph.is_vertex_cut` | Check whether the input vertices form a vertex cut.
249250
:meth:`~GenericGraph.edge_cut` | Return a minimum edge cut between vertices `s` and `t`
250251
:meth:`~GenericGraph.vertex_cut` | Return a minimum vertex cut between non-adjacent vertices `s` and `t`
251252
:meth:`~GenericGraph.flow` | Return a maximum flow in the graph from ``x`` to ``y``
@@ -25003,6 +25004,7 @@ def is_self_complementary(self):
2500325004
from sage.graphs.connectivity import is_cut_edge
2500425005
from sage.graphs.connectivity import is_edge_cut
2500525006
from sage.graphs.connectivity import is_cut_vertex
25007+
from sage.graphs.connectivity import is_vertex_cut
2500625008
from sage.graphs.connectivity import edge_connectivity
2500725009
from sage.graphs.connectivity import vertex_connectivity
2500825010
from sage.graphs.distances_all_pairs import szeged_index

0 commit comments

Comments
 (0)