Skip to main content

problemreductions/rules/
ksatisfiability_acyclicpartition.rs

1//! 3-SAT to the bounded-weight quotient-DAG partition problem.
2//!
3//! Compose the formal SAT-to-clique reduction with an incidence construction.
4//! Unit-weight vertices and zero-weight edge items encode a clique. Two heavy
5//! anchors and polynomial arc costs force exactly two blocks and encode the
6//! incidence closure and clique-size inequalities, without digit-encoded weights.
7
8use crate::models::formula::KSatisfiability;
9use crate::models::graph::{AcyclicPartition, KClique};
10use crate::reduction;
11use crate::rules::ksatisfiability_kclique::Reduction3SATToKClique;
12use crate::rules::traits::{ReduceTo, ReductionResult};
13use crate::topology::{DirectedGraph, Graph, SimpleGraph};
14use crate::variant::K3;
15
16/// Literal-clique extraction follows the incidence partition projection.
17#[derive(Debug, Clone)]
18pub struct Reduction3SATToAcyclicPartition {
19    sat_to_clique: Reduction3SATToKClique,
20    target: AcyclicPartition<i64>,
21    source_vertex: usize,
22}
23
24impl ReductionResult for Reduction3SATToAcyclicPartition {
25    type Source = KSatisfiability<K3>;
26    type Target = AcyclicPartition<i64>;
27
28    fn target_problem(&self) -> &Self::Target {
29        &self.target
30    }
31
32    fn extract_solution(
33        &self,
34        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
35    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
36        crate::rules::traits::validate_target_witness(
37            self.target_problem(),
38            target_solution,
39            |value| value.0,
40            "target partition does not satisfy the acyclic partition constraints",
41        )?;
42        let source_label = target_solution[self.source_vertex];
43        let selected = target_solution[..self.sat_to_clique.target_problem().num_vertices()]
44            .iter()
45            .map(|&label| label == source_label)
46            .collect();
47        self.sat_to_clique.extract_solution(&selected)
48    }
49}
50
51#[crate::aggregate_reduction(identity)]
52impl crate::rules::AggregateReductionResult for Reduction3SATToAcyclicPartition {}
53
54#[reduction(
55    transform = upper_bound {
56        max_numeric_magnitude_bits = "4 * num_clauses + 6",
57        num_vertices = "(9 * num_clauses^2 + 3 * num_clauses + 6) / 2",
58        num_arcs = "18 * num_clauses^2 + 2",
59    }
60)]
61impl ReduceTo<AcyclicPartition<i64>> for KSatisfiability<K3> {
62    type Result = Reduction3SATToAcyclicPartition;
63
64    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
65        let sat_to_clique = ReduceTo::<KClique<SimpleGraph>>::reduce_to(self)?;
66        let clique = sat_to_clique.target_problem();
67        let n = clique.num_vertices();
68        let edges = clique.graph().edges();
69        let e = edges.len();
70        let (target_n, arc_count, capacity, magnitude, weight_bound, cost_bound) =
71            incidence_parameters(n, e, clique.k())?;
72        let items = target_n - 2;
73        let source_vertex = items;
74        let sink_vertex = items + 1;
75        let mut degree = vec![0i64; n];
76        for &(u, v) in &edges {
77            // Degree <= e, whose doubled value fits the checked magnitude.
78            degree[u] += 1;
79            degree[v] += 1;
80        }
81        let mut arcs = Vec::with_capacity(arc_count);
82        let mut arc_costs = Vec::with_capacity(arc_count);
83        let profits = degree.iter().copied().chain(std::iter::repeat_n(0, e));
84        for (item, profit) in profits.enumerate() {
85            arcs.push((source_vertex, item));
86            arc_costs.push(magnitude);
87            arcs.push((item, sink_vertex));
88            arc_costs.push(magnitude - profit);
89        }
90        for (j, &(u, v)) in edges.iter().enumerate() {
91            arcs.push((u, n + j));
92            arc_costs.push(1);
93            arcs.push((v, n + j));
94            arc_costs.push(1);
95        }
96        let mut weights = vec![1; n];
97        weights.resize(items, 0);
98        weights.push(weight_bound - capacity);
99        weights.push(
100            weight_bound
101                - <Self as ReduceTo<AcyclicPartition<i64>>>::exact_i64(
102                    n - clique.k(),
103                    "representing unselected clique vertex count",
104                )?,
105        );
106        let target = AcyclicPartition::new(
107            DirectedGraph::new(target_n, arcs),
108            weights,
109            arc_costs,
110            weight_bound,
111            cost_bound,
112        );
113        Ok(Reduction3SATToAcyclicPartition {
114            sat_to_clique,
115            target,
116            source_vertex,
117        })
118    }
119}
120
121/// All values use polynomial-size counts; check them before graph allocation.
122fn incidence_parameters(
123    n: usize,
124    e: usize,
125    k: usize,
126) -> Result<(usize, usize, i64, i64, i64, i64), crate::rules::ReductionError> {
127    type Source = KSatisfiability<K3>;
128    type Target = AcyclicPartition<i64>;
129    let overflow = || {
130        crate::rules::ReductionError::integer_overflow::<Source, Target>(
131            "encoding clique incidence partition parameters",
132        )
133    };
134    let items = n.checked_add(e).ok_or_else(overflow)?;
135    let target_n = items.checked_add(2).ok_or_else(overflow)?;
136    let arcs = items
137        .checked_add(e)
138        .and_then(|x| x.checked_mul(2))
139        .ok_or_else(overflow)?;
140    let l = i64::try_from(items).map_err(|_| overflow())?;
141    let k = i64::try_from(k).map_err(|_| overflow())?;
142    let capacity = k;
143    let magnitude = l
144        .checked_mul(2)
145        .and_then(|x| x.checked_add(1))
146        .ok_or_else(overflow)?;
147    let bound = l
148        .checked_add(capacity)
149        .and_then(|x| x.checked_mul(2))
150        .and_then(|x| x.checked_add(1))
151        .ok_or_else(overflow)?;
152    let twice_edges = k.checked_mul(k.saturating_sub(1)).ok_or_else(overflow)?;
153    let cost_bound = magnitude
154        .checked_mul(l)
155        .and_then(|x| x.checked_sub(twice_edges))
156        .ok_or_else(overflow)?;
157    Ok((target_n, arcs, capacity, magnitude, bound, cost_bound))
158}
159
160#[cfg(feature = "example-db")]
161pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
162    use crate::export::SolutionPair;
163    use crate::models::formula::CNFClause;
164
165    vec![crate::example_db::specs::RuleExampleSpec {
166        id: "ksatisfiability_to_acyclicpartition",
167        build: || {
168            crate::example_db::specs::rule_example_with_witness::<_, AcyclicPartition<i64>>(
169                KSatisfiability::<K3>::new(1, vec![CNFClause::new(vec![1, 1, 1])]),
170                SolutionPair {
171                    source_config: serde_json::json!(vec![true]),
172                    target_config: serde_json::json!(vec![0, 1, 1, 0, 0, 1, 1, 0, 1]),
173                },
174            )
175        },
176    }]
177}
178
179#[cfg(test)]
180#[path = "../unit_tests/rules/ksatisfiability_acyclicpartition.rs"]
181mod tests;