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