1use 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#[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 graph: G,
48 potential_weights: Vec<(usize, usize, W)>,
50 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 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 pub fn graph(&self) -> &G {
156 &self.graph
157 }
158
159 pub fn potential_weights(&self) -> &[(usize, usize, W)] {
161 &self.potential_weights
162 }
163
164 pub fn budget(&self) -> &W::Sum {
166 &self.budget
167 }
168
169 pub fn num_vertices(&self) -> usize {
171 self.graph.num_vertices()
172 }
173
174 pub fn num_edges(&self) -> usize {
176 self.graph.num_edges()
177 }
178
179 pub fn num_potential_edges(&self) -> usize {
181 self.potential_weights.len()
182 }
183
184 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;