problemreductions/models/misc/minimum_weight_and_or_graph.rs
1//! Minimum Weight AND/OR Graph problem implementation.
2//!
3//! Given a directed acyclic graph with AND/OR gates, find the minimum-weight
4//! solution subgraph from a designated source vertex.
5
6use crate::registry::{FieldInfo, ProblemSchemaEntry};
7use crate::traits::Problem;
8use crate::types::Min;
9use serde::{Deserialize, Deserializer, Serialize};
10
11inventory::submit! {
12 ProblemSchemaEntry {
13 name: "MinimumWeightAndOrGraph",
14 display_name: "Minimum Weight AND/OR Graph",
15 aliases: &[],
16 dimensions: &[],
17 module_path: module_path!(),
18 description: "Find the minimum-weight solution subgraph from a source in a DAG with AND/OR gates",
19 fields: &[
20 FieldInfo { name: "num_vertices", type_name: "usize", description: "Number of vertices in the DAG" },
21 FieldInfo { name: "arcs", type_name: "Vec<(usize, usize)>", description: "Directed arcs (u, v)" },
22 FieldInfo { name: "source", type_name: "usize", description: "Source vertex index" },
23 FieldInfo { name: "gate_types", type_name: "Vec<Option<bool>>", description: "Gate type per vertex: Some(true)=AND, Some(false)=OR, None=leaf" },
24 FieldInfo { name: "arc_weights", type_name: "Vec<i32>", description: "Weight of each arc" },
25 ],
26 }
27}
28
29/// The Minimum Weight AND/OR Graph problem.
30///
31/// Given a directed acyclic graph G = (V, A) where each non-leaf vertex is
32/// either an AND gate or an OR gate, a source vertex s, and arc weights
33/// w: A -> Z, find a solution subgraph of minimum total arc weight.
34///
35/// A solution subgraph is a subset of arcs S such that:
36/// - The source vertex is "solved"
37/// - For each solved AND-gate vertex v: all outgoing arcs from v are in S
38/// - For each solved OR-gate vertex v: at least one outgoing arc from v is in S
39/// - For each arc (u,v) in S: the target vertex v is also solved (recursively)
40/// - Leaf vertices are trivially solved (no outgoing arcs needed)
41///
42/// The configuration space is binary over arcs: each arc is either selected (1)
43/// or not (0).
44///
45/// # Example
46///
47/// ```
48/// use problemreductions::models::misc::MinimumWeightAndOrGraph;
49/// use problemreductions::{Problem, Solver, BruteForce};
50///
51/// // 7 vertices: AND at 0, OR at 1 and 2, leaves 3-6
52/// let problem = MinimumWeightAndOrGraph::new(
53/// 7,
54/// vec![(0,1), (0,2), (1,3), (1,4), (2,5), (2,6)],
55/// 0,
56/// vec![Some(true), Some(false), Some(false), None, None, None, None],
57/// vec![1, 2, 3, 1, 4, 2],
58/// );
59/// let solver = BruteForce::new();
60/// use problemreductions::solvers::Solver as _;
61/// let optimal = solver.solve(&problem);
62/// assert_eq!(optimal, problemreductions::types::Min(Some(6)));
63/// ```
64#[derive(Debug, Clone, Serialize)]
65pub struct MinimumWeightAndOrGraph {
66 /// Number of vertices.
67 num_vertices: usize,
68 /// Directed arcs (u, v).
69 arcs: Vec<(usize, usize)>,
70 /// Source vertex index.
71 source: usize,
72 /// Gate type per vertex: Some(true)=AND, Some(false)=OR, None=leaf.
73 gate_types: Vec<Option<bool>>,
74 /// Weight of each arc.
75 arc_weights: Vec<i32>,
76 /// Precomputed: outgoing arcs for each vertex (arc indices).
77 #[serde(skip)]
78 outgoing: Vec<Vec<usize>>,
79}
80
81#[derive(Deserialize)]
82struct MinimumWeightAndOrGraphData {
83 num_vertices: usize,
84 arcs: Vec<(usize, usize)>,
85 source: usize,
86 gate_types: Vec<Option<bool>>,
87 arc_weights: Vec<i32>,
88}
89
90impl<'de> Deserialize<'de> for MinimumWeightAndOrGraph {
91 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
92 where
93 D: Deserializer<'de>,
94 {
95 let data = MinimumWeightAndOrGraphData::deserialize(deserializer)?;
96 let outgoing = Self::build_outgoing(data.num_vertices, &data.arcs);
97 Ok(Self {
98 num_vertices: data.num_vertices,
99 arcs: data.arcs,
100 source: data.source,
101 gate_types: data.gate_types,
102 arc_weights: data.arc_weights,
103 outgoing,
104 })
105 }
106}
107
108impl MinimumWeightAndOrGraph {
109 /// Create a new Minimum Weight AND/OR Graph instance.
110 ///
111 /// # Panics
112 ///
113 /// Panics if any arc index is out of bounds, if the source is out of bounds,
114 /// if gate_types length does not match num_vertices, if arc_weights length
115 /// does not match the number of arcs, or if the source is a leaf.
116 pub fn new(
117 num_vertices: usize,
118 arcs: Vec<(usize, usize)>,
119 source: usize,
120 gate_types: Vec<Option<bool>>,
121 arc_weights: Vec<i32>,
122 ) -> Self {
123 assert!(
124 source < num_vertices,
125 "Source vertex {} out of bounds for {} vertices",
126 source,
127 num_vertices
128 );
129 assert_eq!(
130 gate_types.len(),
131 num_vertices,
132 "gate_types length {} does not match num_vertices {}",
133 gate_types.len(),
134 num_vertices
135 );
136 assert_eq!(
137 arc_weights.len(),
138 arcs.len(),
139 "arc_weights length {} does not match number of arcs {}",
140 arc_weights.len(),
141 arcs.len()
142 );
143 for (i, &(u, v)) in arcs.iter().enumerate() {
144 assert!(
145 u < num_vertices && v < num_vertices,
146 "Arc {} ({}, {}) out of bounds for {} vertices",
147 i,
148 u,
149 v,
150 num_vertices
151 );
152 }
153 assert!(
154 gate_types[source].is_some(),
155 "Source vertex must be an AND or OR gate, not a leaf"
156 );
157 let outgoing = Self::build_outgoing(num_vertices, &arcs);
158 Self {
159 num_vertices,
160 arcs,
161 source,
162 gate_types,
163 arc_weights,
164 outgoing,
165 }
166 }
167
168 /// Build outgoing arc index lists for each vertex.
169 fn build_outgoing(num_vertices: usize, arcs: &[(usize, usize)]) -> Vec<Vec<usize>> {
170 let mut outgoing = vec![vec![]; num_vertices];
171 for (i, &(u, _v)) in arcs.iter().enumerate() {
172 outgoing[u].push(i);
173 }
174 outgoing
175 }
176
177 /// Get the number of vertices.
178 pub fn num_vertices(&self) -> usize {
179 self.num_vertices
180 }
181
182 /// Get the number of arcs.
183 pub fn num_arcs(&self) -> usize {
184 self.arcs.len()
185 }
186
187 /// Get the arcs.
188 pub fn arcs(&self) -> &[(usize, usize)] {
189 &self.arcs
190 }
191
192 /// Get the source vertex.
193 pub fn source(&self) -> usize {
194 self.source
195 }
196
197 /// Get the gate types.
198 pub fn gate_types(&self) -> &[Option<bool>] {
199 &self.gate_types
200 }
201
202 /// Get the arc weights.
203 pub fn arc_weights(&self) -> &[i32] {
204 &self.arc_weights
205 }
206}
207
208impl Problem for MinimumWeightAndOrGraph {
209 const NAME: &'static str = "MinimumWeightAndOrGraph";
210 type Value = Min<i32>;
211
212 fn variant() -> Vec<(&'static str, &'static str)> {
213 crate::variant_params![]
214 }
215
216 fn dims(&self) -> Vec<usize> {
217 vec![2; self.arcs.len()]
218 }
219
220 fn evaluate(&self, config: &[usize]) -> Min<i32> {
221 if config.len() != self.arcs.len() {
222 return Min(None);
223 }
224
225 // Check all config values are 0 or 1
226 if config.iter().any(|&c| c > 1) {
227 return Min(None);
228 }
229
230 // Determine which arcs are selected
231 let selected: Vec<bool> = config.iter().map(|&c| c == 1).collect();
232
233 // Propagate "solved" status top-down from source
234 let mut solved = vec![false; self.num_vertices];
235 let mut stack = vec![self.source];
236 solved[self.source] = true;
237
238 while let Some(v) = stack.pop() {
239 match self.gate_types[v] {
240 None => {
241 // Leaf vertex: trivially solved, no outgoing arcs needed
242 }
243 Some(is_and) => {
244 let out_arcs = &self.outgoing[v];
245 let selected_out: Vec<usize> = out_arcs
246 .iter()
247 .copied()
248 .filter(|&ai| selected[ai])
249 .collect();
250
251 if is_and {
252 // AND gate: all outgoing arcs must be selected
253 if selected_out.len() != out_arcs.len() {
254 return Min(None);
255 }
256 } else {
257 // OR gate: at least one outgoing arc must be selected
258 if selected_out.is_empty() {
259 return Min(None);
260 }
261 }
262
263 // Mark children of selected arcs as solved
264 for &ai in &selected_out {
265 let (_u, child) = self.arcs[ai];
266 if !solved[child] {
267 solved[child] = true;
268 stack.push(child);
269 }
270 }
271 }
272 }
273 }
274
275 // Check no selected arcs come from non-solved vertices (no dangling arcs)
276 for (ai, &sel) in selected.iter().enumerate() {
277 if sel {
278 let (u, _v) = self.arcs[ai];
279 if !solved[u] {
280 return Min(None);
281 }
282 }
283 }
284
285 // Compute total weight of selected arcs
286 let total_weight: i32 = selected
287 .iter()
288 .enumerate()
289 .filter(|(_, &sel)| sel)
290 .map(|(i, _)| self.arc_weights[i])
291 .sum();
292
293 Min(Some(total_weight))
294 }
295}
296
297crate::declare_variants! {
298 default MinimumWeightAndOrGraph => "2^num_arcs",
299}
300
301#[cfg(feature = "example-db")]
302pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
303 // 7 vertices: source=0 (AND), v1 (OR), v2 (OR), v3-v6 (leaves)
304 // Arcs: (0,1,1), (0,2,2), (1,3,3), (1,4,1), (2,5,4), (2,6,2)
305 // Optimal: AND at 0 requires both arcs to 1 and 2 (cost 1+2=3).
306 // OR at 1: pick arc to 4 (cost 1). OR at 2: pick arc to 6 (cost 2).
307 // Total = 1+2+1+2 = 6... but actually we should check: is there a cheaper?
308 // arc0(0->1,w=1), arc1(0->2,w=2), arc3(1->4,w=1), arc5(2->6,w=2) => 1+2+1+2=6
309 // arc0(0->1,w=1), arc1(0->2,w=2), arc2(1->3,w=3), arc5(2->6,w=2) => 1+2+3+2=8
310 // arc0(0->1,w=1), arc1(0->2,w=2), arc3(1->4,w=1), arc4(2->5,w=4) => 1+2+1+4=8
311 // So optimal is config [1,1,0,1,0,1] with value 6... but wait, let me also check
312 // if val=5 is achievable: 1+2+1+1=5 impossible because OR at 2 must pick at least one.
313 // Actually optimal = 1(arc0) + 2(arc1) + 1(arc3) + 2(arc5) = 6
314 // Hmm, let me reconsider: is there a solution with value 5?
315 // Source is AND, so both arcs 0 and 1 must be selected (cost 1+2=3).
316 // Then OR at 1: cheapest outgoing arc is arc3 (w=1), OR at 2: cheapest is arc5 (w=2).
317 // Total = 3+1+2 = 6. Can't do better since source AND forces both.
318 // Wait — check: what if we change arc weights. The issue says value 5 might be optimal.
319 // Let me re-read: issue example says Config [1,1,0,1,0,1] -> weight 1+2+1+2 = 6 -> Min(6).
320 // So 6 is the correct optimal. But let me verify: is there any config with value < 6?
321 // No — source is AND so arcs 0,1 are forced (cost 3), then OR nodes each need at least one.
322 // Min at OR-1 is 1 (arc3), min at OR-2 is 2 (arc5). Total = 3+1+2 = 6.
323 // Optimal config: [1,1,0,1,0,1]
324 vec![crate::example_db::specs::ModelExampleSpec {
325 id: "minimum_weight_and_or_graph",
326 instance: Box::new(MinimumWeightAndOrGraph::new(
327 7,
328 vec![(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)],
329 0,
330 vec![Some(true), Some(false), Some(false), None, None, None, None],
331 vec![1, 2, 3, 1, 4, 2],
332 )),
333 optimal_config: vec![1, 1, 0, 1, 0, 1],
334 optimal_value: serde_json::json!(6),
335 }]
336}
337
338#[cfg(test)]
339#[path = "../../unit_tests/models/misc/minimum_weight_and_or_graph.rs"]
340mod tests;