Skip to main content

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