Skip to main content

problemreductions/models/graph/
longest_circuit.rs

1//! Longest Circuit problem implementation.
2//!
3//! The Longest Circuit problem asks for a simple circuit in a graph
4//! that maximizes the total edge length.
5
6use crate::registry::{CreateSpec, ProblemSchemaEntry, VariantDimension};
7use crate::topology::{Graph, SimpleGraph};
8use crate::traits::Problem;
9use crate::types::{Max, WeightElement};
10use num_traits::Zero;
11use serde::{Deserialize, Serialize};
12use std::collections::VecDeque;
13
14inventory::submit! {
15    ProblemSchemaEntry {
16        name: "LongestCircuit",
17        display_name: "Longest Circuit",
18        aliases: &[],
19        dimensions: &[
20            VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]),
21            VariantDimension::new("weight", "i64", &["i64"]),
22        ],
23        category: crate::registry::ProblemCategory::Graph,
24        module_path: module_path!(),
25        description: "Find a simple circuit in a graph that maximizes total edge length",
26        fields: LongestCircuitCreateSpec::FIELDS,
27    }
28}
29
30/// The Longest Circuit problem.
31///
32/// Given an undirected graph `G = (V, E)` with positive edge lengths `l(e)`,
33/// find a simple circuit in `G` that maximizes the total edge-length sum.
34///
35/// # Representation
36///
37/// Each edge is assigned a binary variable:
38/// - `0`: edge is not in the circuit
39/// - `1`: edge is in the circuit
40///
41/// A valid configuration must select edges that form exactly one connected
42/// simple circuit using only edges from `graph`.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(try_from = "LongestCircuitData<G, W>")]
45#[serde(bound(deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>"))]
46pub struct LongestCircuit<G, W: WeightElement> {
47    graph: G,
48    edge_lengths: Vec<W>,
49}
50
51#[derive(Deserialize)]
52#[serde(bound(deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>"))]
53struct LongestCircuitData<G, W: WeightElement> {
54    graph: G,
55    edge_lengths: Vec<W>,
56}
57
58impl<G, W> TryFrom<LongestCircuitData<G, W>> for LongestCircuit<G, W>
59where
60    G: Graph,
61    W: WeightElement,
62{
63    type Error = crate::registry::ConstructionError;
64
65    fn try_from(data: LongestCircuitData<G, W>) -> Result<Self, Self::Error> {
66        Self::try_new(data.graph, data.edge_lengths)
67    }
68}
69
70#[derive(Debug, Deserialize, crate::CreateSpec)]
71struct LongestCircuitCreateSpec {
72    #[create(codec = "edge-list")]
73    graph: Vec<(usize, usize)>,
74    num_vertices: Option<usize>,
75    #[create(codec = "comma-separated")]
76    edge_weights: Option<Vec<i64>>,
77}
78
79impl TryFrom<LongestCircuitCreateSpec> for LongestCircuit<SimpleGraph, i64> {
80    type Error = crate::registry::ConstructionError;
81
82    fn try_from(spec: LongestCircuitCreateSpec) -> Result<Self, Self::Error> {
83        let graph = simple_graph_from_create(spec.graph, spec.num_vertices)?;
84        let edge_lengths = spec
85            .edge_weights
86            .unwrap_or_else(|| vec![1; graph.num_edges()]);
87        Self::try_new(graph, edge_lengths)
88    }
89}
90
91fn simple_graph_from_create(
92    edges: Vec<(usize, usize)>,
93    num_vertices: Option<usize>,
94) -> Result<SimpleGraph, crate::registry::ConstructionError> {
95    if edges.is_empty() && num_vertices.is_none() {
96        return Err("num_vertices is required for an empty graph"
97            .to_string()
98            .into());
99    }
100    for (index, &(u, v)) in edges.iter().enumerate() {
101        if u == v {
102            return Err(format!("graph edge {index} is a self-loop at vertex {u}").into());
103        }
104    }
105    let inferred = edges
106        .iter()
107        .flat_map(|&(u, v)| [u, v])
108        .max()
109        .map(|vertex| vertex.checked_add(1).ok_or("vertex count overflows usize"))
110        .transpose()?
111        .unwrap_or(0);
112    let num_vertices = num_vertices.unwrap_or(inferred);
113    if num_vertices < inferred {
114        return Err(format!(
115            "num_vertices {num_vertices} is too small for graph endpoints; need at least {inferred}"
116        )
117        .into());
118    }
119    Ok(SimpleGraph::new(num_vertices, edges))
120}
121
122impl<G: Graph, W: WeightElement> LongestCircuit<G, W> {
123    /// Create a new LongestCircuit instance.
124    ///
125    /// # Panics
126    ///
127    /// Panics if the number of edge lengths does not match the graph's edge
128    /// count, or if any edge length is non-positive.
129    pub fn new(graph: G, edge_lengths: Vec<W>) -> Self {
130        Self::try_new(graph, edge_lengths).unwrap_or_else(|error| panic!("{error}"))
131    }
132
133    fn try_new(graph: G, edge_lengths: Vec<W>) -> Result<Self, crate::registry::ConstructionError> {
134        Self::check_weights(&graph, &edge_lengths)?;
135        Ok(Self {
136            graph,
137            edge_lengths,
138        })
139    }
140
141    /// Get a reference to the underlying graph.
142    pub fn graph(&self) -> &G {
143        &self.graph
144    }
145
146    /// Get the edge lengths.
147    pub fn edge_lengths(&self) -> &[W] {
148        &self.edge_lengths
149    }
150
151    /// Replace the edge lengths.
152    pub fn set_lengths(&mut self, edge_lengths: Vec<W>) {
153        Self::check_weights(&self.graph, &edge_lengths).unwrap_or_else(|error| panic!("{error}"));
154        self.edge_lengths = edge_lengths;
155    }
156
157    fn check_weights(graph: &G, weights: &[W]) -> Result<(), crate::registry::ConstructionError> {
158        if weights.len() != graph.num_edges() {
159            return Err(crate::registry::ConstructionError::length_mismatch(
160                "weights",
161                weights.len(),
162                graph.num_edges(),
163            ));
164        }
165        if !weights
166            .iter()
167            .all(|weight| weight.to_sum() > W::Sum::zero())
168        {
169            return Err("All edge lengths must be positive (> 0)".into());
170        }
171        Ok(())
172    }
173
174    /// Replace the edge lengths via the generic weight-management naming.
175    pub fn set_weights(&mut self, weights: Vec<W>) {
176        self.set_lengths(weights);
177    }
178
179    /// Get the edge lengths as a cloned vector.
180    pub fn weights(&self) -> Vec<W> {
181        self.edge_lengths.clone()
182    }
183
184    /// Smallest h >= 1 with every edge length strictly below 2^h.
185    pub fn max_length_bits(&self) -> u64 {
186        crate::types::max_numeric_magnitude_bits(
187            self.edge_lengths.iter().map(|length| length.to_sum()),
188        )
189    }
190
191    /// Get the number of vertices in the graph.
192    pub fn num_vertices(&self) -> usize {
193        self.graph.num_vertices()
194    }
195
196    /// Get the number of edges in the graph.
197    pub fn num_edges(&self) -> usize {
198        self.graph.num_edges()
199    }
200
201    /// Check if the problem uses a non-unit weight type.
202    pub fn is_weighted(&self) -> bool {
203        !W::IS_UNIT
204    }
205
206    /// Check whether a configuration is a valid simple circuit.
207    pub fn is_valid_solution(&self, config: &[bool]) -> bool {
208        is_simple_circuit(&self.graph, config)
209    }
210}
211
212impl<G, W> Problem for LongestCircuit<G, W>
213where
214    G: Graph + crate::variant::VariantParam,
215    W: WeightElement + crate::variant::VariantParam,
216{
217    const NAME: &'static str = "LongestCircuit";
218    type Solution = Vec<bool>;
219    type Value = Max<W::Sum>;
220
221    crate::problem_parameters![
222        ("max_length_bits", max_length_bits),
223        ("num_edges", num_edges),
224        ("num_vertices", num_vertices),
225    ];
226
227    fn variant() -> Vec<(&'static str, &'static str)> {
228        crate::variant_params![G, W]
229    }
230
231    fn evaluate(
232        &self,
233        config: &Self::Solution,
234    ) -> Result<Max<W::Sum>, crate::traits::EvaluationError> {
235        if config.len() != self.graph.num_edges() {
236            return Err(crate::traits::EvaluationError::InvalidConfiguration(
237                "edge-selection length does not match the graph".into(),
238            ));
239        }
240        Ok({
241            if !is_simple_circuit(&self.graph, config) {
242                return Ok(Max(None));
243            }
244            let mut total = W::Sum::zero();
245            for (idx, &selected) in config.iter().enumerate() {
246                if selected {
247                    total = W::checked_add_to_sum(
248                        total,
249                        self.edge_lengths[idx].to_sum(),
250                        "summing circuit edge lengths",
251                    )?;
252                }
253            }
254            Max(Some(total))
255        })
256    }
257}
258
259impl<G, W> crate::solvers::BruteForceProblem for LongestCircuit<G, W>
260where
261    G: Graph + crate::variant::VariantParam,
262    W: WeightElement + crate::variant::VariantParam,
263{
264    fn dimensions(&self) -> Vec<usize> {
265        vec![2; self.graph.num_edges()]
266    }
267}
268
269/// Check whether a binary edge-selection encodes exactly one simple circuit.
270pub(crate) fn is_simple_circuit<G: Graph>(graph: &G, config: &[bool]) -> bool {
271    if config.len() != graph.num_edges() {
272        return false;
273    }
274
275    let edges = graph.edges();
276    let n = graph.num_vertices();
277    let mut degree = vec![0usize; n];
278    let mut adjacency = vec![Vec::new(); n];
279    let mut selected_count = 0usize;
280    let mut start = None;
281
282    for (idx, &selected) in config.iter().enumerate() {
283        if !selected {
284            continue;
285        }
286        let (u, v) = edges[idx];
287        degree[u] += 1;
288        degree[v] += 1;
289        adjacency[u].push(v);
290        adjacency[v].push(u);
291        selected_count += 1;
292        if start.is_none() {
293            start = Some(u);
294        }
295    }
296
297    if selected_count < 3 {
298        return false;
299    }
300
301    let selected_vertices: Vec<usize> = degree
302        .iter()
303        .enumerate()
304        .filter_map(|(vertex, &deg)| (deg > 0).then_some(vertex))
305        .collect();
306
307    if selected_vertices.is_empty() || selected_vertices.iter().any(|&vertex| degree[vertex] != 2) {
308        return false;
309    }
310
311    let start = match start {
312        Some(vertex) => vertex,
313        None => return false,
314    };
315
316    let mut visited = vec![false; n];
317    let mut queue = VecDeque::new();
318    visited[start] = true;
319    queue.push_back(start);
320    let mut visited_selected_vertices = 0usize;
321
322    while let Some(vertex) = queue.pop_front() {
323        visited_selected_vertices += 1;
324        for &neighbor in &adjacency[vertex] {
325            if !visited[neighbor] {
326                visited[neighbor] = true;
327                queue.push_back(neighbor);
328            }
329        }
330    }
331
332    visited_selected_vertices == selected_vertices.len()
333}
334
335#[cfg(feature = "example-db")]
336pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
337    vec![crate::example_db::specs::ModelExampleSpec {
338        id: "longest_circuit_simplegraph",
339        instance: Box::new(LongestCircuit::new(
340            SimpleGraph::new(
341                6,
342                vec![
343                    (0, 1),
344                    (1, 2),
345                    (2, 3),
346                    (3, 4),
347                    (4, 5),
348                    (5, 0),
349                    (0, 3),
350                    (1, 4),
351                    (2, 5),
352                    (3, 5),
353                ],
354            ),
355            vec![3, 2, 4, 1, 5, 2, 3, 2, 1, 2],
356        )),
357        optimal_config: serde_json::json!(vec![
358            true, false, true, false, true, false, true, true, true, false
359        ]),
360        optimal_value: serde_json::json!(18),
361    }]
362}
363
364crate::impl_random_generate!(LongestCircuit<SimpleGraph, i64>, crate::random::SimpleGraphRandomSpec, |spec| {
365    let graph = spec.graph()?;
366    let lengths = vec![1; graph.num_edges()];
367    Ok(LongestCircuit::new(graph, lengths))
368});
369
370crate::declare_variants! {
371    default LongestCircuit<SimpleGraph, i64> => "2^num_vertices * num_vertices^2" create LongestCircuitCreateSpec random,
372}
373
374crate::register_brute_force! {
375    LongestCircuit<SimpleGraph, i64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
376}
377
378#[cfg(test)]
379#[path = "../../unit_tests/models/graph/longest_circuit.rs"]
380mod tests;
381
382crate::decision_problem_meta!(LongestCircuit<SimpleGraph, i64>, "DecisionLongestCircuit");
383crate::register_decision_variant!(
384    LongestCircuit<SimpleGraph, i64>, "DecisionLongestCircuit", "2^num_vertices * num_vertices^2", &[],
385    "Does a feasible solution have objective value >= the bound?",
386    category: crate::registry::ProblemCategory::Graph,
387    dims: [
388            VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]),
389            VariantDimension::new("weight", "i64", &["i64"]),
390        ],
391    fields: [
392        crate::registry::FieldInfo { name: "graph", type_name: "Vec<(usize,usize)>", description: "Graph edges as comma-separated vertex pairs." },
393        crate::registry::FieldInfo { name: "num_vertices", type_name: "usize", description: "Number of vertices, including isolated vertices." },
394        crate::registry::FieldInfo { name: "edge_weights", type_name: "Vec<i64>", description: "Weights for each edge in graph order." },
395        crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Accept objective values >= this bound" },
396    ],
397    decode: |_, indices: Vec<usize>| crate::config::config_to_bits(&indices)
398);
399
400#[cfg(feature = "example-db")]
401pub(crate) fn decision_canonical_rule_example_specs(
402) -> Vec<crate::example_db::specs::RuleExampleSpec> {
403    vec![crate::example_db::specs::RuleExampleSpec {
404        id: "decision_longest_circuit_to_longest_circuit",
405        build: || {
406            let source = crate::models::decision::Decision::new(
407                LongestCircuit::new(
408                    SimpleGraph::new(
409                        6,
410                        vec![
411                            (0, 1),
412                            (1, 2),
413                            (2, 3),
414                            (3, 4),
415                            (4, 5),
416                            (5, 0),
417                            (0, 3),
418                            (1, 4),
419                            (2, 5),
420                            (3, 5),
421                        ],
422                    ),
423                    vec![3, 2, 4, 1, 5, 2, 3, 2, 1, 2],
424                ),
425                18,
426            );
427            let witness = serde_json::json!(vec![
428                true, false, true, false, true, false, true, true, true, false
429            ]);
430            crate::example_db::specs::rule_example_with_witness::<_, LongestCircuit<SimpleGraph, i64>>(
431                source,
432                crate::export::SolutionPair {
433                    source_config: witness.clone(),
434                    target_config: witness,
435                },
436            )
437        },
438    }]
439}