Skip to main content

problemreductions/rules/
partitionintocliques_minimumcoveringbycliques.rs

1//! Reduction from PartitionIntoCliques to MinimumCoveringByCliques.
2//!
3//! This implements Orlin's classical construction for turning a partition of
4//! the source vertices into an edge-clique cover. The target graph contains
5//! left/right copies of the source vertices, one directed gadget per source
6//! edge, and two side-clique anchors.
7//! The source is satisfiable iff the target optimum is at most min(K,n) + q + 2,
8//! where q counts distinct directed non-loop adjacencies. Each side includes
9//! a private vertex, so its forced clique exists even for an empty source.
10
11use crate::models::decision::Decision;
12use crate::models::graph::{MinimumCoveringByCliques, PartitionIntoCliques};
13use crate::reduction;
14use crate::rules::traits::{ReduceTo, ReductionResult};
15use crate::topology::{Graph, SimpleGraph};
16use std::collections::BTreeMap;
17
18#[derive(Debug, Clone)]
19struct OrlinLayout {
20    num_source_vertices: usize,
21    directed_pairs: Vec<(usize, usize)>,
22}
23
24impl OrlinLayout {
25    fn new(graph: &SimpleGraph) -> Self {
26        let n = graph.num_vertices();
27        let directed_pairs = (0..n)
28            .flat_map(|i| {
29                (0..n).filter_map(move |j| (i != j && graph.has_edge(i, j)).then_some((i, j)))
30            })
31            .collect();
32        Self {
33            num_source_vertices: n,
34            directed_pairs,
35        }
36    }
37
38    fn num_directed_pairs(&self) -> usize {
39        self.directed_pairs.len()
40    }
41
42    fn x(&self, i: usize) -> usize {
43        i
44    }
45
46    fn y(&self, i: usize) -> usize {
47        self.num_source_vertices + i
48    }
49
50    fn a(&self, directed_pair_index: usize) -> usize {
51        2 * self.num_source_vertices + directed_pair_index
52    }
53
54    fn b(&self, directed_pair_index: usize) -> usize {
55        2 * self.num_source_vertices + self.num_directed_pairs() + directed_pair_index
56    }
57
58    fn z_left(&self) -> usize {
59        2 * self.num_source_vertices + 2 * self.num_directed_pairs()
60    }
61
62    fn z_right(&self) -> usize {
63        self.z_left() + 1
64    }
65
66    fn left_vertices(&self) -> Vec<usize> {
67        let mut vertices = (0..self.num_source_vertices)
68            .map(|i| self.x(i))
69            .collect::<Vec<_>>();
70        vertices.extend((0..self.num_directed_pairs()).map(|idx| self.a(idx)));
71        vertices.push(self.z_right() + 1);
72        vertices
73    }
74
75    fn right_vertices(&self) -> Vec<usize> {
76        let mut vertices = (0..self.num_source_vertices)
77            .map(|i| self.y(i))
78            .collect::<Vec<_>>();
79        vertices.extend((0..self.num_directed_pairs()).map(|idx| self.b(idx)));
80        vertices.push(self.z_right() + 2);
81        vertices
82    }
83
84    fn dimensions(&self) -> Result<(usize, usize), crate::rules::ReductionError> {
85        let n = self.num_source_vertices;
86        let q = self.num_directed_pairs();
87        let overflow = |operation: &str| {
88            crate::rules::ReductionError::integer_overflow::<
89                PartitionIntoCliques<SimpleGraph>,
90                Decision<MinimumCoveringByCliques<SimpleGraph>>,
91            >(operation)
92        };
93        // The two sides each have n+q+1 vertices, including their private
94        // vertex. These checked counts bound every subsequent layout index.
95        let side = n
96            .checked_add(q)
97            .and_then(|s| s.checked_add(1))
98            .ok_or_else(|| overflow("counting side vertices"))?;
99        let target_vertices = side
100            .checked_mul(2)
101            .and_then(|s| s.checked_add(2))
102            .ok_or_else(|| overflow("counting target vertices"))?;
103        let target_edges = side
104            .checked_mul(side)
105            .and_then(|s| s.checked_add(side))
106            .and_then(|s| s.checked_add(n))
107            .and_then(|s| q.checked_mul(4).and_then(|cross| s.checked_add(cross)))
108            .ok_or_else(|| overflow("counting target edges"))?;
109        <PartitionIntoCliques<SimpleGraph> as ReduceTo<
110            Decision<MinimumCoveringByCliques<SimpleGraph>>,
111        >>::exact_i64(target_edges, "representing every target cover value")?;
112        Ok((target_vertices, target_edges))
113    }
114}
115
116fn add_clique_edges(vertices: &[usize], edges: &mut Vec<(usize, usize)>) {
117    for i in 0..vertices.len() {
118        for j in (i + 1)..vertices.len() {
119            edges.push((vertices[i], vertices[j]));
120        }
121    }
122}
123
124fn target_clique_bound(
125    num_cliques: i64,
126    num_directed_pairs: i64,
127) -> Result<i64, crate::rules::ReductionError> {
128    num_directed_pairs
129        .checked_add(2)
130        .and_then(|offset| num_cliques.checked_add(offset))
131        .ok_or_else(|| {
132            crate::rules::ReductionError::integer_overflow::<
133                PartitionIntoCliques<SimpleGraph>,
134                Decision<MinimumCoveringByCliques<SimpleGraph>>,
135            >("computing target clique bound")
136        })
137}
138
139/// Result of reducing PartitionIntoCliques to MinimumCoveringByCliques.
140#[derive(Debug, Clone)]
141pub struct ReductionPartitionIntoCliquesToMinimumCoveringByCliques {
142    target: Decision<MinimumCoveringByCliques<SimpleGraph>>,
143    num_source_vertices: usize,
144}
145
146impl ReductionResult for ReductionPartitionIntoCliquesToMinimumCoveringByCliques {
147    type Source = PartitionIntoCliques<SimpleGraph>;
148    type Target = Decision<MinimumCoveringByCliques<SimpleGraph>>;
149
150    fn target_problem(&self) -> &Self::Target {
151        &self.target
152    }
153
154    fn extract_solution(
155        &self,
156        target_solution: &<Self::Target as crate::traits::Problem>::Solution,
157    ) -> crate::rules::ExtractionResult<<Self::Source as crate::traits::Problem>::Solution> {
158        crate::rules::traits::validate_target_witness(
159            self.target_problem(),
160            target_solution,
161            |value| value.0,
162            "target cover does not certify the source clique bound",
163        )?;
164
165        Ok({
166            let n = self.num_source_vertices;
167            let target_edges = self.target.inner().graph().edges();
168            let mut matching_labels = vec![None; n];
169            for ((u, v), &label) in target_edges.iter().zip(target_solution.iter()) {
170                let matching_index = if *u < n && *v == n + *u {
171                    Some(*u)
172                } else if *v < n && *u == n + *v {
173                    Some(*v)
174                } else {
175                    None
176                };
177
178                if let Some(i) = matching_index {
179                    matching_labels[i] = Some(label);
180                }
181            }
182
183            let mut label_map = BTreeMap::new();
184            let extracted = matching_labels
185                .into_iter()
186                .map(|label| {
187                    let label = label.ok_or_else(|| {
188                        crate::rules::ExtractionError::invalid(
189                            "target cover does not label every matching gadget edge",
190                        )
191                    })?;
192                    let next = label_map.len();
193                    Ok(*label_map.entry(label).or_insert(next))
194                })
195                .collect::<crate::rules::ExtractionResult<Vec<_>>>()?;
196
197            // Equal matching-edge labels imply pairwise source adjacency.
198            // The target certificate leaves at most K labels for these edges.
199            extracted
200        })
201    }
202}
203
204#[crate::aggregate_reduction(identity)]
205impl crate::rules::AggregateReductionResult
206    for ReductionPartitionIntoCliquesToMinimumCoveringByCliques
207{
208}
209
210#[reduction(
211    transform = upper_bound {
212        num_vertices = "2 * num_vertices + 4 * num_edges + 4",
213        num_edges = "(num_vertices + 2 * num_edges)^2 + 4 * num_vertices + 14 * num_edges + 2",
214    }
215)]
216impl ReduceTo<Decision<MinimumCoveringByCliques<SimpleGraph>>>
217    for PartitionIntoCliques<SimpleGraph>
218{
219    type Result = ReductionPartitionIntoCliquesToMinimumCoveringByCliques;
220
221    fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
222        let n = self.num_vertices();
223        let layout = OrlinLayout::new(self.graph());
224        let q = layout.num_directed_pairs();
225        let (target_vertices, target_edges) = layout.dimensions()?;
226        let source_bound =
227            <Self as ReduceTo<Decision<MinimumCoveringByCliques<SimpleGraph>>>>::exact_i64(
228                self.num_cliques().min(n),
229                "converting effective clique bound",
230            )?;
231        let directed_pairs =
232            <Self as ReduceTo<Decision<MinimumCoveringByCliques<SimpleGraph>>>>::exact_i64(
233                q,
234                "converting gadget count",
235            )?;
236        let target_bound = target_clique_bound(source_bound, directed_pairs)?;
237        let left_vertices = layout.left_vertices();
238        let right_vertices = layout.right_vertices();
239        let mut edges = Vec::with_capacity(target_edges);
240
241        // Step 1-2: L and R are cliques.
242        add_clique_edges(&left_vertices, &mut edges);
243        add_clique_edges(&right_vertices, &mut edges);
244
245        // Step 3-4: connect z_L and z_R to their respective sides.
246        for &u in &left_vertices {
247            edges.push((layout.z_left(), u));
248        }
249        for &u in &right_vertices {
250            edges.push((layout.z_right(), u));
251        }
252
253        // Step 5: matching edges x_i y_i.
254        for i in 0..self.num_vertices() {
255            edges.push((layout.x(i), layout.y(i)));
256        }
257
258        // Step 6: one 4-vertex gadget for each directed source edge.
259        for (idx, &(i, j)) in layout.directed_pairs.iter().enumerate() {
260            edges.push((layout.x(i), layout.y(j)));
261            edges.push((layout.x(i), layout.b(idx)));
262            edges.push((layout.a(idx), layout.y(j)));
263            edges.push((layout.a(idx), layout.b(idx)));
264        }
265
266        let target_graph = SimpleGraph::new(target_vertices, edges);
267        let target = MinimumCoveringByCliques::new(target_graph);
268
269        Ok(ReductionPartitionIntoCliquesToMinimumCoveringByCliques {
270            target: Decision::new(target, target_bound),
271            num_source_vertices: n,
272        })
273    }
274}
275
276#[cfg(any(test, feature = "example-db"))]
277fn edge_labels_from_clique_cover(graph: &SimpleGraph, cliques: &[Vec<usize>]) -> Vec<usize> {
278    let clique_sets = cliques
279        .iter()
280        .map(|clique| {
281            clique
282                .iter()
283                .copied()
284                .collect::<std::collections::BTreeSet<_>>()
285        })
286        .collect::<Vec<_>>();
287
288    graph
289        .edges()
290        .into_iter()
291        .map(|(u, v)| {
292            clique_sets
293                .iter()
294                .position(|clique| clique.contains(&u) && clique.contains(&v))
295                .expect("canonical cover should cover every target edge")
296        })
297        .collect()
298}
299
300#[cfg(feature = "example-db")]
301pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
302    use crate::export::SolutionPair;
303
304    vec![crate::example_db::specs::RuleExampleSpec {
305        id: "partitionintocliques_to_minimumcoveringbycliques",
306        build: || {
307            let source = PartitionIntoCliques::new(SimpleGraph::new(3, vec![(0, 1)]), 2);
308            let reduction =
309                ReduceTo::<Decision<MinimumCoveringByCliques<SimpleGraph>>>::reduce_to(&source)
310                    .expect("reduction should succeed");
311            let layout = OrlinLayout::new(source.graph());
312
313            let target_config = edge_labels_from_clique_cover(
314                reduction.target_problem().inner().graph(),
315                &[
316                    vec![layout.x(0), layout.x(1), layout.y(0), layout.y(1)],
317                    vec![layout.x(2), layout.y(2)],
318                    vec![layout.x(0), layout.a(0), layout.b(0), layout.y(1)],
319                    vec![layout.x(1), layout.a(1), layout.b(1), layout.y(0)],
320                    {
321                        let mut clique = layout.left_vertices();
322                        clique.push(layout.z_left());
323                        clique
324                    },
325                    {
326                        let mut clique = layout.right_vertices();
327                        clique.push(layout.z_right());
328                        clique
329                    },
330                ],
331            );
332
333            crate::example_db::specs::rule_example_with_witness::<
334                _,
335                Decision<MinimumCoveringByCliques<SimpleGraph>>,
336            >(
337                source,
338                SolutionPair {
339                    source_config: serde_json::json!(vec![0, 0, 1]),
340                    target_config: serde_json::to_value(target_config)
341                        .expect("solution serialization must succeed"),
342                },
343            )
344        },
345    }]
346}
347
348#[cfg(test)]
349#[path = "../unit_tests/rules/partitionintocliques_minimumcoveringbycliques.rs"]
350mod tests;