Skip to main content

problemreductions/models/graph/
biconnectivity_augmentation.rs

1//! Biconnectivity augmentation problem implementation.
2//!
3//! Given a graph, weighted potential edges, and a budget, determine whether
4//! adding some subset of the potential edges can make the graph biconnected
5//! without exceeding the budget.
6
7use crate::registry::{CreateSpec, ProblemSchemaEntry, VariantDimension};
8use crate::topology::{Graph, SimpleGraph};
9use crate::traits::Problem;
10use crate::types::WeightElement;
11use num_traits::Zero;
12use serde::{Deserialize, Serialize};
13use std::collections::BTreeSet;
14
15inventory::submit! {
16    ProblemSchemaEntry {
17        name: "BiconnectivityAugmentation",
18        display_name: "Biconnectivity Augmentation",
19        aliases: &[],
20        dimensions: &[
21            VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]),
22            VariantDimension::new("weight", "i64", &["i64"]),
23        ],
24        category: crate::registry::ProblemCategory::Graph,
25        module_path: module_path!(),
26        description: "Add weighted potential edges to make a graph biconnected within budget",
27        fields: BiconnectivityAugmentationCreateSpec::FIELDS,
28    }
29}
30
31/// The Biconnectivity Augmentation problem.
32///
33/// Given a graph `G = (V, E)`, weighted potential edges, and a budget `B`,
34/// determine whether there exists a subset of potential edges `E'` such that:
35/// - `sum_{e in E'} w(e) <= B`
36/// - `(V, E union E')` is biconnected
37#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(bound(
39    serialize = "G: serde::Serialize, W: serde::Serialize, W::Sum: serde::Serialize",
40    deserialize = "G: serde::Deserialize<'de>, W: serde::Deserialize<'de>, W::Sum: serde::Deserialize<'de>"
41))]
42pub struct BiconnectivityAugmentation<G, W>
43where
44    W: WeightElement,
45{
46    /// The underlying graph.
47    graph: G,
48    /// Potential augmentation edges with their weights.
49    potential_weights: Vec<(usize, usize, W)>,
50    /// Maximum total weight of selected potential edges.
51    budget: W::Sum,
52}
53
54#[derive(Debug, Deserialize, crate::CreateSpec)]
55struct BiconnectivityAugmentationCreateSpec {
56    #[create(codec = "edge-list")]
57    graph: Vec<(usize, usize)>,
58    num_vertices: Option<usize>,
59    potential_weights: Vec<(usize, usize, i64)>,
60    budget: i64,
61}
62
63impl TryFrom<BiconnectivityAugmentationCreateSpec>
64    for BiconnectivityAugmentation<SimpleGraph, i64>
65{
66    type Error = crate::registry::ConstructionError;
67    fn try_from(spec: BiconnectivityAugmentationCreateSpec) -> Result<Self, Self::Error> {
68        if spec.graph.is_empty() && spec.num_vertices.is_none() {
69            return Err("num_vertices is required for an empty graph".into());
70        }
71        for &(u, v) in &spec.graph {
72            if u == v {
73                return Err(format!("self-loop {u}-{v} is not allowed").into());
74            }
75        }
76        let inferred = spec
77            .graph
78            .iter()
79            .flat_map(|&(u, v)| [u, v])
80            .max()
81            .map(|v| v.checked_add(1).ok_or("vertex count overflows usize"))
82            .transpose()?
83            .unwrap_or(0);
84        let count = spec.num_vertices.unwrap_or(inferred);
85        if count < inferred {
86            return Err("num_vertices is too small for graph endpoints".into());
87        }
88        let graph = SimpleGraph::new(count, spec.graph);
89        let mut seen = BTreeSet::new();
90        for &(u, v, _) in &spec.potential_weights {
91            if u >= count || v >= count {
92                return Err("potential edge endpoint is out of bounds".into());
93            }
94            if u == v {
95                return Err("potential edge is a self-loop".into());
96            }
97            let edge = normalize_edge(u, v);
98            if graph.has_edge(edge.0, edge.1) {
99                return Err("potential edge already exists in graph".into());
100            }
101            if !seen.insert(edge) {
102                return Err("duplicate potential edge".into());
103            }
104        }
105        Ok(Self {
106            graph,
107            potential_weights: spec.potential_weights,
108            budget: spec.budget,
109        })
110    }
111}
112
113impl<G: Graph, W: WeightElement> BiconnectivityAugmentation<G, W> {
114    /// Create a new biconnectivity augmentation instance.
115    ///
116    /// # Panics
117    /// Panics if any potential edge references a vertex index outside the graph,
118    /// is a self-loop, duplicates another candidate edge, or already exists in
119    /// the input graph.
120    pub fn new(graph: G, potential_weights: Vec<(usize, usize, W)>, budget: W::Sum) -> Self {
121        let num_vertices = graph.num_vertices();
122        let mut seen_potential_edges = BTreeSet::new();
123        for &(u, v, _) in &potential_weights {
124            assert!(
125                u < num_vertices && v < num_vertices,
126                "potential edge ({}, {}) references vertex >= num_vertices ({})",
127                u,
128                v,
129                num_vertices
130            );
131            assert!(u != v, "potential edge ({}, {}) is a self-loop", u, v);
132            let edge = normalize_edge(u, v);
133            assert!(
134                !graph.has_edge(edge.0, edge.1),
135                "potential edge ({}, {}) already exists in the graph",
136                edge.0,
137                edge.1
138            );
139            assert!(
140                seen_potential_edges.insert(edge),
141                "potential edge ({}, {}) is duplicated",
142                edge.0,
143                edge.1
144            );
145        }
146
147        Self {
148            graph,
149            potential_weights,
150            budget,
151        }
152    }
153
154    /// Get a reference to the underlying graph.
155    pub fn graph(&self) -> &G {
156        &self.graph
157    }
158
159    /// Get the weighted potential edges.
160    pub fn potential_weights(&self) -> &[(usize, usize, W)] {
161        &self.potential_weights
162    }
163
164    /// Get the budget.
165    pub fn budget(&self) -> &W::Sum {
166        &self.budget
167    }
168
169    /// Get the number of vertices in the underlying graph.
170    pub fn num_vertices(&self) -> usize {
171        self.graph.num_vertices()
172    }
173
174    /// Get the number of edges in the underlying graph.
175    pub fn num_edges(&self) -> usize {
176        self.graph.num_edges()
177    }
178
179    /// Get the number of potential augmentation edges.
180    pub fn num_potential_edges(&self) -> usize {
181        self.potential_weights.len()
182    }
183
184    /// Check if the problem uses a non-unit weight type.
185    pub fn is_weighted(&self) -> bool {
186        !W::IS_UNIT
187    }
188
189    fn augmented_graph(
190        &self,
191        config: &[bool],
192    ) -> Result<Option<SimpleGraph>, crate::traits::EvaluationError> {
193        if config.len() != self.num_potential_edges() {
194            return Ok(None);
195        }
196
197        let mut total = W::Sum::zero();
198        let mut edges = BTreeSet::new();
199
200        for (u, v) in self.graph.edges() {
201            edges.insert(normalize_edge(u, v));
202        }
203
204        for (selected, &(u, v, ref weight)) in config.iter().zip(&self.potential_weights) {
205            if *selected {
206                total = W::checked_add_to_sum(
207                    total,
208                    weight.to_sum(),
209                    "summing biconnectivity augmentation weights",
210                )?;
211                edges.insert(normalize_edge(u, v));
212            }
213        }
214        if total > self.budget.clone() {
215            return Ok(None);
216        }
217
218        Ok(Some(SimpleGraph::new(
219            self.num_vertices(),
220            edges.into_iter().collect(),
221        )))
222    }
223}
224
225impl<G, W> Problem for BiconnectivityAugmentation<G, W>
226where
227    G: Graph + crate::variant::VariantParam,
228    W: WeightElement + crate::variant::VariantParam,
229{
230    const NAME: &'static str = "BiconnectivityAugmentation";
231    type Solution = Vec<bool>;
232    type Value = crate::types::Or;
233
234    crate::problem_parameters![
235        ("num_edges", num_edges),
236        ("num_potential_edges", num_potential_edges),
237        ("num_vertices", num_vertices),
238    ];
239
240    fn variant() -> Vec<(&'static str, &'static str)> {
241        crate::variant_params![G, W]
242    }
243
244    fn evaluate(
245        &self,
246        config: &Self::Solution,
247    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
248        if config.len() != self.num_potential_edges() {
249            return Err(crate::traits::EvaluationError::InvalidConfiguration(
250                "edge-selection length does not match the candidate edges".into(),
251            ));
252        }
253        Ok({
254            crate::types::Or({
255                self.augmented_graph(config)?
256                    .is_some_and(|graph| is_biconnected(&graph))
257            })
258        })
259    }
260}
261
262impl<G, W> crate::solvers::BruteForceProblem for BiconnectivityAugmentation<G, W>
263where
264    G: Graph + crate::variant::VariantParam,
265    W: WeightElement + crate::variant::VariantParam,
266{
267    fn dimensions(&self) -> Vec<usize> {
268        vec![2; self.num_potential_edges()]
269    }
270}
271
272fn normalize_edge(u: usize, v: usize) -> (usize, usize) {
273    if u <= v {
274        (u, v)
275    } else {
276        (v, u)
277    }
278}
279
280struct DfsState {
281    visited: Vec<bool>,
282    discovery_time: Vec<usize>,
283    low: Vec<usize>,
284    parent: Vec<Option<usize>>,
285    time: usize,
286    has_articulation_point: bool,
287}
288
289fn dfs_articulation_points<G: Graph>(graph: &G, vertex: usize, state: &mut DfsState) {
290    if state.has_articulation_point {
291        return;
292    }
293
294    state.visited[vertex] = true;
295    state.time += 1;
296    state.discovery_time[vertex] = state.time;
297    state.low[vertex] = state.time;
298
299    let mut child_count = 0;
300    for neighbor in graph.neighbors(vertex) {
301        if !state.visited[neighbor] {
302            child_count += 1;
303            state.parent[neighbor] = Some(vertex);
304            dfs_articulation_points(graph, neighbor, state);
305            state.low[vertex] = state.low[vertex].min(state.low[neighbor]);
306
307            if state.parent[vertex].is_none() && child_count > 1 {
308                state.has_articulation_point = true;
309                return;
310            }
311
312            if state.parent[vertex].is_some() && state.low[neighbor] >= state.discovery_time[vertex]
313            {
314                state.has_articulation_point = true;
315                return;
316            }
317        } else if state.parent[vertex] != Some(neighbor) {
318            state.low[vertex] = state.low[vertex].min(state.discovery_time[neighbor]);
319        }
320    }
321}
322
323fn is_biconnected<G: Graph>(graph: &G) -> bool {
324    let num_vertices = graph.num_vertices();
325    if num_vertices <= 1 {
326        return true;
327    }
328
329    let mut state = DfsState {
330        visited: vec![false; num_vertices],
331        discovery_time: vec![0; num_vertices],
332        low: vec![0; num_vertices],
333        parent: vec![None; num_vertices],
334        time: 0,
335        has_articulation_point: false,
336    };
337
338    dfs_articulation_points(graph, 0, &mut state);
339
340    !state.has_articulation_point && state.visited.into_iter().all(|seen| seen)
341}
342
343crate::declare_variants! {
344    default BiconnectivityAugmentation<SimpleGraph, i64> => "2^num_potential_edges" create BiconnectivityAugmentationCreateSpec,
345}
346
347crate::register_brute_force! {
348    BiconnectivityAugmentation<SimpleGraph, i64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
349}
350
351#[cfg(feature = "example-db")]
352pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
353    vec![crate::example_db::specs::ModelExampleSpec {
354        id: "biconnectivity_augmentation",
355        instance: Box::new(BiconnectivityAugmentation::new(
356            SimpleGraph::path(6),
357            vec![
358                (0, 2, 1),
359                (0, 3, 2),
360                (0, 4, 3),
361                (1, 3, 1),
362                (1, 4, 2),
363                (1, 5, 3),
364                (2, 4, 1),
365                (2, 5, 2),
366                (3, 5, 1),
367            ],
368            4,
369        )),
370        optimal_config: serde_json::json!(vec![
371            true, false, false, true, false, false, true, false, true
372        ]),
373        optimal_value: serde_json::json!(true),
374    }]
375}
376
377#[cfg(test)]
378pub(crate) fn example_instance() -> BiconnectivityAugmentation<SimpleGraph, i64> {
379    BiconnectivityAugmentation::new(
380        SimpleGraph::path(6),
381        vec![
382            (0, 2, 1),
383            (0, 3, 2),
384            (0, 4, 3),
385            (1, 3, 1),
386            (1, 4, 2),
387            (1, 5, 3),
388            (2, 4, 1),
389            (2, 5, 2),
390            (3, 5, 1),
391        ],
392        4,
393    )
394}
395
396#[cfg(test)]
397#[path = "../../unit_tests/models/graph/biconnectivity_augmentation.rs"]
398mod tests;