Skip to main content

problemreductions/models/graph/
multiple_choice_branching.rs

1//! Multiple Choice Branching problem implementation.
2//!
3//! Given a directed graph with arc weights, a partition of the arcs, and a
4//! threshold, determine whether there exists a high-weight branching that
5//! picks at most one arc from each partition group.
6
7use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension};
8use crate::topology::DirectedGraph;
9use crate::traits::Problem;
10use crate::types::WeightElement;
11use num_traits::Zero;
12use serde::de::Error as _;
13use serde::{Deserialize, Serialize};
14
15inventory::submit! {
16    ProblemSchemaEntry {
17        name: "MultipleChoiceBranching",
18        display_name: "Multiple Choice Branching",
19        aliases: &[],
20        dimensions: &[
21            VariantDimension::new("weight", "i32", &["i32"]),
22        ],
23        module_path: module_path!(),
24        description: "Find a branching with partition constraints and weight at least K",
25        fields: &[
26            FieldInfo { name: "graph", type_name: "DirectedGraph", description: "The directed graph G=(V,A)" },
27            FieldInfo { name: "weights", type_name: "Vec<W>", description: "Arc weights w(a) for each arc a in A" },
28            FieldInfo { name: "partition", type_name: "Vec<Vec<usize>>", description: "Partition of arc indices; each arc index must appear in exactly one group" },
29            FieldInfo { name: "threshold", type_name: "W::Sum", description: "Weight threshold K" },
30        ],
31    }
32}
33
34/// The Multiple Choice Branching problem.
35///
36/// Given a directed graph G = (V, A), arc weights w(a), a partition of A into
37/// disjoint groups A_1, ..., A_m, and a threshold K, determine whether there
38/// exists a subset A' of arcs such that:
39/// - the selected arcs have total weight at least K
40/// - every vertex has in-degree at most one in the selected subgraph
41/// - the selected subgraph is acyclic
42/// - at most one arc is selected from each partition group
43#[derive(Debug, Clone, Serialize)]
44pub struct MultipleChoiceBranching<W: WeightElement> {
45    graph: DirectedGraph,
46    weights: Vec<W>,
47    partition: Vec<Vec<usize>>,
48    threshold: W::Sum,
49}
50
51#[derive(Debug, Deserialize)]
52struct MultipleChoiceBranchingUnchecked<W: WeightElement> {
53    graph: DirectedGraph,
54    weights: Vec<W>,
55    partition: Vec<Vec<usize>>,
56    threshold: W::Sum,
57}
58
59impl<'de, W> Deserialize<'de> for MultipleChoiceBranching<W>
60where
61    W: WeightElement + Deserialize<'de>,
62    W::Sum: Deserialize<'de>,
63{
64    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
65    where
66        D: serde::Deserializer<'de>,
67    {
68        let unchecked = MultipleChoiceBranchingUnchecked::<W>::deserialize(deserializer)?;
69        let num_arcs = unchecked.graph.num_arcs();
70        if unchecked.weights.len() != num_arcs {
71            return Err(D::Error::custom(format!(
72                "weights length must match graph num_arcs (expected {num_arcs}, got {})",
73                unchecked.weights.len()
74            )));
75        }
76        if let Some(message) = partition_validation_error(&unchecked.partition, num_arcs) {
77            return Err(D::Error::custom(message));
78        }
79
80        Ok(Self {
81            graph: unchecked.graph,
82            weights: unchecked.weights,
83            partition: unchecked.partition,
84            threshold: unchecked.threshold,
85        })
86    }
87}
88
89impl<W: WeightElement> MultipleChoiceBranching<W> {
90    /// Create a new Multiple Choice Branching instance.
91    pub fn new(
92        graph: DirectedGraph,
93        weights: Vec<W>,
94        partition: Vec<Vec<usize>>,
95        threshold: W::Sum,
96    ) -> Self {
97        let num_arcs = graph.num_arcs();
98        assert_eq!(
99            weights.len(),
100            num_arcs,
101            "weights length must match graph num_arcs"
102        );
103        validate_partition(&partition, num_arcs);
104        Self {
105            graph,
106            weights,
107            partition,
108            threshold,
109        }
110    }
111
112    /// Get the underlying directed graph.
113    pub fn graph(&self) -> &DirectedGraph {
114        &self.graph
115    }
116
117    /// Get the arc weights.
118    pub fn weights(&self) -> &[W] {
119        &self.weights
120    }
121
122    /// Replace the arc weights.
123    pub fn set_weights(&mut self, weights: Vec<W>) {
124        assert_eq!(
125            weights.len(),
126            self.graph.num_arcs(),
127            "weights length must match graph num_arcs"
128        );
129        self.weights = weights;
130    }
131
132    /// Check whether this problem uses a non-unit weight type.
133    pub fn is_weighted(&self) -> bool {
134        !W::IS_UNIT
135    }
136
137    /// Get the partition groups.
138    pub fn partition(&self) -> &[Vec<usize>] {
139        &self.partition
140    }
141
142    /// Get the threshold K.
143    pub fn threshold(&self) -> &W::Sum {
144        &self.threshold
145    }
146
147    /// Get the number of vertices.
148    pub fn num_vertices(&self) -> usize {
149        self.graph.num_vertices()
150    }
151
152    /// Get the number of arcs.
153    pub fn num_arcs(&self) -> usize {
154        self.graph.num_arcs()
155    }
156
157    /// Get the number of partition groups.
158    pub fn num_partition_groups(&self) -> usize {
159        self.partition.len()
160    }
161
162    /// Check whether a configuration is a satisfying solution.
163    pub fn is_valid_solution(&self, config: &[usize]) -> bool {
164        is_valid_multiple_choice_branching(
165            &self.graph,
166            &self.weights,
167            &self.partition,
168            &self.threshold,
169            config,
170        )
171    }
172}
173
174impl<W> Problem for MultipleChoiceBranching<W>
175where
176    W: WeightElement + crate::variant::VariantParam,
177{
178    const NAME: &'static str = "MultipleChoiceBranching";
179    type Value = crate::types::Or;
180
181    fn variant() -> Vec<(&'static str, &'static str)> {
182        crate::variant_params![W]
183    }
184
185    fn dims(&self) -> Vec<usize> {
186        vec![2; self.graph.num_arcs()]
187    }
188
189    fn evaluate(&self, config: &[usize]) -> crate::types::Or {
190        crate::types::Or({
191            is_valid_multiple_choice_branching(
192                &self.graph,
193                &self.weights,
194                &self.partition,
195                &self.threshold,
196                config,
197            )
198        })
199    }
200}
201
202fn validate_partition(partition: &[Vec<usize>], num_arcs: usize) {
203    if let Some(message) = partition_validation_error(partition, num_arcs) {
204        panic!("{message}");
205    }
206}
207
208fn partition_validation_error(partition: &[Vec<usize>], num_arcs: usize) -> Option<String> {
209    let mut seen = vec![false; num_arcs];
210    for group in partition {
211        for &arc_index in group {
212            if arc_index >= num_arcs {
213                return Some(format!(
214                    "partition arc index {} out of range for {} arcs",
215                    arc_index, num_arcs
216                ));
217            }
218            if seen[arc_index] {
219                return Some(format!(
220                    "partition arc index {} appears more than once",
221                    arc_index
222                ));
223            }
224            seen[arc_index] = true;
225        }
226    }
227    if seen.iter().all(|present| *present) {
228        None
229    } else {
230        Some("partition must cover every arc exactly once".to_string())
231    }
232}
233
234fn is_valid_multiple_choice_branching<W: WeightElement>(
235    graph: &DirectedGraph,
236    weights: &[W],
237    partition: &[Vec<usize>],
238    threshold: &W::Sum,
239    config: &[usize],
240) -> bool {
241    if config.len() != graph.num_arcs() {
242        return false;
243    }
244    if config.iter().any(|&value| value >= 2) {
245        return false;
246    }
247
248    for group in partition {
249        if group
250            .iter()
251            .filter(|&&arc_index| config[arc_index] == 1)
252            .count()
253            > 1
254        {
255            return false;
256        }
257    }
258
259    let arcs = graph.arcs();
260    let mut in_degree = vec![0usize; graph.num_vertices()];
261    let mut selected_successors = vec![Vec::new(); graph.num_vertices()];
262    let mut total = W::Sum::zero();
263    for (index, &selected) in config.iter().enumerate() {
264        if selected == 1 {
265            let (source, target) = arcs[index];
266            in_degree[target] += 1;
267            if in_degree[target] > 1 {
268                return false;
269            }
270            selected_successors[source].push(target);
271            total += weights[index].to_sum();
272        }
273    }
274
275    if total < *threshold {
276        return false;
277    }
278
279    let mut queue: Vec<usize> = (0..graph.num_vertices())
280        .filter(|&vertex| in_degree[vertex] == 0)
281        .collect();
282    let mut visited = 0usize;
283    while let Some(source) = queue.pop() {
284        visited += 1;
285        for &target in &selected_successors[source] {
286            in_degree[target] -= 1;
287            if in_degree[target] == 0 {
288                queue.push(target);
289            }
290        }
291    }
292
293    visited == graph.num_vertices()
294}
295
296crate::declare_variants! {
297    default MultipleChoiceBranching<i32> => "2^num_arcs",
298}
299
300#[cfg(feature = "example-db")]
301pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
302    vec![crate::example_db::specs::ModelExampleSpec {
303        id: "multiple_choice_branching_i32",
304        instance: Box::new(MultipleChoiceBranching::new(
305            DirectedGraph::new(
306                6,
307                vec![
308                    (0, 1),
309                    (0, 2),
310                    (1, 3),
311                    (2, 3),
312                    (1, 4),
313                    (3, 5),
314                    (4, 5),
315                    (2, 4),
316                ],
317            ),
318            vec![3, 2, 4, 1, 2, 3, 1, 3],
319            vec![vec![0, 1], vec![2, 3], vec![4, 7], vec![5, 6]],
320            10,
321        )),
322        optimal_config: vec![1, 0, 1, 0, 0, 1, 0, 1],
323        optimal_value: serde_json::json!(true),
324    }]
325}
326
327#[cfg(test)]
328#[path = "../../unit_tests/models/graph/multiple_choice_branching.rs"]
329mod tests;