1use crate::sat::{prove_unsat, UnsatOutcome};
11use crate::ProofExpr;
12
13#[derive(Clone, Debug, PartialEq)]
15pub struct MinResult {
16 pub optimum: i64,
18 pub witness: Vec<(String, bool)>,
20 pub minimal_certified: bool,
23}
24
25pub fn minimize_certified(
28 feasible_at: impl Fn(i64) -> ProofExpr,
29 lo: i64,
30 hi: i64,
31) -> Option<MinResult> {
32 if lo > hi {
33 return None;
34 }
35 if !matches!(prove_unsat(&feasible_at(hi)), UnsatOutcome::Sat(_)) {
37 return None;
38 }
39 let mut l = lo;
40 let mut h = hi;
41 while l < h {
42 let mid = l + (h - l) / 2;
43 match prove_unsat(&feasible_at(mid)) {
44 UnsatOutcome::Sat(_) => h = mid,
45 UnsatOutcome::Refuted => l = mid + 1,
46 UnsatOutcome::Unsupported => return None,
47 }
48 }
49 let optimum = l;
50 let witness = match prove_unsat(&feasible_at(optimum)) {
51 UnsatOutcome::Sat(m) => m,
52 _ => return None,
53 };
54 let minimal_certified = optimum <= lo
55 || matches!(prove_unsat(&feasible_at(optimum - 1)), UnsatOutcome::Refuted);
56 Some(MinResult { optimum, witness, minimal_certified })
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62 use crate::cardinality::at_most;
63
64 fn atom(s: &str) -> ProofExpr {
65 ProofExpr::Atom(s.to_string())
66 }
67 fn or(a: ProofExpr, b: ProofExpr) -> ProofExpr {
68 ProofExpr::Or(Box::new(a), Box::new(b))
69 }
70 fn and(a: ProofExpr, b: ProofExpr) -> ProofExpr {
71 ProofExpr::And(Box::new(a), Box::new(b))
72 }
73 fn contradiction() -> ProofExpr {
74 let f = atom("__f");
75 and(f.clone(), ProofExpr::Not(Box::new(f)))
76 }
77
78 #[test]
80 fn finds_certified_minimum_hitting_count() {
81 let (a, b, c) = (atom("a"), atom("b"), atom("c"));
84 let vars = vec![a.clone(), b.clone(), c.clone()];
85 let clauses = and(
86 and(or(a.clone(), b.clone()), or(b.clone(), c.clone())),
87 or(a.clone(), c.clone()),
88 );
89 let feasible_at = |bound: i64| {
90 if bound < 0 {
91 contradiction()
92 } else {
93 and(clauses.clone(), at_most(&vars, bound as usize, "cost"))
94 }
95 };
96 let res = minimize_certified(feasible_at, 0, 3).expect("feasible at 3");
97 assert_eq!(res.optimum, 2, "minimum hitting count is 2");
98 assert!(res.minimal_certified, "a 1-true solution must be RUP-refuted");
99 let ons = res
101 .witness
102 .iter()
103 .filter(|(n, v)| *v && matches!(n.as_str(), "a" | "b" | "c"))
104 .count();
105 assert!(ons >= 2, "witness must satisfy the clauses: {:?}", res.witness);
106 }
107
108 #[test]
109 fn zero_cost_optimum_when_unconstrained() {
110 let vars = vec![atom("a"), atom("b")];
112 let feasible_at = |bound: i64| at_most(&vars, bound.max(0) as usize, "c");
113 let res = minimize_certified(feasible_at, 0, 2).unwrap();
114 assert_eq!(res.optimum, 0);
115 assert!(res.minimal_certified);
116 }
117
118 #[test]
119 fn infeasible_range_returns_none() {
120 let (a, b, c) = (atom("a"), atom("b"), atom("c"));
122 let vars = vec![a.clone(), b.clone(), c.clone()];
123 let clauses = and(
124 and(or(a.clone(), b.clone()), or(b.clone(), c.clone())),
125 or(a.clone(), c.clone()),
126 );
127 let feasible_at =
128 |bound: i64| and(clauses.clone(), at_most(&vars, bound.max(0) as usize, "cost"));
129 assert!(minimize_certified(feasible_at, 0, 1).is_none());
130 }
131
132 #[test]
133 fn larger_cover_minimum_is_three() {
134 let (a, b, c, d) = (atom("a"), atom("b"), atom("c"), atom("d"));
139 let vars = vec![a.clone(), b.clone(), c.clone(), d.clone()];
140 let clauses = and(
141 and(a.clone(), or(b.clone(), c.clone())),
142 and(or(c.clone(), d.clone()), or(b.clone(), d.clone())),
143 );
144 let feasible_at =
145 |bound: i64| and(clauses.clone(), at_most(&vars, bound.max(0) as usize, "cost"));
146 let res = minimize_certified(feasible_at, 0, 4).unwrap();
147 assert_eq!(res.optimum, 3);
148 assert!(res.minimal_certified);
149 }
150
151 #[test]
152 fn singleton_range_returns_that_bound() {
153 let vars = vec![atom("a"), atom("b")];
155 let clauses = and(atom("a"), atom("b"));
156 let feasible_at =
157 |bound: i64| and(clauses.clone(), at_most(&vars, bound.max(0) as usize, "c"));
158 let res = minimize_certified(feasible_at, 2, 2).unwrap();
159 assert_eq!(res.optimum, 2);
160 assert!(res.minimal_certified, "optimum == lo is trivially minimal");
161 }
162
163 #[test]
164 fn lo_greater_than_hi_is_none() {
165 let vars = vec![atom("a")];
166 let feasible_at = |bound: i64| at_most(&vars, bound.max(0) as usize, "c");
167 assert!(minimize_certified(feasible_at, 5, 2).is_none());
168 }
169}