1use std::collections::HashSet;
15
16use crate::algorithms::properties::multiplicity::has_multiple;
17use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
18
19pub fn trussness(graph: &Graph) -> IgraphResult<Vec<u32>> {
66 if graph.is_directed() {
67 return Err(IgraphError::InvalidArgument(
68 "trussness is only defined for undirected graphs".into(),
69 ));
70 }
71
72 let edge_count = graph.ecount();
73 if edge_count == 0 {
74 return Ok(Vec::new());
75 }
76
77 if has_multiple(graph)? {
78 return Err(IgraphError::Unsupported(
79 "trussness does not support multigraphs",
80 ));
81 }
82
83 let adj = build_simple_adj(graph)?;
84 let mut support = compute_support(graph, &adj)?;
85 peel_trussness(graph, &adj, &mut support)
86}
87
88fn build_simple_adj(graph: &Graph) -> IgraphResult<Vec<Vec<VertexId>>> {
89 let vert_count = graph.vcount();
90 let mut adj: Vec<Vec<VertexId>> = Vec::with_capacity(vert_count as usize);
91 for vid in 0..vert_count {
92 let raw = graph.neighbors(vid)?;
93 let mut simple: Vec<VertexId> = raw.into_iter().filter(|&nb| nb != vid).collect();
94 simple.sort_unstable();
95 simple.dedup();
96 adj.push(simple);
97 }
98 Ok(adj)
99}
100
101fn compute_support(graph: &Graph, adj: &[Vec<VertexId>]) -> IgraphResult<Vec<u32>> {
102 let vert_count = graph.vcount();
103 let mut support: Vec<u32> = vec![0; graph.ecount()];
104 let mut mark: Vec<u32> = vec![0; vert_count as usize];
105
106 for v1 in 0..vert_count {
107 let nei1 = &adj[v1 as usize];
108 if nei1.len() < 2 {
109 continue;
110 }
111 let v1_mark = v1
112 .checked_add(1)
113 .ok_or(IgraphError::Internal("vertex id overflow"))?;
114
115 for &v2 in nei1 {
116 if v2 >= v1 {
117 break;
118 }
119 mark[v2 as usize] = v1_mark;
120 }
121
122 for &v2 in nei1 {
123 if v2 >= v1 {
124 break;
125 }
126 for &v3 in &adj[v2 as usize] {
127 if v3 >= v2 {
128 break;
129 }
130 if mark[v3 as usize] == v1_mark {
131 if let Some(eid) = graph.find_eid(v1, v2)? {
132 support[eid as usize] = support[eid as usize].saturating_add(1);
133 }
134 if let Some(eid) = graph.find_eid(v1, v3)? {
135 support[eid as usize] = support[eid as usize].saturating_add(1);
136 }
137 if let Some(eid) = graph.find_eid(v2, v3)? {
138 support[eid as usize] = support[eid as usize].saturating_add(1);
139 }
140 }
141 }
142 }
143 }
144 Ok(support)
145}
146
147#[allow(clippy::similar_names)]
148fn peel_trussness(
149 graph: &Graph,
150 adj: &[Vec<VertexId>],
151 support: &mut [u32],
152) -> IgraphResult<Vec<u32>> {
153 let edge_count = support.len();
154 let max_support = support.iter().copied().max().unwrap_or(0);
155 let bucket_count = (max_support as usize).saturating_add(1);
156
157 let mut truss: Vec<u32> = vec![0; edge_count];
158 let mut completed: Vec<bool> = vec![false; edge_count];
159 let mut buckets: Vec<HashSet<u32>> = vec![HashSet::new(); bucket_count];
160
161 for eid in 0..edge_count {
162 let eid_u32 = u32::try_from(eid).map_err(|_| IgraphError::Internal("edge id overflow"))?;
163 buckets[support[eid] as usize].insert(eid_u32);
164 }
165
166 for level in 0..bucket_count {
167 let level_u32 =
168 u32::try_from(level).map_err(|_| IgraphError::Internal("level overflow"))?;
169
170 while let Some(&eid) = buckets[level].iter().next() {
171 buckets[level].remove(&eid);
172 completed[eid as usize] = true;
173 truss[eid as usize] = level_u32
174 .checked_add(2)
175 .ok_or(IgraphError::Internal("trussness overflow"))?;
176
177 let (src, dst) = graph.edge(eid)?;
178
179 if src == dst {
180 continue;
181 }
182
183 let common = sorted_intersection(&adj[src as usize], &adj[dst as usize]);
184
185 for nb in common {
186 let Some(edge_sn) = graph.find_eid(src, nb)? else {
187 continue;
188 };
189 let Some(edge_dn) = graph.find_eid(dst, nb)? else {
190 continue;
191 };
192
193 if completed[edge_sn as usize] || completed[edge_dn as usize] {
194 continue;
195 }
196
197 for &edge in &[edge_sn, edge_dn] {
198 let old_sup = support[edge as usize];
199 if old_sup > level_u32 {
200 buckets[old_sup as usize].remove(&edge);
201 let new_sup = old_sup.saturating_sub(1);
202 support[edge as usize] = new_sup;
203 let target = (new_sup as usize).max(level);
204 buckets[target].insert(edge);
205 }
206 }
207 }
208 }
209 }
210
211 Ok(truss)
212}
213
214fn sorted_intersection(a: &[VertexId], b: &[VertexId]) -> Vec<VertexId> {
215 let mut result = Vec::new();
216 let (mut ia, mut ib) = (0, 0);
217 while ia < a.len() && ib < b.len() {
218 match a[ia].cmp(&b[ib]) {
219 std::cmp::Ordering::Less => ia += 1,
220 std::cmp::Ordering::Greater => ib += 1,
221 std::cmp::Ordering::Equal => {
222 result.push(a[ia]);
223 ia += 1;
224 ib += 1;
225 }
226 }
227 }
228 result
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 fn make_undirected(n: u32, edges: &[(u32, u32)]) -> Graph {
236 let mut g = Graph::with_vertices(n);
237 for &(u, v) in edges {
238 g.add_edge(u, v).unwrap();
239 }
240 g
241 }
242
243 #[test]
244 fn empty_graph() {
245 let g = Graph::with_vertices(0);
246 let t = trussness(&g).unwrap();
247 assert!(t.is_empty());
248 }
249
250 #[test]
251 fn singleton_no_edges() {
252 let g = Graph::with_vertices(1);
253 let t = trussness(&g).unwrap();
254 assert!(t.is_empty());
255 }
256
257 #[test]
258 fn no_edges() {
259 let g = Graph::with_vertices(10);
260 let t = trussness(&g).unwrap();
261 assert!(t.is_empty());
262 }
263
264 #[test]
265 fn path_graph() {
266 let g = make_undirected(3, &[(0, 1), (1, 2)]);
267 let t = trussness(&g).unwrap();
268 assert_eq!(t, vec![2, 2]);
269 }
270
271 #[test]
272 fn triangle_k3() {
273 let g = make_undirected(3, &[(0, 1), (0, 2), (1, 2)]);
274 let t = trussness(&g).unwrap();
275 assert_eq!(t, vec![3, 3, 3]);
276 }
277
278 #[test]
279 fn k4_complete() {
280 let g = make_undirected(4, &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]);
281 let t = trussness(&g).unwrap();
282 assert!(t.iter().all(|&x| x == 4), "K4 trussness: {t:?}");
283 }
284
285 #[test]
286 fn k5_complete() {
287 let g = make_undirected(
288 5,
289 &[
290 (0, 1),
291 (0, 2),
292 (0, 3),
293 (0, 4),
294 (1, 2),
295 (1, 3),
296 (1, 4),
297 (2, 3),
298 (2, 4),
299 (3, 4),
300 ],
301 );
302 let t = trussness(&g).unwrap();
303 assert!(t.iter().all(|&x| x == 5), "K5 trussness: {t:?}");
304 }
305
306 #[test]
307 fn igraph_c_test_graph() {
308 let g = make_undirected(
309 12,
310 &[
311 (0, 1),
312 (0, 2),
313 (0, 3),
314 (0, 4),
315 (1, 2),
316 (1, 3),
317 (1, 4),
318 (2, 3),
319 (2, 4),
320 (3, 4),
321 (3, 6),
322 (3, 11),
323 (4, 5),
324 (4, 6),
325 (5, 6),
326 (5, 7),
327 (5, 8),
328 (5, 9),
329 (6, 7),
330 (6, 10),
331 (6, 11),
332 (7, 8),
333 (7, 9),
334 (8, 9),
335 (8, 10),
336 ],
337 );
338 let t = trussness(&g).unwrap();
339 #[rustfmt::skip]
340 let expected = vec![
341 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 3, 3, 3, 3, 3, 4, 4, 4, 3, 2, 3, 4, 4, 4, 2, ];
349 assert_eq!(t, expected);
350 }
351
352 #[test]
353 fn graph_with_self_loops() {
354 let mut g = make_undirected(3, &[(0, 1), (0, 2), (1, 2)]);
355 g.add_edge(0, 0).unwrap();
356 g.add_edge(2, 2).unwrap();
357 let t = trussness(&g).unwrap();
358 assert_eq!(t, vec![3, 3, 3, 2, 2]);
359 }
360
361 #[test]
362 fn directed_graph_rejected() {
363 let g = Graph::new(3, true).unwrap();
364 let err = trussness(&g).unwrap_err();
365 assert!(matches!(err, IgraphError::InvalidArgument(_)));
366 }
367
368 #[test]
369 fn multigraph_rejected() {
370 let mut g = Graph::with_vertices(3);
371 g.add_edge(0, 1).unwrap();
372 g.add_edge(0, 1).unwrap();
373 g.add_edge(1, 2).unwrap();
374 let err = trussness(&g).unwrap_err();
375 assert!(matches!(err, IgraphError::Unsupported(_)));
376 }
377
378 #[test]
379 fn star_graph() {
380 let g = make_undirected(5, &[(0, 1), (0, 2), (0, 3), (0, 4)]);
381 let t = trussness(&g).unwrap();
382 assert!(t.iter().all(|&x| x == 2));
383 }
384
385 #[test]
386 fn two_triangles_sharing_edge() {
387 let g = make_undirected(4, &[(0, 1), (0, 2), (1, 2), (0, 3), (1, 3)]);
388 let t = trussness(&g).unwrap();
389 assert!(
390 t.iter().all(|&x| x == 3),
391 "two triangles sharing edge: {t:?}"
392 );
393 }
394
395 #[test]
396 fn single_edge() {
397 let g = make_undirected(2, &[(0, 1)]);
398 let t = trussness(&g).unwrap();
399 assert_eq!(t, vec![2]);
400 }
401
402 #[test]
403 fn disconnected_triangles() {
404 let g = make_undirected(6, &[(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5)]);
405 let t = trussness(&g).unwrap();
406 assert!(t.iter().all(|&x| x == 3));
407 }
408
409 #[test]
410 fn bridge_between_triangles() {
411 let g = make_undirected(6, &[(0, 1), (0, 2), (1, 2), (2, 3), (3, 4), (3, 5), (4, 5)]);
412 let t = trussness(&g).unwrap();
413 assert_eq!(t[0], 3); assert_eq!(t[1], 3); assert_eq!(t[2], 3); assert_eq!(t[3], 2); assert_eq!(t[4], 3); assert_eq!(t[5], 3); assert_eq!(t[6], 3); }
421}
422
423#[cfg(all(test, feature = "proptest-harness"))]
424mod proptest_tests {
425 use super::*;
426 use proptest::prelude::*;
427
428 fn arb_simple_undirected(max_n: u32, max_e: usize) -> impl Strategy<Value = Graph> {
429 (3..=max_n).prop_flat_map(move |n| {
430 let possible = ((n as u64) * (n as u64 - 1) / 2) as usize;
431 let cap = max_e.min(possible);
432 proptest::collection::hash_set((0..n, 0..n), 0..=cap).prop_map(move |pairs| {
433 let mut g = Graph::with_vertices(n);
434 for (a, b) in pairs {
435 if a != b {
436 let (lo, hi) = if a < b { (a, b) } else { (b, a) };
437 if g.find_eid(lo, hi).ok().flatten().is_none() {
438 g.add_edge(lo, hi).unwrap();
439 }
440 }
441 }
442 g
443 })
444 })
445 }
446
447 proptest! {
448 #[test]
449 fn trussness_at_least_two(g in arb_simple_undirected(20, 40)) {
450 let t = trussness(&g).unwrap();
451 for &val in &t {
452 prop_assert!(val >= 2, "trussness must be >= 2, got {val}");
453 }
454 }
455
456 #[test]
457 fn trussness_k_complete(n in 3u32..=8) {
458 let mut g = Graph::with_vertices(n);
459 for u in 0..n {
460 for v in (u + 1)..n {
461 g.add_edge(u, v).unwrap();
462 }
463 }
464 let t = trussness(&g).unwrap();
465 for &val in &t {
466 prop_assert_eq!(val, n, "K{} trussness should be {}, got {}", n, n, val);
467 }
468 }
469
470 #[test]
471 fn trussness_bounded_by_graph_size(g in arb_simple_undirected(15, 30)) {
472 let t = trussness(&g).unwrap();
473 let n = g.vcount();
474 for &val in &t {
475 prop_assert!(val <= n, "trussness {val} exceeds vertex count {n}");
476 }
477 }
478 }
479}