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        if spec.source >= spec.num_vertices {
92            return Err("source is outside the graph".to_string().into());
93        }
94        if spec.gate_types.len() != spec.num_vertices {
95            return Err("gate_types length must equal num_vertices"
96                .to_string()
97                .into());
98        }
99        if spec.gate_types[spec.source].is_none() {
100            return Err("source must be an AND or OR gate".to_string().into());
101        }
102        if let Some(&(u, v)) = spec
103            .arcs
104            .iter()
105            .find(|&&(u, v)| u >= spec.num_vertices || v >= spec.num_vertices)
106        {
107            return Err(format!("arc ({u}, {v}) is out of bounds").into());
108        }
109        let count = spec.arcs.len();
110        let arc_weights = spec.arc_weights.unwrap_or_else(|| vec![1; count]);
111        if arc_weights.len() != count {
112            return Err(format!(
113                "arc_weights has {} entries, expected {count}",
114                arc_weights.len()
115            )
116            .into());
117        }
118        Ok(Self::new(
119            spec.num_vertices,
120            spec.arcs,
121            spec.source,
122            spec.gate_types,
123            arc_weights,
124        ))
125    }
126}
127
128#[derive(Deserialize)]
129struct MinimumWeightAndOrGraphData {
130    num_vertices: usize,
131    arcs: Vec<(usize, usize)>,
132    source: usize,
133    gate_types: Vec<Option<bool>>,
134    arc_weights: Vec<i64>,
135}
136
137impl<'de> Deserialize<'de> for MinimumWeightAndOrGraph {
138    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
139    where
140        D: Deserializer<'de>,
141    {
142        let data = MinimumWeightAndOrGraphData::deserialize(deserializer)?;
143        let outgoing = Self::build_outgoing(data.num_vertices, &data.arcs);
144        Ok(Self {
145            num_vertices: data.num_vertices,
146            arcs: data.arcs,
147            source: data.source,
148            gate_types: data.gate_types,
149            arc_weights: data.arc_weights,
150            outgoing,
151        })
152    }
153}
154
155impl MinimumWeightAndOrGraph {
156    /// Create a new Minimum Weight AND/OR Graph instance.
157    ///
158    /// # Panics
159    ///
160    /// Panics if any arc index is out of bounds, if the source is out of bounds,
161    /// if gate_types length does not match num_vertices, if arc_weights length
162    /// does not match the number of arcs, or if the source is a leaf.
163    pub fn new(
164        num_vertices: usize,
165        arcs: Vec<(usize, usize)>,
166        source: usize,
167        gate_types: Vec<Option<bool>>,
168        arc_weights: Vec<i64>,
169    ) -> Self {
170        assert!(
171            source < num_vertices,
172            "Source vertex {} out of bounds for {} vertices",
173            source,
174            num_vertices
175        );
176        assert_eq!(
177            gate_types.len(),
178            num_vertices,
179            "gate_types length {} does not match num_vertices {}",
180            gate_types.len(),
181            num_vertices
182        );
183        assert_eq!(
184            arc_weights.len(),
185            arcs.len(),
186            "arc_weights length {} does not match number of arcs {}",
187            arc_weights.len(),
188            arcs.len()
189        );
190        for (i, &(u, v)) in arcs.iter().enumerate() {
191            assert!(
192                u < num_vertices && v < num_vertices,
193                "Arc {} ({}, {}) out of bounds for {} vertices",
194                i,
195                u,
196                v,
197                num_vertices
198            );
199        }
200        assert!(
201            gate_types[source].is_some(),
202            "Source vertex must be an AND or OR gate, not a leaf"
203        );
204        let outgoing = Self::build_outgoing(num_vertices, &arcs);
205        Self {
206            num_vertices,
207            arcs,
208            source,
209            gate_types,
210            arc_weights,
211            outgoing,
212        }
213    }
214
215    /// Build outgoing arc index lists for each vertex.
216    fn build_outgoing(num_vertices: usize, arcs: &[(usize, usize)]) -> Vec<Vec<usize>> {
217        let mut outgoing = vec![vec![]; num_vertices];
218        for (i, &(u, _v)) in arcs.iter().enumerate() {
219            outgoing[u].push(i);
220        }
221        outgoing
222    }
223
224    /// Get the number of vertices.
225    pub fn num_vertices(&self) -> usize {
226        self.num_vertices
227    }
228
229    /// Get the number of arcs.
230    pub fn num_arcs(&self) -> usize {
231        self.arcs.len()
232    }
233
234    /// Get the arcs.
235    pub fn arcs(&self) -> &[(usize, usize)] {
236        &self.arcs
237    }
238
239    /// Get the source vertex.
240    pub fn source(&self) -> usize {
241        self.source
242    }
243
244    /// Get the gate types.
245    pub fn gate_types(&self) -> &[Option<bool>] {
246        &self.gate_types
247    }
248
249    /// Get the arc weights.
250    pub fn arc_weights(&self) -> &[i64] {
251        &self.arc_weights
252    }
253}
254
255impl Problem for MinimumWeightAndOrGraph {
256    const NAME: &'static str = "MinimumWeightAndOrGraph";
257    type Solution = Vec<bool>;
258    type Value = Min<i64>;
259
260    crate::problem_parameters![("num_arcs", num_arcs), ("num_vertices", num_vertices),];
261
262    fn variant() -> Vec<(&'static str, &'static str)> {
263        crate::variant_params![]
264    }
265
266    fn evaluate(
267        &self,
268        config: &Self::Solution,
269    ) -> Result<Min<i64>, crate::traits::EvaluationError> {
270        Ok({
271            if config.len() != self.arcs.len() {
272                return Err(crate::traits::EvaluationError::InvalidConfiguration(
273                    "arc-selection length does not match the graph".into(),
274                ));
275            }
276
277            // Check all config values are 0 or 1
278            // Determine which arcs are selected
279            let selected = config;
280
281            // Propagate "solved" status top-down from source
282            let mut solved = vec![false; self.num_vertices];
283            let mut stack = vec![self.source];
284            solved[self.source] = true;
285
286            while let Some(v) = stack.pop() {
287                match self.gate_types[v] {
288                    None => {
289                        // Leaf vertex: trivially solved, no outgoing arcs needed
290                    }
291                    Some(is_and) => {
292                        let out_arcs = &self.outgoing[v];
293                        let selected_out: Vec<usize> = out_arcs
294                            .iter()
295                            .copied()
296                            .filter(|&ai| selected[ai])
297                            .collect();
298
299                        if is_and {
300                            // AND gate: all outgoing arcs must be selected
301                            if selected_out.len() != out_arcs.len() {
302                                return Ok(Min(None));
303                            }
304                        } else {
305                            // OR gate: at least one outgoing arc must be selected
306                            if selected_out.is_empty() {
307                                return Ok(Min(None));
308                            }
309                        }
310
311                        // Mark children of selected arcs as solved
312                        for &ai in &selected_out {
313                            let (_u, child) = self.arcs[ai];
314                            if !solved[child] {
315                                solved[child] = true;
316                                stack.push(child);
317                            }
318                        }
319                    }
320                }
321            }
322
323            // Check no selected arcs come from non-solved vertices (no dangling arcs)
324            for (ai, &sel) in selected.iter().enumerate() {
325                if sel {
326                    let (u, _v) = self.arcs[ai];
327                    if !solved[u] {
328                        return Ok(Min(None));
329                    }
330                }
331            }
332
333            // Compute total weight of selected arcs
334            let total_weight = selected
335                .iter()
336                .enumerate()
337                .filter(|(_, &sel)| sel)
338                .map(|(i, _)| self.arc_weights[i])
339                .try_fold(0_i64, |total, weight| {
340                    total.checked_add(weight).ok_or_else(|| {
341                        crate::traits::EvaluationError::IntegerOverflow(
342                            "summing selected AND/OR graph arc weights".into(),
343                        )
344                    })
345                })?;
346
347            Min(Some(total_weight))
348        })
349    }
350}
351
352impl crate::solvers::BruteForceProblem for MinimumWeightAndOrGraph {
353    fn dimensions(&self) -> Vec<usize> {
354        vec![2; self.arcs.len()]
355    }
356}
357
358crate::declare_variants! {
359    default MinimumWeightAndOrGraph => "2^num_arcs" create MinimumWeightAndOrGraphCreateSpec,
360}
361
362crate::register_brute_force! {
363    MinimumWeightAndOrGraph decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
364}
365
366#[cfg(feature = "example-db")]
367pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
368    // 7 vertices: source=0 (AND), v1 (OR), v2 (OR), v3-v6 (leaves)
369    // Arcs: (0,1,1), (0,2,2), (1,3,3), (1,4,1), (2,5,4), (2,6,2)
370    // Optimal: AND at 0 requires both arcs to 1 and 2 (cost 1+2=3).
371    // OR at 1: pick arc to 4 (cost 1). OR at 2: pick arc to 6 (cost 2).
372    // Total = 1+2+1+2 = 6... but actually we should check: is there a cheaper?
373    // 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
374    // 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
375    // 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
376    // So optimal is config [1,1,0,1,0,1] with value 6... but wait, let me also check
377    // if val=5 is achievable: 1+2+1+1=5 impossible because OR at 2 must pick at least one.
378    // Actually optimal = 1(arc0) + 2(arc1) + 1(arc3) + 2(arc5) = 6
379    // Hmm, let me reconsider: is there a solution with value 5?
380    // Source is AND, so both arcs 0 and 1 must be selected (cost 1+2=3).
381    // Then OR at 1: cheapest outgoing arc is arc3 (w=1), OR at 2: cheapest is arc5 (w=2).
382    // Total = 3+1+2 = 6. Can't do better since source AND forces both.
383    // Wait — check: what if we change arc weights. The issue says value 5 might be optimal.
384    // Let me re-read: issue example says Config [1,1,0,1,0,1] -> weight 1+2+1+2 = 6 -> Min(6).
385    // So 6 is the correct optimal. But let me verify: is there any config with value < 6?
386    // No — source is AND so arcs 0,1 are forced (cost 3), then OR nodes each need at least one.
387    // Min at OR-1 is 1 (arc3), min at OR-2 is 2 (arc5). Total = 3+1+2 = 6.
388    // Optimal config: [1,1,0,1,0,1]
389    vec![crate::example_db::specs::ModelExampleSpec {
390        id: "minimum_weight_and_or_graph",
391        instance: Box::new(MinimumWeightAndOrGraph::new(
392            7,
393            vec![(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)],
394            0,
395            vec![Some(true), Some(false), Some(false), None, None, None, None],
396            vec![1, 2, 3, 1, 4, 2],
397        )),
398        optimal_config: serde_json::json!(vec![true, true, false, true, false, true]),
399        optimal_value: serde_json::json!(6),
400    }]
401}
402
403#[cfg(test)]
404#[path = "../../unit_tests/models/misc/minimum_weight_and_or_graph.rs"]
405mod tests;