Skip to content

Conversation

@DanielLacina
Copy link
Contributor

No description provided.

Comment on lines +86 to +124
pub fn lazy_greedy_dominating_set<G: StaticGraphViewOps>(g: &G) -> HashSet<VID> {
let n_nodes = g.count_nodes();
let mut dominating_set: HashSet<VID> = HashSet::new();
let mut covered_count = 0;
let mut covered_nodes: Vec<bool> = 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
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://kk.up45.ac.id/scholarhub/Lazy%20and%20Eager%20Approaches%20for%20the%20Set%20Cover%20Problem.pdf
Algorithm 3
Used a bucket queue instead of a heap to decrease the time complexity of maximum() and insert() from O(logn) to O(1).

Comment on lines +28 to +50
pub fn from_graph<G: StaticGraphViewOps>(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,
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each raphtory node corresponds to the linked node - linked_nodes[node index] in the Dominating Set Queue.
A node can cover 0 - n uncovered nodes. Therefore, the uncovered_count_map contains n + 1 linked lists where the ith linked list in the map corresponds to all the nodes who cover i uncovered nodes.
The max_uncovered_count will stay at n but will be decremented later on request.
When each node is being processed in the for loop, their corresponding linked_node is updated and is set as the head of the ith linked list of the uncovered_count_map where i is the number of uncovered nodes it covers.

Comment on lines +52 to +68
pub fn maximum(&mut self) -> Option<usize> {
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
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Decrements self.max_uncovered_count until it finds a non empty linked list. Then it removes the head of the linked list and makes its successor (if any) the head of the linked list. The node index corresponding to the head of the linked list is returned.
Note: Any future value inserted into the Queue will have a value less than self.max_uncovered_count.

Comment on lines +70 to +77
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);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modifies the raphtory node corresponding linked node and makes it the head of the ith linked list.
Note: Probably not a good practice to assume that the uncovered count should be less than the max uncovered count (assuming inputs probably is not ideal). Makes the code non reusable across modules. Also, should probably somehow integrate the insert method/function into the from_graph constructor to abstract upon the insertion logic.

self.uncovered_count_map[uncovered_count] = Some(index);
}

pub fn node_details(&self, index: usize) -> (VID, usize) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

straightforward

Comment on lines +126 to +144
pub fn is_dominating_set<G: StaticGraphViewOps>(g: &G, dominating_set: &HashSet<VID>) -> bool {
let n_nodes = g.count_nodes();
let mut covered_nodes: Vec<bool> = 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
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Obtains the count of all unique nodes covered by all the nodes in the dominating set. By definition of dominating set, the count should be equal to n_nodes.

Comment on lines +42 to +214
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 = &current_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 = &current_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 = &current_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 = &current_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 = &current_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 = &current_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 = &current_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 = &current_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 = &current_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 = &current_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 = &current_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 = &current_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 = &current_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
Copy link
Contributor Author

@DanielLacina DanielLacina Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://ac.informatik.uni-freiburg.de/teaching/ss_12/netalg/lectures/chapter7.pdf
p. 5
Uses a double buffer method to prioritize minimal memory allocation
For each par_iter -> next_node_config is synchronized with current_node_config
and next_node_config is updated which becomes current_node_config. The two variables swap pointers. Logic is repeated for next par_iter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant