Skip to main content

rust_igraph/algorithms/constructors/
realize_bipartite_degree_sequence.rs

1//! Bipartite degree sequence realization (ALGO-CO-034).
2//!
3//! Counterpart of `igraph_realize_bipartite_degree_sequence()` from
4//! `references/igraph/src/misc/degree_sequence.cpp`.
5//!
6//! Constructs a bipartite simple graph with a given bidegree sequence
7//! using a Havel-Hakimi–like algorithm.
8
9use crate::core::error::IgraphError;
10use crate::core::{Graph, IgraphResult, VertexId};
11
12use super::realize_degree_sequence::RealizeDegseqMethod;
13
14/// Realize a bipartite simple graph from two degree sequences.
15///
16/// Uses a Havel-Hakimi–like algorithm: repeatedly pick a vertex from
17/// one partition and connect it to the vertices with the highest
18/// remaining degrees in the opposite partition.
19///
20/// The resulting graph has `degrees1.len() + degrees2.len()` vertices.
21/// Vertices `0..n1` belong to partition 1 and vertices `n1..n1+n2` to
22/// partition 2.
23///
24/// # Arguments
25///
26/// * `degrees1` — target degrees for partition-1 vertices.
27/// * `degrees2` — target degrees for partition-2 vertices.
28/// * `method` — vertex selection order (see [`RealizeDegseqMethod`]).
29///
30/// # Errors
31///
32/// Returns `InvalidArgument` if:
33/// - The sums of `degrees1` and `degrees2` differ.
34/// - The bidegree sequence is not bigraphical.
35///
36/// # Examples
37///
38/// ```
39/// use rust_igraph::{realize_bipartite_degree_sequence, RealizeDegseqMethod};
40///
41/// // Complete bipartite K_{2,3}: partition 1 has degrees [3,3], partition 2 has [2,2,2]
42/// let g = realize_bipartite_degree_sequence(&[3, 3], &[2, 2, 2], RealizeDegseqMethod::Largest).unwrap();
43/// assert_eq!(g.vcount(), 5);
44/// assert_eq!(g.ecount(), 6);
45/// ```
46pub fn realize_bipartite_degree_sequence(
47    degrees1: &[u32],
48    degrees2: &[u32],
49    method: RealizeDegseqMethod,
50) -> IgraphResult<Graph> {
51    let n1 = degrees1.len();
52    let n2 = degrees2.len();
53    let n = n1.checked_add(n2).ok_or_else(|| {
54        IgraphError::InvalidArgument("total vertex count overflows usize".to_string())
55    })?;
56
57    if n == 0 {
58        return Graph::new(0, false);
59    }
60
61    let n_u32 = u32::try_from(n)
62        .map_err(|_| IgraphError::InvalidArgument("vertex count exceeds u32::MAX".to_string()))?;
63
64    let sum1: u64 = degrees1.iter().map(|&d| u64::from(d)).sum();
65    let sum2: u64 = degrees2.iter().map(|&d| u64::from(d)).sum();
66
67    if sum1 != sum2 {
68        return Err(IgraphError::InvalidArgument(
69            "degree sums of the two partitions must be equal".to_string(),
70        ));
71    }
72
73    if sum1 == 0 {
74        return Graph::new(n_u32, false);
75    }
76
77    #[allow(clippy::cast_possible_truncation)]
78    let num_edges = sum1 as usize;
79
80    let offset = u32::try_from(n1)
81        .map_err(|_| IgraphError::InvalidArgument("partition 1 too large".to_string()))?;
82
83    // Build (vertex_id, remaining_degree) pairs for each partition.
84    // Partition 2 vertices are offset by n1.
85    let mut vd1: Vec<(u32, u32)> = degrees1
86        .iter()
87        .enumerate()
88        .map(|(i, &d)| {
89            #[allow(clippy::cast_possible_truncation)]
90            let idx = i as u32;
91            (idx, d)
92        })
93        .collect();
94
95    let mut vd2: Vec<(u32, u32)> = degrees2
96        .iter()
97        .enumerate()
98        .map(|(i, &d)| {
99            #[allow(clippy::cast_possible_truncation)]
100            let idx = (i as u32)
101                .checked_add(offset)
102                .ok_or_else(|| IgraphError::InvalidArgument("vertex index overflow".to_string()));
103            (idx.unwrap_or(0), d)
104        })
105        .collect();
106
107    let mut edges: Vec<(VertexId, VertexId)> = Vec::with_capacity(num_edges);
108
109    match method {
110        RealizeDegseqMethod::Largest => {
111            realize_largest_or_smallest(&mut vd1, &mut vd2, &mut edges, true)?;
112        }
113        RealizeDegseqMethod::Smallest => {
114            realize_largest_or_smallest(&mut vd1, &mut vd2, &mut edges, false)?;
115        }
116        RealizeDegseqMethod::Index => realize_index(&mut vd1, &mut vd2, &mut edges)?,
117    }
118
119    let mut graph = Graph::new(n_u32, false)?;
120    graph.add_edges(edges)?;
121    Ok(graph)
122}
123
124fn realize_largest_or_smallest(
125    vd1: &mut Vec<(u32, u32)>,
126    vd2: &mut Vec<(u32, u32)>,
127    edges: &mut Vec<(VertexId, VertexId)>,
128    largest: bool,
129) -> IgraphResult<()> {
130    while !vd1.is_empty() && !vd2.is_empty() {
131        // Sort both partitions by degree descending
132        vd1.sort_unstable_by_key(|x| std::cmp::Reverse(x.1));
133        vd2.sort_unstable_by_key(|x| std::cmp::Reverse(x.1));
134
135        // Select the hub: from the partition that has the largest (or smallest) degree vertex
136        let (src_vs, dest_vs) = if largest {
137            let max1 = vd1[0].1;
138            let max2 = vd2[0].1;
139            if max1 >= max2 {
140                (vd1 as &mut Vec<(u32, u32)>, vd2 as &mut Vec<(u32, u32)>)
141            } else {
142                (vd2 as &mut Vec<(u32, u32)>, vd1 as &mut Vec<(u32, u32)>)
143            }
144        } else {
145            let min1 = vd1.last().map_or(0, |v| v.1);
146            let min2 = vd2.last().map_or(0, |v| v.1);
147            if min1 <= min2 {
148                (vd1 as &mut Vec<(u32, u32)>, vd2 as &mut Vec<(u32, u32)>)
149            } else {
150                (vd2 as &mut Vec<(u32, u32)>, vd1 as &mut Vec<(u32, u32)>)
151            }
152        };
153
154        let hub = if largest {
155            let h = src_vs[0];
156            src_vs.remove(0);
157            h
158        } else {
159            src_vs.pop().unwrap_or((0, 0))
160        };
161
162        if hub.1 == 0 {
163            continue;
164        }
165
166        let d = hub.1 as usize;
167        if d > dest_vs.len() {
168            return Err(IgraphError::InvalidArgument(
169                "bidegree sequence is not bigraphical (not realizable as simple bipartite graph)"
170                    .to_string(),
171            ));
172        }
173
174        // Connect hub to the d vertices with highest degree in the opposite partition
175        // dest_vs is already sorted descending
176        for item in dest_vs.iter_mut().take(d) {
177            if item.1 == 0 {
178                return Err(IgraphError::InvalidArgument(
179                    "bidegree sequence is not bigraphical (not realizable as simple bipartite graph)"
180                        .to_string(),
181                ));
182            }
183            edges.push((hub.0, item.0));
184            item.1 -= 1;
185        }
186    }
187
188    // Verify all remaining degrees are zero
189    for vs in [&*vd1, &*vd2] {
190        for &(v, d) in vs {
191            if d != 0 {
192                return Err(IgraphError::InvalidArgument(format!(
193                    "bidegree sequence not bigraphical: vertex {v} has {d} remaining degree"
194                )));
195            }
196        }
197    }
198
199    Ok(())
200}
201
202fn realize_index(
203    vd1: &mut Vec<(u32, u32)>,
204    vd2: &mut Vec<(u32, u32)>,
205    edges: &mut Vec<(VertexId, VertexId)>,
206) -> IgraphResult<()> {
207    // In index mode, always pick from partition 1 in order, connect to
208    // highest-degree vertices in partition 2.
209    while !vd1.is_empty() && !vd2.is_empty() {
210        // Sort partition 2 by degree descending
211        vd2.sort_unstable_by_key(|x| std::cmp::Reverse(x.1));
212
213        let hub = vd1.remove(0);
214
215        if hub.1 == 0 {
216            continue;
217        }
218
219        let d = hub.1 as usize;
220        if d > vd2.len() {
221            return Err(IgraphError::InvalidArgument(
222                "bidegree sequence is not bigraphical (not realizable as simple bipartite graph)"
223                    .to_string(),
224            ));
225        }
226
227        for item in vd2.iter_mut().take(d) {
228            if item.1 == 0 {
229                return Err(IgraphError::InvalidArgument(
230                    "bidegree sequence is not bigraphical (not realizable as simple bipartite graph)"
231                        .to_string(),
232                ));
233            }
234            edges.push((hub.0, item.0));
235            item.1 -= 1;
236        }
237    }
238
239    // Verify remaining degrees
240    for vs in [&*vd1, &*vd2] {
241        for &(v, d) in vs {
242            if d != 0 {
243                return Err(IgraphError::InvalidArgument(format!(
244                    "bidegree sequence not bigraphical: vertex {v} has {d} remaining degree"
245                )));
246            }
247        }
248    }
249
250    Ok(())
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    fn verify_bipartite_degrees(graph: &Graph, expected1: &[u32], expected2: &[u32]) {
258        let n1 = expected1.len();
259        let n2 = expected2.len();
260        assert_eq!(graph.vcount() as usize, n1 + n2);
261
262        for (i, &exp) in expected1.iter().enumerate() {
263            #[allow(clippy::cast_possible_truncation)]
264            let v = i as u32;
265            let deg = graph.degree(v).unwrap();
266            assert_eq!(
267                deg, exp as usize,
268                "partition 1 vertex {v}: got degree {deg}, expected {exp}"
269            );
270        }
271        for (i, &exp) in expected2.iter().enumerate() {
272            #[allow(clippy::cast_possible_truncation)]
273            let v = (i + n1) as u32;
274            let deg = graph.degree(v).unwrap();
275            assert_eq!(
276                deg, exp as usize,
277                "partition 2 vertex {v}: got degree {deg}, expected {exp}"
278            );
279        }
280    }
281
282    fn verify_bipartite_structure(graph: &Graph, n1: usize) {
283        // Every edge must cross partitions
284        for eid in 0..graph.ecount() {
285            #[allow(clippy::cast_possible_truncation)]
286            let eid_u32 = eid as u32;
287            let (s, t) = graph.edge(eid_u32).unwrap();
288            let s_in_p1 = (s as usize) < n1;
289            let t_in_p1 = (t as usize) < n1;
290            assert_ne!(
291                s_in_p1, t_in_p1,
292                "edge ({s}, {t}) does not cross partitions (n1={n1})"
293            );
294        }
295    }
296
297    #[test]
298    fn empty_sequences() {
299        let g = realize_bipartite_degree_sequence(&[], &[], RealizeDegseqMethod::Largest).unwrap();
300        assert_eq!(g.vcount(), 0);
301        assert_eq!(g.ecount(), 0);
302    }
303
304    #[test]
305    fn one_partition_empty() {
306        let g =
307            realize_bipartite_degree_sequence(&[0, 0], &[], RealizeDegseqMethod::Largest).unwrap();
308        assert_eq!(g.vcount(), 2);
309        assert_eq!(g.ecount(), 0);
310    }
311
312    #[test]
313    fn all_zeros() {
314        let g =
315            realize_bipartite_degree_sequence(&[0, 0], &[0, 0, 0], RealizeDegseqMethod::Largest)
316                .unwrap();
317        assert_eq!(g.vcount(), 5);
318        assert_eq!(g.ecount(), 0);
319    }
320
321    #[test]
322    fn single_edge() {
323        let g =
324            realize_bipartite_degree_sequence(&[1], &[1], RealizeDegseqMethod::Largest).unwrap();
325        assert_eq!(g.vcount(), 2);
326        assert_eq!(g.ecount(), 1);
327        verify_bipartite_degrees(&g, &[1], &[1]);
328        verify_bipartite_structure(&g, 1);
329    }
330
331    #[test]
332    fn complete_bipartite_k23() {
333        let g =
334            realize_bipartite_degree_sequence(&[3, 3], &[2, 2, 2], RealizeDegseqMethod::Largest)
335                .unwrap();
336        assert_eq!(g.vcount(), 5);
337        assert_eq!(g.ecount(), 6);
338        verify_bipartite_degrees(&g, &[3, 3], &[2, 2, 2]);
339        verify_bipartite_structure(&g, 2);
340    }
341
342    #[test]
343    fn complete_bipartite_k33() {
344        let g =
345            realize_bipartite_degree_sequence(&[3, 3, 3], &[3, 3, 3], RealizeDegseqMethod::Largest)
346                .unwrap();
347        assert_eq!(g.vcount(), 6);
348        assert_eq!(g.ecount(), 9);
349        verify_bipartite_degrees(&g, &[3, 3, 3], &[3, 3, 3]);
350        verify_bipartite_structure(&g, 3);
351    }
352
353    #[test]
354    fn star_bipartite() {
355        // One vertex in p1 connected to all in p2
356        let g =
357            realize_bipartite_degree_sequence(&[4], &[1, 1, 1, 1], RealizeDegseqMethod::Largest)
358                .unwrap();
359        assert_eq!(g.vcount(), 5);
360        assert_eq!(g.ecount(), 4);
361        verify_bipartite_degrees(&g, &[4], &[1, 1, 1, 1]);
362        verify_bipartite_structure(&g, 1);
363    }
364
365    #[test]
366    fn unequal_sums_fails() {
367        let result =
368            realize_bipartite_degree_sequence(&[2, 2], &[1, 1], RealizeDegseqMethod::Largest);
369        assert!(result.is_err());
370    }
371
372    #[test]
373    fn not_bigraphical_fails() {
374        // Sum matches (4 = 4) but can't realize: one vertex needs degree 4
375        // but partition 2 has only 2 vertices
376        let result = realize_bipartite_degree_sequence(&[4], &[2, 2], RealizeDegseqMethod::Largest);
377        assert!(result.is_err());
378    }
379
380    #[test]
381    fn method_smallest() {
382        let g = realize_bipartite_degree_sequence(
383            &[2, 2, 1],
384            &[1, 2, 2],
385            RealizeDegseqMethod::Smallest,
386        )
387        .unwrap();
388        assert_eq!(g.ecount(), 5);
389        verify_bipartite_degrees(&g, &[2, 2, 1], &[1, 2, 2]);
390        verify_bipartite_structure(&g, 3);
391    }
392
393    #[test]
394    fn method_index() {
395        let g =
396            realize_bipartite_degree_sequence(&[2, 1, 1], &[2, 1, 1], RealizeDegseqMethod::Index)
397                .unwrap();
398        assert_eq!(g.ecount(), 4);
399        verify_bipartite_degrees(&g, &[2, 1, 1], &[2, 1, 1]);
400        verify_bipartite_structure(&g, 3);
401    }
402
403    #[test]
404    fn larger_bipartite() {
405        let g = realize_bipartite_degree_sequence(
406            &[3, 3, 2, 2],
407            &[2, 2, 2, 2, 2],
408            RealizeDegseqMethod::Largest,
409        )
410        .unwrap();
411        assert_eq!(g.vcount(), 9);
412        assert_eq!(g.ecount(), 10);
413        verify_bipartite_degrees(&g, &[3, 3, 2, 2], &[2, 2, 2, 2, 2]);
414        verify_bipartite_structure(&g, 4);
415    }
416
417    #[test]
418    fn asymmetric_partitions() {
419        let g =
420            realize_bipartite_degree_sequence(&[5], &[1, 1, 1, 1, 1], RealizeDegseqMethod::Largest)
421                .unwrap();
422        assert_eq!(g.vcount(), 6);
423        assert_eq!(g.ecount(), 5);
424        verify_bipartite_degrees(&g, &[5], &[1, 1, 1, 1, 1]);
425        verify_bipartite_structure(&g, 1);
426    }
427}