problemreductions/models/graph/
multiple_choice_branching.rs1use 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#[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 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 pub fn graph(&self) -> &DirectedGraph {
114 &self.graph
115 }
116
117 pub fn weights(&self) -> &[W] {
119 &self.weights
120 }
121
122 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 pub fn is_weighted(&self) -> bool {
134 !W::IS_UNIT
135 }
136
137 pub fn partition(&self) -> &[Vec<usize>] {
139 &self.partition
140 }
141
142 pub fn threshold(&self) -> &W::Sum {
144 &self.threshold
145 }
146
147 pub fn num_vertices(&self) -> usize {
149 self.graph.num_vertices()
150 }
151
152 pub fn num_arcs(&self) -> usize {
154 self.graph.num_arcs()
155 }
156
157 pub fn num_partition_groups(&self) -> usize {
159 self.partition.len()
160 }
161
162 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;