Skip to main content

problemreductions/models/misc/
minimum_disjunctive_normal_form.rs

1//! Minimum Disjunctive Normal Form (DNF) problem implementation.
2//!
3//! Given a Boolean function specified by its truth table, find a DNF formula
4//! with the minimum number of terms (prime implicants) equivalent to the function.
5//! NP-hard (Masek 1979, via reduction from Minimum Cover).
6
7use crate::registry::{FieldInfo, ProblemSchemaEntry};
8use crate::traits::Problem;
9use crate::types::Min;
10use serde::{Deserialize, Serialize};
11
12inventory::submit! {
13    ProblemSchemaEntry {
14        name: "MinimumDisjunctiveNormalForm",
15        display_name: "Minimum Disjunctive Normal Form",
16        aliases: &["MinDNF"],
17        dimensions: &[],
18        module_path: module_path!(),
19        description: "Find minimum-term DNF formula equivalent to a Boolean function",
20        fields: &[
21            FieldInfo { name: "num_variables", type_name: "usize", description: "Number of Boolean variables" },
22            FieldInfo { name: "truth_table", type_name: "Vec<bool>", description: "Truth table of length 2^n" },
23        ],
24    }
25}
26
27/// A prime implicant, represented as a pattern over n variables.
28/// Each entry is `Some(true)` (positive literal), `Some(false)` (negative literal),
29/// or `None` (don't care).
30#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
31pub struct PrimeImplicant {
32    /// Pattern: one entry per variable.
33    pub pattern: Vec<Option<bool>>,
34}
35
36impl PrimeImplicant {
37    /// Check if this prime implicant covers a given minterm (as a bit pattern).
38    pub fn covers(&self, minterm: usize) -> bool {
39        for (i, &p) in self.pattern.iter().enumerate() {
40            if let Some(val) = p {
41                let bit = ((minterm >> (self.pattern.len() - 1 - i)) & 1) == 1;
42                if bit != val {
43                    return false;
44                }
45            }
46        }
47        true
48    }
49}
50
51/// Minimum Disjunctive Normal Form problem.
52///
53/// Given a Boolean function by its truth table, find the minimum number of
54/// prime implicants whose disjunction (OR) is equivalent to the function.
55///
56/// The constructor computes all prime implicants via Quine-McCluskey.
57/// The configuration is a binary selection over prime implicants.
58///
59/// # Example
60///
61/// ```
62/// use problemreductions::models::misc::MinimumDisjunctiveNormalForm;
63/// use problemreductions::{Problem, Solver, BruteForce};
64///
65/// // f(x1,x2,x3) = 1 when exactly 1 or 2 variables are true
66/// let truth_table = vec![false, true, true, true, true, true, true, false];
67/// let problem = MinimumDisjunctiveNormalForm::new(3, truth_table);
68/// let solver = BruteForce::new();
69/// let value = solver.solve(&problem);
70/// ```
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct MinimumDisjunctiveNormalForm {
73    /// Number of Boolean variables.
74    num_variables: usize,
75    /// Truth table of length 2^n.
76    truth_table: Vec<bool>,
77    /// Precomputed prime implicants.
78    prime_implicants: Vec<PrimeImplicant>,
79    /// Minterms (indices where truth table is true).
80    minterms: Vec<usize>,
81}
82
83impl MinimumDisjunctiveNormalForm {
84    /// Create a new MinimumDisjunctiveNormalForm problem.
85    ///
86    /// # Panics
87    /// - If truth_table length != 2^num_variables
88    /// - If the function is identically false (no minterms)
89    pub fn new(num_variables: usize, truth_table: Vec<bool>) -> Self {
90        assert!(num_variables >= 1, "Need at least 1 variable");
91        assert_eq!(
92            truth_table.len(),
93            1 << num_variables,
94            "Truth table must have 2^n entries"
95        );
96
97        let minterms: Vec<usize> = truth_table
98            .iter()
99            .enumerate()
100            .filter_map(|(i, &v)| if v { Some(i) } else { None })
101            .collect();
102        assert!(
103            !minterms.is_empty(),
104            "Function must have at least one minterm"
105        );
106
107        let prime_implicants = compute_prime_implicants(num_variables, &minterms);
108
109        Self {
110            num_variables,
111            truth_table,
112            prime_implicants,
113            minterms,
114        }
115    }
116
117    /// Get the number of variables.
118    pub fn num_variables(&self) -> usize {
119        self.num_variables
120    }
121
122    /// Get the truth table.
123    pub fn truth_table(&self) -> &[bool] {
124        &self.truth_table
125    }
126
127    /// Get the prime implicants.
128    pub fn prime_implicants(&self) -> &[PrimeImplicant] {
129        &self.prime_implicants
130    }
131
132    /// Get the number of prime implicants.
133    pub fn num_prime_implicants(&self) -> usize {
134        self.prime_implicants.len()
135    }
136
137    /// Get the minterms.
138    pub fn minterms(&self) -> &[usize] {
139        &self.minterms
140    }
141}
142
143impl Problem for MinimumDisjunctiveNormalForm {
144    const NAME: &'static str = "MinimumDisjunctiveNormalForm";
145    type Value = Min<usize>;
146
147    fn dims(&self) -> Vec<usize> {
148        vec![2; self.prime_implicants.len()]
149    }
150
151    fn evaluate(&self, config: &[usize]) -> Min<usize> {
152        if config.len() != self.prime_implicants.len() {
153            return Min(None);
154        }
155
156        // Collect selected prime implicants
157        let selected: Vec<usize> = config
158            .iter()
159            .enumerate()
160            .filter_map(|(i, &v)| if v == 1 { Some(i) } else { None })
161            .collect();
162
163        if selected.is_empty() {
164            return Min(None);
165        }
166
167        // Check that all minterms are covered
168        for &mt in &self.minterms {
169            let covered = selected
170                .iter()
171                .any(|&pi_idx| self.prime_implicants[pi_idx].covers(mt));
172            if !covered {
173                return Min(None);
174            }
175        }
176
177        Min(Some(selected.len()))
178    }
179
180    fn variant() -> Vec<(&'static str, &'static str)> {
181        crate::variant_params![]
182    }
183}
184
185crate::declare_variants! {
186    default MinimumDisjunctiveNormalForm => "2^(3^num_variables)",
187}
188
189/// Compute all prime implicants of a Boolean function using Quine-McCluskey.
190///
191/// Each implicant is represented as a Vec<Option<bool>> of length num_variables.
192fn compute_prime_implicants(num_vars: usize, minterms: &[usize]) -> Vec<PrimeImplicant> {
193    use std::collections::HashSet;
194
195    if minterms.is_empty() {
196        return vec![];
197    }
198
199    type Pattern = Vec<Option<bool>>;
200
201    let mut current: Vec<Pattern> = minterms
202        .iter()
203        .map(|&mt| {
204            (0..num_vars)
205                .map(|i| Some(((mt >> (num_vars - 1 - i)) & 1) == 1))
206                .collect()
207        })
208        .collect();
209
210    let mut all_prime: HashSet<Pattern> = HashSet::new();
211
212    loop {
213        let mut next_set: HashSet<Pattern> = HashSet::new();
214        let mut used = vec![false; current.len()];
215
216        for i in 0..current.len() {
217            for j in (i + 1)..current.len() {
218                if let Some(merged) = try_merge(&current[i], &current[j]) {
219                    next_set.insert(merged);
220                    used[i] = true;
221                    used[j] = true;
222                }
223            }
224        }
225
226        for (i, &was_used) in used.iter().enumerate() {
227            if !was_used {
228                all_prime.insert(current[i].clone());
229            }
230        }
231
232        if next_set.is_empty() {
233            break;
234        }
235        current = next_set.into_iter().collect();
236    }
237
238    let mut result: Vec<PrimeImplicant> = all_prime
239        .into_iter()
240        .map(|pattern| PrimeImplicant { pattern })
241        .collect();
242    // Sort for deterministic output (HashSet iteration order is non-deterministic)
243    result.sort_by(|a, b| a.pattern.cmp(&b.pattern));
244    result
245}
246
247/// Try to merge two implicant patterns that differ in exactly one position.
248/// Returns the merged pattern (with that position set to None) or None if they can't merge.
249fn try_merge(a: &[Option<bool>], b: &[Option<bool>]) -> Option<Vec<Option<bool>>> {
250    if a.len() != b.len() {
251        return None;
252    }
253
254    let mut diff_count = 0;
255    let mut diff_pos = 0;
256
257    for (i, (va, vb)) in a.iter().zip(b.iter()).enumerate() {
258        if va != vb {
259            diff_count += 1;
260            diff_pos = i;
261            if diff_count > 1 {
262                return None;
263            }
264        }
265    }
266
267    if diff_count == 1 {
268        let mut merged = a.to_vec();
269        merged[diff_pos] = None;
270        Some(merged)
271    } else {
272        None
273    }
274}
275
276#[cfg(feature = "example-db")]
277pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
278    vec![crate::example_db::specs::ModelExampleSpec {
279        id: "minimum_disjunctive_normal_form",
280        instance: Box::new(MinimumDisjunctiveNormalForm::new(
281            3,
282            vec![false, true, true, true, true, true, true, false],
283        )),
284        // Select prime implicants: p1(¬x1∧x2), p4(x1∧¬x3), p5(¬x2∧x3)
285        // The order of PIs depends on the QMC algorithm output.
286        // We'll verify this in tests.
287        optimal_config: vec![1, 0, 0, 1, 1, 0],
288        optimal_value: serde_json::json!(3),
289    }]
290}
291
292#[cfg(test)]
293#[path = "../../unit_tests/models/misc/minimum_disjunctive_normal_form.rs"]
294mod tests;