VeloGraphX
High-performance dynamic graph analytics in C++20
Loading...
Searching...
No Matches
connected_components.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <cstddef>
5#include <cstdint>
6#include <numeric>
7#include <stdexcept>
8#include <unordered_set>
9#include <vector>
10
13
14namespace velographx {
15
16template <class Graph>
18 public:
19 explicit BasicIncrementalComponents(Graph& g) : g_(g) {
20 if (is_directed(g_)) {
21 throw std::invalid_argument(
22 "IncrementalComponents requires an undirected graph; use an explicitly defined weak/strong connectivity algorithm for directed graphs");
23 }
24 rebuild();
25 }
26
27 [[nodiscard]] std::uint32_t component(VertexId v) {
28 if (v >= parent_.size()) throw std::out_of_range("vertex id outside graph");
29 return find(v);
30 }
31 [[nodiscard]] std::size_t last_repaired_vertices() const noexcept {
32 return last_repaired_vertices_;
33 }
34
35 void apply(const UpdateBatch& batch) {
36 if (batch.empty()) {
37 last_repaired_vertices_ = 0;
38 return;
39 }
40
41 const auto canonical = canonicalize(batch);
42 std::unordered_set<VertexId> affected_roots;
43 affected_roots.reserve(canonical.updates.size() * 2 + 1);
44
45 // Only final, effective deletions can split a pre-batch component. Mark the
46 // complete pre-batch component so it can be rebuilt exactly after mutation.
47 for (const auto& e : canonical.updates) {
48 if (!e.add && e.src < parent_.size() && e.dst < parent_.size() &&
49 has_edge(g_, e.src, e.dst)) {
50 affected_roots.insert(find(e.src));
51 affected_roots.insert(find(e.dst));
52 }
53 }
54
55 std::vector<std::uint8_t> affected(parent_.size(), 0);
56 last_repaired_vertices_ = 0;
57 if (!affected_roots.empty()) {
58 for (VertexId v = 0; v < parent_.size(); ++v) {
59 if (affected_roots.contains(find(v))) {
60 affected[v] = 1;
61 ++last_repaired_vertices_;
62 }
63 }
64 }
65
66 apply_updates(g_, batch);
67 ensure_capacity();
68 affected.resize(parent_.size(), 0);
69
70 if (!affected_roots.empty()) {
71 for (VertexId v = 0; v < affected.size(); ++v) {
72 if (affected[v]) {
73 parent_[v] = v;
74 rank_[v] = 0;
75 }
76 }
77
78 // Rebuild connectivity inside the old affected components using the
79 // final graph state. Additions that cross the old boundary are handled by
80 // the canonical-addition pass below.
81 for (VertexId u = 0; u < affected.size(); ++u) {
82 if (!affected[u]) continue;
83 for_each_neighbor(g_, u, [&](VertexId v) {
84 if (v < affected.size() && affected[v]) unite(u, v);
85 });
86 }
87 }
88
89 // Replay only the final operation for each logical undirected edge. This is
90 // the key correctness rule for add->remove / remove->add conflicts.
91 for (const auto& e : canonical.updates) {
92 if (e.add && e.src < parent_.size() && e.dst < parent_.size() &&
93 has_edge(g_, e.src, e.dst)) {
94 unite(e.src, e.dst);
95 }
96 }
97 }
98
99 private:
100 static std::uint64_t edge_key(VertexId u, VertexId v) noexcept {
101 if (v < u) std::swap(u, v);
102 return (static_cast<std::uint64_t>(u) << 32U) | static_cast<std::uint64_t>(v);
103 }
104
105 static UpdateBatch canonicalize(const UpdateBatch& batch) {
106 UpdateBatch out;
107 out.updates.reserve(batch.updates.size());
108 std::unordered_set<std::uint64_t> seen;
109 seen.reserve(batch.updates.size() * 2 + 1);
110
111 for (auto it = batch.updates.rbegin(); it != batch.updates.rend(); ++it) {
112 if (it->src == it->dst) continue;
113 auto op = *it;
114 if (op.dst < op.src) std::swap(op.src, op.dst);
115 if (seen.insert(edge_key(op.src, op.dst)).second) out.updates.push_back(op);
116 }
117 std::reverse(out.updates.begin(), out.updates.end());
118 return out;
119 }
120
121 void ensure_capacity() {
122 const auto old_size = parent_.size();
123 if (old_size >= vertex_count(g_)) return;
124 parent_.resize(vertex_count(g_));
125 rank_.resize(vertex_count(g_), 0);
126 for (VertexId v = static_cast<VertexId>(old_size); v < vertex_count(g_); ++v) parent_[v] = v;
127 }
128
129 void rebuild() {
130 parent_.resize(vertex_count(g_));
131 rank_.assign(vertex_count(g_), 0);
132 std::iota(parent_.begin(), parent_.end(), 0);
133 for (VertexId u = 0; u < vertex_count(g_); ++u) {
134 for_each_neighbor(g_, u, [&](VertexId v) { unite(u, v); });
135 }
136 last_repaired_vertices_ = vertex_count(g_);
137 }
138
139 VertexId find(VertexId x) { return parent_[x] == x ? x : parent_[x] = find(parent_[x]); }
140
141 void unite(VertexId a, VertexId b) {
142 ensure_capacity();
143 a = find(a);
144 b = find(b);
145 if (a == b) return;
146 if (rank_[a] < rank_[b]) std::swap(a, b);
147 parent_[b] = a;
148 if (rank_[a] == rank_[b]) ++rank_[a];
149 }
150
151 Graph& g_;
152 std::vector<VertexId> parent_;
153 std::vector<std::uint8_t> rank_;
154 std::size_t last_repaired_vertices_{0};
155};
156
158
159} // namespace velographx
std::size_t last_repaired_vertices() const noexcept
void apply_updates(Graph &graph, const Batch &batch)
void for_each_neighbor(const Graph &graph, VertexId u, Fn &&fn)
bool has_edge(const Graph &graph, VertexId u, VertexId v)
constexpr bool is_directed(const Graph &graph)
std::uint32_t VertexId
Definition frontier.hpp:6
constexpr std::size_t vertex_count(const Graph &graph)
bool empty() const noexcept