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::{CreateSpec, 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", "i64", &["i64"]),
22        ],
23        category: crate::registry::ProblemCategory::Graph,
24        module_path: module_path!(),
25        description: "Find a branching with partition constraints and weight at least K",
26        fields: MultipleChoiceBranchingCreateSpec::FIELDS,
27    }
28}
29
30/// The Multiple Choice Branching problem.
31///
32/// Given a directed graph G = (V, A), arc weights w(a), a partition of A into
33/// disjoint groups A_1, ..., A_m, and a threshold K, determine whether there
34/// exists a subset A' of arcs such that:
35/// - the selected arcs have total weight at least K
36/// - every vertex has in-degree at most one in the selected subgraph
37/// - the selected subgraph is acyclic
38/// - at most one arc is selected from each partition group
39#[derive(Debug, Clone, Serialize)]
40pub struct MultipleChoiceBranching<W: WeightElement> {
41    graph: DirectedGraph,
42    weights: Vec<W>,
43    partition: Vec<Vec<usize>>,
44    threshold: W::Sum,
45}
46
47#[derive(Debug, Deserialize, crate::CreateSpec)]
48struct MultipleChoiceBranchingCreateSpec {
49    /// Directed graph arcs.
50    #[create(codec = "arc-list")]
51    arcs: Vec<(usize, usize)>,
52    /// Vertex count, needed to preserve isolated vertices.
53    num_vertices: Option<usize>,
54    /// Arc weights w(a) for each arc a in A.
55    weights: Vec<i64>,
56    /// Partition of arc indices; each arc must appear exactly once.
57    partition: Vec<Vec<usize>>,
58    /// Weight threshold K.
59    threshold: i64,
60}
61
62impl TryFrom<MultipleChoiceBranchingCreateSpec> for MultipleChoiceBranching<i64> {
63    type Error = crate::registry::ConstructionError;
64    fn try_from(spec: MultipleChoiceBranchingCreateSpec) -> Result<Self, Self::Error> {
65        let inferred = spec
66            .arcs
67            .iter()
68            .flat_map(|&(u, v)| [u, v])
69            .max()
70            .map(|vertex| vertex.checked_add(1).ok_or("vertex count overflows usize"))
71            .transpose()?
72            .unwrap_or(0);
73        let num_vertices = spec.num_vertices.unwrap_or(inferred);
74        if num_vertices < inferred {
75            return Err("num_vertices is too small for arc endpoints"
76                .to_string()
77                .into());
78        }
79        let graph = DirectedGraph::new(num_vertices, spec.arcs);
80        let num_arcs = graph.num_arcs();
81        if spec.weights.len() != num_arcs {
82            return Err(format!(
83                "weights has {} entries, expected {num_arcs}",
84                spec.weights.len()
85            )
86            .into());
87        }
88        if let Some(message) = partition_validation_error(&spec.partition, num_arcs) {
89            return Err(message.into());
90        }
91        Ok(Self::new(
92            graph,
93            spec.weights,
94            spec.partition,
95            spec.threshold,
96        ))
97    }
98}
99
100#[derive(Debug, Deserialize)]
101struct MultipleChoiceBranchingUnchecked<W: WeightElement> {
102    graph: DirectedGraph,
103    weights: Vec<W>,
104    partition: Vec<Vec<usize>>,
105    threshold: W::Sum,
106}
107
108impl<'de, W> Deserialize<'de> for MultipleChoiceBranching<W>
109where
110    W: WeightElement + Deserialize<'de>,
111    W::Sum: Deserialize<'de>,
112{
113    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
114    where
115        D: serde::Deserializer<'de>,
116    {
117        let unchecked = MultipleChoiceBranchingUnchecked::<W>::deserialize(deserializer)?;
118        let num_arcs = unchecked.graph.num_arcs();
119        if unchecked.weights.len() != num_arcs {
120            return Err(D::Error::custom(format!(
121                "weights length must match graph num_arcs (expected {num_arcs}, got {})",
122                unchecked.weights.len()
123            )));
124        }
125        if let Some(message) = partition_validation_error(&unchecked.partition, num_arcs) {
126            return Err(D::Error::custom(message));
127        }
128
129        Ok(Self {
130            graph: unchecked.graph,
131            weights: unchecked.weights,
132            partition: unchecked.partition,
133            threshold: unchecked.threshold,
134        })
135    }
136}
137
138impl<W: WeightElement> MultipleChoiceBranching<W> {
139    /// Create a new Multiple Choice Branching instance.
140    pub fn new(
141        graph: DirectedGraph,
142        weights: Vec<W>,
143        partition: Vec<Vec<usize>>,
144        threshold: W::Sum,
145    ) -> Self {
146        let num_arcs = graph.num_arcs();
147        assert_eq!(
148            weights.len(),
149            num_arcs,
150            "weights length must match graph num_arcs"
151        );
152        validate_partition(&partition, num_arcs);
153        Self {
154            graph,
155            weights,
156            partition,
157            threshold,
158        }
159    }
160
161    /// Get the underlying directed graph.
162    pub fn graph(&self) -> &DirectedGraph {
163        &self.graph
164    }
165
166    /// Get the arc weights.
167    pub fn weights(&self) -> &[W] {
168        &self.weights
169    }
170
171    /// Replace the arc weights.
172    pub fn set_weights(&mut self, weights: Vec<W>) {
173        assert_eq!(
174            weights.len(),
175            self.graph.num_arcs(),
176            "weights length must match graph num_arcs"
177        );
178        self.weights = weights;
179    }
180
181    /// Check whether this problem uses a non-unit weight type.
182    pub fn is_weighted(&self) -> bool {
183        !W::IS_UNIT
184    }
185
186    /// Get the partition groups.
187    pub fn partition(&self) -> &[Vec<usize>] {
188        &self.partition
189    }
190
191    /// Get the threshold K.
192    pub fn threshold(&self) -> &W::Sum {
193        &self.threshold
194    }
195
196    /// Get the number of vertices.
197    pub fn num_vertices(&self) -> usize {
198        self.graph.num_vertices()
199    }
200
201    /// Get the number of arcs.
202    pub fn num_arcs(&self) -> usize {
203        self.graph.num_arcs()
204    }
205
206    /// Get the number of partition groups.
207    pub fn num_partition_groups(&self) -> usize {
208        self.partition.len()
209    }
210
211    /// Check whether a configuration is a satisfying solution.
212    pub fn is_valid_solution(
213        &self,
214        config: &[bool],
215    ) -> Result<bool, crate::traits::EvaluationError> {
216        is_valid_multiple_choice_branching(
217            &self.graph,
218            &self.weights,
219            &self.partition,
220            &self.threshold,
221            config,
222        )
223    }
224}
225
226impl<W> Problem for MultipleChoiceBranching<W>
227where
228    W: WeightElement + crate::variant::VariantParam,
229{
230    const NAME: &'static str = "MultipleChoiceBranching";
231    type Solution = Vec<bool>;
232    type Value = crate::types::Or;
233
234    crate::problem_parameters![
235        ("num_vertices", num_vertices),
236        ("num_arcs", num_arcs),
237        ("num_partition_groups", num_partition_groups),
238    ];
239
240    fn variant() -> Vec<(&'static str, &'static str)> {
241        crate::variant_params![W]
242    }
243
244    fn evaluate(
245        &self,
246        config: &Self::Solution,
247    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
248        if config.len() != self.graph.num_arcs() {
249            return Err(crate::traits::EvaluationError::InvalidConfiguration(
250                "arc-selection length does not match the graph".into(),
251            ));
252        }
253        Ok({
254            crate::types::Or({
255                is_valid_multiple_choice_branching(
256                    &self.graph,
257                    &self.weights,
258                    &self.partition,
259                    &self.threshold,
260                    config,
261                )?
262            })
263        })
264    }
265}
266
267impl<W> crate::solvers::BruteForceProblem for MultipleChoiceBranching<W>
268where
269    W: WeightElement + crate::variant::VariantParam,
270{
271    fn dimensions(&self) -> Vec<usize> {
272        vec![2; self.graph.num_arcs()]
273    }
274}
275
276fn validate_partition(partition: &[Vec<usize>], num_arcs: usize) {
277    if let Some(message) = partition_validation_error(partition, num_arcs) {
278        panic!("{message}");
279    }
280}
281
282fn partition_validation_error(partition: &[Vec<usize>], num_arcs: usize) -> Option<String> {
283    let mut seen = vec![false; num_arcs];
284    for group in partition {
285        for &arc_index in group {
286            if arc_index >= num_arcs {
287                return Some(format!(
288                    "partition arc index {} out of range for {} arcs",
289                    arc_index, num_arcs
290                ));
291            }
292            if seen[arc_index] {
293                return Some(format!(
294                    "partition arc index {} appears more than once",
295                    arc_index
296                ));
297            }
298            seen[arc_index] = true;
299        }
300    }
301    if seen.iter().all(|present| *present) {
302        None
303    } else {
304        Some("partition must cover every arc exactly once".to_string())
305    }
306}
307
308fn is_valid_multiple_choice_branching<W: WeightElement>(
309    graph: &DirectedGraph,
310    weights: &[W],
311    partition: &[Vec<usize>],
312    threshold: &W::Sum,
313    config: &[bool],
314) -> Result<bool, crate::traits::EvaluationError> {
315    if config.len() != graph.num_arcs() {
316        return Ok(false);
317    }
318    for group in partition {
319        if group.iter().filter(|&&arc_index| config[arc_index]).count() > 1 {
320            return Ok(false);
321        }
322    }
323
324    let arcs = graph.arcs();
325    let mut in_degree = vec![0usize; graph.num_vertices()];
326    let mut selected_successors = vec![Vec::new(); graph.num_vertices()];
327    let mut total = W::Sum::zero();
328    for (index, &selected) in config.iter().enumerate() {
329        if selected {
330            let (source, target) = arcs[index];
331            in_degree[target] += 1;
332            if in_degree[target] > 1 {
333                return Ok(false);
334            }
335            selected_successors[source].push(target);
336            total = W::checked_add_to_sum(
337                total,
338                weights[index].to_sum(),
339                "summing multiple-choice branching weights",
340            )?;
341        }
342    }
343
344    if total < *threshold {
345        return Ok(false);
346    }
347
348    let mut queue: Vec<usize> = (0..graph.num_vertices())
349        .filter(|&vertex| in_degree[vertex] == 0)
350        .collect();
351    let mut visited = 0usize;
352    while let Some(source) = queue.pop() {
353        visited += 1;
354        for &target in &selected_successors[source] {
355            in_degree[target] -= 1;
356            if in_degree[target] == 0 {
357                queue.push(target);
358            }
359        }
360    }
361
362    Ok(visited == graph.num_vertices())
363}
364
365crate::declare_variants! {
366    default MultipleChoiceBranching<i64> => "2^num_arcs" create MultipleChoiceBranchingCreateSpec,
367}
368
369crate::register_brute_force! {
370    MultipleChoiceBranching<i64> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
371}
372
373#[cfg(feature = "example-db")]
374pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
375    vec![crate::example_db::specs::ModelExampleSpec {
376        id: "multiple_choice_branching",
377        instance: Box::new(MultipleChoiceBranching::new(
378            DirectedGraph::new(
379                6,
380                vec![
381                    (0, 1),
382                    (0, 2),
383                    (1, 3),
384                    (2, 3),
385                    (1, 4),
386                    (3, 5),
387                    (4, 5),
388                    (2, 4),
389                ],
390            ),
391            vec![3, 2, 4, 1, 2, 3, 1, 3],
392            vec![vec![0, 1], vec![2, 3], vec![4, 7], vec![5, 6]],
393            10,
394        )),
395        optimal_config: serde_json::json!(vec![true, false, true, false, false, true, false, true]),
396        optimal_value: serde_json::json!(true),
397    }]
398}
399
400#[cfg(test)]
401#[path = "../../unit_tests/models/graph/multiple_choice_branching.rs"]
402mod tests;