Skip to main content

problemreductions/models/graph/
strong_connectivity_augmentation.rs

1//! Strong Connectivity Augmentation problem implementation.
2//!
3//! The Strong Connectivity Augmentation problem asks whether adding a bounded
4//! set of weighted candidate arcs can make a directed graph strongly connected.
5
6use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension};
7use crate::topology::DirectedGraph;
8use crate::traits::Problem;
9use crate::types::WeightElement;
10use num_traits::Zero;
11use serde::{Deserialize, Deserializer, Serialize};
12use std::cmp::Ordering;
13use std::collections::BTreeSet;
14
15inventory::submit! {
16    ProblemSchemaEntry {
17        name: "StrongConnectivityAugmentation",
18        display_name: "Strong Connectivity Augmentation",
19        aliases: &[],
20        dimensions: &[
21            VariantDimension::new("weight", "i64", &["i64"]),
22        ],
23        category: crate::registry::ProblemCategory::Graph,
24        module_path: module_path!(),
25        description: "Add a bounded set of weighted candidate arcs to make a digraph strongly connected",
26        fields: &[
27            FieldInfo { name: "graph", type_name: "DirectedGraph", description: "The initial directed graph G=(V,A)" },
28            FieldInfo { name: "candidate_arcs", type_name: "Vec<(usize, usize, W)>", description: "Candidate augmenting arcs (u, v, w(u,v)) not already present in G" },
29            FieldInfo { name: "bound", type_name: "W::Sum", description: "Upper bound B on the total added weight" },
30        ],
31    }
32}
33
34/// Strong Connectivity Augmentation.
35///
36/// Given a directed graph `G = (V, A)`, weighted candidate arcs not already in
37/// `A`, and a bound `B`, determine whether some subset of the candidate arcs
38/// has total weight at most `B` and makes the augmented digraph strongly
39/// connected.
40#[derive(Debug, Clone, Serialize)]
41pub struct StrongConnectivityAugmentation<W: WeightElement> {
42    graph: DirectedGraph,
43    candidate_arcs: Vec<(usize, usize, W)>,
44    bound: W::Sum,
45}
46
47impl<W: WeightElement> StrongConnectivityAugmentation<W> {
48    /// Fallible constructor used by CLI validation and deserialization.
49    pub fn try_new(
50        graph: DirectedGraph,
51        candidate_arcs: Vec<(usize, usize, W)>,
52        bound: W::Sum,
53    ) -> Result<Self, crate::registry::ConstructionError> {
54        if !matches!(
55            bound.partial_cmp(&W::Sum::zero()),
56            Some(Ordering::Equal | Ordering::Greater)
57        ) {
58            return Err("bound must be nonnegative".to_string().into());
59        }
60
61        let num_vertices = graph.num_vertices();
62        let mut seen_pairs = BTreeSet::new();
63
64        for (u, v, weight) in &candidate_arcs {
65            if *u >= num_vertices || *v >= num_vertices {
66                return Err(format!(
67                    "candidate arc ({}, {}) references vertex >= num_vertices ({})",
68                    u, v, num_vertices
69                )
70                .into());
71            }
72            if !matches!(
73                weight.to_sum().partial_cmp(&W::Sum::zero()),
74                Some(Ordering::Greater)
75            ) {
76                return Err(format!("candidate arc ({}, {}) weight must be positive", u, v).into());
77            }
78            if graph.has_arc(*u, *v) {
79                return Err(format!(
80                    "candidate arc ({}, {}) already exists in the base graph",
81                    u, v
82                )
83                .into());
84            }
85            if !seen_pairs.insert((*u, *v)) {
86                return Err(format!("duplicate candidate arc ({}, {})", u, v).into());
87            }
88        }
89
90        Ok(Self {
91            graph,
92            candidate_arcs,
93            bound,
94        })
95    }
96
97    /// Create a new strong connectivity augmentation instance.
98    ///
99    /// # Panics
100    ///
101    /// Panics if a candidate arc endpoint is out of range, if a candidate arc
102    /// already exists in the base graph, or if candidate arcs contain
103    /// duplicates.
104    pub fn new(
105        graph: DirectedGraph,
106        candidate_arcs: Vec<(usize, usize, W)>,
107        bound: W::Sum,
108    ) -> Self {
109        Self::try_new(graph, candidate_arcs, bound).unwrap_or_else(|msg| panic!("{msg}"))
110    }
111
112    /// Get the base directed graph.
113    pub fn graph(&self) -> &DirectedGraph {
114        &self.graph
115    }
116
117    /// Get the candidate augmenting arcs.
118    pub fn candidate_arcs(&self) -> &[(usize, usize, W)] {
119        &self.candidate_arcs
120    }
121
122    /// Get the upper bound on the total added weight.
123    pub fn bound(&self) -> &W::Sum {
124        &self.bound
125    }
126
127    /// Smallest h >= 1 bounding candidate-weight and budget magnitudes strictly by 2^h.
128    pub fn max_numeric_magnitude_bits(&self) -> u64 {
129        crate::types::max_numeric_magnitude_bits(
130            self.candidate_arcs
131                .iter()
132                .map(|(_, _, weight)| weight.to_sum())
133                .chain(std::iter::once(self.bound.clone())),
134        )
135    }
136
137    /// Get the number of vertices in the base graph.
138    pub fn num_vertices(&self) -> usize {
139        self.graph.num_vertices()
140    }
141
142    /// Get the number of arcs in the base graph.
143    pub fn num_arcs(&self) -> usize {
144        self.graph.num_arcs()
145    }
146
147    /// Get the number of potential augmenting arcs.
148    pub fn num_potential_arcs(&self) -> usize {
149        self.candidate_arcs.len()
150    }
151
152    /// Check whether the problem uses non-unit weights.
153    pub fn is_weighted(&self) -> bool {
154        !W::IS_UNIT
155    }
156
157    /// Check whether a configuration is a satisfying augmentation.
158    pub fn is_valid_solution(
159        &self,
160        config: &[bool],
161    ) -> Result<bool, crate::traits::EvaluationError> {
162        self.evaluate_config(config)
163    }
164
165    fn evaluate_config(&self, config: &[bool]) -> Result<bool, crate::traits::EvaluationError> {
166        if config.len() != self.candidate_arcs.len() {
167            return Ok(false);
168        }
169
170        let mut total = W::Sum::zero();
171        let mut augmented_arcs = self.graph.arcs();
172
173        for ((u, v, weight), &selected) in self.candidate_arcs.iter().zip(config.iter()) {
174            if selected {
175                total = W::checked_add_to_sum(
176                    total,
177                    weight.to_sum(),
178                    "summing strong-connectivity augmentation weights",
179                )?;
180                if total > self.bound {
181                    return Ok(false);
182                }
183                augmented_arcs.push((*u, *v));
184            }
185        }
186
187        Ok(DirectedGraph::new(self.graph.num_vertices(), augmented_arcs).is_strongly_connected())
188    }
189}
190
191impl<W> Problem for StrongConnectivityAugmentation<W>
192where
193    W: WeightElement + crate::variant::VariantParam,
194{
195    const NAME: &'static str = "StrongConnectivityAugmentation";
196    type Solution = Vec<bool>;
197    type Value = crate::types::Or;
198
199    crate::problem_parameters![
200        ("max_numeric_magnitude_bits", max_numeric_magnitude_bits),
201        ("num_arcs", num_arcs),
202        ("num_potential_arcs", num_potential_arcs),
203        ("num_vertices", num_vertices),
204    ];
205
206    fn variant() -> Vec<(&'static str, &'static str)> {
207        crate::variant_params![W]
208    }
209
210    fn evaluate(
211        &self,
212        config: &Self::Solution,
213    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
214        if config.len() != self.candidate_arcs.len() {
215            return Err(crate::traits::EvaluationError::InvalidConfiguration(
216                "arc-selection length does not match the candidate arcs".into(),
217            ));
218        }
219        Ok(crate::types::Or(self.evaluate_config(config)?))
220    }
221}
222
223impl<W> crate::solvers::BruteForceProblem for StrongConnectivityAugmentation<W>
224where
225    W: WeightElement + crate::variant::VariantParam,
226{
227    fn dimensions(&self) -> Vec<usize> {
228        vec![2; self.candidate_arcs.len()]
229    }
230}
231
232crate::declare_variants! {
233    default StrongConnectivityAugmentation<i64> => "2^num_potential_arcs",
234}
235
236crate::register_brute_force! {
237    StrongConnectivityAugmentation<i64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
238}
239
240#[derive(Deserialize)]
241struct StrongConnectivityAugmentationData<W: WeightElement> {
242    graph: DirectedGraph,
243    candidate_arcs: Vec<(usize, usize, W)>,
244    bound: W::Sum,
245}
246
247impl<'de, W> Deserialize<'de> for StrongConnectivityAugmentation<W>
248where
249    W: WeightElement + Deserialize<'de>,
250    W::Sum: Deserialize<'de>,
251{
252    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
253    where
254        D: Deserializer<'de>,
255    {
256        let data = StrongConnectivityAugmentationData::<W>::deserialize(deserializer)?;
257        Self::try_new(data.graph, data.candidate_arcs, data.bound).map_err(serde::de::Error::custom)
258    }
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: "strong_connectivity_augmentation",
265        // Path digraph 0→1→2→3→4 (not strongly connected — no back-edges).
266        // Nine candidate arcs are all individually affordable, but only the
267        // pair (4→1, w=3) + (1→0, w=5) = 8 = B achieves strong connectivity.
268        instance: Box::new(StrongConnectivityAugmentation::new(
269            DirectedGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]),
270            vec![
271                (4, 0, 10), // direct fix, too expensive
272                (4, 3, 3),  // 4-escape to dead end
273                (4, 2, 3),  // 4-escape to dead end
274                (4, 1, 3),  // correct 4-escape
275                (3, 0, 7),  // too expensive to combine
276                (3, 1, 3),  // dead-end intermediate
277                (2, 0, 7),  // too expensive to combine
278                (2, 1, 3),  // dead-end intermediate
279                (1, 0, 5),  // the closing arc
280            ],
281            8,
282        )),
283        optimal_config: serde_json::json!(vec![
284            false, false, false, true, false, false, false, false, true
285        ]),
286        optimal_value: serde_json::json!(true),
287    }]
288}
289
290#[cfg(test)]
291#[path = "../../unit_tests/models/graph/strong_connectivity_augmentation.rs"]
292mod tests;