From 6931377a8b95648cb2c3c605ce268152c8cb1214 Mon Sep 17 00:00:00 2001 From: Yiming Zhang <61700160+inmzhang@users.noreply.github.com> Date: Sat, 20 Jan 2024 08:17:48 +0800 Subject: [PATCH] Add function to return the bridges of the graph (#1058) * rename release note * fix inconsistent func signature * single pass of topo sort to check cycles * add interface of `bridges` * impl bridges and add tests * Use internal function to avoid breaking change * add bridges to documentation * Resolve indentation error * Run cargo fmt --------- Co-authored-by: Ivan Carvalho <8753214+IvanIsCoding@users.noreply.github.com> --- .../connectivity_and_cycles.rst | 1 + ...-articulation-points-18b24dea2e909082.yaml | 31 +++ .../src/connectivity/biconnected.rs | 207 +++++++++++++----- rustworkx-core/src/connectivity/mod.rs | 1 + rustworkx/connectivity.pyi | 1 + src/connectivity/mod.rs | 26 +++ src/lib.rs | 1 + .../rustworkx_tests/graph/test_biconnected.py | 9 + 8 files changed, 226 insertions(+), 51 deletions(-) create mode 100644 releasenotes/notes/add-bridges-and-upgrade-articulation-points-18b24dea2e909082.yaml diff --git a/docs/source/api/algorithm_functions/connectivity_and_cycles.rst b/docs/source/api/algorithm_functions/connectivity_and_cycles.rst index b4f08f5ad0..4978bcaf15 100644 --- a/docs/source/api/algorithm_functions/connectivity_and_cycles.rst +++ b/docs/source/api/algorithm_functions/connectivity_and_cycles.rst @@ -18,6 +18,7 @@ Connectivity and Cycles rustworkx.simple_cycles rustworkx.digraph_find_cycle rustworkx.articulation_points + rustworkx.bridges rustworkx.biconnected_components rustworkx.chain_decomposition rustworkx.all_simple_paths diff --git a/releasenotes/notes/add-bridges-and-upgrade-articulation-points-18b24dea2e909082.yaml b/releasenotes/notes/add-bridges-and-upgrade-articulation-points-18b24dea2e909082.yaml new file mode 100644 index 0000000000..f51c076873 --- /dev/null +++ b/releasenotes/notes/add-bridges-and-upgrade-articulation-points-18b24dea2e909082.yaml @@ -0,0 +1,31 @@ +--- +features: + - | + Added a new function, :func:`~rustworkx.bridges` that finds the bridges of + an undirected :class:`~rustworkx.PyGraph`. + Bridges are edges that, if removed, would increase the number of connected + components of a graph. For example: + + .. jupyter-execute:: + + import rustworkx + from rustworkx.visualization import mpl_draw + + graph = rustworkx.PyGraph() + graph.extend_from_edge_list([ + (0, 1), (1, 2), (0, 2), (1, 3) + ]) + bridges = rustworkx.bridges(graph) + bridges_set = [set(edge) for edge in bridges] + + colors = [] + for edge in graph.edge_list(): + color = "red" if set(edge) in bridges_set else "black" + colors.append(color) + mpl_draw(graph, edge_color=colors) + - | + Added a new function ``bridges`` to the ``rustworkx_core:connectivity:biconnected`` + module that finds the bridges of an undirected graph. + Bridges are edges that, if removed, would increase the number of connected + components of a graph. For example: + diff --git a/rustworkx-core/src/connectivity/biconnected.rs b/rustworkx-core/src/connectivity/biconnected.rs index e74c3bd637..147eaa54e7 100644 --- a/rustworkx-core/src/connectivity/biconnected.rs +++ b/rustworkx-core/src/connectivity/biconnected.rs @@ -32,53 +32,10 @@ fn is_root(parent: &[usize], u: usize) -> bool { parent[u] == NULL } -/// Return the articulation points of an undirected graph. -/// -/// An articulation point or cut vertex is any node whose removal (along with -/// all its incident edges) increases the number of connected components of -/// a graph. An undirected connected graph without articulation points is -/// biconnected. -/// -/// At the same time, you can record the biconnected components in `components`. -/// -/// Biconnected components are maximal subgraphs such that the removal -/// of a node (and all edges incident on that node) will not disconnect -/// the subgraph. Note that nodes may be part of more than one biconnected -/// component. Those nodes are articulation points, or cut vertices. The -/// algorithm computes how many biconnected components are in the graph, -/// and assigning each component an integer label. -/// -/// # Note -/// The function implicitly assumes that there are no parallel edges -/// or self loops. It may produce incorrect/unexpected results if the -/// input graph has self loops or parallel edges. -/// -/// -/// # Example: -/// ```rust -/// use std::iter::FromIterator; -/// use hashbrown::{HashMap, HashSet}; -/// -/// use rustworkx_core::connectivity::articulation_points; -/// use rustworkx_core::petgraph::graph::UnGraph; -/// use rustworkx_core::petgraph::graph::node_index as nx; -/// -/// let graph = UnGraph::<(), ()>::from_edges(&[ -/// (0, 1), (0, 2), (1, 2), (1, 3), -/// ]); -/// -/// let mut bicomp = HashMap::new(); -/// let a_points = articulation_points(&graph, Some(&mut bicomp)); -/// -/// assert_eq!(a_points, HashSet::from_iter([nx(1)])); -/// assert_eq!(bicomp, HashMap::from_iter([ -/// ((nx(0), nx(2)), 1), ((nx(2), nx(1)), 1), ((nx(1), nx(0)), 1), -/// ((nx(1), nx(3)), 0) -/// ])); -/// ``` -pub fn articulation_points( +fn _articulation_points( graph: G, components: Option<&mut HashMap, usize>>, + bridges: Option<&mut HashSet>>, ) -> HashSet where G: GraphProp @@ -99,11 +56,18 @@ where let mut points = HashSet::new(); let mut edge_stack = Vec::new(); - let mut tmp_components = if components.is_some() { + let need_components = components.is_some(); + let mut tmp_components = if need_components { HashMap::with_capacity(graph.edge_count()) } else { HashMap::new() }; + let need_bridges = bridges.is_some(); + let mut tmp_bridges = if need_bridges { + HashSet::with_capacity(graph.edge_count()) + } else { + HashSet::new() + }; let mut num_components: usize = 0; depth_first_search(graph, graph.node_identifiers(), |event| match event { @@ -119,7 +83,7 @@ where if is_root(&parent, u) { root_children += 1; } - if components.is_some() { + if need_components { edge_stack.push((u_id, v_id)); } } @@ -130,7 +94,7 @@ where // do *not* consider ``(u, v)`` as a back edge if ``(v, u)`` is a tree edge. if v != parent[u] { low[u] = low[u].min(disc[v]); - if components.is_some() { + if need_components { edge_stack.push((u_id, v_id)); } } @@ -152,7 +116,7 @@ where points.insert(pu_id); // now find a biconnected component that the // current articulation point belongs. - if components.is_some() { + if need_components { if let Some(at) = edge_stack.iter().rposition(|&x| x == (pu_id, u_id)) { tmp_components.extend( edge_stack[at..].iter().map(|edge| (*edge, num_components)), @@ -161,9 +125,12 @@ where num_components += 1; } } + if need_bridges && low[u] != disc[pu] { + tmp_bridges.insert((pu_id, u_id)); + } } - if is_root(&parent, pu) && components.is_some() { + if is_root(&parent, pu) && need_components { if let Some(at) = edge_stack.iter().position(|&x| x == (pu_id, u_id)) { tmp_components .extend(edge_stack[at..].iter().map(|edge| (*edge, num_components))); @@ -179,13 +146,119 @@ where if let Some(x) = components { *x = tmp_components; } + if let Some(x) = bridges { + *x = tmp_bridges; + } points } +/// Return the articulation points of an undirected graph. +/// +/// An articulation point or cut vertex is any node whose removal (along with +/// all its incident edges) increases the number of connected components of +/// a graph. An undirected connected graph without articulation points is +/// biconnected. +/// +/// At the same time, you can record the biconnected components in `components`. +/// +/// Biconnected components are maximal subgraphs such that the removal +/// of a node (and all edges incident on that node) will not disconnect +/// the subgraph. Note that nodes may be part of more than one biconnected +/// component. Those nodes are articulation points, or cut vertices. The +/// algorithm computes how many biconnected components are in the graph, +/// and assigning each component an integer label. +/// +/// # Note +/// The function implicitly assumes that there are no parallel edges +/// or self loops. It may produce incorrect/unexpected results if the +/// input graph has self loops or parallel edges. +/// +/// +/// # Example: +/// ```rust +/// use std::iter::FromIterator; +/// use hashbrown::{HashMap, HashSet}; +/// +/// use rustworkx_core::connectivity::articulation_points; +/// use rustworkx_core::petgraph::graph::UnGraph; +/// use rustworkx_core::petgraph::graph::node_index as nx; +/// +/// let graph = UnGraph::<(), ()>::from_edges(&[ +/// (0, 1), (0, 2), (1, 2), (1, 3), +/// ]); +/// +/// let mut bicomp = HashMap::new(); +/// let a_points = articulation_points(&graph, Some(&mut bicomp)); +/// +/// assert_eq!(a_points, HashSet::from_iter([nx(1)])); +/// assert_eq!(bicomp, HashMap::from_iter([ +/// ((nx(0), nx(2)), 1), ((nx(2), nx(1)), 1), ((nx(1), nx(0)), 1), +/// ((nx(1), nx(3)), 0) +/// ])); +/// ``` +pub fn articulation_points( + graph: G, + components: Option<&mut HashMap, usize>>, +) -> HashSet +where + G: GraphProp + + EdgeCount + + IntoEdges + + Visitable + + NodeIndexable + + IntoNodeIdentifiers, + G::NodeId: Eq + Hash, +{ + _articulation_points(graph, components, None) +} + +/// Return the bridges of an undirected graph. +/// +/// Bridges are edges that, if removed, would increase the number of +/// connected components of a graph. +/// +/// # Note +/// The function implicitly assumes that there are no parallel edges +/// or self loops. It may produce incorrect/unexpected results if the +/// input graph has self loops or parallel edges. +/// +/// +/// # Example: +/// ```rust +/// use std::iter::FromIterator; +/// use hashbrown::{HashMap, HashSet}; +/// +/// use rustworkx_core::connectivity::bridges; +/// use rustworkx_core::petgraph::graph::UnGraph; +/// use rustworkx_core::petgraph::graph::node_index as nx; +/// +/// let graph = UnGraph::<(), ()>::from_edges(&[ +/// (0, 1), (0, 2), (1, 2), (1, 3), +/// ]); +/// +/// let bridges = bridges(&graph); +/// +/// assert_eq!(bridges, HashSet::from_iter([(nx(1), nx(3))])); +/// ``` +pub fn bridges(graph: G) -> HashSet> +where + G: GraphProp + + EdgeCount + + IntoEdges + + Visitable + + NodeIndexable + + IntoNodeIdentifiers, + G::NodeId: Eq + Hash, +{ + let mut bridges = HashSet::new(); + _articulation_points(graph, None, Some(&mut bridges)); + bridges +} + #[cfg(test)] mod tests { - use crate::connectivity::articulation_points; + use crate::connectivity::{articulation_points, bridges}; use hashbrown::{HashMap, HashSet}; use petgraph::graph::node_index as nx; use petgraph::prelude::*; @@ -210,6 +283,36 @@ mod tests { assert_eq!(a_points, HashSet::from_iter([nx(1)])); } + #[test] + fn test_single_bridge() { + let graph = UnGraph::<(), ()>::from_edges([ + (1, 2), + (2, 3), + (3, 4), + (3, 5), + (5, 6), + (6, 7), + (7, 8), + (5, 9), + (9, 10), + // Nontree edges. + (1, 3), + (1, 4), + (2, 5), + (5, 10), + (6, 8), + ]); + + assert_eq!(bridges(&graph), HashSet::from_iter([(nx(5), nx(6))])); + } + + #[test] + // generate test cases for bridges + fn test_bridges_cycle() { + let graph = UnGraph::<(), ()>::from_edges([(0, 1), (1, 2), (2, 0), (1, 3), (3, 4), (4, 1)]); + assert_eq!(bridges(&graph), HashSet::from_iter([])); + } + #[test] fn test_biconnected_components_cycle() { // create a cycle graph @@ -360,8 +463,10 @@ mod tests { let mut components = HashMap::new(); let a_points = articulation_points(&graph, Some(&mut components)); + let bridges = bridges(&graph); assert_eq!(a_points, HashSet::new()); + assert_eq!(bridges, HashSet::new()); assert_eq!(components, HashMap::new()); } } diff --git a/rustworkx-core/src/connectivity/mod.rs b/rustworkx-core/src/connectivity/mod.rs index bc58513242..fa236d8b61 100644 --- a/rustworkx-core/src/connectivity/mod.rs +++ b/rustworkx-core/src/connectivity/mod.rs @@ -26,6 +26,7 @@ pub use all_simple_paths::{ all_simple_paths_multiple_targets, longest_simple_path_multiple_targets, }; pub use biconnected::articulation_points; +pub use biconnected::bridges; pub use chain::chain_decomposition; pub use conn_components::bfs_undirected; pub use conn_components::connected_components; diff --git a/rustworkx/connectivity.pyi b/rustworkx/connectivity.pyi index a49b075fb1..3e690360b6 100644 --- a/rustworkx/connectivity.pyi +++ b/rustworkx/connectivity.pyi @@ -46,6 +46,7 @@ def graph_adjacency_matrix( ) -> np.ndarray: ... def cycle_basis(graph: PyGraph, /, root: int | None = ...) -> list[list[int]]: ... def articulation_points(graph: PyGraph, /) -> set[int]: ... +def bridges(graph: PyGraph, /) -> set[tuple[int]]: ... def biconnected_components(graph: PyGraph, /) -> BiconnectedComponents: ... def chain_decomposition(graph: PyGraph, /, source: int | None = ...) -> Chains: ... def digraph_find_cycle( diff --git a/src/connectivity/mod.rs b/src/connectivity/mod.rs index 77c5c4ad5b..7f970b080d 100644 --- a/src/connectivity/mod.rs +++ b/src/connectivity/mod.rs @@ -921,6 +921,32 @@ pub fn articulation_points(graph: &graph::PyGraph) -> HashSet { .collect() } +/// Return the bridges of an undirected graph. +/// +/// A bridge is any edge whose removal increases the number of connected +/// components of a graph. +/// +/// .. note:: +/// +/// The function implicitly assumes that there are no parallel edges +/// or self loops. It may produce incorrect/unexpected results if the +/// input graph has self loops or parallel edges. +/// +/// :param PyGraph: The undirected graph to be used. +/// +/// :returns: A set with edges of the bridges in the graph, each edge is +/// represented by a pair of node index. +/// :rtype: set +#[pyfunction] +#[pyo3(text_signature = "(graph, /)")] +pub fn bridges(graph: &graph::PyGraph) -> HashSet<(usize, usize)> { + let bridges = connectivity::bridges(&graph.graph); + bridges + .into_iter() + .map(|(a, b)| (a.index(), b.index())) + .collect() +} + /// Return the biconnected components of an undirected graph. /// /// Biconnected components are maximal subgraphs such that the removal diff --git a/src/lib.rs b/src/lib.rs index bdd4bbd0fe..cb5c60502e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -512,6 +512,7 @@ fn rustworkx(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_wrapped(wrap_pyfunction!(digraph_dfs_search))?; m.add_wrapped(wrap_pyfunction!(graph_dfs_search))?; m.add_wrapped(wrap_pyfunction!(articulation_points))?; + m.add_wrapped(wrap_pyfunction!(bridges))?; m.add_wrapped(wrap_pyfunction!(biconnected_components))?; m.add_wrapped(wrap_pyfunction!(chain_decomposition))?; m.add_wrapped(wrap_pyfunction!(graph_isolates))?; diff --git a/tests/rustworkx_tests/graph/test_biconnected.py b/tests/rustworkx_tests/graph/test_biconnected.py index e455df5a9b..a02250a495 100644 --- a/tests/rustworkx_tests/graph/test_biconnected.py +++ b/tests/rustworkx_tests/graph/test_biconnected.py @@ -15,6 +15,10 @@ import rustworkx +def sorted_edges(edges): + return set([tuple(sorted(edge)) for edge in edges]) + + class TestBiconnected(unittest.TestCase): def setUp(self): super().setUp() @@ -56,6 +60,7 @@ def setUp(self): def test_null_graph(self): graph = rustworkx.PyGraph() self.assertEqual(rustworkx.articulation_points(graph), set()) + self.assertEqual(rustworkx.bridges(graph), set()) self.assertEqual(rustworkx.biconnected_components(graph), {}) def test_graph(self): @@ -77,6 +82,7 @@ def test_graph(self): } self.assertEqual(rustworkx.biconnected_components(self.graph), components) self.assertEqual(rustworkx.articulation_points(self.graph), {4, 5}) + self.assertEqual(sorted_edges(rustworkx.bridges(self.graph)), {(4, 5)}) def test_barbell_graph(self): components = { @@ -90,6 +96,7 @@ def test_barbell_graph(self): } self.assertEqual(rustworkx.biconnected_components(self.barbell_graph), components) self.assertEqual(rustworkx.articulation_points(self.barbell_graph), {2, 3}) + self.assertEqual(sorted_edges(rustworkx.bridges(self.barbell_graph)), {(2, 3)}) def test_disconnected_graph(self): graph = rustworkx.union(self.barbell_graph, self.barbell_graph) @@ -113,6 +120,7 @@ def test_disconnected_graph(self): } self.assertEqual(rustworkx.biconnected_components(graph), components) self.assertEqual(rustworkx.articulation_points(graph), {2, 3, 8, 9}) + self.assertEqual(sorted_edges(rustworkx.bridges(graph)), {(2, 3), (8, 9)}) def test_biconnected_graph(self): graph = rustworkx.PyGraph() @@ -133,6 +141,7 @@ def test_biconnected_graph(self): ) num_edges = graph.num_edges() self.assertEqual(rustworkx.articulation_points(graph), set()) + self.assertEqual(rustworkx.bridges(graph), set()) bicomp = rustworkx.biconnected_components(graph) self.assertEqual(len(bicomp), num_edges) self.assertEqual(list(bicomp.values()), [0] * num_edges)