-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathtopological_sort.hpp
More file actions
522 lines (489 loc) · 20.6 KB
/
Copy pathtopological_sort.hpp
File metadata and controls
522 lines (489 loc) · 20.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
/**
* @file topological_sort.hpp
*
* @brief Topological sorting algorithm for directed acyclic graphs (DAGs).
*
* Topological sort produces a linear ordering of vertices such that for every directed
* edge (u,v), vertex u appears before vertex v in the ordering. This is essential for:
* - Task scheduling with dependencies
* - Build system ordering (makefiles, compilers)
* - Course prerequisite planning
* - Package dependency resolution
*
* The algorithm uses depth-first search to compute finish times for all vertices, then
* outputs vertices in reverse finish-time order. This produces a valid topological
* ordering if and only if the graph is acyclic (a DAG). If a cycle is detected via a
* back edge during DFS, the algorithm returns false.
*
* Three variants are provided:
* 1. Full-graph: topological_sort(g, result) - sorts all vertices
* 2. Single-source: topological_sort(g, source, result) - sorts vertices reachable from one vertex
* 3. Multi-source: topological_sort(g, sources, result) - sorts vertices reachable from multiple vertices
*
* **Complexity Analysis:**
*
* **Time Complexity:**
* | Case | Complexity | Notes |
* |------|-----------|-------|
* | Best case | O(V + E) | Always linear in graph size |
* | Average case | O(V + E) | DFS visits each vertex and edge once |
* | Worst case | O(V + E) | No pathological cases |
*
* Where:
* - V = number of vertices (or V_r = reachable vertices for source variants)
* - E = number of edges (or E_r = reachable edges for source variants)
*
* **Space Complexity:**
* | Component | Space | Purpose |
* |-----------|-------|---------|
* | Color array | O(V) | Track vertex state (White/Gray/Black) |
* | Finish order | O(V) | Collect vertices in finish order |
* | DFS stack | O(V) | Store stack frames for iterative DFS |
* | **Total** | **O(V)** | Auxiliary space, excluding graph |
*
* **Supported Graph Properties:**
*
* **Directedness:**
* - ✅ Directed graphs (required)
* - ❌ Undirected graphs (topological sort only defined for directed graphs)
*
* **Edge Properties:**
* - ✅ Unweighted edges (edge weights ignored)
* - ✅ Weighted edges (weights ignored for ordering)
* - ✅ Multi-edges (all edges traversed, may affect DFS tree)
* - ✅ Self-loops (detected as cycles, returns false)
*
* **Graph Structure:**
* - ✅ Connected graphs
* - ✅ Disconnected graphs (processes all components in full-graph variant)
* - ✅ Must be acyclic (DAG) - returns false if cycle detected
* - ❌ Cyclic graphs (algorithm detects and returns false)
*
* **Container Requirements:**
* - Requires: `adjacency_list<G>` concept
* - Requires: `std::output_iterator<OutputIterator, vertex_id_t<G>>`
* - Works with: All `dynamic_graph` container combinations (index and map-based)
* - Works with: Any graph type satisfying the concept
*
* **Implementation Notes:**
*
* **Algorithm Overview:**
* 1. Initialize color array: all vertices White (undiscovered)
* 2. For each unvisited vertex (or specified sources):
* - Perform iterative DFS using explicit stack
* - Mark vertices Gray when discovered (on stack)
* - Detect cycles: back edge to Gray vertex
* - Mark vertices Black when finished (all descendants processed)
* - Record finish time by appending to finish_order
* 3. Output vertices in reverse finish order (topological order)
*
* **Three-Color Scheme:**
* - **White:** Vertex not yet discovered
* - **Gray:** Vertex discovered but not finished (currently on DFS stack)
* - **Black:** Vertex finished (all descendants processed)
*
* **Cycle Detection:**
* - **Back edge:** Edge to Gray vertex → cycle detected → return false
* - **Tree edge:** Edge to White vertex → continue DFS
* - **Forward/Cross edge:** Edge to Black vertex → ignore (already processed)
*
* **Data Structures:**
* - **Color array:** `vertex_property_map<G, Color>` — lazy init, absent keys default to White
* - **Finish order:** `std::vector<vertex_id_t<G>>` collects vertices as finished
* - **DFS stack:** `std::stack<StackFrame>` stores (vertex_id, edge_iterator, edge_sentinel)
* - **Output:** Reversed finish_order produces topological ordering
*
* **Design Decisions:**
* 1. **Why iterative DFS instead of recursive?**
* - Avoids stack overflow on deep graphs
* - Allows inspection/modification of stack state
* - More portable across platforms with limited stack size
*
* 2. **Why three variants instead of one general function?**
* - Full-graph variant is most common use case (optimize for this)
* - Single-source avoids creating temporary array wrapper
* - Multi-source is most general, others delegate to it
*
* 3. **Why return bool instead of throwing on cycle?**
* - Cycles are expected conditions in many applications
* - Boolean return is more efficient (no exception overhead)
* - Easier to reason about control flow
*
* 4. **Why output to iterator instead of returning vector?**
* - Flexibility: can output to any container or adapter
* - Avoids allocation if caller already has container
* - Composable with other algorithms
*
* **Optimization Opportunities:**
* - For small graphs: recursive DFS may be slightly faster (avoid stack allocation)
* - For disconnected graphs: could optimize full-graph to skip isolated vertices
* - Could add early termination if only partial ordering needed
*
* @copyright Copyright (c) 2024
*
* SPDX-License-Identifier: BSL-1.0
*
* @authors Andrew Lumsdaine, Phil Ratzloff
*/
#include "graph/graph.hpp"
#include "graph/algorithm/depth_first_search.hpp"
#include "graph/algorithm/traversal_common.hpp"
#include "graph/adj_list/vertex_property_map.hpp"
#include "graph/views/incidence.hpp"
#include <vector>
#include <stack>
#include <ranges>
#ifndef GRAPH_TOPOSORT_ALGORITHM_HPP
# define GRAPH_TOPOSORT_ALGORITHM_HPP
namespace graph {
// Using declarations for new namespace structure
using adj_list::adjacency_list;
using adj_list::vertex_id_t;
using adj_list::vertices;
using adj_list::vertex_id;
using adj_list::target_id;
namespace detail {
// Vertex color states for DFS in topological sort
enum class TopoColor : uint8_t {
White, // Undiscovered
Gray, // Discovered but not finished (on stack)
Black // Finished
};
/**
* @brief Helper function for DFS visit during topological sort.
*
* Performs iterative DFS from a source vertex, collecting finish order and detecting cycles.
*
* @tparam G Graph type
* @tparam ColorMap Vertex property map type for colors
* @tparam Alloc Allocator type for the internal DFS stack.
* @param g The graph
* @param source Starting vertex ID
* @param color Color map for tracking vertex state
* @param finish_order Vector to collect vertices in finish order
* @param has_cycle Flag set to true if cycle detected
* @param alloc Allocator instance used for the internal DFS stack
*/
template <adjacency_list G, typename ColorMap, class Alloc>
void topological_sort_dfs_visit(const G& g,
const vertex_id_t<G>& start_vertex_id,
ColorMap& color,
auto& finish_order,
bool& has_cycle,
const Alloc& alloc) {
using Color = TopoColor;
using id_type = vertex_id_t<G>;
using namespace graph::views;
using inc_range_t = decltype(basic_incidence(g, start_vertex_id));
using inc_iterator_t = std::ranges::iterator_t<inc_range_t>;
using inc_sentinel_t = std::ranges::sentinel_t<inc_range_t>;
struct StackFrame {
id_type vertex_id;
inc_iterator_t it;
inc_sentinel_t end;
};
// Discover source and push its stack frame
color[start_vertex_id] = Color::Gray;
using FrameAlloc = typename std::allocator_traits<Alloc>::template rebind_alloc<StackFrame>;
std::stack<StackFrame, std::deque<StackFrame, FrameAlloc>> S{std::deque<StackFrame, FrameAlloc>(FrameAlloc(alloc))};
{
auto inc = basic_incidence(g, start_vertex_id);
S.push({start_vertex_id, std::ranges::begin(inc), std::ranges::end(inc)});
}
while (!S.empty() && !has_cycle) {
auto& frame = S.top();
if (frame.it == frame.end) {
// All edges exhausted: mark vertex finished and record finish time
color[frame.vertex_id] = Color::Black;
finish_order.push_back(frame.vertex_id);
S.pop();
continue;
}
// Process next edge from this vertex
auto&& [vid] = *frame.it;
++frame.it;
const Color target_color = vertex_property_map_get(color, vid, Color::White);
if (target_color == Color::White) {
// Tree edge: discover target and push its frame
color[vid] = Color::Gray;
auto inc = basic_incidence(g, vid);
S.push({vid, std::ranges::begin(inc), std::ranges::end(inc)});
} else if (target_color == Color::Gray) {
// Back edge: cycle detected
has_cycle = true;
return;
}
// Black vertices (forward/cross edges) are ignored - already processed
}
}
} // namespace detail
/**
* @brief Compute topological ordering of vertices reachable from multiple sources.
*
* Performs topological sort starting from multiple source vertices using depth-first
* search. Outputs all vertices reachable from any source in reverse finish-time order.
* Returns false if a cycle is detected in the reachable subgraph.
*
* @tparam G Graph type satisfying adjacency_list concept.
* @tparam Sources Input range of source vertex IDs.
* @tparam OutputIterator Output iterator for writing vertex IDs in topological order.
* @tparam Alloc Allocator type for the internal finish-order vector and DFS stack.
* Defaults to std::allocator<std::byte>.
*
* @param g The directed graph to sort.
* @param sources Range of starting vertex IDs for traversal.
* @param result Output iterator where vertex IDs are written in topological order.
* @param alloc Allocator instance used for the internal DFS stack and finish-order vector
* (default: Alloc())
*
* @return true if reachable subgraph is acyclic, false if cycle detected.
*
* **Mandates:**
* - G must satisfy adjacency_list
* - Sources must satisfy std::ranges::input_range with values convertible to vertex_id_t<G>
* - OutputIterator must satisfy std::output_iterator<vertex_id_t<G>>
*
* **Preconditions:**
* - Graph g must be directed
* - All vertex IDs in sources must be valid
* - Reachable subgraph should be a DAG for successful result
*
* **Effects:**
* - Performs DFS from each source vertex, recording finish order
* - Outputs reachable vertices in reverse finish-time order (topological order)
* - Does not modify graph g
*
* **Postconditions:**
* - If returns true: for every edge (u,v) where both are reachable, u appears before v.
* Each reachable vertex appears exactly once. Unreachable vertices are excluded.
* - If returns false: cycle was detected; output may contain partial results
* - If sources is empty: returns true with no output
*
* **Returns:**
* - true if reachable subgraph is acyclic and ordering is valid (bool)
* - false if cycle detected in reachable subgraph
* - Attribute: [[nodiscard]] recommended
*
* **Throws:**
* - std::bad_alloc if memory allocation fails
* - May propagate exceptions from sources range iteration
* - Exception guarantee: Basic. Graph g remains unchanged; output may be partial.
*
* **Complexity:**
* - Time: O(V_r + E_r) where V_r = reachable vertices, E_r = reachable edges
* - Space: O(V) for color array, O(V_r) for finish order
*
* **Remarks:**
* - This is the most general form; single-source and full-graph variants delegate to it
* - Redundant sources (where one is reachable from another) are handled efficiently
* - Sources can be in any order; components are processed as encountered
*
* ## Example Usage
*
* ```cpp
* std::vector<uint32_t> sources = {0, 1};
* std::vector<uint32_t> order;
* if (topological_sort(g, sources, std::back_inserter(order))) {
* // order contains reachable vertices in valid topological order
* }
* ```
*
* @see topological_sort(const G&, OutputIterator) for full-graph variant
* @see topological_sort(const G&, vertex_id_t<G>, OutputIterator) for single-source variant
*/
template <adjacency_list G, std::ranges::input_range Sources, class OutputIterator,
class Alloc = std::allocator<std::byte>>
requires std::convertible_to<std::ranges::range_value_t<Sources>, vertex_id_t<G>> &&
std::output_iterator<OutputIterator, vertex_id_t<G>>
bool topological_sort(const G& g, const Sources& sources, OutputIterator result,
const Alloc& alloc = Alloc()) {
using id_type = vertex_id_t<G>;
using Color = detail::TopoColor;
using out_iter_t = std::remove_cvref_t<OutputIterator>;
using out_id_type = std::conditional_t<
requires { typename out_iter_t::container_type::value_type; },
typename out_iter_t::container_type::value_type,
id_type>;
// Lazy init: index graphs get a sized vector (value-init → White=0),
// mapped graphs get an empty reserved map (absent key → White via get).
auto color = make_vertex_property_map<std::remove_reference_t<G>, Color>(g);
using IdAlloc = typename std::allocator_traits<Alloc>::template rebind_alloc<id_type>;
std::vector<id_type, IdAlloc> finish_order{IdAlloc(alloc)};
finish_order.reserve(num_vertices(g));
bool has_cycle = false;
// Run DFS from each source (skipping already-visited vertices)
for (auto source_vid : sources) {
if (vertex_property_map_get(color, source_vid, Color::White) == Color::White) {
detail::topological_sort_dfs_visit(g, source_vid, color, finish_order, has_cycle, alloc);
if (has_cycle) {
return false; // Cycle detected
}
}
}
// Output vertices in reverse finish order (topological order)
for (const auto vid : finish_order | std::views::reverse) {
*result++ = static_cast<out_id_type>(vid);
}
return true;
}
/**
* @brief Compute topological ordering of vertices reachable from a single source.
*
* Performs topological sort starting from a single source vertex. Only vertices
* reachable from the source are included in the output.
*
* @tparam G Graph type satisfying adjacency_list concept.
* @tparam OutputIterator Output iterator for writing vertex IDs in topological order.
* @tparam Alloc Allocator type for internal storage. Defaults to std::allocator<std::byte>.
*
* @param g The directed graph to sort.
* @param source Starting vertex ID for traversal.
* @param result Output iterator where vertex IDs are written in topological order.
* @param alloc Allocator instance forwarded to the multi-source version (default: Alloc())
*
* @return true if reachable subgraph is acyclic, false if cycle detected.
*
* **Mandates:**
* - G must satisfy adjacency_list
* - OutputIterator must satisfy std::output_iterator<vertex_id_t<G>>
*
* **Preconditions:**
* - Graph g must be directed
* - source must be a valid vertex ID in the graph
*
* **Effects:**
* - Delegates to multi-source variant with single-element array
* - Does not modify graph g
*
* **Postconditions:**
* - If returns true: reachable vertices in valid topological order, source vertex included
* - If returns false: cycle detected; output may contain partial results
*
* **Returns:**
* - true if reachable subgraph is acyclic (bool)
* - false if cycle detected
*
* **Throws:**
* - std::bad_alloc if memory allocation fails
* - Exception guarantee: Basic. Graph g remains unchanged.
*
* **Complexity:**
* - Time: O(V_r + E_r) where V_r = reachable vertices, E_r = reachable edges
* - Space: O(V) for color array, O(V_r) for finish order
*
* ## Example Usage
*
* ```cpp
* std::vector<uint32_t> order;
* if (topological_sort(g, 0, std::back_inserter(order))) {
* // order contains vertices reachable from 0 in topological order
* }
* ```
*
* @see topological_sort(const G&, OutputIterator) for full-graph variant
* @see topological_sort(const G&, const Sources&, OutputIterator) for multi-source variant
*/
template <adjacency_list G, class OutputIterator, class Alloc = std::allocator<std::byte>>
requires std::output_iterator<OutputIterator, vertex_id_t<G>>
bool topological_sort(const G& g, const vertex_id_t<G>& start_vertex_id, OutputIterator result,
const Alloc& alloc = Alloc()) {
// Delegate to multi-source version with single source
std::array<vertex_id_t<G>, 1> sources = {start_vertex_id};
return topological_sort(g, sources, result, alloc);
}
/**
* @brief Compute topological ordering of all vertices in a directed acyclic graph (DAG).
*
* Performs topological sort of the entire graph using depth-first search. Outputs all
* vertices in reverse finish-time order. Returns false if a cycle is detected.
*
* @tparam G Graph type satisfying adjacency_list concept.
* @tparam OutputIterator Output iterator for writing vertex IDs in topological order.
* @tparam Alloc Allocator type for internal storage. Defaults to std::allocator<std::byte>.
*
* @param g The directed graph to sort.
* @param result Output iterator where vertex IDs are written in topological order.
* @param alloc Allocator instance used for the internal finish-order vector and DFS stack
* (default: Alloc())
*
* @return true if graph is acyclic, false if cycle detected.
*
* **Mandates:**
* - G must satisfy adjacency_list
* - OutputIterator must satisfy std::output_iterator<vertex_id_t<G>>
*
* **Preconditions:**
* - Graph g must be directed
* - Graph should be a DAG for successful result
*
* **Effects:**
* - Performs DFS from each unvisited vertex, recording finish order
* - Outputs all vertices in reverse finish-time order (topological order)
* - Does not modify graph g
*
* **Postconditions:**
* - If returns true: for every directed edge (u,v), u appears before v in output.
* All vertices appear exactly once.
* - If returns false: cycle detected; output may contain partial results
*
* **Returns:**
* - true if graph is acyclic and ordering is valid (bool)
* - false if cycle detected (graph is not a DAG)
*
* **Throws:**
* - std::bad_alloc if memory allocation fails
* - Exception guarantee: Basic. Graph g remains unchanged; output may be partial.
*
* **Complexity:**
* - Time: O(V + E) where V = number of vertices, E = number of edges
* - Space: O(V) for color array, finish order vector, and DFS stack
*
* ## Example Usage
*
* ```cpp
* std::vector<uint32_t> order;
* if (topological_sort(g, std::back_inserter(order))) {
* // order contains all vertices in valid topological order
* } else {
* // Graph contains a cycle
* }
* ```
*
* @see topological_sort(const G&, vertex_id_t<G>, OutputIterator) for single-source variant
* @see topological_sort(const G&, const Sources&, OutputIterator) for multi-source variant
*/
template <adjacency_list G, class OutputIterator, class Alloc = std::allocator<std::byte>>
requires std::output_iterator<OutputIterator, vertex_id_t<G>>
bool topological_sort(const G& g, OutputIterator result, const Alloc& alloc = Alloc()) {
using id_type = vertex_id_t<G>;
using Color = detail::TopoColor;
using out_iter_t = std::remove_cvref_t<OutputIterator>;
using out_id_type = std::conditional_t<
requires { typename out_iter_t::container_type::value_type; },
typename out_iter_t::container_type::value_type,
id_type>;
// Lazy init: index graphs get a sized vector (value-init → White=0),
// mapped graphs get an empty reserved map (absent key → White via get).
auto color = make_vertex_property_map<std::remove_reference_t<G>, Color>(g);
using IdAlloc = typename std::allocator_traits<Alloc>::template rebind_alloc<id_type>;
std::vector<id_type, IdAlloc> finish_order{IdAlloc(alloc)};
finish_order.reserve(num_vertices(g));
bool has_cycle = false;
// Run DFS from each unvisited vertex
for (auto v : vertices(g)) {
id_type vid = vertex_id(g, v);
if (vertex_property_map_get(color, vid, Color::White) == Color::White) {
detail::topological_sort_dfs_visit(g, vid, color, finish_order, has_cycle, alloc);
if (has_cycle) {
return false; // Cycle detected
}
}
}
// Output vertices in reverse finish order (topological order)
for (const auto vid : finish_order | std::views::reverse) {
*result++ = static_cast<out_id_type>(vid);
}
return true;
}
} // namespace graph
#endif // GRAPH_TOPOSORT_ALGORITHM_HPP