use crate::error::{MinCutError, Result};
use crate::graph::{DynamicGraph, EdgeId, VertexId, Weight};
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::Arc;
pub type Conductance = f64;
#[derive(Debug, Clone)]
pub struct ExpanderComponent {
pub id: usize,
pub vertices: HashSet<VertexId>,
pub conductance: Conductance,
pub level: usize,
pub boundary_edges: Vec<EdgeId>,
pub volume: f64,
}
impl ExpanderComponent {
fn new(id: usize, vertices: HashSet<VertexId>, level: usize) -> Self {
Self {
id,
vertices,
conductance: 0.0,
level,
boundary_edges: Vec::new(),
volume: 0.0,
}
}
pub fn contains(&self, v: VertexId) -> bool {
self.vertices.contains(&v)
}
pub fn size(&self) -> usize {
self.vertices.len()
}
}
pub struct ExpanderDecomposition {
levels: Vec<Vec<ExpanderComponent>>,
vertex_to_component: Vec<HashMap<VertexId, usize>>,
phi: Conductance,
graph: Arc<DynamicGraph>,
next_component_id: usize,
}
impl ExpanderDecomposition {
pub fn build(graph: Arc<DynamicGraph>, phi: Conductance) -> Result<Self> {
if phi <= 0.0 || phi >= 1.0 {
return Err(MinCutError::InvalidParameter(format!(
"Conductance phi must be in (0, 1), got {}",
phi
)));
}
let mut decomp = Self {
levels: Vec::new(),
vertex_to_component: Vec::new(),
phi,
graph: graph.clone(),
next_component_id: 0,
};
decomp.build_hierarchy()?;
Ok(decomp)
}
pub fn component_at_level(&self, v: VertexId, level: usize) -> Option<&ExpanderComponent> {
if level >= self.levels.len() {
return None;
}
let component_id = self.vertex_to_component[level].get(&v)?;
self.levels[level].iter().find(|c| c.id == *component_id)
}
pub fn insert_edge(&mut self, u: VertexId, v: VertexId) -> Result<()> {
for level in 0..self.levels.len() {
if let (Some(u_comp_id), Some(v_comp_id)) = (
self.vertex_to_component[level].get(&u),
self.vertex_to_component[level].get(&v),
) {
if u_comp_id != v_comp_id {
if let Some(edge) = self.graph.get_edge(u, v) {
for comp in &mut self.levels[level] {
if comp.id == *u_comp_id || comp.id == *v_comp_id {
comp.boundary_edges.push(edge.id);
}
}
}
} else {
let comp_id = *u_comp_id;
if let Some(comp) = self.levels[level].iter().find(|c| c.id == comp_id) {
let vertices = comp.vertices.clone();
let conductance = self.compute_conductance(&vertices);
let volume = self.compute_volume(&vertices);
if let Some(comp) = self.levels[level].iter_mut().find(|c| c.id == comp_id)
{
comp.conductance = conductance;
comp.volume = volume;
}
}
}
}
}
Ok(())
}
pub fn delete_edge(&mut self, u: VertexId, _v: VertexId) -> Result<()> {
for level in 0..self.levels.len() {
if let Some(u_comp_id) = self.vertex_to_component[level].get(&u) {
let comp_id = *u_comp_id;
if let Some(comp) = self.levels[level].iter().find(|c| c.id == comp_id) {
let vertices = comp.vertices.clone();
let is_connected = self.is_connected(&vertices);
if !is_connected {
let _components = self.find_connected_components(&vertices);
}
let conductance = self.compute_conductance(&vertices);
let volume = self.compute_volume(&vertices);
if let Some(comp) = self.levels[level].iter_mut().find(|c| c.id == comp_id) {
comp.conductance = conductance;
comp.volume = volume;
}
}
}
}
Ok(())
}
pub fn num_levels(&self) -> usize {
self.levels.len()
}
fn build_hierarchy(&mut self) -> Result<()> {
let all_vertices: HashSet<VertexId> = self.graph.vertices().into_iter().collect();
if all_vertices.is_empty() {
return Ok(());
}
let components = deterministic_decompose(&self.graph, &all_vertices, self.phi);
let mut level_components = Vec::new();
let mut vertex_map = HashMap::new();
for vertices in components {
let comp_id = self.next_component_id;
self.next_component_id += 1;
let mut component = ExpanderComponent::new(comp_id, vertices.clone(), 0);
component.conductance = self.compute_conductance(&vertices);
component.volume = self.compute_volume(&vertices);
for &v in &vertices {
vertex_map.insert(v, comp_id);
}
level_components.push(component);
}
self.levels.push(level_components);
self.vertex_to_component.push(vertex_map);
Ok(())
}
fn compute_conductance(&self, vertices: &HashSet<VertexId>) -> Conductance {
if vertices.is_empty() {
return 0.0;
}
let all_vertices: HashSet<VertexId> = self.graph.vertices().into_iter().collect();
let complement: HashSet<VertexId> = all_vertices.difference(vertices).copied().collect();
if complement.is_empty() {
return 0.0;
}
let mut cut_value = 0.0;
for &u in vertices {
for (v, _) in self.graph.neighbors(u) {
if !vertices.contains(&v) {
if let Some(weight) = self.graph.edge_weight(u, v) {
cut_value += weight;
}
}
}
}
let vol_s = self.compute_volume(vertices);
let vol_complement = self.compute_volume(&complement);
if vol_s == 0.0 || vol_complement == 0.0 {
return 0.0;
}
let min_vol = vol_s.min(vol_complement);
cut_value / min_vol
}
fn compute_volume(&self, vertices: &HashSet<VertexId>) -> f64 {
vertices.iter().map(|&v| self.graph.degree(v) as f64).sum()
}
fn prune(&self, component: &ExpanderComponent) -> Option<HashSet<VertexId>> {
for &start in &component.vertices {
if let Some(cut) = self.local_cut_search(start, self.phi, &component.vertices) {
let conductance = self.compute_conductance(&cut);
if conductance < self.phi {
return Some(cut);
}
}
}
None
}
fn local_cut_search(
&self,
start: VertexId,
target_conductance: Conductance,
vertices: &HashSet<VertexId>,
) -> Option<HashSet<VertexId>> {
let mut current_set = HashSet::new();
current_set.insert(start);
let mut visited = HashSet::new();
visited.insert(start);
let mut queue = VecDeque::new();
queue.push_back(start);
let mut best_set = current_set.clone();
let mut best_conductance = self.compute_conductance(¤t_set);
while let Some(u) = queue.pop_front() {
for (v, _) in self.graph.neighbors(u) {
if !vertices.contains(&v) || visited.contains(&v) {
continue;
}
visited.insert(v);
current_set.insert(v);
let conductance = self.compute_conductance(¤t_set);
if conductance < best_conductance {
best_conductance = conductance;
best_set = current_set.clone();
}
if conductance < target_conductance {
return Some(current_set);
}
queue.push_back(v);
if current_set.len() >= vertices.len() / 2 {
break;
}
}
}
if best_conductance < self.phi {
Some(best_set)
} else {
None
}
}
fn is_connected(&self, vertices: &HashSet<VertexId>) -> bool {
if vertices.is_empty() {
return true;
}
let start = *vertices.iter().next().unwrap();
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
visited.insert(start);
queue.push_back(start);
while let Some(u) = queue.pop_front() {
for (v, _) in self.graph.neighbors(u) {
if vertices.contains(&v) && visited.insert(v) {
queue.push_back(v);
}
}
}
visited.len() == vertices.len()
}
fn find_connected_components(&self, vertices: &HashSet<VertexId>) -> Vec<HashSet<VertexId>> {
let mut visited = HashSet::new();
let mut components = Vec::new();
for &start in vertices {
if visited.contains(&start) {
continue;
}
let mut component = HashSet::new();
let mut queue = VecDeque::new();
component.insert(start);
visited.insert(start);
queue.push_back(start);
while let Some(u) = queue.pop_front() {
for (v, _) in self.graph.neighbors(u) {
if vertices.contains(&v) && visited.insert(v) {
component.insert(v);
queue.push_back(v);
}
}
}
components.push(component);
}
components
}
}
pub fn estimate_conductance(graph: &DynamicGraph, vertices: &HashSet<VertexId>) -> Conductance {
if vertices.is_empty() {
return 0.0;
}
let all_vertices: HashSet<VertexId> = graph.vertices().into_iter().collect();
let complement: HashSet<VertexId> = all_vertices.difference(vertices).copied().collect();
if complement.is_empty() {
return 0.0;
}
let mut cut_value = 0.0;
for &u in vertices {
for (v, _) in graph.neighbors(u) {
if !vertices.contains(&v) {
if let Some(weight) = graph.edge_weight(u, v) {
cut_value += weight;
}
}
}
}
let vol_s: f64 = vertices.iter().map(|&v| graph.degree(v) as f64).sum();
let vol_complement: f64 = complement.iter().map(|&v| graph.degree(v) as f64).sum();
if vol_s == 0.0 || vol_complement == 0.0 {
return 0.0;
}
let min_vol = vol_s.min(vol_complement);
cut_value / min_vol
}
pub fn deterministic_decompose(
graph: &DynamicGraph,
vertices: &HashSet<VertexId>,
phi: Conductance,
) -> Vec<HashSet<VertexId>> {
if vertices.is_empty() {
return Vec::new();
}
if vertices.len() == 1 {
return vec![vertices.clone()];
}
if let Some(cut) = find_low_conductance_cut(graph, vertices, phi) {
if !cut.is_empty() && cut.len() < vertices.len() {
let complement: HashSet<VertexId> = vertices.difference(&cut).copied().collect();
let mut result = deterministic_decompose(graph, &cut, phi);
result.extend(deterministic_decompose(graph, &complement, phi));
return result;
}
}
vec![vertices.clone()]
}
fn find_low_conductance_cut(
graph: &DynamicGraph,
vertices: &HashSet<VertexId>,
phi: Conductance,
) -> Option<HashSet<VertexId>> {
for &start in vertices {
if let Some(cut) = bfs_local_search(graph, start, vertices, phi) {
let conductance = estimate_conductance(graph, &cut);
if conductance < phi {
return Some(cut);
}
}
}
None
}
fn bfs_local_search(
graph: &DynamicGraph,
start: VertexId,
vertices: &HashSet<VertexId>,
target_phi: Conductance,
) -> Option<HashSet<VertexId>> {
let mut current_set = HashSet::new();
current_set.insert(start);
let mut visited = HashSet::new();
visited.insert(start);
let mut queue = VecDeque::new();
queue.push_back(start);
let mut best_set = current_set.clone();
let mut best_conductance = estimate_conductance(graph, ¤t_set);
while let Some(u) = queue.pop_front() {
for (v, _) in graph.neighbors(u) {
if !vertices.contains(&v) || visited.contains(&v) {
continue;
}
visited.insert(v);
current_set.insert(v);
let conductance = estimate_conductance(graph, ¤t_set);
if conductance < best_conductance {
best_conductance = conductance;
best_set = current_set.clone();
}
if conductance < target_phi {
return Some(current_set);
}
queue.push_back(v);
if current_set.len() >= vertices.len() / 2 {
break;
}
}
}
if best_conductance < target_phi {
Some(best_set)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_graph() -> Arc<DynamicGraph> {
let graph = Arc::new(DynamicGraph::new());
graph.insert_edge(1, 2, 1.0).unwrap();
graph.insert_edge(2, 3, 1.0).unwrap();
graph.insert_edge(3, 1, 1.0).unwrap();
graph
}
fn create_expander_graph() -> Arc<DynamicGraph> {
let graph = Arc::new(DynamicGraph::new());
for i in 1..=5 {
for j in (i + 1)..=5 {
graph.insert_edge(i, j, 1.0).unwrap();
}
}
graph
}
fn create_separable_graph() -> Arc<DynamicGraph> {
let graph = Arc::new(DynamicGraph::new());
graph.insert_edge(1, 2, 1.0).unwrap();
graph.insert_edge(2, 3, 1.0).unwrap();
graph.insert_edge(3, 1, 1.0).unwrap();
graph.insert_edge(3, 4, 1.0).unwrap();
graph.insert_edge(4, 5, 1.0).unwrap();
graph.insert_edge(5, 6, 1.0).unwrap();
graph.insert_edge(6, 4, 1.0).unwrap();
graph
}
#[test]
fn test_build_simple() {
let graph = create_test_graph();
let phi = 0.1;
let decomp = ExpanderDecomposition::build(graph, phi).unwrap();
assert!(decomp.num_levels() > 0);
}
#[test]
fn test_build_invalid_phi() {
let graph = create_test_graph();
assert!(ExpanderDecomposition::build(graph.clone(), 0.0).is_err());
assert!(ExpanderDecomposition::build(graph.clone(), 1.0).is_err());
assert!(ExpanderDecomposition::build(graph.clone(), 1.5).is_err());
}
#[test]
fn test_compute_conductance_triangle() {
let graph = create_test_graph();
let phi = 0.1;
let decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
let mut single_vertex = HashSet::new();
single_vertex.insert(1);
let conductance = decomp.compute_conductance(&single_vertex);
assert_eq!(conductance, 1.0);
}
#[test]
fn test_compute_conductance_complete() {
let graph = create_expander_graph();
let phi = 0.1;
let decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
let mut single_vertex = HashSet::new();
single_vertex.insert(1);
let conductance = decomp.compute_conductance(&single_vertex);
assert_eq!(conductance, 1.0);
}
#[test]
fn test_compute_volume() {
let graph = create_test_graph();
let phi = 0.1;
let decomp = ExpanderDecomposition::build(graph, phi).unwrap();
let mut vertices = HashSet::new();
vertices.insert(1);
vertices.insert(2);
let volume = decomp.compute_volume(&vertices);
assert_eq!(volume, 4.0);
}
#[test]
fn test_estimate_conductance() {
let graph = create_test_graph();
let mut vertices = HashSet::new();
vertices.insert(1);
let conductance = estimate_conductance(&graph, &vertices);
assert_eq!(conductance, 1.0);
}
#[test]
fn test_deterministic_decompose_triangle() {
let graph = create_test_graph();
let vertices: HashSet<VertexId> = graph.vertices().into_iter().collect();
let phi = 0.5;
let components = deterministic_decompose(&graph, &vertices, phi);
assert!(components.len() >= 1);
assert_eq!(components.iter().map(|c| c.len()).sum::<usize>(), 3);
}
#[test]
fn test_deterministic_decompose_separable() {
let graph = create_separable_graph();
let vertices: HashSet<VertexId> = graph.vertices().into_iter().collect();
let phi = 0.3;
let components = deterministic_decompose(&graph, &vertices, phi);
assert!(components.len() >= 1);
}
#[test]
fn test_component_at_level() {
let graph = create_test_graph();
let phi = 0.1;
let decomp = ExpanderDecomposition::build(graph, phi).unwrap();
if decomp.num_levels() > 0 {
let comp = decomp.component_at_level(1, 0);
assert!(comp.is_some());
if let Some(c) = comp {
assert!(c.contains(1));
}
}
}
#[test]
fn test_insert_edge() {
let graph = create_test_graph();
let phi = 0.1;
let mut decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
graph.insert_edge(1, 4, 1.0).unwrap();
let result = decomp.insert_edge(1, 4);
assert!(result.is_ok());
}
#[test]
fn test_delete_edge() {
let graph = create_test_graph();
let phi = 0.1;
let mut decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
graph.delete_edge(1, 2).unwrap();
let result = decomp.delete_edge(1, 2);
assert!(result.is_ok());
}
#[test]
fn test_is_connected() {
let graph = create_test_graph();
let phi = 0.1;
let decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
let vertices: HashSet<VertexId> = vec![1, 2, 3].into_iter().collect();
assert!(decomp.is_connected(&vertices));
let disconnected: HashSet<VertexId> = vec![1, 2].into_iter().collect();
assert!(decomp.is_connected(&disconnected));
}
#[test]
fn test_find_connected_components() {
let graph = Arc::new(DynamicGraph::new());
graph.insert_edge(1, 2, 1.0).unwrap();
graph.insert_edge(3, 4, 1.0).unwrap();
let phi = 0.1;
let decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
let vertices: HashSet<VertexId> = vec![1, 2, 3, 4].into_iter().collect();
let components = decomp.find_connected_components(&vertices);
assert_eq!(components.len(), 2);
}
#[test]
fn test_local_cut_search() {
let graph = create_separable_graph();
let phi = 0.3;
let decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
let vertices: HashSet<VertexId> = graph.vertices().into_iter().collect();
if let Some(cut) = decomp.local_cut_search(1, phi, &vertices) {
assert!(!cut.is_empty());
assert!(cut.len() < vertices.len());
}
}
#[test]
fn test_prune() {
let graph = create_separable_graph();
let phi = 0.3;
let decomp = ExpanderDecomposition::build(graph.clone(), phi).unwrap();
if decomp.num_levels() > 0 && !decomp.levels[0].is_empty() {
let component = &decomp.levels[0][0];
let result = decomp.prune(component);
assert!(result.is_some() || result.is_none());
}
}
#[test]
fn test_expander_component_methods() {
let mut vertices = HashSet::new();
vertices.insert(1);
vertices.insert(2);
vertices.insert(3);
let component = ExpanderComponent::new(0, vertices, 0);
assert_eq!(component.id, 0);
assert_eq!(component.level, 0);
assert_eq!(component.size(), 3);
assert!(component.contains(1));
assert!(!component.contains(4));
}
#[test]
fn test_empty_graph() {
let graph = Arc::new(DynamicGraph::new());
let phi = 0.1;
let decomp = ExpanderDecomposition::build(graph, phi).unwrap();
assert_eq!(decomp.num_levels(), 0);
}
#[test]
fn test_single_vertex() {
let graph = Arc::new(DynamicGraph::new());
graph.add_vertex(1);
let phi = 0.1;
let decomp = ExpanderDecomposition::build(graph, phi).unwrap();
assert!(decomp.num_levels() > 0);
}
#[test]
fn test_large_expander() {
let graph = Arc::new(DynamicGraph::new());
for i in 1..=10 {
for j in (i + 1)..=10 {
graph.insert_edge(i, j, 1.0).unwrap();
}
}
let phi = 0.1;
let decomp = ExpanderDecomposition::build(graph, phi).unwrap();
assert!(decomp.num_levels() > 0);
}
}