Skip to main content

problemreductions/models/graph/
biclique_cover.rs

1//! Biclique Cover problem implementation.
2//!
3//! Given a bipartite graph `G = (L ∪ R, E)` and a bound `k`, find up to `k`
4//! bicliques whose union covers every edge of `G`. Each biclique must be a
5//! *complete bipartite subgraph of `G`* — if `L_b ⊆ L` and `R_b ⊆ R` describe
6//! biclique `b`, every pair `(l, r) ∈ L_b × R_b` must itself be an edge of `G`.
7//! A biclique is *not* simply any pair `(L_b, R_b)`; the subgraph it induces
8//! has to be complete. The objective minimizes the total number of vertex
9//! memberships across all bicliques.
10//!
11//! Under this classical definition, the minimum `k` for which a rank-`k`
12//! biclique cover exists is exactly the _Boolean rank_ of the biadjacency
13//! matrix of `G` (Monson, Pullman, Rees 1995), matching exact Boolean
14//! Matrix Factorization.
15
16use crate::registry::{CreateSpec, ProblemSchemaEntry};
17use crate::topology::BipartiteGraph;
18use crate::traits::Problem;
19use crate::types::Min;
20use serde::{Deserialize, Serialize};
21use std::collections::HashSet;
22
23inventory::submit! {
24    ProblemSchemaEntry {
25        name: "BicliqueCover",
26        display_name: "Biclique Cover",
27        aliases: &[],
28        dimensions: &[],
29        category: crate::registry::ProblemCategory::Graph,
30        module_path: module_path!(),
31        description: "Cover bipartite edges with k bicliques",
32        fields: BicliqueCoverCreateSpec::FIELDS,
33    }
34}
35
36/// The Biclique Cover problem.
37///
38/// Given a bipartite graph with vertex sets L and R, find k bicliques
39/// that together cover all edges. Each vertex can be in any subset of the k bicliques.
40///
41/// # Example
42///
43/// ```
44/// use problemreductions::models::graph::BicliqueCover;
45/// use problemreductions::topology::BipartiteGraph;
46/// use problemreductions::{Problem, BruteForce};
47///
48/// // Bipartite graph: L = {0, 1}, R = {0, 1}
49/// // Edges: (0,0), (0,1), (1,0) in bipartite-local coordinates
50/// let graph = BipartiteGraph::new(2, 2, vec![(0, 0), (0, 1), (1, 0)]);
51/// let problem = BicliqueCover::new(graph, 2);
52///
53/// let solver = BruteForce::new();
54/// let solutions = solver.find_all_witnesses(&problem).unwrap();
55///
56/// // Check coverage
57/// for sol in &solutions {
58///     assert!(problem.is_valid_cover(sol));
59/// }
60/// ```
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct BicliqueCover {
63    /// The bipartite graph.
64    graph: BipartiteGraph,
65    /// Number of bicliques to use.
66    k: usize,
67}
68
69#[derive(Debug, Deserialize, crate::CreateSpec)]
70struct BicliqueCoverCreateSpec {
71    /// Number of vertices in the left partition.
72    left: usize,
73    /// Number of vertices in the right partition.
74    right: usize,
75    /// Bipartite edges in left-local, right-local coordinates.
76    #[create(codec = "bipartite-edge-list")]
77    biedges: Vec<(usize, usize)>,
78    /// Number of bicliques available to cover the edges.
79    k: usize,
80}
81
82impl TryFrom<BicliqueCoverCreateSpec> for BicliqueCover {
83    type Error = crate::registry::ConstructionError;
84
85    fn try_from(spec: BicliqueCoverCreateSpec) -> Result<Self, Self::Error> {
86        for (edge_index, &(left_vertex, right_vertex)) in spec.biedges.iter().enumerate() {
87            if left_vertex >= spec.left {
88                return Err(format!(
89                    "biedges[{edge_index}] left vertex {left_vertex} is out of bounds for left partition size {}",
90                    spec.left
91                ).into());
92            }
93            if right_vertex >= spec.right {
94                return Err(format!(
95                    "biedges[{edge_index}] right vertex {right_vertex} is out of bounds for right partition size {}",
96                    spec.right
97                ).into());
98            }
99        }
100
101        let graph = BipartiteGraph::new(spec.left, spec.right, spec.biedges);
102        Ok(Self::new(graph, spec.k))
103    }
104}
105
106impl BicliqueCover {
107    /// Create a new Biclique Cover problem.
108    ///
109    /// # Arguments
110    /// * `graph` - The bipartite graph
111    /// * `k` - Number of bicliques
112    pub fn new(graph: BipartiteGraph, k: usize) -> Self {
113        Self { graph, k }
114    }
115
116    /// Create from a bipartite adjacency matrix.
117    ///
118    /// `Matrix[i][j] = 1` means edge between left vertex i and right vertex j.
119    pub fn from_matrix(matrix: &[Vec<u8>], k: usize) -> Self {
120        let left_size = matrix.len();
121        let right_size = if left_size > 0 { matrix[0].len() } else { 0 };
122
123        let mut edges = Vec::new();
124        for (i, row) in matrix.iter().enumerate() {
125            for (j, &val) in row.iter().enumerate() {
126                if val != 0 {
127                    edges.push((i, j));
128                }
129            }
130        }
131
132        Self {
133            graph: BipartiteGraph::new(left_size, right_size, edges),
134            k,
135        }
136    }
137
138    /// Get the bipartite graph.
139    pub fn graph(&self) -> &BipartiteGraph {
140        &self.graph
141    }
142
143    /// Get the left partition size.
144    pub fn left_size(&self) -> usize {
145        self.graph.left_size()
146    }
147
148    /// Get the right partition size.
149    pub fn right_size(&self) -> usize {
150        self.graph.right_size()
151    }
152
153    /// Get the number of vertices.
154    pub fn num_vertices(&self) -> usize {
155        self.graph.left_size() + self.graph.right_size()
156    }
157
158    /// Get the number of edges.
159    pub fn num_edges(&self) -> usize {
160        self.graph.left_edges().len()
161    }
162
163    /// Get k (number of bicliques).
164    pub fn k(&self) -> usize {
165        self.k
166    }
167
168    /// Get the rank (alias for `k()`).
169    pub fn rank(&self) -> usize {
170        self.k()
171    }
172
173    /// Convert a configuration to biclique memberships.
174    ///
175    /// Each row gives one vertex's membership in the `k` bicliques.
176    /// Returns: (left_memberships, right_memberships) where each is a Vec of k HashSets.
177    fn get_biclique_memberships(
178        &self,
179        config: &[Vec<bool>],
180    ) -> (Vec<HashSet<usize>>, Vec<HashSet<usize>>) {
181        let n = self.num_vertices();
182        let left_size = self.graph.left_size();
183        let mut left_bicliques: Vec<HashSet<usize>> = vec![HashSet::new(); self.k];
184        let mut right_bicliques: Vec<HashSet<usize>> = vec![HashSet::new(); self.k];
185
186        for v in 0..n {
187            for b in 0..self.k {
188                if config
189                    .get(b)
190                    .and_then(|memberships| memberships.get(v))
191                    .copied()
192                    .unwrap_or(false)
193                {
194                    if v < left_size {
195                        left_bicliques[b].insert(v);
196                    } else {
197                        right_bicliques[b].insert(v);
198                    }
199                }
200            }
201        }
202
203        (left_bicliques, right_bicliques)
204    }
205
206    /// Check if an edge is covered by the bicliques.
207    ///
208    /// Takes edge endpoints in unified vertex space.
209    fn is_edge_covered(&self, left: usize, right: usize, config: &[Vec<bool>]) -> bool {
210        let (left_bicliques, right_bicliques) = self.get_biclique_memberships(config);
211
212        // Edge is covered if both endpoints are in the same biclique
213        for b in 0..self.k {
214            if left_bicliques[b].contains(&left) && right_bicliques[b].contains(&right) {
215                return true;
216            }
217        }
218        false
219    }
220
221    /// Check if a configuration is a valid biclique cover.
222    pub fn is_valid_solution(&self, config: &[Vec<bool>]) -> bool {
223        self.is_valid_cover(config)
224    }
225
226    /// Check if the configuration is a valid biclique cover.
227    ///
228    /// Under the classical definition, a biclique is a complete bipartite
229    /// subgraph of the input graph: every pair `(l, r)` with `l ∈ L_b`
230    /// and `r ∈ R_b` must be an edge of `G`. A configuration is a valid
231    /// biclique cover iff every biclique is a sub-biclique of `G` and
232    /// every edge of `G` is covered by at least one biclique.
233    pub fn is_valid_cover(&self, config: &[Vec<bool>]) -> bool {
234        use crate::topology::Graph;
235        let (left_bicliques, right_bicliques) = self.get_biclique_memberships(config);
236        let left_size = self.graph.left_size();
237        // Every biclique must be a sub-biclique of G (no non-edges covered).
238        for b in 0..self.k {
239            for &l in &left_bicliques[b] {
240                for &r in &right_bicliques[b] {
241                    // Endpoints come from get_biclique_memberships in unified
242                    // vertex space: l < left_size, r >= left_size.
243                    debug_assert!(l < left_size && r >= left_size);
244                    if !self.graph.has_edge(l, r) {
245                        return false;
246                    }
247                }
248            }
249        }
250        // Every edge of G must be covered by at least one biclique.
251        self.graph
252            .edges()
253            .iter()
254            .all(|&(l, r)| self.is_edge_covered(l, r, config))
255    }
256
257    /// Count covered edges.
258    pub fn count_covered_edges(
259        &self,
260        config: &[Vec<bool>],
261    ) -> Result<i64, crate::traits::EvaluationError> {
262        use crate::topology::Graph;
263        let count = self
264            .graph
265            .edges()
266            .iter()
267            .filter(|&&(l, r)| self.is_edge_covered(l, r, config))
268            .count();
269        i64::try_from(count).map_err(|_| {
270            crate::traits::EvaluationError::IntegerOverflow(
271                "converting covered-edge count to i64".into(),
272            )
273        })
274    }
275
276    /// Count total biclique size (sum of vertices in all bicliques).
277    pub fn total_biclique_size(
278        &self,
279        config: &[Vec<bool>],
280    ) -> Result<i64, crate::traits::EvaluationError> {
281        let size = config
282            .iter()
283            .flatten()
284            .filter(|&&selected| selected)
285            .count();
286        i64::try_from(size).map_err(|_| {
287            crate::traits::EvaluationError::IntegerOverflow(
288                "converting total biclique size to i64".into(),
289            )
290        })
291    }
292}
293
294/// Check if a biclique configuration covers all edges.
295#[cfg(test)]
296pub(crate) fn is_biclique_cover(
297    edges: &[(usize, usize)],
298    left_bicliques: &[HashSet<usize>],
299    right_bicliques: &[HashSet<usize>],
300) -> bool {
301    let edge_set: HashSet<(usize, usize)> = edges
302        .iter()
303        .map(|&(u, v)| if u <= v { (u, v) } else { (v, u) })
304        .collect();
305
306    let all_bicliques_are_subgraphs =
307        left_bicliques
308            .iter()
309            .zip(right_bicliques.iter())
310            .all(|(lb, rb)| {
311                lb.iter().all(|&l| {
312                    rb.iter().all(|&r| {
313                        let edge = if l <= r { (l, r) } else { (r, l) };
314                        edge_set.contains(&edge)
315                    })
316                })
317            });
318
319    all_bicliques_are_subgraphs
320        && edges.iter().all(|&(l, r)| {
321            left_bicliques
322                .iter()
323                .zip(right_bicliques.iter())
324                .any(|(lb, rb)| lb.contains(&l) && rb.contains(&r))
325        })
326}
327
328impl Problem for BicliqueCover {
329    const NAME: &'static str = "BicliqueCover";
330    type Solution = Vec<Vec<bool>>;
331    type Value = Min<i64>;
332
333    crate::problem_parameters![
334        ("left_size", left_size),
335        ("num_edges", num_edges),
336        ("num_vertices", num_vertices),
337        ("rank", rank),
338        ("right_size", right_size),
339    ];
340
341    fn evaluate(
342        &self,
343        solution: &Self::Solution,
344    ) -> Result<Min<i64>, crate::traits::EvaluationError> {
345        if solution.len() != self.k
346            || solution
347                .iter()
348                .any(|biclique| biclique.len() != self.num_vertices())
349        {
350            return Err(crate::traits::EvaluationError::InvalidConfiguration(
351                "biclique membership dimensions do not match the instance".into(),
352            ));
353        }
354        Ok({
355            if !self.is_valid_cover(solution) {
356                return Ok(Min(None));
357            }
358            Min(Some(self.total_biclique_size(solution)?))
359        })
360    }
361
362    fn variant() -> Vec<(&'static str, &'static str)> {
363        crate::variant_params![]
364    }
365}
366
367impl crate::solvers::BruteForceProblem for BicliqueCover {
368    fn dimensions(&self) -> Vec<usize> {
369        // Each vertex has k binary variables (one per biclique)
370        vec![2; self.num_vertices() * self.k]
371    }
372}
373
374crate::declare_variants! {
375    default BicliqueCover => "2^(num_vertices * rank)" create BicliqueCoverCreateSpec,
376}
377
378crate::register_brute_force! {
379    BicliqueCover decode |problem: &BicliqueCover, indices: Vec<usize>| (0..problem.rank()).map(|biclique| (0..problem.num_vertices()).map(|vertex| indices[vertex * problem.rank() + biclique] != 0).collect()).collect(),
380}
381
382#[cfg(feature = "example-db")]
383pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
384    use crate::topology::BipartiteGraph;
385    // Biclique 0: L_0={ℓ_1}, R_0={r_1, r_2} — covers edges (0,0), (0,1).
386    // Biclique 1: L_1={ℓ_2}, R_1={r_2, r_3} — covers edges (1,1), (1,2).
387    // Both are sub-bicliques of G; every edge covered; total size = 6.
388    // One incidence row per biclique over the five vertices.
389    vec![crate::example_db::specs::ModelExampleSpec {
390        id: "biclique_cover",
391        instance: Box::new(BicliqueCover::new(
392            BipartiteGraph::new(2, 3, vec![(0, 0), (0, 1), (1, 1), (1, 2)]),
393            2,
394        )),
395        optimal_config: serde_json::json!(vec![
396            vec![true, false, true, true, false],
397            vec![false, true, false, true, true]
398        ]),
399        optimal_value: serde_json::json!(6),
400    }]
401}
402
403#[cfg(test)]
404#[path = "../../unit_tests/models/graph/biclique_cover.rs"]
405mod tests;