diff --git a/raphtory-benchmark/benches/algobench.rs b/raphtory-benchmark/benches/algobench.rs index 3465253c76..4971d945dc 100644 --- a/raphtory-benchmark/benches/algobench.rs +++ b/raphtory-benchmark/benches/algobench.rs @@ -11,8 +11,12 @@ use raphtory::{ global_temporal_three_node_motifs::global_temporal_three_node_motif, local_triangle_count::local_triangle_count, }, + covering::{ + fast_distributed_dominating_set::fast_distributed_dominating_set, + dominating_set::lazy_greedy_dominating_set + } }, - graphgen::random_attachment::random_attachment, + graphgen::{preferential_attachment::ba_preferential_attachment, random_attachment::random_attachment}, prelude::*, }; use raphtory_benchmark::common::bench; @@ -130,6 +134,45 @@ pub fn temporal_motifs(c: &mut Criterion) { group.finish(); } +pub fn dominating_set(c: &mut Criterion) { + let mut group = c.benchmark_group("dominating_set_scaling"); + group.sample_size(100); + group.sampling_mode(SamplingMode::Flat); + + let sizes = [1_000, 10_000, 100_000, 300_000]; + let seed: [u8; 32] = [1; 32]; + + for &size in &sizes { + let g = Graph::new(); + ba_preferential_attachment(&g, size, 2, Some(seed)); + + + group.bench_with_input( + BenchmarkId::new("fast_distributed", size), + &g, + |b, graph| { + b.iter(|| { + let result = fast_distributed_dominating_set(graph); + black_box(result); + }) + } + ); + + group.bench_with_input( + BenchmarkId::new("lazy_greedy", size), + &g, + |b, graph| { + b.iter(|| { + let result = lazy_greedy_dominating_set(graph); + black_box(result); + }) + } + ); + } + + group.finish(); +} + criterion_group!( benches, local_triangle_count_analysis, @@ -138,5 +181,6 @@ criterion_group!( graphgen_large_pagerank, graphgen_large_concomp, temporal_motifs, + dominating_set ); criterion_main!(benches); diff --git a/raphtory/src/algorithms/covering/dominating_set.rs b/raphtory/src/algorithms/covering/dominating_set.rs new file mode 100644 index 0000000000..78a904fa57 --- /dev/null +++ b/raphtory/src/algorithms/covering/dominating_set.rs @@ -0,0 +1,338 @@ +use crate::{db::api::view::StaticGraphViewOps}; +use crate::db::api::view::node::NodeViewOps; +use raphtory_api::core::{ + entities::{ + VID, + }, +}; +use std::{ + collections::HashSet, +}; +use crate::db::api::view::graph::GraphViewOps; + +#[derive(Default, Clone)] +struct LinkedListNode { + next: Option, + prev: Option, + uncovered_count: usize, + vid: VID +} + +struct DominatingSetQueue { + linked_nodes: Vec, + uncovered_count_map: Vec>, + max_uncovered_count: usize, +} + +impl DominatingSetQueue { + pub fn from_graph(g: &G)-> Self { + let n_nodes = g.count_nodes(); + let mut linked_nodes = vec![LinkedListNode::default(); n_nodes]; + let mut uncovered_count_map = vec![None; n_nodes + 1]; + for node in g.nodes() { + let vid = node.node; + let index = vid.index(); + let uncovered_count = node.degree() + 1; + let current_linked_node = &mut linked_nodes[index]; + current_linked_node.uncovered_count = uncovered_count; + current_linked_node.vid = vid; + if let Some(existing_index) = uncovered_count_map[uncovered_count] { + current_linked_node.next = Some(existing_index); + linked_nodes[existing_index].prev = Some(index); + } + uncovered_count_map[uncovered_count] = Some(index); + } + Self { + linked_nodes, + uncovered_count_map, + max_uncovered_count: n_nodes, + } + } + + pub fn maximum(&mut self) -> Option { + while self.max_uncovered_count > 0 { + if let Some(index) = self.uncovered_count_map[self.max_uncovered_count] { + if let Some(next_index) = self.linked_nodes[index].next { + let next_linked_node = &mut self.linked_nodes[next_index]; + next_linked_node.prev = None; + self.uncovered_count_map[self.max_uncovered_count] = Some(next_index); + } else { + self.uncovered_count_map[self.max_uncovered_count] = None; + } + self.linked_nodes[index].next = None; + return Some(index); + } + self.max_uncovered_count -= 1; + } + None + } + // uncovered count should be less than the max uncovered count + pub fn insert(&mut self, index: usize, uncovered_count: usize) { + self.linked_nodes[index].uncovered_count = uncovered_count; + if let Some(existing_index) = self.uncovered_count_map[uncovered_count] { + self.linked_nodes[index].next = Some(existing_index); + self.linked_nodes[existing_index].prev = Some(index); + } + self.uncovered_count_map[uncovered_count] = Some(index); + } + + pub fn node_details(&self, index: usize) -> (VID, usize) { + let linked_node = &self.linked_nodes[index]; + (linked_node.vid, linked_node.uncovered_count) + } +} + + +pub fn lazy_greedy_dominating_set(g: &G) -> HashSet { + let n_nodes = g.count_nodes(); + let mut dominating_set: HashSet = HashSet::new(); + let mut covered_count = 0; + let mut covered_nodes: Vec = vec![false; n_nodes]; + let mut queue = DominatingSetQueue::from_graph(g); + while covered_count < n_nodes { + let index = queue.maximum().unwrap(); + let (vid, stale_uncovered_count) = queue.node_details(index); + let node = g.node(vid).unwrap(); + let mut actual_uncovered_count = 0; + if !covered_nodes[vid.index()] { + actual_uncovered_count += 1; + } + for neighbor in node.neighbours() { + if !covered_nodes[neighbor.node.index()] { + actual_uncovered_count += 1; + } + } + if actual_uncovered_count == stale_uncovered_count { + dominating_set.insert(vid); + if !covered_nodes[vid.index()] { + covered_nodes[vid.index()] = true; + covered_count += 1; + } + for neighbor in node.neighbours() { + if !covered_nodes[neighbor.node.index()] { + covered_nodes[neighbor.node.index()] = true; + covered_count += 1; + } + } + } else { + if actual_uncovered_count > 0 { + queue.insert(index, actual_uncovered_count); + } + } + } + dominating_set +} + +pub fn is_dominating_set(g: &G, dominating_set: &HashSet) -> bool { + let n_nodes = g.count_nodes(); + let mut covered_nodes: Vec = vec![false; n_nodes]; + let mut covered_count = 0; + for &vid in dominating_set { + if !covered_nodes[vid.index()] { + covered_nodes[vid.index()] = true; + covered_count += 1; + } + let node = g.node(vid).unwrap(); + for neighbor in node.neighbours() { + if !covered_nodes[neighbor.node.index()] { + covered_nodes[neighbor.node.index()] = true; + covered_count += 1; + } + } + } + covered_count == n_nodes +} + + + +#[cfg(test)] +mod tests { + use super::*; + use crate::prelude::*; + + #[test] + fn test_empty_graph() { + let graph = Graph::new(); + let ds = lazy_greedy_dominating_set(&graph); + + assert!(ds.is_empty(), "Empty graph should have an empty dominating set"); + } + + #[test] + fn test_single_node_graph() { + let graph = Graph::new(); + graph.add_node(0, 1, NO_PROPS, None).unwrap(); + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 1, "Single node should dominate itself"); + assert!(ds.contains(&VID(0)), "The single node should be in the dominating set"); + } + + #[test] + fn test_two_connected_nodes() { + let graph = Graph::new(); + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 1, "One node should be sufficient to dominate both nodes in an edge"); + } + + #[test] + fn test_star_graph() { + let graph = Graph::new(); + // Star with center 0 and leaves 1-5 + for i in 1..=5 { + graph.add_edge(0, 0, i, NO_PROPS, None).unwrap(); + } + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 1, "Star graph center should be the only node in the dominating set"); + assert!(ds.contains(&VID(0)), "Center node should be in the dominating set"); + } + + #[test] + fn test_path_graph() { + let graph = Graph::new(); + // Path: 1-2-3-4-5 + for i in 1..5 { + graph.add_edge(0, i, i + 1, NO_PROPS, None).unwrap(); + } + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + // For a path of 5 nodes, we need at most 2 nodes + assert!(ds.len() <= 2, "Path of 5 nodes should need at most 2 nodes in dominating set"); + } + + #[test] + fn test_triangle_graph() { + let graph = Graph::new(); + // Triangle: 1-2-3-1 + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); + graph.add_edge(0, 3, 1, NO_PROPS, None).unwrap(); + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 1, "Triangle should need only 1 node in dominating set"); + } + + #[test] + fn test_complete_graph() { + let graph = Graph::new(); + // Complete graph K4 + for i in 1..=4 { + for j in (i+1)..=4 { + graph.add_edge(0, i, j, NO_PROPS, None).unwrap(); + } + } + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 1, "Complete graph should need only 1 node in dominating set"); + } + + #[test] + fn test_disconnected_graph() { + let graph = Graph::new(); + // Two separate edges: 1-2 and 3-4 + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 3, 4, NO_PROPS, None).unwrap(); + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 2, "Two disconnected components need at least 2 nodes"); + } + + #[test] + fn test_cycle_graph() { + let graph = Graph::new(); + // Cycle: 1-2-3-4-5-1 + for i in 1..=5 { + let next = if i == 5 { 1 } else { i + 1 }; + graph.add_edge(0, i, next, NO_PROPS, None).unwrap(); + } + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + // For a cycle of 5 nodes, we need at most 2 nodes + assert!(ds.len() <= 2, "Cycle of 5 nodes should need at most 2 nodes in dominating set"); + } + + #[test] + fn test_bipartite_graph() { + let graph = Graph::new(); + // Complete bipartite graph K_{2,3} + for i in 1..=2 { + for j in 3..=5 { + graph.add_edge(0, i, j, NO_PROPS, None).unwrap(); + } + } + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + // Can be dominated by either all nodes from one partition (2 or 3 nodes) + assert!(ds.len() <= 3); + } + + #[test] + fn test_isolated_nodes() { + let graph = Graph::new(); + // Add isolated nodes without edges + graph.add_node(0, 1, NO_PROPS, None).unwrap(); + graph.add_node(0, 2, NO_PROPS, None).unwrap(); + graph.add_node(0, 3, NO_PROPS, None).unwrap(); + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert_eq!(ds.len(), 3, "All isolated nodes must be in the dominating set"); + } + + #[test] + fn test_mixed_graph() { + let graph = Graph::new(); + // Mix of connected and isolated nodes + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); + graph.add_node(0, 10, NO_PROPS, None).unwrap(); // Isolated node + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + assert!(ds.contains(&VID(3)), "Isolated node must be in the dominating set"); + // Should have at least 2 nodes: one for the connected part, one for the isolated node + assert!(ds.len() >= 2, "Should have at least 2 nodes in dominating set"); + } + + #[test] + fn test_larger_graph() { + let graph = Graph::new(); + // Create a more complex graph structure + // Central hub connected to multiple smaller clusters + for i in 1..=3 { + graph.add_edge(0, 0, i, NO_PROPS, None).unwrap(); + for j in 1..=2 { + let node_id = i * 10 + j; + graph.add_edge(0, i, node_id, NO_PROPS, None).unwrap(); + } + } + + let ds = lazy_greedy_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds), "Result should be a valid dominating set"); + println!("Larger graph dominating set size: {}", ds.len()); + } +} diff --git a/raphtory/src/algorithms/covering/fast_distributed_dominating_set.rs b/raphtory/src/algorithms/covering/fast_distributed_dominating_set.rs new file mode 100644 index 0000000000..d56886d602 --- /dev/null +++ b/raphtory/src/algorithms/covering/fast_distributed_dominating_set.rs @@ -0,0 +1,397 @@ +use crate::db::api::view::StaticGraphViewOps; +use crate::db::api::view::node::NodeViewOps; +use raphtory_api::core::{ + entities::{ + VID, + }, +}; +use std::{ + collections::HashSet, +}; +use crate::db::api::view::graph::GraphViewOps; +use rayon::prelude::*; + +#[derive(Default, Clone)] +struct NodeCoveringState { + vid: VID, + is_covered: bool, + is_active: bool, + is_candidate: bool, + has_no_coverage: bool, + support: usize, + candidates: usize, + weight_rounded: usize, + weight: usize, + add_to_dominating_set: bool +} + + +pub fn fast_distributed_dominating_set(g: &G) -> HashSet { + let mut dominating_set = HashSet::new(); + let mut covered_count = 0; + let n_nodes = g.count_nodes(); + let mut adj_list: Vec> = vec![vec![]; n_nodes]; + let mut current_node_configs = vec![NodeCoveringState::default(); n_nodes]; + let mut next_node_configs = vec![NodeCoveringState::default(); n_nodes]; + for node in g.nodes() { + let vid = node.node; + current_node_configs[vid.index()].vid = vid; + next_node_configs[vid.index()].vid = vid; + adj_list[vid.index()] = node.neighbours().iter().map(|n| n.node.index()).collect(); + } + while covered_count < n_nodes { + next_node_configs.par_iter_mut().for_each(|next_node_config| { + let node_index = next_node_config.vid.index(); + let current_node_config = ¤t_node_configs[node_index]; + next_node_config.is_covered = current_node_config.is_covered; + if current_node_config.has_no_coverage { + return; + } + let mut node_weight = 0 as u64; + if !current_node_config.is_covered { + node_weight += 1; + } + for neighbor_index in &adj_list[node_index] { + let neighbor_config = ¤t_node_configs[*neighbor_index]; + if !neighbor_config.is_covered { + node_weight += 1; + } + } + if node_weight == 0 { + next_node_config.has_no_coverage = true; + next_node_config.weight = 0; + next_node_config.weight_rounded = 0; + } else { + let node_weight_rounded = (2 as u64).pow(node_weight.ilog2()) as usize; + next_node_config.weight = node_weight as usize; + next_node_config.weight_rounded = node_weight_rounded; + } + }); + std::mem::swap(&mut current_node_configs, &mut next_node_configs); + next_node_configs.par_iter_mut().for_each(|next_node_config| { + let node_index = next_node_config.vid.index(); + let current_node_config = ¤t_node_configs[node_index]; + next_node_config.has_no_coverage = current_node_config.has_no_coverage; + next_node_config.weight = current_node_config.weight; + next_node_config.weight_rounded = current_node_config.weight_rounded; + if current_node_config.has_no_coverage { + next_node_config.is_active = false; + return; + } + let mut max_weight_rounded = current_node_config.weight_rounded; + for neighbor_index in &adj_list[node_index] { + let neighbor_config = ¤t_node_configs[*neighbor_index]; + if neighbor_config.weight_rounded > max_weight_rounded { + max_weight_rounded = neighbor_config.weight_rounded; + } + for second_neighbor_index in &adj_list[*neighbor_index] { + let second_neighbor_config = ¤t_node_configs[*second_neighbor_index]; + if second_neighbor_config.weight_rounded > max_weight_rounded { + max_weight_rounded = second_neighbor_config.weight_rounded; + } + } + } + if current_node_config.weight_rounded == max_weight_rounded { + next_node_config.is_active = true; + } else { + next_node_config.is_active = false; + } + }); + std::mem::swap(&mut current_node_configs, &mut next_node_configs); + next_node_configs.par_iter_mut().for_each(|next_node_config| { + let node_index = next_node_config.vid.index(); + let current_node_config = ¤t_node_configs[node_index]; + next_node_config.is_active = current_node_config.is_active; + if current_node_config.has_no_coverage { + next_node_config.support = 0; + return; + } + let mut support = 0; + if current_node_config.is_active { + support += 1; + } + for neighbor_index in &adj_list[node_index] { + let neighbor_config = ¤t_node_configs[*neighbor_index]; + if neighbor_config.is_active { + support += 1; + } + } + next_node_config.support = support; + }); + std::mem::swap(&mut current_node_configs, &mut next_node_configs); + next_node_configs.par_iter_mut().for_each(|next_node_config| { + let node_index = next_node_config.vid.index(); + let current_node_config = ¤t_node_configs[node_index]; + next_node_config.support = current_node_config.support; + if !current_node_config.is_active{ + next_node_config.is_candidate = false; + return; + } + let mut max_support = 0; + if !current_node_config.is_covered { + max_support = current_node_config.support; + } + for neighbor_index in &adj_list[node_index] { + let neighbor_config = ¤t_node_configs[*neighbor_index]; + if !neighbor_config.is_covered && neighbor_config.support > max_support { + max_support = neighbor_config.support; + } + } + let p = 1.0/(max_support as f64); + let r: f64 = rand::random(); + if r < p { + next_node_config.is_candidate = true; + } else { + next_node_config.is_candidate = false; + } + }); + std::mem::swap(&mut current_node_configs, &mut next_node_configs); + next_node_configs.par_iter_mut().for_each(|next_node_config| { + let node_index = next_node_config.vid.index(); + let current_node_config = ¤t_node_configs[node_index]; + next_node_config.is_candidate = current_node_config.is_candidate; + if current_node_config.has_no_coverage { + next_node_config.candidates = 0; + return; + } + let mut candidates = 0; + if current_node_config.is_candidate { + candidates += 1; + } + for neighbor_index in &adj_list[node_index] { + let neighbor_config = ¤t_node_configs[*neighbor_index]; + if neighbor_config.is_candidate { + candidates += 1; + } + } + next_node_config.candidates = candidates; + }); + std::mem::swap(&mut current_node_configs, &mut next_node_configs); + next_node_configs.par_iter_mut().for_each(|next_node_config| { + let node_index = next_node_config.vid.index(); + let current_node_config = ¤t_node_configs[node_index]; + next_node_config.candidates = current_node_config.candidates; + if !current_node_config.is_candidate { + return; + } + let mut sum_candidates = 0; + if !current_node_config.is_covered { + sum_candidates += current_node_config.candidates; + } + for neighbor_index in &adj_list[node_index] { + let neighbor_config = ¤t_node_configs[*neighbor_index]; + if !neighbor_config.is_covered { + sum_candidates += neighbor_config.candidates; + } + } + if sum_candidates <= 3 * current_node_config.weight_rounded { + next_node_config.add_to_dominating_set = true; + } + }); + std::mem::swap(&mut current_node_configs, &mut next_node_configs); + for i in 0..n_nodes { + let add_to_dominating_set = current_node_configs[i].add_to_dominating_set; + if add_to_dominating_set { + { + let node_config = &mut current_node_configs[i]; + dominating_set.insert(node_config.vid); + node_config.add_to_dominating_set = false; + if !node_config.is_covered { + node_config.is_covered = true; + covered_count += 1; + } + } + for neighbor_index in &adj_list[i] { + let neighbor_config = &mut current_node_configs[*neighbor_index]; + if !neighbor_config.is_covered { + neighbor_config.is_covered = true; + covered_count += 1; + } + } + } + } + } + dominating_set +} + + +#[cfg(test)] +mod tests { + use super::*; + use crate::prelude::*; + use super::super::dominating_set::is_dominating_set; + + #[test] + fn test_single_node_graph() { + let graph = Graph::new(); + graph.add_node(0, 1, NO_PROPS, None).unwrap(); + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert_eq!(ds.len(), 1, "Single node should dominate itself"); + } + + #[test] + fn test_two_connected_nodes() { + let graph = Graph::new(); + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert_eq!(ds.len(), 1, "One node should dominate an edge"); + } + + #[test] + fn test_star_graph() { + let graph = Graph::new(); + // Star with center 0 and leaves 1-5 + for i in 1..=5 { + graph.add_edge(0, 0, i, NO_PROPS, None).unwrap(); + } + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert_eq!(ds.len(), 1, "Star graph center should be the dominating set"); + } + + #[test] + fn test_path_graph() { + let graph = Graph::new(); + // Path: 1-2-3-4-5 + for i in 1..5 { + graph.add_edge(0, i, i + 1, NO_PROPS, None).unwrap(); + } + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + println!("Path graph dominating set: {:?}", ds); + } + + #[test] + fn test_triangle_graph() { + let graph = Graph::new(); + // Triangle: 1-2-3-1 + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); + graph.add_edge(0, 3, 1, NO_PROPS, None).unwrap(); + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert!(ds.len() <= 2, "At most 2 nodes needed to dominate a triangle"); + } + + #[test] + fn test_complete_graph_k4() { + let graph = Graph::new(); + // Complete graph K4 + for i in 1..=4 { + for j in (i + 1)..=4 { + graph.add_edge(0, i, j, NO_PROPS, None).unwrap(); + } + } + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert!(!ds.is_empty()); + } + + #[test] + fn test_cycle_graph() { + let graph = Graph::new(); + // Cycle: 1-2-3-4-5-1 + for i in 1..=5 { + graph.add_edge(0, i, if i == 5 { 1 } else { i + 1 }, NO_PROPS, None).unwrap(); + } + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + println!("Cycle graph dominating set: {:?}", ds); + } + + #[test] + fn test_disconnected_components() { + let graph = Graph::new(); + // Component 1: 1-2-3 + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); + // Component 2: 4-5 + graph.add_edge(0, 4, 5, NO_PROPS, None).unwrap(); + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert!(ds.len() >= 2, "Should have at least one node per component"); + } + + #[test] + fn test_grid_graph() { + let graph = Graph::new(); + // 3x3 grid + // 1-2-3 + // | | | + // 4-5-6 + // | | | + // 7-8-9 + let edges = vec![ + (1, 2), (2, 3), (1, 4), (2, 5), (3, 6), + (4, 5), (5, 6), (4, 7), (5, 8), (6, 9), + (7, 8), (8, 9), + ]; + + for (src, dst) in edges { + graph.add_edge(0, src, dst, NO_PROPS, None).unwrap(); + } + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + println!("Grid dominating set size: {}", ds.len()); + } + + #[test] + fn test_with_isolated_nodes() { + let graph = Graph::new(); + // Connected: 1-2-3 + graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + graph.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); + // Isolated nodes + graph.add_node(0, 4, NO_PROPS, None).unwrap(); + graph.add_node(0, 5, NO_PROPS, None).unwrap(); + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + assert!(ds.len() >= 3, "Each isolated node must be in the dominating set"); + } + + #[test] + fn test_larger_random_structure() { + let graph = Graph::new(); + // Create a more complex structure + let edges = vec![ + (1, 2), (1, 3), (1, 4), // Node 1 hub + (2, 5), (3, 6), (4, 7), // Branches + (5, 8), (6, 8), (7, 8), // Converge to 8 + (8, 9), (8, 10), // From 8 + (9, 10), // Triangle + ]; + + for (src, dst) in edges { + graph.add_edge(0, src, dst, NO_PROPS, None).unwrap(); + } + + let ds = fast_distributed_dominating_set(&graph); + + assert!(is_dominating_set(&graph, &ds)); + println!("Complex structure dominating set size: {}", ds.len()); + } +} + + diff --git a/raphtory/src/algorithms/covering/mod.rs b/raphtory/src/algorithms/covering/mod.rs new file mode 100644 index 0000000000..38fd5df508 --- /dev/null +++ b/raphtory/src/algorithms/covering/mod.rs @@ -0,0 +1,2 @@ +pub mod dominating_set; +pub mod fast_distributed_dominating_set; \ No newline at end of file diff --git a/raphtory/src/algorithms/mod.rs b/raphtory/src/algorithms/mod.rs index 9cfdb23900..25541e6632 100644 --- a/raphtory/src/algorithms/mod.rs +++ b/raphtory/src/algorithms/mod.rs @@ -32,6 +32,7 @@ pub mod community_detection; pub mod bipartite; pub mod components; pub mod cores; +pub mod covering; pub mod dynamics; pub mod embeddings; pub mod layout; diff --git a/raphtory/tests/algo_tests/mod.rs b/raphtory/tests/algo_tests/mod.rs index 3887662709..26ae22df08 100644 --- a/raphtory/tests/algo_tests/mod.rs +++ b/raphtory/tests/algo_tests/mod.rs @@ -6,3 +6,4 @@ mod embeddings; mod metrics; mod motifs; mod pathing; +