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
8use crate::models::graph::{MinimumCoveringByCliques, PartitionIntoCliques};
9use crate::reduction;
10use crate::rules::traits::{ReduceTo, ReductionResult};
11use crate::topology::{Graph, SimpleGraph};
12use std::collections::BTreeMap;
13
14#[derive(Debug, Clone)]
15struct OrlinLayout {
16    num_source_vertices: usize,
17    directed_pairs: Vec<(usize, usize)>,
18}
19
20impl OrlinLayout {
21    fn new(graph: &SimpleGraph) -> Self {
22        let n = graph.num_vertices();
23        let directed_pairs = (0..n)
24            .flat_map(|i| {
25                (0..n).filter_map(move |j| (i != j && graph.has_edge(i, j)).then_some((i, j)))
26            })
27            .collect();
28        Self {
29            num_source_vertices: n,
30            directed_pairs,
31        }
32    }
33
34    fn num_directed_pairs(&self) -> usize {
35        self.directed_pairs.len()
36    }
37
38    fn x(&self, i: usize) -> usize {
39        i
40    }
41
42    fn y(&self, i: usize) -> usize {
43        self.num_source_vertices + i
44    }
45
46    fn a(&self, directed_pair_index: usize) -> usize {
47        2 * self.num_source_vertices + directed_pair_index
48    }
49
50    fn b(&self, directed_pair_index: usize) -> usize {
51        2 * self.num_source_vertices + self.num_directed_pairs() + directed_pair_index
52    }
53
54    fn z_left(&self) -> usize {
55        2 * self.num_source_vertices + 2 * self.num_directed_pairs()
56    }
57
58    fn z_right(&self) -> usize {
59        self.z_left() + 1
60    }
61
62    fn left_vertices(&self) -> Vec<usize> {
63        let mut vertices = (0..self.num_source_vertices)
64            .map(|i| self.x(i))
65            .collect::<Vec<_>>();
66        vertices.extend((0..self.num_directed_pairs()).map(|idx| self.a(idx)));
67        vertices
68    }
69
70    fn right_vertices(&self) -> Vec<usize> {
71        let mut vertices = (0..self.num_source_vertices)
72            .map(|i| self.y(i))
73            .collect::<Vec<_>>();
74        vertices.extend((0..self.num_directed_pairs()).map(|idx| self.b(idx)));
75        vertices
76    }
77
78    fn total_vertices(&self) -> usize {
79        self.z_right() + 1
80    }
81}
82
83fn add_clique_edges(vertices: &[usize], edges: &mut Vec<(usize, usize)>) {
84    for i in 0..vertices.len() {
85        for j in (i + 1)..vertices.len() {
86            edges.push((vertices[i], vertices[j]));
87        }
88    }
89}
90
91fn invalid_source_solution(num_source_vertices: usize, num_source_cliques: usize) -> Vec<usize> {
92    vec![num_source_cliques; num_source_vertices]
93}
94
95/// Result of reducing PartitionIntoCliques to MinimumCoveringByCliques.
96#[derive(Debug, Clone)]
97pub struct ReductionPartitionIntoCliquesToMinimumCoveringByCliques {
98    target: MinimumCoveringByCliques<SimpleGraph>,
99    source_graph: SimpleGraph,
100    source_num_cliques: usize,
101}
102
103impl ReductionResult for ReductionPartitionIntoCliquesToMinimumCoveringByCliques {
104    type Source = PartitionIntoCliques<SimpleGraph>;
105    type Target = MinimumCoveringByCliques<SimpleGraph>;
106
107    fn target_problem(&self) -> &Self::Target {
108        &self.target
109    }
110
111    fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
112        let n = self.source_graph.num_vertices();
113        let target_edges = self.target.graph().edges();
114        if target_solution.len() != target_edges.len() {
115            return invalid_source_solution(n, self.source_num_cliques);
116        }
117
118        let mut matching_labels = vec![None; n];
119        for ((u, v), &label) in target_edges.iter().zip(target_solution.iter()) {
120            let matching_index = if *u < n && *v == n + *u {
121                Some(*u)
122            } else if *v < n && *u == n + *v {
123                Some(*v)
124            } else {
125                None
126            };
127
128            if let Some(i) = matching_index {
129                matching_labels[i] = Some(label);
130            }
131        }
132
133        if matching_labels.iter().any(Option::is_none) {
134            return invalid_source_solution(n, self.source_num_cliques);
135        }
136
137        let mut label_map = BTreeMap::new();
138        let extracted = matching_labels
139            .into_iter()
140            .map(|label| {
141                let label = label.expect("checked above");
142                let next = label_map.len();
143                *label_map.entry(label).or_insert(next)
144            })
145            .collect::<Vec<_>>();
146
147        if label_map.len() > self.source_num_cliques {
148            return invalid_source_solution(n, self.source_num_cliques);
149        }
150
151        let source_problem =
152            PartitionIntoCliques::new(self.source_graph.clone(), self.source_num_cliques);
153        if <PartitionIntoCliques<SimpleGraph> as crate::traits::Problem>::evaluate(
154            &source_problem,
155            &extracted,
156        )
157        .0
158        {
159            extracted
160        } else {
161            invalid_source_solution(n, self.source_num_cliques)
162        }
163    }
164}
165
166#[reduction(
167    overhead = {
168        num_vertices = "2 * num_vertices + 4 * num_edges + 2",
169        num_edges = "(num_vertices + 2 * num_edges)^2 + 2 * num_vertices + 10 * num_edges",
170    }
171)]
172impl ReduceTo<MinimumCoveringByCliques<SimpleGraph>> for PartitionIntoCliques<SimpleGraph> {
173    type Result = ReductionPartitionIntoCliquesToMinimumCoveringByCliques;
174
175    fn reduce_to(&self) -> Self::Result {
176        let layout = OrlinLayout::new(self.graph());
177        let left_vertices = layout.left_vertices();
178        let right_vertices = layout.right_vertices();
179        let mut edges = Vec::new();
180
181        // Step 1-2: L and R are cliques.
182        add_clique_edges(&left_vertices, &mut edges);
183        add_clique_edges(&right_vertices, &mut edges);
184
185        // Step 3-4: connect z_L and z_R to their respective sides.
186        for &u in &left_vertices {
187            edges.push((layout.z_left(), u));
188        }
189        for &u in &right_vertices {
190            edges.push((layout.z_right(), u));
191        }
192
193        // Step 5: matching edges x_i y_i.
194        for i in 0..self.num_vertices() {
195            edges.push((layout.x(i), layout.y(i)));
196        }
197
198        // Step 6: one 4-vertex gadget for each directed source edge.
199        for (idx, &(i, j)) in layout.directed_pairs.iter().enumerate() {
200            edges.push((layout.x(i), layout.y(j)));
201            edges.push((layout.x(i), layout.b(idx)));
202            edges.push((layout.a(idx), layout.y(j)));
203            edges.push((layout.a(idx), layout.b(idx)));
204        }
205
206        let target_graph = SimpleGraph::new(layout.total_vertices(), edges);
207        let target = MinimumCoveringByCliques::new(target_graph);
208
209        ReductionPartitionIntoCliquesToMinimumCoveringByCliques {
210            target,
211            source_graph: self.graph().clone(),
212            source_num_cliques: self.num_cliques(),
213        }
214    }
215}
216
217#[cfg(any(test, feature = "example-db"))]
218fn edge_labels_from_clique_cover(graph: &SimpleGraph, cliques: &[Vec<usize>]) -> Vec<usize> {
219    let clique_sets = cliques
220        .iter()
221        .map(|clique| {
222            clique
223                .iter()
224                .copied()
225                .collect::<std::collections::BTreeSet<_>>()
226        })
227        .collect::<Vec<_>>();
228
229    graph
230        .edges()
231        .into_iter()
232        .map(|(u, v)| {
233            clique_sets
234                .iter()
235                .position(|clique| clique.contains(&u) && clique.contains(&v))
236                .expect("canonical cover should cover every target edge")
237        })
238        .collect()
239}
240
241#[cfg(feature = "example-db")]
242pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
243    use crate::export::SolutionPair;
244
245    vec![crate::example_db::specs::RuleExampleSpec {
246        id: "partitionintocliques_to_minimumcoveringbycliques",
247        build: || {
248            let source = PartitionIntoCliques::new(SimpleGraph::new(3, vec![(0, 1)]), 2);
249            let reduction = ReduceTo::<MinimumCoveringByCliques<SimpleGraph>>::reduce_to(&source);
250            let layout = OrlinLayout::new(source.graph());
251
252            let target_config = edge_labels_from_clique_cover(
253                reduction.target_problem().graph(),
254                &[
255                    vec![layout.x(0), layout.x(1), layout.y(0), layout.y(1)],
256                    vec![layout.x(2), layout.y(2)],
257                    vec![layout.x(0), layout.a(0), layout.b(0), layout.y(1)],
258                    vec![layout.x(1), layout.a(1), layout.b(1), layout.y(0)],
259                    {
260                        let mut clique = layout.left_vertices();
261                        clique.push(layout.z_left());
262                        clique
263                    },
264                    {
265                        let mut clique = layout.right_vertices();
266                        clique.push(layout.z_right());
267                        clique
268                    },
269                ],
270            );
271
272            crate::example_db::specs::rule_example_with_witness::<
273                _,
274                MinimumCoveringByCliques<SimpleGraph>,
275            >(
276                source,
277                SolutionPair {
278                    source_config: vec![0, 0, 1],
279                    target_config,
280                },
281            )
282        },
283    }]
284}
285
286#[cfg(test)]
287#[path = "../unit_tests/rules/partitionintocliques_minimumcoveringbycliques.rs"]
288mod tests;