Skip to main content

problemreductions/models/graph/
length_bounded_disjoint_paths.rs

1//! Length-Bounded Disjoint Paths problem implementation.
2//!
3//! The problem maximizes the number of internally vertex-disjoint `s-t` paths,
4//! each using at most `K` edges, over up to `max_paths` path slots.
5
6use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension};
7use crate::topology::{Graph, SimpleGraph};
8use crate::traits::Problem;
9use crate::types::Max;
10use crate::variant::VariantParam;
11use serde::{Deserialize, Serialize};
12
13inventory::submit! {
14    ProblemSchemaEntry {
15        name: "LengthBoundedDisjointPaths",
16        display_name: "Length-Bounded Disjoint Paths",
17        aliases: &[],
18        dimensions: &[
19            VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]),
20        ],
21        module_path: module_path!(),
22        description: "Maximize the number of internally vertex-disjoint s-t paths of length at most K",
23        fields: &[
24            FieldInfo { name: "graph", type_name: "G", description: "The underlying graph G=(V,E)" },
25            FieldInfo { name: "source", type_name: "usize", description: "The shared source vertex s" },
26            FieldInfo { name: "sink", type_name: "usize", description: "The shared sink vertex t" },
27            FieldInfo { name: "max_paths", type_name: "usize", description: "Upper bound on the number of path slots" },
28            FieldInfo { name: "max_length", type_name: "usize", description: "Maximum path length K in edges" },
29        ],
30    }
31}
32
33/// Length-Bounded Disjoint Paths on an undirected graph.
34///
35/// A configuration uses `max_paths * |V|` binary choices. For each path slot
36/// `j` and vertex `v`, `x_{j,v} = 1` means that `v` belongs to slot `j`'s
37/// path. Each non-empty slot must induce a simple `s-t` path, and the internal
38/// vertices of different slots must be disjoint. Empty slots (all zeros) are
39/// unused and do not count toward the objective. The objective is to maximize
40/// the number of non-empty valid path slots.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(bound(deserialize = "G: serde::Deserialize<'de>"))]
43pub struct LengthBoundedDisjointPaths<G> {
44    graph: G,
45    source: usize,
46    sink: usize,
47    max_paths: usize,
48    max_length: usize,
49}
50
51impl<G: Graph> LengthBoundedDisjointPaths<G> {
52    /// Create a new Length-Bounded Disjoint Paths instance.
53    ///
54    /// The `max_paths` upper bound is computed automatically as
55    /// `min(deg(source), deg(sink))`.
56    ///
57    /// # Panics
58    ///
59    /// Panics if `source` or `sink` is not a valid graph vertex, if `source ==
60    /// sink`, or if `max_length == 0`.
61    pub fn new(graph: G, source: usize, sink: usize, max_length: usize) -> Self {
62        assert!(
63            source < graph.num_vertices(),
64            "source must be a valid graph vertex"
65        );
66        assert!(
67            sink < graph.num_vertices(),
68            "sink must be a valid graph vertex"
69        );
70        assert_ne!(source, sink, "source and sink must be distinct");
71        assert!(max_length > 0, "max_length must be positive");
72        let deg_s = graph.neighbors(source).len();
73        let deg_t = graph.neighbors(sink).len();
74        let max_paths = deg_s.min(deg_t);
75        Self {
76            graph,
77            source,
78            sink,
79            max_paths,
80            max_length,
81        }
82    }
83
84    /// Get a reference to the underlying graph.
85    pub fn graph(&self) -> &G {
86        &self.graph
87    }
88
89    /// Get the shared source vertex.
90    pub fn source(&self) -> usize {
91        self.source
92    }
93
94    /// Get the shared sink vertex.
95    pub fn sink(&self) -> usize {
96        self.sink
97    }
98
99    /// Get the upper bound on the number of path slots.
100    pub fn max_paths(&self) -> usize {
101        self.max_paths
102    }
103
104    /// Get the maximum permitted path length in edges.
105    pub fn max_length(&self) -> usize {
106        self.max_length
107    }
108
109    /// Get the number of vertices in the graph.
110    pub fn num_vertices(&self) -> usize {
111        self.graph.num_vertices()
112    }
113
114    /// Get the number of edges in the graph.
115    pub fn num_edges(&self) -> usize {
116        self.graph.num_edges()
117    }
118}
119
120impl<G> Problem for LengthBoundedDisjointPaths<G>
121where
122    G: Graph + VariantParam,
123{
124    const NAME: &'static str = "LengthBoundedDisjointPaths";
125    type Value = Max<usize>;
126
127    fn variant() -> Vec<(&'static str, &'static str)> {
128        crate::variant_params![G]
129    }
130
131    fn dims(&self) -> Vec<usize> {
132        vec![2; self.max_paths * self.graph.num_vertices()]
133    }
134
135    fn evaluate(&self, config: &[usize]) -> Max<usize> {
136        validate_path_collection(
137            &self.graph,
138            self.source,
139            self.sink,
140            self.max_paths,
141            self.max_length,
142            config,
143        )
144    }
145}
146
147/// Validate a path collection and return the number of valid non-empty paths,
148/// or `None` if any non-empty slot is structurally invalid.
149fn validate_path_collection<G: Graph>(
150    graph: &G,
151    source: usize,
152    sink: usize,
153    max_paths: usize,
154    max_length: usize,
155    config: &[usize],
156) -> Max<usize> {
157    let num_vertices = graph.num_vertices();
158    if config.len() != max_paths * num_vertices {
159        return Max(None);
160    }
161    if config.iter().any(|&value| value > 1) {
162        return Max(None);
163    }
164
165    let mut used_internal = vec![false; num_vertices];
166    let mut used_direct_path = false;
167    let mut count = 0usize;
168    for slot in config.chunks(num_vertices) {
169        // Check if slot is empty (all zeros)
170        if slot.iter().all(|&v| v == 0) {
171            continue;
172        }
173        if !is_valid_path_slot(
174            graph,
175            source,
176            sink,
177            max_length,
178            slot,
179            &mut used_internal,
180            &mut used_direct_path,
181        ) {
182            return Max(None);
183        }
184        count += 1;
185    }
186    Max(Some(count))
187}
188
189fn is_valid_path_slot<G: Graph>(
190    graph: &G,
191    source: usize,
192    sink: usize,
193    max_length: usize,
194    slot: &[usize],
195    used_internal: &mut [bool],
196    used_direct_path: &mut bool,
197) -> bool {
198    if slot.len() != graph.num_vertices()
199        || slot.get(source) != Some(&1)
200        || slot.get(sink) != Some(&1)
201    {
202        return false;
203    }
204
205    let selected = slot
206        .iter()
207        .enumerate()
208        .filter_map(|(vertex, &chosen)| (chosen == 1).then_some(vertex))
209        .collect::<Vec<_>>();
210    if selected.len() < 2 {
211        return false;
212    }
213
214    let mut in_path = vec![false; graph.num_vertices()];
215    for &vertex in &selected {
216        in_path[vertex] = true;
217        if vertex != source && vertex != sink && used_internal[vertex] {
218            return false;
219        }
220    }
221
222    let mut degree_sum = 0usize;
223    for &vertex in &selected {
224        let degree = graph
225            .neighbors(vertex)
226            .into_iter()
227            .filter(|&neighbor| in_path[neighbor])
228            .count();
229        degree_sum += degree;
230
231        if vertex == source || vertex == sink {
232            if degree != 1 {
233                return false;
234            }
235        } else if degree != 2 {
236            return false;
237        }
238    }
239
240    let edge_count = degree_sum / 2;
241    if edge_count + 1 != selected.len() || edge_count > max_length {
242        return false;
243    }
244    if edge_count == 1 {
245        if *used_direct_path {
246            return false;
247        }
248        *used_direct_path = true;
249    }
250
251    let mut seen = vec![false; graph.num_vertices()];
252    let mut stack = vec![source];
253    seen[source] = true;
254    let mut seen_count = 0usize;
255    while let Some(vertex) = stack.pop() {
256        seen_count += 1;
257        for neighbor in graph.neighbors(vertex) {
258            if in_path[neighbor] && !seen[neighbor] {
259                seen[neighbor] = true;
260                stack.push(neighbor);
261            }
262        }
263    }
264
265    if !seen[sink] || seen_count != selected.len() {
266        return false;
267    }
268
269    for &vertex in &selected {
270        if vertex != source && vertex != sink {
271            used_internal[vertex] = true;
272        }
273    }
274    true
275}
276
277#[cfg(feature = "example-db")]
278fn encode_paths(num_vertices: usize, max_paths: usize, slots: &[&[usize]]) -> Vec<usize> {
279    let mut config = vec![0; num_vertices * max_paths];
280    for (slot_index, slot_vertices) in slots.iter().enumerate() {
281        let offset = slot_index * num_vertices;
282        for &vertex in *slot_vertices {
283            config[offset + vertex] = 1;
284        }
285    }
286    config
287}
288
289#[cfg(feature = "example-db")]
290pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
291    let graph = SimpleGraph::new(5, vec![(0, 1), (1, 4), (0, 2), (2, 4), (0, 3), (3, 4)]);
292    // max_paths = min(deg(0), deg(4)) = min(3, 3) = 3
293    // 3 * 5 = 15 binary variables → 2^15 = 32768 configs (brute-force feasible)
294    // Optimal: 3 disjoint paths [0,1,4], [0,2,4], [0,3,4]
295    vec![crate::example_db::specs::ModelExampleSpec {
296        id: "length_bounded_disjoint_paths_simplegraph",
297        instance: Box::new(LengthBoundedDisjointPaths::new(graph, 0, 4, 3)),
298        optimal_config: encode_paths(5, 3, &[&[0, 1, 4], &[0, 2, 4], &[0, 3, 4]]),
299        optimal_value: serde_json::json!(3),
300    }]
301}
302
303crate::declare_variants! {
304    default LengthBoundedDisjointPaths<SimpleGraph> => "2^(max_paths * num_vertices)",
305}
306
307#[cfg(test)]
308#[path = "../../unit_tests/models/graph/length_bounded_disjoint_paths.rs"]
309mod tests;