Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 14 additions & 66 deletions src/models/graph/biconnectivity_augmentation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use crate::topology::{Graph, SimpleGraph};
use crate::traits::Problem;
use crate::types::WeightElement;
use num_traits::Zero;
use petgraph::algo::{articulation_points::articulation_points, connected_components};
use petgraph::graph::{NodeIndex, UnGraph};
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;

Expand Down Expand Up @@ -189,7 +191,7 @@ impl<G: Graph, W: WeightElement> BiconnectivityAugmentation<G, W> {
fn augmented_graph(
&self,
config: &[bool],
) -> Result<Option<SimpleGraph>, crate::traits::EvaluationError> {
) -> Result<Option<UnGraph<(), ()>>, crate::traits::EvaluationError> {
if config.len() != self.num_potential_edges() {
return Ok(None);
}
Expand All @@ -215,10 +217,14 @@ impl<G: Graph, W: WeightElement> BiconnectivityAugmentation<G, W> {
return Ok(None);
}

Ok(Some(SimpleGraph::new(
self.num_vertices(),
edges.into_iter().collect(),
)))
let mut graph = UnGraph::new_undirected();
for _ in 0..self.num_vertices() {
graph.add_node(());
}
for (u, v) in edges {
graph.add_edge(NodeIndex::new(u), NodeIndex::new(v), ());
}
Ok(Some(graph))
}
}

Expand Down Expand Up @@ -277,67 +283,9 @@ fn normalize_edge(u: usize, v: usize) -> (usize, usize) {
}
}

struct DfsState {
visited: Vec<bool>,
discovery_time: Vec<usize>,
low: Vec<usize>,
parent: Vec<Option<usize>>,
time: usize,
has_articulation_point: bool,
}

fn dfs_articulation_points<G: Graph>(graph: &G, vertex: usize, state: &mut DfsState) {
if state.has_articulation_point {
return;
}

state.visited[vertex] = true;
state.time += 1;
state.discovery_time[vertex] = state.time;
state.low[vertex] = state.time;

let mut child_count = 0;
for neighbor in graph.neighbors(vertex) {
if !state.visited[neighbor] {
child_count += 1;
state.parent[neighbor] = Some(vertex);
dfs_articulation_points(graph, neighbor, state);
state.low[vertex] = state.low[vertex].min(state.low[neighbor]);

if state.parent[vertex].is_none() && child_count > 1 {
state.has_articulation_point = true;
return;
}

if state.parent[vertex].is_some() && state.low[neighbor] >= state.discovery_time[vertex]
{
state.has_articulation_point = true;
return;
}
} else if state.parent[vertex] != Some(neighbor) {
state.low[vertex] = state.low[vertex].min(state.discovery_time[neighbor]);
}
}
}

