Skip to content

Commit

Permalink
Add function to return the bridges of the graph (#1058)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
inmzhang and IvanIsCoding authored Jan 20, 2024
1 parent e8e380b commit 6931377
Show file tree
Hide file tree
Showing 8 changed files with 226 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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:
207 changes: 156 additions & 51 deletions rustworkx-core/src/connectivity/biconnected.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<G>(
fn _articulation_points<G>(
graph: G,
components: Option<&mut HashMap<Edge<G>, usize>>,
bridges: Option<&mut HashSet<Edge<G>>>,
) -> HashSet<G::NodeId>
where
G: GraphProp<EdgeType = Undirected>
Expand All @@ -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 {
Expand All @@ -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));
}
}
Expand All @@ -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));
}
}
Expand All @@ -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)),
Expand All @@ -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)));
Expand All @@ -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<G>(
graph: G,
components: Option<&mut HashMap<Edge<G>, usize>>,
) -> HashSet<G::NodeId>
where
G: GraphProp<EdgeType = Undirected>
+ 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<G>(graph: G) -> HashSet<Edge<G>>
where
G: GraphProp<EdgeType = Undirected>
+ 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::*;
Expand All @@ -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
Expand Down Expand Up @@ -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());
}
}
1 change: 1 addition & 0 deletions rustworkx-core/src/connectivity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions rustworkx/connectivity.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
26 changes: 26 additions & 0 deletions src/connectivity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,32 @@ pub fn articulation_points(graph: &graph::PyGraph) -> HashSet<usize> {
.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
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))?;
Expand Down
Loading

0 comments on commit 6931377

Please sign in to comment.