VeloGraphX
High-performance dynamic graph analytics in C++20
Loading...
Searching...
No Matches
bfs.hpp
Go to the documentation of this file.
1#pragma once
2#include <algorithm>
3#include <cstdint>
4#include <functional>
5#include <limits>
6#include <queue>
7#include <utility>
8#include <vector>
11
12namespace velographx {
13template <class Graph>
15 public:
16 static constexpr std::uint32_t unreachable = std::numeric_limits<std::uint32_t>::max();
17
18 BasicIncrementalBFS(Graph& g, VertexId source, double deletion_fallback_fraction = 0.35)
19 : g_(g), source_(source), deletion_fallback_fraction_(deletion_fallback_fraction) {
20 recompute();
21 }
22
23 [[nodiscard]] const std::vector<std::uint32_t>& distances() const noexcept { return dist_; }
24 [[nodiscard]] std::size_t reachable_count() const noexcept { return reachable_count_; }
25 [[nodiscard]] std::size_t last_deletion_candidates() const noexcept { return last_deletion_candidates_; }
26 [[nodiscard]] std::size_t last_affected_vertices() const noexcept { return last_affected_vertices_; }
27 [[nodiscard]] bool last_used_full_recompute() const noexcept { return last_used_full_recompute_; }
28
29 void apply(const UpdateBatch& batch) {
30 last_deletion_candidates_ = 0;
31 last_affected_vertices_ = 0;
32 last_used_full_recompute_ = false;
33 if (batch.empty()) return;
34
35 ensure_workspace(vertex_count(g_));
36 prepare_batch_workspace(batch.updates.size());
37
38 for (auto it = batch.updates.rbegin(); it != batch.updates.rend(); ++it) {
39 auto u = it->src;
40 auto v = it->dst;
41 if (!is_directed(g_) && v < u) std::swap(u, v);
42 const auto key = edge_key(u, v);
43 if (!seen_final_updates_.insert(key)) continue;
44 if (it->add) {
45 final_additions_.emplace_back(it->src, it->dst);
46 } else {
47 final_deletions_.emplace_back(it->src, it->dst);
48 final_deletion_keys_.insert(key);
49 }
50 }
51
52 deletion_candidates_.reserve(final_deletions_.size() * (is_directed(g_) ? 1 : 2));
53 existing_deletions_.reserve(final_deletions_.size());
54 for (const auto& [u, v] : final_deletions_) {
55 if (!has_edge(g_, u, v)) continue;
56 existing_deletions_.emplace_back(u, v);
57 if (is_shortest_parent(u, v)) deletion_candidates_.push_back(v);
58 if (!is_directed(g_) && is_shortest_parent(v, u)) deletion_candidates_.push_back(u);
59 }
60 std::sort(deletion_candidates_.begin(), deletion_candidates_.end());
61 deletion_candidates_.erase(std::unique(deletion_candidates_.begin(), deletion_candidates_.end()),
62 deletion_candidates_.end());
63 last_deletion_candidates_ = deletion_candidates_.size();
64
65 affected_vertices_.reserve(std::min<std::size_t>(deletion_candidates_.size() * 2 + 8,
66 vertex_count(g_)));
67 bool fallback_needed = false;
68 if (!deletion_candidates_.empty()) {
69 fallback_needed = !compute_affected_prebatch(existing_deletions_, final_deletion_keys_);
70 }
71
72 apply_updates(g_, batch);
73 if (dist_.size() < vertex_count(g_)) dist_.resize(vertex_count(g_), unreachable);
74 ensure_workspace(vertex_count(g_));
75
76 if (fallback_needed) {
77 clear_workspace();
78 recompute();
79 last_used_full_recompute_ = true;
80 return;
81 }
82
83 if (!affected_vertices_.empty()) {
84 old_affected_dist_.reserve(affected_vertices_.size());
85 for (auto v : affected_vertices_) {
86 old_affected_dist_.push_back(v < dist_.size() ? dist_[v] : unreachable);
87 }
88 repair_affected();
89 }
90
91 bfs_queue_.clear();
92 for (const auto& [u, v] : final_additions_) {
93 relax_edge(u, v, bfs_queue_);
94 if (!is_directed(g_)) relax_edge(v, u, bfs_queue_);
95 }
96 propagate_decreases(bfs_queue_);
97 clear_workspace();
98 }
99
100 void recompute() {
101 dist_.assign(vertex_count(g_), unreachable);
102 reachable_count_ = 0;
103 ensure_workspace(vertex_count(g_));
104 if (source_ >= vertex_count(g_)) return;
105 bfs_queue_.clear();
106 bfs_queue_.reserve(std::max(bfs_queue_.capacity(), vertex_count(g_)));
107 dist_[source_] = 0;
108 bfs_queue_.push_back(source_);
109 std::size_t head = 0;
110 while (head < bfs_queue_.size()) {
111 const auto u = bfs_queue_[head++];
112 for_each_neighbor(g_, u, [&](VertexId v) {
113 if (dist_[v] == unreachable) {
114 dist_[v] = dist_[u] + 1;
115 bfs_queue_.push_back(v);
116 }
117 });
118 }
119 reachable_count_ = bfs_queue_.size();
120 }
121
122 private:
123 class ReusableKeySet {
124 public:
125 void reset(std::size_t expected_entries) {
126 std::size_t required = 8;
127 const auto target = expected_entries * 2 + 1;
128 while (required < target) required <<= 1;
129 if (keys_.size() < required) {
130 keys_.assign(required, 0);
131 stamps_.assign(required, 0);
132 mask_ = required - 1;
133 generation_ = 1;
134 return;
135 }
136 ++generation_;
137 if (generation_ == 0) {
138 std::fill(stamps_.begin(), stamps_.end(), 0);
139 generation_ = 1;
140 }
141 }
142
143 bool insert(std::uint64_t key) noexcept {
144 std::size_t slot = static_cast<std::size_t>(mix(key)) & mask_;
145 while (stamps_[slot] == generation_) {
146 if (keys_[slot] == key) return false;
147 slot = (slot + 1) & mask_;
148 }
149 keys_[slot] = key;
150 stamps_[slot] = generation_;
151 return true;
152 }
153
154 [[nodiscard]] bool contains(std::uint64_t key) const noexcept {
155 if (keys_.empty()) return false;
156 std::size_t slot = static_cast<std::size_t>(mix(key)) & mask_;
157 while (stamps_[slot] == generation_) {
158 if (keys_[slot] == key) return true;
159 slot = (slot + 1) & mask_;
160 }
161 return false;
162 }
163
164 private:
165 static std::uint64_t mix(std::uint64_t value) noexcept {
166 value += 0x9e3779b97f4a7c15ULL;
167 value = (value ^ (value >> 30)) * 0xbf58476d1ce4e5b9ULL;
168 value = (value ^ (value >> 27)) * 0x94d049bb133111ebULL;
169 return value ^ (value >> 31);
170 }
171
172 std::vector<std::uint64_t> keys_;
173 std::vector<std::uint32_t> stamps_;
174 std::size_t mask_{0};
175 std::uint32_t generation_{0};
176 };
177
178 [[nodiscard]] std::uint64_t edge_key(VertexId u, VertexId v) const noexcept {
179 if (!is_directed(g_) && v < u) std::swap(u, v);
180 return (static_cast<std::uint64_t>(u) << 32) | static_cast<std::uint64_t>(v);
181 }
182
183 void ensure_workspace(std::size_t vertices) {
184 if (affected_.size() < vertices) affected_.resize(vertices, 0);
185 if (lost_parent_count_.size() < vertices) lost_parent_count_.resize(vertices, 0);
186 if (shortest_parent_count_.size() < vertices) shortest_parent_count_.resize(vertices, 0);
187 }
188
189 void prepare_batch_workspace(std::size_t operations) {
190 seen_final_updates_.reset(operations);
191 final_deletion_keys_.reset(operations);
192 final_additions_.clear();
193 final_deletions_.clear();
194 existing_deletions_.clear();
195 deletion_candidates_.clear();
196 affected_vertices_.clear();
197 old_affected_dist_.clear();
198 invalidate_.clear();
199 touched_loss_vertices_.clear();
200
201 if (final_additions_.capacity() < operations) final_additions_.reserve(operations);
202 if (final_deletions_.capacity() < operations) final_deletions_.reserve(operations);
203 if (existing_deletions_.capacity() < operations) existing_deletions_.reserve(operations);
204 }
205
206 [[nodiscard]] bool is_shortest_parent(VertexId u, VertexId v) const noexcept {
207 return u < dist_.size() && v < dist_.size() && dist_[u] != unreachable &&
208 dist_[v] != unreachable && dist_[u] + 1 == dist_[v];
209 }
210
211 [[nodiscard]] std::uint32_t shortest_parent_count(VertexId v) {
212 if (v >= dist_.size() || v == source_ || dist_[v] == unreachable) return 0;
213 if (shortest_parent_count_[v] != 0) return shortest_parent_count_[v];
214 std::uint32_t count = 0;
215 for_each_in_neighbor(g_, v, [&](VertexId p) {
216 if (is_shortest_parent(p, v)) ++count;
217 });
218 shortest_parent_count_[v] = count;
219 return count;
220 }
221
222 bool compute_affected_prebatch(
223 const std::vector<std::pair<VertexId, VertexId>>& existing_deletions,
224 const ReusableKeySet& final_deletion_keys) {
225 const auto fallback_limit = std::max<std::size_t>(
226 1, static_cast<std::size_t>(static_cast<double>(vertex_count(g_)) * deletion_fallback_fraction_));
227 invalidate_.reserve(std::min<std::size_t>(existing_deletions.size() * 2 + 8, vertex_count(g_)));
228
229 auto record_parent_loss = [&](VertexId v) {
230 if (v >= dist_.size() || v == source_ || dist_[v] == unreachable || affected_[v]) return;
231 if (lost_parent_count_[v] == 0) touched_loss_vertices_.push_back(v);
232 ++lost_parent_count_[v];
233 const auto support = shortest_parent_count(v);
234 if (support != 0 && lost_parent_count_[v] >= support) {
235 affected_[v] = 1;
236 affected_vertices_.push_back(v);
237 invalidate_.push_back(v);
238 ++last_affected_vertices_;
239 }
240 };
241
242 for (const auto& [u, v] : existing_deletions) {
243 if (is_shortest_parent(u, v)) record_parent_loss(v);
244 if (!is_directed(g_) && is_shortest_parent(v, u)) record_parent_loss(u);
245 }
246
247 std::size_t head = 0;
248 while (head < invalidate_.size()) {
249 if (last_affected_vertices_ > fallback_limit) return false;
250 const auto u = invalidate_[head++];
251 if (u >= dist_.size() || dist_[u] == unreachable) continue;
252 for_each_neighbor(g_, u, [&](VertexId v) {
253 if (v >= dist_.size() || dist_[v] != dist_[u] + 1) return;
254 if (final_deletion_keys.contains(edge_key(u, v))) return;
255 record_parent_loss(v);
256 });
257 }
258 return last_affected_vertices_ <= fallback_limit;
259 }
260
261 [[nodiscard]] std::uint32_t best_boundary_distance(VertexId v) const {
262 std::uint32_t best = unreachable;
263 for_each_in_neighbor(g_, v, [&](VertexId p) {
264 if (p >= dist_.size() || p >= affected_.size() || affected_[p] || dist_[p] == unreachable) return;
265 const auto candidate = dist_[p] + 1;
266 if (candidate < best) best = candidate;
267 });
268 return best;
269 }
270
271 void repair_affected() {
272 for (auto v : affected_vertices_) {
273 if (v < dist_.size() && dist_[v] != unreachable) {
274 dist_[v] = unreachable;
275 --reachable_count_;
276 }
277 }
278
279 using Item = std::pair<std::uint32_t, VertexId>;
280 const auto compare = std::greater<Item>{};
281 repair_heap_.clear();
282 if (repair_heap_.capacity() < affected_vertices_.size()) {
283 repair_heap_.reserve(affected_vertices_.size());
284 }
285 for (auto v : affected_vertices_) {
286 const auto best = best_boundary_distance(v);
287 if (best != unreachable) {
288 if (dist_[v] == unreachable) ++reachable_count_;
289 dist_[v] = best;
290 repair_heap_.emplace_back(best, v);
291 std::push_heap(repair_heap_.begin(), repair_heap_.end(), compare);
292 }
293 }
294
295 while (!repair_heap_.empty()) {
296 std::pop_heap(repair_heap_.begin(), repair_heap_.end(), compare);
297 const auto [du, u] = repair_heap_.back();
298 repair_heap_.pop_back();
299 if (u >= dist_.size() || du != dist_[u]) continue;
300 for_each_neighbor(g_, u, [&](VertexId v) {
301 if (v >= affected_.size() || !affected_[v]) return;
302 const auto candidate = du + 1;
303 if (candidate < dist_[v]) {
304 if (dist_[v] == unreachable) ++reachable_count_;
305 dist_[v] = candidate;
306 repair_heap_.emplace_back(candidate, v);
307 std::push_heap(repair_heap_.begin(), repair_heap_.end(), compare);
308 }
309 });
310 }
311
312 bfs_queue_.clear();
313 for (std::size_t i = 0; i < affected_vertices_.size(); ++i) {
314 const auto v = affected_vertices_[i];
315 const auto old_dist = i < old_affected_dist_.size() ? old_affected_dist_[i] : unreachable;
316 if (v < dist_.size() && dist_[v] != unreachable && dist_[v] < old_dist) bfs_queue_.push_back(v);
317 }
318 propagate_decreases(bfs_queue_);
319 }
320
321 void clear_workspace() {
322 for (auto v : affected_vertices_) {
323 if (v < affected_.size()) affected_[v] = 0;
324 }
325 for (auto v : touched_loss_vertices_) {
326 if (v < lost_parent_count_.size()) lost_parent_count_[v] = 0;
327 if (v < shortest_parent_count_.size()) shortest_parent_count_[v] = 0;
328 }
329 touched_loss_vertices_.clear();
330 }
331
332 void relax_edge(VertexId u, VertexId v, std::vector<VertexId>& q) {
333 if (u >= dist_.size() || v >= dist_.size() || dist_[u] == unreachable) return;
334 if (dist_[u] + 1 < dist_[v]) {
335 if (dist_[v] == unreachable) ++reachable_count_;
336 dist_[v] = dist_[u] + 1;
337 q.push_back(v);
338 }
339 }
340
341 void propagate_decreases(std::vector<VertexId>& q) {
342 std::size_t head = 0;
343 while (head < q.size()) {
344 const auto u = q[head++];
345 if (dist_[u] == unreachable) continue;
346 for_each_neighbor(g_, u, [&](VertexId v) {
347 if (dist_[u] + 1 < dist_[v]) {
348 if (dist_[v] == unreachable) ++reachable_count_;
349 dist_[v] = dist_[u] + 1;
350 q.push_back(v);
351 }
352 });
353 }
354 }
355
356 Graph& g_;
357 VertexId source_;
358 double deletion_fallback_fraction_{0.35};
359 std::vector<std::uint32_t> dist_;
360 std::size_t reachable_count_{0};
361 std::vector<std::uint8_t> affected_;
362 std::vector<std::uint32_t> lost_parent_count_;
363 std::vector<std::uint32_t> shortest_parent_count_;
364 std::vector<VertexId> touched_loss_vertices_;
365
366 ReusableKeySet seen_final_updates_;
367 ReusableKeySet final_deletion_keys_;
368 std::vector<std::pair<VertexId, VertexId>> final_additions_;
369 std::vector<std::pair<VertexId, VertexId>> final_deletions_;
370 std::vector<std::pair<VertexId, VertexId>> existing_deletions_;
371 std::vector<VertexId> deletion_candidates_;
372 std::vector<VertexId> affected_vertices_;
373 std::vector<std::uint32_t> old_affected_dist_;
374 std::vector<VertexId> invalidate_;
375 std::vector<VertexId> bfs_queue_;
376 std::vector<std::pair<std::uint32_t, VertexId>> repair_heap_;
377
378 std::size_t last_deletion_candidates_{0};
379 std::size_t last_affected_vertices_{0};
380 bool last_used_full_recompute_{false};
381};
382
384
385} // namespace velographx
std::size_t last_deletion_candidates() const noexcept
Definition bfs.hpp:25
const std::vector< std::uint32_t > & distances() const noexcept
Definition bfs.hpp:23
static constexpr std::uint32_t unreachable
Definition bfs.hpp:16
std::size_t reachable_count() const noexcept
Definition bfs.hpp:24
void apply(const UpdateBatch &batch)
Definition bfs.hpp:29
BasicIncrementalBFS(Graph &g, VertexId source, double deletion_fallback_fraction=0.35)
Definition bfs.hpp:18
bool last_used_full_recompute() const noexcept
Definition bfs.hpp:27
std::size_t last_affected_vertices() const noexcept
Definition bfs.hpp:26
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)
void for_each_in_neighbor(const Graph &graph, VertexId v, Fn &&fn)
bool empty() const noexcept
std::vector< EdgeUpdate > updates