11#pragma once
22
3+ #include " bioimage_cpp/detail/indexed_heap.hxx"
34#include " bioimage_cpp/detail/union_find.hxx"
45#include " bioimage_cpp/graph/connected_components.hxx"
56#include " bioimage_cpp/graph/multicut/objective.hxx"
67#include " bioimage_cpp/graph/undirected_graph.hxx"
78
8- #include < algorithm>
99#include < cmath>
1010#include < cstddef>
1111#include < cstdint>
12- #include < queue >
12+ #include < limits >
1313#include < random>
14- #include < unordered_map>
15- #include < unordered_set>
1614#include < utility>
1715#include < vector>
1816
1917namespace bioimage_cpp ::graph::multicut::detail {
2018
19+ inline constexpr std::size_t no_edge = std::numeric_limits<std::size_t >::max();
20+
21+ // Per-super-node adjacency entry.
22+ struct NeighborEntry {
23+ std::size_t neighbor;
24+ std::size_t edge_id;
25+ };
26+
27+ // Per-edge data stored in a flat vector indexed by stable edge_id. Edge ids
28+ // never grow past the original number of input edges — when two edges fold
29+ // into one during a merge, one id survives and the other becomes orphaned
30+ // (no adjacency entry or heap entry references it again).
2131struct DynamicEdge {
32+ std::size_t u = 0 ;
33+ std::size_t v = 0 ;
2234 double weight = 0.0 ;
23- std:: uint64_t edition = 0 ;
35+ unsigned char is_constraint = 0 ;
2436};
2537
26- struct DynamicGraph {
27- explicit DynamicGraph (const std::size_t size) : adjacency(size), constraints(size), alive(size, true ), alive_count(size) {
28- }
38+ // Heap keyed by dense stable edge_id. The vector-backed DenseLocator updates
39+ // in O(1) per sift step, which was the missing ingredient for greedy_additive
40+ // to beat the std::priority_queue baseline.
41+ using EdgeHeap = bioimage_cpp::detail::DenseIndexedHeap<double >;
2942
43+ struct DynamicGraph {
3044 explicit DynamicGraph (const UndirectedGraph &graph)
3145 : adjacency(static_cast <std::size_t >(graph.number_of_nodes())),
32- constraints(static_cast <std::size_t >(graph.number_of_nodes())),
3346 alive(static_cast <std::size_t >(graph.number_of_nodes()), true),
34- alive_count(static_cast <std::size_t >(graph.number_of_nodes())) {
47+ alive_count(static_cast <std::size_t >(graph.number_of_nodes())),
48+ scratch_edge_id(static_cast <std::size_t >(graph.number_of_nodes()), no_edge) {
3549 for (std::uint64_t node = 0 ; node < graph.number_of_nodes (); ++node) {
3650 const auto degree = graph.node_adjacency (node).size ();
3751 adjacency[static_cast <std::size_t >(node)].reserve (degree);
3852 }
53+ edges.resize (static_cast <std::size_t >(graph.number_of_edges ()));
3954 }
4055
41- [[nodiscard]] bool edge_exists (const std::size_t u, const std::size_t v) const {
42- return alive[u] && alive[v] && adjacency[u].find (v) != adjacency[u].end ();
43- }
44-
45- [[nodiscard]] const DynamicEdge *edge (const std::size_t u, const std::size_t v) const {
46- if (!alive[u] || !alive[v]) {
47- return nullptr ;
56+ // O(degree(u)). Returns no_edge when (u, v) is not an edge.
57+ [[nodiscard]] std::size_t find_edge (const std::size_t u, const std::size_t v) const {
58+ if (u >= adjacency.size () || v >= adjacency.size ()) {
59+ return no_edge;
4860 }
49- const auto found = adjacency[u].find (v);
50- return found == adjacency[u].end () ? nullptr : &found->second ;
51- }
52-
53- [[nodiscard]] bool has_constraint (const std::size_t u, const std::size_t v) const {
54- return constraints[u].find (v) != constraints[u].end ();
55- }
56-
57- void add_constraint (const std::size_t u, const std::size_t v) {
58- constraints[u].insert (v);
59- constraints[v].insert (u);
60- }
61-
62- void set_initial_edge (const std::size_t u, const std::size_t v, const double weight) {
63- adjacency[u][v] = DynamicEdge{weight, 0 };
64- adjacency[v][u] = DynamicEdge{weight, 0 };
61+ for (const auto &entry : adjacency[u]) {
62+ if (entry.neighbor == v) {
63+ return entry.edge_id ;
64+ }
65+ }
66+ return no_edge;
6567 }
6668
67- std::uint64_t update_edge (const std::size_t u, const std::size_t v, const double weight) {
68- const auto current = adjacency[u].find (v);
69- const auto edition = current == adjacency[u].end () ? 0 : current->second .edition + 1 ;
70- adjacency[u][v] = DynamicEdge{weight, edition};
71- adjacency[v][u] = DynamicEdge{weight, edition};
72- return edition;
69+ [[nodiscard]] bool edge_exists (const std::size_t u, const std::size_t v) const {
70+ return find_edge (u, v) != no_edge;
7371 }
7472
75- void remove_node (const std::size_t node) {
76- for (const auto &entry : adjacency[node]) {
77- adjacency[entry.first ].erase (node);
78- constraints[entry.first ].erase (node);
79- }
80- adjacency[node].clear ();
81- constraints[node].clear ();
82- alive[node] = false ;
83- --alive_count;
73+ [[nodiscard]] bool has_constraint (const std::size_t u, const std::size_t v) const {
74+ const auto id = find_edge (u, v);
75+ return id != no_edge && edges[id].is_constraint != 0 ;
8476 }
8577
86- std::vector<std::unordered_map<std:: size_t , DynamicEdge >> adjacency;
87- std::vector<std::unordered_set<std:: size_t >> constraints ;
78+ std::vector<std::vector<NeighborEntry >> adjacency;
79+ std::vector<DynamicEdge> edges ;
8880 std::vector<bool > alive;
8981 std::size_t alive_count;
82+ // Sized to #super-nodes, all entries == no_edge between merges. Used by
83+ // merge_dynamic_nodes to find the existing stable-side edge for each of
84+ // removed's neighbors in O(1) without hashing.
85+ std::vector<std::size_t > scratch_edge_id;
9086};
9187
92- struct QueueEdge {
93- std::size_t u = 0 ;
94- std::size_t v = 0 ;
95- double priority = 0.0 ;
96- std::uint64_t edition = 0 ;
97-
98- bool operator <(const QueueEdge &other) const {
99- return priority < other.priority ;
100- }
101- };
88+ inline double priority_for (const double weight, const bool absolute_priority) {
89+ return absolute_priority ? std::abs (weight) : weight;
90+ }
10291
10392inline void initialize_dynamic_graph (
10493 const UndirectedGraph &graph,
10594 const std::vector<double > &costs,
10695 DynamicGraph &dynamic_graph,
107- std::priority_queue<QueueEdge> &queue ,
96+ EdgeHeap &heap ,
10897 const bool absolute_priority,
10998 const bool add_noise = false ,
11099 const int seed = 42 ,
111100 const double sigma = 1.0
112101) {
102+ const auto n_edges = static_cast <std::size_t >(graph.number_of_edges ());
103+ heap.reset_capacity (n_edges);
104+
113105 std::mt19937 generator (seed);
114106 std::normal_distribution<double > noise (0.0 , sigma);
115107 for (std::uint64_t edge = 0 ; edge < graph.number_of_edges (); ++edge) {
@@ -120,8 +112,15 @@ inline void initialize_dynamic_graph(
120112 if (add_noise) {
121113 weight += noise (generator);
122114 }
123- dynamic_graph.set_initial_edge (u, v, weight);
124- queue.push (QueueEdge{u, v, absolute_priority ? std::abs (weight) : weight, 0 });
115+ const auto edge_id = static_cast <std::size_t >(edge);
116+ auto &e = dynamic_graph.edges [edge_id];
117+ e.u = u;
118+ e.v = v;
119+ e.weight = weight;
120+ e.is_constraint = 0 ;
121+ dynamic_graph.adjacency [u].push_back ({v, edge_id});
122+ dynamic_graph.adjacency [v].push_back ({u, edge_id});
123+ heap.push (edge_id, priority_for (weight, absolute_priority));
125124 }
126125}
127126
@@ -138,10 +137,45 @@ inline std::size_t stop_node_count(const UndirectedGraph &graph, const double no
138137 : static_cast <std::size_t >(double (graph.number_of_nodes ()) * node_num_stop + 0.5 );
139138}
140139
140+ namespace internal {
141+
142+ // Erase the first entry with neighbor == target from `list` (swap-with-back).
143+ // Returns whether anything was erased.
144+ inline bool erase_by_neighbor (std::vector<NeighborEntry> &list, const std::size_t target) {
145+ for (std::size_t i = 0 ; i < list.size (); ++i) {
146+ if (list[i].neighbor == target) {
147+ list[i] = list.back ();
148+ list.pop_back ();
149+ return true ;
150+ }
151+ }
152+ return false ;
153+ }
154+
155+ // Rename the first entry whose neighbor == from_node to point to to_node.
156+ inline void rename_neighbor (
157+ std::vector<NeighborEntry> &list,
158+ const std::size_t from_node,
159+ const std::size_t to_node
160+ ) {
161+ for (auto &entry : list) {
162+ if (entry.neighbor == from_node) {
163+ entry.neighbor = to_node;
164+ return ;
165+ }
166+ }
167+ }
168+
169+ } // namespace internal
170+
171+ // Contract the edge between u and v in the dynamic graph. The smaller-degree
172+ // super-node is folded into the larger-degree one to keep the per-super-node
173+ // adjacency growth amortized. The heap is kept in sync without staleness: each
174+ // edge id appears at most once.
141175inline std::size_t merge_dynamic_nodes (
142176 DynamicGraph &dynamic_graph,
143177 bioimage_cpp::detail::UnionFind &sets,
144- std::priority_queue<QueueEdge> &queue ,
178+ EdgeHeap &heap ,
145179 std::size_t u,
146180 std::size_t v,
147181 const bool absolute_priority
@@ -151,36 +185,79 @@ inline std::size_t merge_dynamic_nodes(
151185 if (u == v) {
152186 return u;
153187 }
188+
154189 auto stable = u;
155190 auto removed = v;
156191 if (dynamic_graph.adjacency [stable].size () < dynamic_graph.adjacency [removed].size ()) {
157192 std::swap (stable, removed);
158193 }
159194 sets.merge_to (stable, removed);
160- std::vector<std::pair<std::size_t , DynamicEdge>> neighbors (
161- dynamic_graph.adjacency [removed].begin (),
162- dynamic_graph.adjacency [removed].end ()
163- );
164- dynamic_graph.adjacency [stable].erase (removed);
165- dynamic_graph.constraints [stable].erase (removed);
166- for (const auto &[neighbor, removed_edge] : neighbors) {
195+
196+ // Stamp stable's neighbors so each removed-neighbor lookup is O(1).
197+ for (const auto &entry : dynamic_graph.adjacency [stable]) {
198+ dynamic_graph.scratch_edge_id [entry.neighbor ] = entry.edge_id ;
199+ }
200+
201+ // The contracted edge (stable, removed) must be in stable's adjacency.
202+ const auto contracted_edge_id = dynamic_graph.scratch_edge_id [removed];
203+ heap.erase (contracted_edge_id);
204+ dynamic_graph.scratch_edge_id [removed] = no_edge;
205+ internal::erase_by_neighbor (dynamic_graph.adjacency [stable], removed);
206+
207+ // Snapshot removed's neighbors before mutating its adjacency.
208+ const auto removed_neighbors = dynamic_graph.adjacency [removed];
209+
210+ for (const auto &entry : removed_neighbors) {
211+ const auto neighbor = entry.neighbor ;
212+ const auto removed_edge_id = entry.edge_id ;
167213 if (neighbor == stable) {
168214 continue ;
169215 }
170- const auto current = dynamic_graph.edge (stable, neighbor);
171- const auto merged_weight = (current == nullptr ? 0.0 : current->weight ) + removed_edge.weight ;
172- if (dynamic_graph.has_constraint (removed, neighbor)) {
173- dynamic_graph.add_constraint (stable, neighbor);
216+
217+ const auto existing_id = dynamic_graph.scratch_edge_id [neighbor];
218+ if (existing_id == no_edge) {
219+ // No existing stable-side edge to `neighbor`. Re-key the
220+ // removed-side edge by replacing `removed` with `stable` on both
221+ // sides. The edge_id, weight and constraint flag are preserved,
222+ // so the heap entry remains valid without any priority change.
223+ dynamic_graph.adjacency [stable].push_back ({neighbor, removed_edge_id});
224+ dynamic_graph.scratch_edge_id [neighbor] = removed_edge_id;
225+ internal::rename_neighbor (dynamic_graph.adjacency [neighbor], removed, stable);
226+ auto &e = dynamic_graph.edges [removed_edge_id];
227+ if (e.u == removed) {
228+ e.u = stable;
229+ } else {
230+ e.v = stable;
231+ }
232+ } else {
233+ // Stable already has an edge to `neighbor`; fold removed's weight
234+ // (and constraint flag) into it. Then drop the removed-side edge.
235+ auto &keep = dynamic_graph.edges [existing_id];
236+ const auto &fold = dynamic_graph.edges [removed_edge_id];
237+ keep.weight += fold.weight ;
238+ const bool propagated_constraint =
239+ keep.is_constraint != 0 || fold.is_constraint != 0 ;
240+ keep.is_constraint = propagated_constraint ? 1 : 0 ;
241+
242+ heap.erase (removed_edge_id);
243+ internal::erase_by_neighbor (dynamic_graph.adjacency [neighbor], removed);
244+
245+ if (propagated_constraint) {
246+ heap.erase (existing_id);
247+ } else {
248+ heap.change (existing_id, priority_for (keep.weight , absolute_priority));
249+ }
174250 }
175- const auto edition = dynamic_graph.update_edge (stable, neighbor, merged_weight);
176- queue.push (QueueEdge{
177- std::min (stable, neighbor),
178- std::max (stable, neighbor),
179- absolute_priority ? std::abs (merged_weight) : merged_weight,
180- edition
181- });
182251 }
183- dynamic_graph.remove_node (removed);
252+
253+ // Clear scratch via the updated stable adjacency (includes appended entries).
254+ for (const auto &entry : dynamic_graph.adjacency [stable]) {
255+ dynamic_graph.scratch_edge_id [entry.neighbor ] = no_edge;
256+ }
257+
258+ dynamic_graph.adjacency [removed].clear ();
259+ dynamic_graph.alive [removed] = false ;
260+ --dynamic_graph.alive_count ;
184261 return stable;
185262}
186263
0 commit comments