fn is_biconnected<G: Graph>(graph: &G) -> bool {
let num_vertices = graph.num_vertices();
if num_vertices <= 1 {
return true;
}

let mut state = DfsState {
visited: vec![false; num_vertices],
discovery_time: vec![0; num_vertices],
low: vec![0; num_vertices],
parent: vec![None; num_vertices],
time: 0,
has_articulation_point: false,
};

dfs_articulation_points(graph, 0, &mut state);

!state.has_articulation_point && state.visited.into_iter().all(|seen| seen)
fn is_biconnected(graph: &UnGraph<(), ()>) -> bool {
graph.node_count() <= 1
|| (connected_components(graph) == 1 && articulation_points(graph).is_empty())
}

crate::declare_variants! {
Expand Down
32 changes: 2 additions & 30 deletions src/models/graph/minimum_dummy_activities_pert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::registry::{CreateSpec, ProblemSchemaEntry};
use crate::topology::DirectedGraph;
use crate::traits::Problem;
use crate::types::Min;
use petgraph::unionfind::UnionFind;
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::{BTreeMap, BTreeSet};

Expand Down Expand Up @@ -161,7 +162,7 @@ impl MinimumDummyActivitiesPert {
}

let roots: Vec<usize> = (0..2 * num_tasks)
.map(|endpoint| uf.find(endpoint))
.map(|endpoint| uf.find_mut(endpoint))
.collect();
let mut root_to_dense = BTreeMap::new();
for &root in &roots {
Expand Down Expand Up @@ -288,35 +289,6 @@ struct CandidatePertNetwork {
num_dummy_arcs: usize,
}

#[derive(Debug)]
struct UnionFind {
parent: Vec<usize>,
}

impl UnionFind {
fn new(size: usize) -> Self {
Self {
parent: (0..size).collect(),
}
}

fn find(&mut self, x: usize) -> usize {
if self.parent[x] != x {
let root = self.find(self.parent[x]);
self.parent[x] = root;
}
self.parent[x]
}

fn union(&mut self, a: usize, b: usize) {
let root_a = self.find(a);
let root_b = self.find(b);
if root_a != root_b {
self.parent[root_b] = root_a;
}
}
}

fn start_endpoint(task: usize) -> usize {
2 * task
}
Expand Down
15 changes: 3 additions & 12 deletions src/models/graph/partition_into_forests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension};
use crate::topology::{Graph, SimpleGraph};
use crate::traits::Problem;
use crate::variant::VariantParam;
use petgraph::unionfind::UnionFind;
use serde::{Deserialize, Serialize};

inventory::submit! {
Expand Down Expand Up @@ -159,27 +160,17 @@ fn is_valid_forest_partition<G: Graph>(graph: &G, num_forests: usize, config: &[
// For each forest class, verify the induced subgraph is acyclic using union-find.
// An undirected graph is acyclic iff union-find never sees an edge (u, v) where
// u and v already share a component.
let mut parent: Vec<usize> = (0..n).collect();

fn find(parent: &mut Vec<usize>, x: usize) -> usize {
if parent[x] != x {
parent[x] = find(parent, parent[x]);
}
parent[x]
}
let mut components = UnionFind::<usize>::new(n);

for (u, v) in graph.edges() {
if config[u] != config[v] {
// Edge crosses classes — not in any induced subgraph
continue;
}
// Both u and v are in the same class; check for cycle
let ru = find(&mut parent, u);
let rv = find(&mut parent, v);
if ru == rv {
if !components.union(u, v) {
return false; // Cycle detected
}
parent[ru] = rv; // Union
}

true
Expand Down
26 changes: 4 additions & 22 deletions src/rules/prizecollectingsteinerforest_steinertree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ impl ReductionResult for ReductionPCSFToSteinerTree {
let m = self.num_source_edges;
let mut selected_vertices = vec![false; n];
let mut selected_edges = vec![false; m];
let edges = self.target.graph().edges();

// Mark vertices included via their gadget include-edge `(v, t_v)`,
// and edges via the matching original edge.
Expand All @@ -91,20 +92,9 @@ impl ReductionResult for ReductionPCSFToSteinerTree {
selected_vertices[v] = true;
} else if let Some(src_edge) = self.target_to_source_edge[target_idx] {
selected_edges[src_edge] = true;
}
}

// Any original edge selected in `T*` forces both endpoints into
// `V_F`. The PCSF model rejects configurations where a selected
// edge has an unselected endpoint, so we mark endpoints explicitly
// (this also covers prize-zero endpoints, which have no gadget).
let edges = self.target.graph().edges();
for (target_idx, &(_, _)) in edges.iter().enumerate() {
if !target_solution[target_idx] {
continue;
}
if let Some(src_edge) = self.target_to_source_edge[target_idx] {
let (u, v) = self.source_edge_pair(src_edge);
// Include both endpoints, including prize-zero vertices
// that have no inclusion gadget.
let (u, v) = edges[target_idx];
selected_vertices[u] = true;
selected_vertices[v] = true;
}
Expand All @@ -115,14 +105,6 @@ impl ReductionResult for ReductionPCSFToSteinerTree {
}
}

impl ReductionPCSFToSteinerTree {
/// Look up the endpoint pair of the `idx`-th source edge in the target
/// graph's edge list (source edges are placed first by construction).
fn source_edge_pair(&self, src_edge_idx: usize) -> (usize, usize) {
self.target.graph().edges()[src_edge_idx]
}
}

#[reduction(
transform = exact {
num_vertices = "num_vertices + num_vertices_with_prize + 1",
Expand Down
18 changes: 14 additions & 4 deletions src/unit_tests/models/graph/biconnectivity_augmentation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,10 +159,20 @@ fn test_biconnectivity_augmentation_paper_example() {

#[test]
fn test_is_biconnected() {
assert!(is_biconnected(&SimpleGraph::cycle(4)));
assert!(is_biconnected(&SimpleGraph::complete(3)));
assert!(!is_biconnected(&SimpleGraph::path(4)));
assert!(!is_biconnected(&SimpleGraph::new(4, vec![(0, 1), (2, 3)])));
for (graph, expected) in [
(SimpleGraph::empty(0), true),
(SimpleGraph::empty(1), true),
(SimpleGraph::empty(2), false),
(SimpleGraph::path(2), true),
(SimpleGraph::cycle(4), true),
(SimpleGraph::complete(3), true),
(SimpleGraph::path(4), false),
(SimpleGraph::new(4, vec![(0, 1), (2, 3)]), false),
(SimpleGraph::new(2, vec![(0, 0), (0, 1), (0, 1)]), true),
] {
let problem = BiconnectivityAugmentation::<_, i64>::new(graph, vec![], 0);
assert_eq!(problem.evaluate(&vec![]).unwrap().0, expected);
}
}

#[test]
Expand Down
20 changes: 20 additions & 0 deletions src/unit_tests/rules/prizecollectingsteinerforest_steinertree.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,24 @@
use super::*;

#[test]
fn test_extraction_includes_zero_prize_connector() {
let source = PrizeCollectingSteinerForest::<SimpleGraph, i64>::new(
SimpleGraph::path(3),
vec![10, 0, 10],
vec![1, 1],
1,
5,
)
.unwrap();
let reduction = ReduceTo::<SteinerTree<SimpleGraph, i64>>::reduce_to(&source).unwrap();
let target_solution = BruteForce::new()
.solve(reduction.target_problem())
.unwrap()
.unwrap();
let recovered = reduction.extract_solution(&target_solution).unwrap();
assert_eq!(recovered, (vec![true; 3], vec![true; 2]));
assert_eq!(source.evaluate(&recovered).unwrap(), Min(Some(7)));
}
use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target;
use crate::solvers::BruteForce;
use crate::topology::SimpleGraph;
Expand Down
Loading