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