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::{FieldInfo, 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", "i32", &["i32"]),
23        ],
24        module_path: module_path!(),
25        description: "Add weighted potential edges to make a graph biconnected within budget",
26        fields: &[
27            FieldInfo { name: "graph", type_name: "G", description: "The underlying graph G=(V,E)" },
28            FieldInfo { name: "potential_weights", type_name: "Vec<(usize, usize, W)>", description: "Potential edges with augmentation weights" },
29            FieldInfo { name: "budget", type_name: "W::Sum", description: "Maximum total augmentation weight B" },
30        ],
31    }
32}
33
34/// The Biconnectivity Augmentation problem.
35///
36/// Given a graph `G = (V, E)`, weighted potential edges, and a budget `B`,
37/// determine whether there exists a subset of potential edges `E'` such that:
38/// - `sum_{e in E'} w(e) <= B`
39/// - `(V, E union E')` is biconnected
40#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(bound(
42    serialize = "G: serde::Serialize, W: serde::Serialize, W::Sum: serde::Serialize",
43    deserialize = "G: serde::Deserialize<'de>, W: serde::Deserialize<'de>, W::Sum: serde::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
57impl<G: Graph, W: WeightElement> BiconnectivityAugmentation<G, W> {
58    /// Create a new biconnectivity augmentation instance.
59    ///
60    /// # Panics
61    /// Panics if any potential edge references a vertex index outside the graph,
62    /// is a self-loop, duplicates another candidate edge, or already exists in
63    /// the input graph.
64    pub fn new(graph: G, potential_weights: Vec<(usize, usize, W)>, budget: W::Sum) -> Self {
65        let num_vertices = graph.num_vertices();
66        let mut seen_potential_edges = BTreeSet::new();
67        for &(u, v, _) in &potential_weights {
68            assert!(
69                u < num_vertices && v < num_vertices,
70                "potential edge ({}, {}) references vertex >= num_vertices ({})",
71                u,
72                v,
73                num_vertices
74            );
75            assert!(u != v, "potential edge ({}, {}) is a self-loop", u, v);
76            let edge = normalize_edge(u, v);
77            assert!(
78                !graph.has_edge(edge.0, edge.1),
79                "potential edge ({}, {}) already exists in the graph",
80                edge.0,
81                edge.1
82            );
83            assert!(
84                seen_potential_edges.insert(edge),
85                "potential edge ({}, {}) is duplicated",
86                edge.0,
87                edge.1
88            );
89        }
90
91        Self {
92            graph,
93            potential_weights,
94            budget,
95        }
96    }
97
98    /// Get a reference to the underlying graph.
99    pub fn graph(&self) -> &G {
100        &self.graph
101    }
102
103    /// Get the weighted potential edges.
104    pub fn potential_weights(&self) -> &[(usize, usize, W)] {
105        &self.potential_weights
106    }
107
108    /// Get the budget.
109    pub fn budget(&self) -> &W::Sum {
110        &self.budget
111    }
112
113    /// Get the number of vertices in the underlying graph.
114    pub fn num_vertices(&self) -> usize {
115        self.graph.num_vertices()
116    }
117
118    /// Get the number of edges in the underlying graph.
119    pub fn num_edges(&self) -> usize {
120        self.graph.num_edges()
121    }
122
123    /// Get the number of potential augmentation edges.
124    pub fn num_potential_edges(&self) -> usize {
125        self.potential_weights.len()
126    }
127
128    /// Check if the problem uses a non-unit weight type.
129    pub fn is_weighted(&self) -> bool {
130        !W::IS_UNIT
131    }
132
133    fn augmented_graph(&self, config: &[usize]) -> Option<SimpleGraph> {
134        if config.len() != self.num_potential_edges() || config.iter().any(|&value| value >= 2) {
135            return None;
136        }
137
138        let mut total = W::Sum::zero();
139        let mut edges = BTreeSet::new();
140
141        for (u, v) in self.graph.edges() {
142            edges.insert(normalize_edge(u, v));
143        }
144
145        for (selected, &(u, v, ref weight)) in config.iter().zip(&self.potential_weights) {
146            if *selected == 1 {
147                total += weight.to_sum();
148                if total > self.budget.clone() {
149                    return None;
150                }
151                edges.insert(normalize_edge(u, v));
152            }
153        }
154
155        Some(SimpleGraph::new(
156            self.num_vertices(),
157            edges.into_iter().collect(),
158        ))
159    }
160}
161
162impl<G, W> Problem for BiconnectivityAugmentation<G, W>
163where
164    G: Graph + crate::variant::VariantParam,
165    W: WeightElement + crate::variant::VariantParam,
166{
167    const NAME: &'static str = "BiconnectivityAugmentation";
168    type Value = crate::types::Or;
169
170    fn variant() -> Vec<(&'static str, &'static str)> {
171        crate::variant_params![G, W]
172    }
173
174    fn dims(&self) -> Vec<usize> {
175        vec![2; self.num_potential_edges()]
176    }
177
178    fn evaluate(&self, config: &[usize]) -> crate::types::Or {
179        crate::types::Or({
180            self.augmented_graph(config)
181                .is_some_and(|graph| is_biconnected(&graph))
182        })
183    }
184}
185
186fn normalize_edge(u: usize, v: usize) -> (usize, usize) {
187    if u <= v {
188        (u, v)
189    } else {
190        (v, u)
191    }
192}
193
194struct DfsState {
195    visited: Vec<bool>,
196    discovery_time: Vec<usize>,
197    low: Vec<usize>,
198    parent: Vec<Option<usize>>,
199    time: usize,
200    has_articulation_point: bool,
201}
202
203fn dfs_articulation_points<G: Graph>(graph: &G, vertex: usize, state: &mut DfsState) {
204    if state.has_articulation_point {
205        return;
206    }
207
208    state.visited[vertex] = true;
209    state.time += 1;
210    state.discovery_time[vertex] = state.time;
211    state.low[vertex] = state.time;
212
213    let mut child_count = 0;
214    for neighbor in graph.neighbors(vertex) {
215        if !state.visited[neighbor] {
216            child_count += 1;
217            state.parent[neighbor] = Some(vertex);
218            dfs_articulation_points(graph, neighbor, state);
219            state.low[vertex] = state.low[vertex].min(state.low[neighbor]);
220
221            if state.parent[vertex].is_none() && child_count > 1 {
222                state.has_articulation_point = true;
223                return;
224            }
225
226            if state.parent[vertex].is_some() && state.low[neighbor] >= state.discovery_time[vertex]
227            {
228                state.has_articulation_point = true;
229                return;
230            }
231        } else if state.parent[vertex] != Some(neighbor) {
232            state.low[vertex] = state.low[vertex].min(state.discovery_time[neighbor]);
233        }
234    }
235}
236
237fn is_biconnected<G: Graph>(graph: &G) -> bool {
238    let num_vertices = graph.num_vertices();
239    if num_vertices <= 1 {
240        return true;
241    }
242
243    let mut state = DfsState {
244        visited: vec![false; num_vertices],
245        discovery_time: vec![0; num_vertices],
246        low: vec![0; num_vertices],
247        parent: vec![None; num_vertices],
248        time: 0,
249        has_articulation_point: false,
250    };
251
252    dfs_articulation_points(graph, 0, &mut state);
253
254    !state.has_articulation_point && state.visited.into_iter().all(|seen| seen)
255}
256
257crate::declare_variants! {
258    default BiconnectivityAugmentation<SimpleGraph, i32> => "2^num_potential_edges",
259}
260
261#[cfg(feature = "example-db")]
262pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
263    vec![crate::example_db::specs::ModelExampleSpec {
264        id: "biconnectivity_augmentation",
265        instance: Box::new(BiconnectivityAugmentation::new(
266            SimpleGraph::path(6),
267            vec![
268                (0, 2, 1),
269                (0, 3, 2),
270                (0, 4, 3),
271                (1, 3, 1),
272                (1, 4, 2),
273                (1, 5, 3),
274                (2, 4, 1),
275                (2, 5, 2),
276                (3, 5, 1),
277            ],
278            4,
279        )),
280        optimal_config: vec![1, 0, 0, 1, 0, 0, 1, 0, 1],
281        optimal_value: serde_json::json!(true),
282    }]
283}
284
285#[cfg(test)]
286pub(crate) fn example_instance() -> BiconnectivityAugmentation<SimpleGraph, i32> {
287    BiconnectivityAugmentation::new(
288        SimpleGraph::path(6),
289        vec![
290            (0, 2, 1),
291            (0, 3, 2),
292            (0, 4, 3),
293            (1, 3, 1),
294            (1, 4, 2),
295            (1, 5, 3),
296            (2, 4, 1),
297            (2, 5, 2),
298            (3, 5, 1),
299        ],
300        4,
301    )
302}
303
304#[cfg(test)]
305#[path = "../../unit_tests/models/graph/biconnectivity_augmentation.rs"]
306mod tests;