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::{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;