1use 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#[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 graph: G,
51 potential_weights: Vec<(usize, usize, W)>,
53 budget: W::Sum,
55}
56
57impl<G: Graph, W: WeightElement> BiconnectivityAugmentation<G, W> {
58 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 pub fn graph(&self) -> &G {
100 &self.graph
101 }
102
103 pub fn potential_weights(&self) -> &[(usize, usize, W)] {
105 &self.potential_weights
106 }
107
108 pub fn budget(&self) -> &W::Sum {
110 &self.budget
111 }
112
113 pub fn num_vertices(&self) -> usize {
115 self.graph.num_vertices()
116 }
117
118 pub fn num_edges(&self) -> usize {
120 self.graph.num_edges()
121 }
122
123 pub fn num_potential_edges(&self) -> usize {
125 self.potential_weights.len()
126 }
127
128 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;