-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmst.hpp
More file actions
912 lines (852 loc) · 34.9 KB
/
mst.hpp
File metadata and controls
912 lines (852 loc) · 34.9 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
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
/**
* @file mst.hpp
*
* @brief Minimum Spanning Tree (MST) algorithms: Kruskal's and Prim's.
*
* A minimum spanning tree is a subset of edges in a weighted, connected, undirected graph
* that connects all vertices with the minimum total edge weight, and contains no cycles.
* For a graph with V vertices, an MST contains exactly V-1 edges. If the graph is disconnected,
* the algorithms produce a minimum spanning forest (MST for each connected component).
*
* **Use Cases:**
* - Network design (minimum cost cable/pipe/road layout)
* - Clustering algorithms (single-linkage clustering)
* - Image segmentation
* - Approximation algorithms for NP-hard problems (TSP, Steiner tree)
* - Circuit design (minimizing wire length)
*
* **Algorithm Selection Guide:**
*
* **Kruskal's Algorithm:**
* - Best for sparse graphs (E << V²)
* - Processes edges in sorted order by weight
* - Uses union-find (disjoint-set) data structure
* - Works on edge list representation
* - Better cache locality for edge-oriented operations
*
* **Prim's Algorithm:**
* - Best for dense graphs (E ≈ V²)
* - Grows MST from a seed vertex
* - Uses priority queue (min-heap)
* - Works on adjacency list representation
* - Generates connected tree (single component only)
*
* **Implementation Variants:**
* 1. `kruskal()` - Standard Kruskal, copies edge list for sorting
* 2. `inplace_kruskal()` - Sorts input edge list in place (destructive)
* 3. `prim()` - Prim's algorithm with customizable comparison
*
* ---
*
* **Complexity Analysis:**
*
* **Kruskal's Algorithm:**
*
* | Case | Complexity | Notes |
* |------|-----------|-------|
* | Best case | O(E log E) | Dominated by edge sorting |
* | Average case | O(E log E) | Same for all inputs |
* | Worst case | O(E log E) | Equivalent to O(E log V) since E < V² |
*
* Where:
* - V = number of vertices
* - E = number of edges
* - Union-find operations are nearly O(1) with path compression and union by rank
*
* **Space Complexity (Kruskal):**
* | Component | Space | Purpose |
* |-----------|-------|---------|
* | Edge copy | O(E) | Sorted edge list (standard variant) |
* | Disjoint sets | O(V) | Union-find data structure |
* | Output MST | O(V) | V-1 edges in resulting tree |
* | **Total** | **O(E + V)** | Auxiliary space |
*
* **Prim's Algorithm:**
*
* | Case | Complexity | Notes |
* |------|-----------|-------|
* | Best case | O(E log V) | Using binary heap |
* | Average case | O(E log V) | With binary heap priority queue |
* | Worst case | O(E log V) | All edges examined |
*
* Alternative implementations:
* - With Fibonacci heap: O(E + V log V)
* - With simple array: O(V²) - good for dense graphs
*
* **Space Complexity (Prim):**
* | Component | Space | Purpose |
* |-----------|-------|---------|
* | Distance array | O(V) | Track minimum edge weights |
* | Priority queue | O(V) | Vertices to process |
* | Predecessor array | O(V) | Store MST structure (output) |
* | Weight array | O(V) | Edge weights in MST (output) |
* | **Total** | **O(V)** | Auxiliary space |
*
* ---
*
* **Supported Graph Properties:**
*
* **Directedness:**
* - ✅ Undirected graphs (primary use case)
* - ⚠️ Directed graphs (treats edges as undirected, may produce unexpected results)
* - Note: For directed graphs, MST is not well-defined; use minimum spanning arborescence
*
* **Edge Properties:**
* - ✅ Weighted edges (required for non-trivial MST)
* - ✅ Integer or floating-point weights
* - ⚠️ Negative weights (algorithm works but MST concept assumes non-negative)
* - ✅ Duplicate edges (uses edge with minimum weight)
* - ✅ Self-loops (ignored by union-find)
* - ✅ Custom comparison operators (min or max spanning tree)
*
* **Graph Structure:**
* - ✅ Connected graphs (produces single spanning tree)
* - ✅ Disconnected graphs (produces spanning forest, one tree per component)
* - ✅ Complete graphs
* - ✅ Sparse graphs (Kruskal is optimal)
* - ✅ Dense graphs (Prim is optimal)
*
* **Container Requirements:**
*
* **Kruskal's Algorithm:**
* - Requires: `x_index_edgelist_range<IELR>` - forward range of edge descriptors
* - Requires: `integral<source_id_type>` and `integral<target_id_type>`
* - Requires: Edge value type that supports comparison
* - Works with: `std::vector` of edge structs with `source_id`, `target_id`, `value`
* - Output: Any container supporting `push_back()` with same edge descriptor type
*
* **Prim's Algorithm:**
* - Requires: `adjacency_list<G>` concept
* - Requires: `vertex_property_map_for<Predecessor, G>` and `vertex_property_map_for<Weight, G>`
* - Works with: All `dynamic_graph` container combinations (contiguous and mapped IDs)
* - Works with: Any graph supporting incidence view
*
* ---
*
* **Implementation Notes:**
*
* **Kruskal's Algorithm:**
* 1. Sort all edges by weight (ascending for minimum, descending for maximum)
* 2. Find maximum vertex ID to size disjoint-set data structure
* 3. Initialize union-find: each vertex is its own set
* 4. For each edge (u,v) in sorted order:
* - Check if u and v are in different sets (union-find query)
* - If different: add edge to MST, union the sets
* - If same: skip edge (would create cycle)
* 5. Stop when V-1 edges added (or all edges processed for disconnected graphs)
*
* **Union-Find Optimizations:**
* - **Path compression:** `disjoint_find()` flattens tree during lookups
* - **Union by rank:** `disjoint_union()` attaches smaller tree to larger
* - Combined: nearly O(1) amortized time per operation
*
* **Prim's Algorithm:**
* 1. Initialize all distances to infinity, seed vertex to 0
* 2. Add seed vertex to priority queue
* 3. While queue not empty:
* - Extract vertex u with minimum distance
* - For each neighbor v of u:
* - If edge weight w < current distance[v]:
* - Update distance[v] = w
* - Set predecessor[v] = u
* - Add/update v in priority queue
* 4. MST is implicit in predecessor array
*
* **Design Decisions:**
*
* 1. **Why two Kruskal variants?**
* - Standard `kruskal()`: Safe, preserves input edge list
* - `inplace_kruskal()`: Memory-efficient for large edge lists
* - Trade-off: safety vs. memory when input is no longer needed
*
* 2. **Why does Kruskal copy the edge list?**
* - Sorting modifies the container
* - Preserving input follows principle of least surprise
* - Caller can explicitly use `inplace_kruskal()` for efficiency
*
* 3. **Why separate output container for Kruskal?**
* - Flexibility: output to any container type
* - Avoids allocation if caller provides pre-sized container
* - Allows different edge descriptor types for input/output
*
* 4. **Why does Prim output (predecessor, weight) instead of edges?**
* - Implicit tree representation is more memory-efficient
* - Predecessors are standard for tree algorithms (DFS, BFS, Dijkstra)
* - Easy to reconstruct edges or paths from predecessors
* - Weight array provides O(1) edge weight lookup
*
* 5. **Why customizable comparison operators?**
* - Enables maximum spanning tree (reverse comparison)
* - Supports custom weight types (complex costs, multi-criteria)
* - Allows tie-breaking strategies (lexicographic ordering)
*
* 6. **Why no return value?**
* - Success is implicit (MST always exists for non-empty valid input)
* - Output containers provide all results
* - Exception-based error handling for invalid inputs
*
* **Optimization Opportunities:**
* - Kruskal: Can stop early after V-1 edges if graph is known to be connected
* - Kruskal: Partial sorting (nth_element) can reduce constant factors
* - Prim: Fibonacci heap reduces complexity to O(E + V log V) for sparse graphs
* - Prim: Array-based implementation O(V²) is faster for very dense graphs
*
* **Performance Notes:**
*
* **Prim's Priority Queue:**
* This implementation uses a binary heap (std::priority_queue) which provides O(E log V)
* complexity. While Fibonacci heap implementations achieve better theoretical complexity
* O(E + V log V), they have significantly higher constant factors and more complex
* bookkeeping. In practice:
* - **Binary heap is faster** for most real-world graphs (used here)
* - **Fibonacci heap** only wins for extremely dense graphs where E ≈ V²
* and the improved amortized decrease-key operation dominates
* - **Simple array** (O(V²)) is fastest for complete graphs where E = V(V-1)/2
*
* Benchmark testing shows binary heap is optimal for graphs with 100-100,000 vertices
* and typical densities (E = O(V) to O(V^1.5)).
*
* ---
*
* **Exception Safety:**
*
* **Guarantee:** Basic exception safety
*
* **Throws:**
* - `std::bad_alloc` if internal containers cannot allocate memory
* - `std::out_of_range` if preconditions are violated (seed vertex, container sizes)
* - May propagate exceptions from comparison operators (should be noexcept)
* - May propagate exceptions from container operations
*
* **State after exception:**
*
* **Kruskal variants:**
* - Graph is never modified (edge list input only)
* - Input edge list `e` unchanged (standard variant)
* - Input edge list `e` may be partially sorted (inplace variant)
* - Output container `t` may contain partial MST (indeterminate state)
* - Output container `t` is NOT cleared on exception
* - Client should discard contents of `t` and retry after fixing preconditions
*
* **Prim's algorithm:**
* - Graph `g` remains unchanged (read-only)
* - `predecessor` array may be partially modified (indeterminate state)
* - `weight` array may be partially modified (indeterminate state)
* - Client should re-initialize output arrays before retry
*
* **Recommendations:**
* - Use `noexcept` comparison operators when possible
* - Pre-validate input sizes and seed vertex before calling
* - Reserve space in output containers to reduce allocation failures
* - For production code, wrap calls in try-catch and handle cleanup
*
* @copyright Copyright (c) 2024
*
* SPDX-License-Identifier: BSL-1.0
*
* @authors
* Andrew Lumsdaine
* Phil Ratzloff
* Kevin Deweese
*/
#include "graph/graph.hpp"
#include "graph/adj_list/vertex_property_map.hpp"
#include "graph/edge_list/edge_list.hpp"
#include "graph/views/edgelist.hpp"
#include "graph/algorithm/dijkstra_shortest_paths.hpp"
#include <queue>
#include <format>
#ifndef GRAPH_MST_HPP
# define GRAPH_MST_HPP
namespace graph {
// Using declarations for new namespace structure
using adj_list::adjacency_list;
using adj_list::index_adjacency_list;
using adj_list::index_vertex_range;
using adj_list::vertex_id_t;
using adj_list::vertices;
using adj_list::edges;
using adj_list::num_vertices;
using adj_list::find_vertex;
using adj_list::target_id;
using adj_list::source_id;
using adj_list::edge_t;
using adj_list::edge_value;
/**
* @brief Element in disjoint-set (union-find) data structure.
*
* Represents a set in the union-find forest. The `id` field points to the parent
* in the tree (or itself if root). The `count` field stores the rank (approximate
* tree height) for union-by-rank optimization.
*
* @tparam VId Vertex ID type (must be integral)
*/
template <class VId>
struct disjoint_element {
VId id = VId(); ///< Parent in union-find tree (id == itself for root)
size_t count = 0; ///< Rank for union-by-rank (approximate tree depth)
};
template <class VId>
using disjoint_vector = std::vector<disjoint_element<VId>>;
/**
* @brief Find the root of the set containing a vertex.
*
* Implements path compression optimization: all nodes along the search path
* are updated to point directly to the root, flattening the tree structure.
* This reduces future query time to nearly O(1) amortized.
*
* @tparam VId Vertex ID type
* @param subsets The disjoint-set data structure
* @param vtx The vertex to find
* @return VId The root representative of the set containing vtx
*
* **Complexity:** O(α(V)) amortized, where α is the inverse Ackermann function (effectively constant)
*/
template <class VId>
VId disjoint_find(disjoint_vector<VId>& subsets, VId vtx) {
// Phase 1: Find the root by following parent pointers
VId parent = subsets[vtx].id;
while (parent != subsets[parent].id) {
parent = subsets[parent].id; // Keep climbing until we reach root (parent == self)
}
// Phase 2: Path compression - make all nodes on path point directly to root
while (vtx != parent) {
VId next = subsets[vtx].id; // Save next node before overwriting
subsets[vtx].id = parent; // Point current node directly to root
vtx = next; // Move to next node in original path
}
return parent; // Return the root representative of this set
}
/**
* @brief Union two sets by merging their roots.
*
* Implements union-by-rank optimization: the tree with smaller rank is attached
* under the root of the tree with larger rank. This keeps trees balanced and
* maintains nearly O(1) operation time.
*
* @tparam VId Vertex ID type
* @param subsets The disjoint-set data structure
* @param u First vertex
* @param v Second vertex
*
* **Complexity:** O(α(V)) amortized with path compression
*/
template <class VId>
void disjoint_union(disjoint_vector<VId>& subsets, VId u, VId v) {
// Find root representatives of both vertices
VId u_root = disjoint_find(subsets, u);
VId v_root = disjoint_find(subsets, v);
// Union by rank: attach smaller tree under root of larger tree
if (subsets[u_root].count < subsets[v_root].count)
subsets[u_root].id = v_root; // Attach u's tree to v's root
else if (subsets[u_root].count > subsets[v_root].count)
subsets[v_root].id = u_root; // Attach v's tree to u's root
else {
// Equal rank: attach v to u and increment u's rank
subsets[v_root].id = u_root;
subsets[u_root].count++;
}
}
/**
* @brief Check if two vertices are in different sets and union them if so.
*
* Combines find and union operations for Kruskal's algorithm. Returns true if
* the vertices were in different sets (edge should be added to MST), false if
* they were already in the same set (edge would create a cycle).
*
* @tparam VId Vertex ID type
* @param subsets The disjoint-set data structure
* @param u First vertex (source)
* @param v Second vertex (target)
* @return true if u and v were in different sets (now merged)
* @return false if u and v were already in the same set
*
* **Complexity:** O(α(V)) amortized
*/
template <class VId>
bool disjoint_union_find(disjoint_vector<VId>& subsets, VId u, VId v) {
// Find root representatives of both vertices
VId u_root = disjoint_find(subsets, u);
VId v_root = disjoint_find(subsets, v);
// If vertices are in different sets, merge them
if (u_root != v_root) {
// Union by rank: attach smaller tree under larger tree
if (subsets[u_root].count < subsets[v_root].count)
subsets[u_root].id = v_root; // Attach u's tree to v's root
else if (subsets[u_root].count > subsets[v_root].count)
subsets[v_root].id = u_root; // Attach v's tree to u's root
else {
// Equal rank: attach v to u and increment u's rank
subsets[v_root].id = u_root;
subsets[u_root].count++;
}
return true; // Edge added to MST (connects different components)
}
return false; // Edge rejected (would create cycle)
}
// Note: The following concepts are not currently used because the x_index_edgelist_range
// concept covers the necessary constraints. These are preserved for potential future use
// if more fine-grained concept checking becomes necessary.
//
// template <typename ED>
// concept has_integral_vertices =
// requires(ED ed) { requires integral<decltype(ed.source_id)> && integral<decltype(ed.target_id)>; };
//
// template <typename ED>
// concept has_same_vertex_type = requires(ED ed) { requires same_as<decltype(ed.source_id), decltype(ed.target_id)>; };
//
// template <typename ED>
// concept has_edge_value = requires(ED ed) { requires !same_as<decltype(ed.value), void>; };
template <class ELVT> // For exposition only
concept _has_edgelist_value = !is_void_v<typename ELVT::value_type>;
template <class ELVT> // For exposition only
concept _basic_edgelist_type = is_same_v<typename ELVT::target_id_type, typename ELVT::source_id_type>;
template <class ELVT> // For exposition only
concept _basic_index_edgelist_type = _basic_edgelist_type<ELVT> && integral<typename ELVT::target_id_type>;
template <class ELVT> // For exposition only
concept _edgelist_type = _basic_edgelist_type<ELVT> && _has_edgelist_value<ELVT>;
template <class ELVT> // For exposition only
concept _index_edgelist_type = _basic_index_edgelist_type<ELVT> && _has_edgelist_value<ELVT>;
template <class EL> // For exposition only
concept x_basic_edgelist_range = forward_range<EL> && _basic_edgelist_type<range_value_t<EL>>;
template <class EL> // For exposition only
concept x_basic_index_edgelist_range = forward_range<EL> && _basic_index_edgelist_type<range_value_t<EL>>;
template <class EL> // For exposition only
concept x_edgelist_range = forward_range<EL> && _edgelist_type<range_value_t<EL>>;
template <class EL> // For exposition only
concept x_index_edgelist_range = forward_range<EL> && _index_edgelist_type<range_value_t<EL>>;
/*template<typename T, typename = void>
struct has_edge : false_type { };
template<typename T>
struct has_edge<T, decltype(declval<T>().edge, void())> : true_type { };*/
/**
* @ingroup graph_algorithms
* @brief Find the minimum weight spanning tree using Kruskal's algorithm.
*
* Processes edges in sorted order by weight, using union-find to detect cycles.
* Uses default comparison (operator<) for minimum spanning tree.
*
* @tparam IELR Input edge list range type.
* @tparam OELR Output edge list range type.
*
* @param e Input edge list with source_id, target_id, and value members.
* @param t Output edge list for MST edges. Must support push_back() and reserve().
*
* @return std::pair<EV, size_t> with total MST weight and number of connected components.
*
* **Mandates:**
* - IELR must satisfy x_index_edgelist_range (forward range of integral edge descriptors with values)
* - OELR must satisfy x_index_edgelist_range with push_back() and reserve()
*
* **Preconditions:**
* - Edge descriptors must have integral source_id and target_id
* - Edge values must be comparable with operator<
*
* **Effects:**
* - Copies and sorts edge list internally; input e is unchanged
* - Appends MST edges to output container t
*
* **Postconditions:**
* - t contains V-1 edges forming minimum spanning tree (or fewer for disconnected graphs)
* - Input edge list e is unchanged
*
* **Returns:**
* - first: Total weight of the minimum spanning tree/forest (EV)
* - second: Number of connected components (size_t)
*
* **Throws:**
* - std::bad_alloc if internal containers cannot allocate memory
* - Exception guarantee: Basic. Input e unchanged; output t may be partially written.
*
* **Complexity:**
* - Time: O(E log E) — dominated by edge sorting
* - Space: O(E + V) for edge copy and disjoint-set structure
*
* ## Example Usage
*
* ```cpp
* std::vector<edge_descriptor<uint32_t, int>> edges = {
* {0, 1, 4}, {1, 2, 8}, {2, 3, 7}, {3, 0, 9}, {0, 2, 2}, {1, 3, 5}
* };
* std::vector<edge_descriptor<uint32_t, int>> mst;
* auto [total_weight, num_components] = kruskal(edges, mst);
* ```
*/
template <x_index_edgelist_range IELR, x_index_edgelist_range OELR>
auto kruskal(IELR&& e, OELR&& t) {
return kruskal(e, t, [](auto&& i, auto&& j) { return i < j; });
}
/**
* @ingroup graph_algorithms
* @brief Find the minimum (or maximum) weight spanning tree using Kruskal's algorithm with custom comparison.
*
* Processes edges in sorted order determined by the comparison function.
*
* @tparam IELR Input edge list range type.
* @tparam OELR Output edge list range type.
* @tparam CompareOp Comparison operator type.
*
* @param e Input edge list with source_id, target_id, and value members.
* @param t Output edge list for spanning tree edges. Must support push_back() and reserve().
* @param compare Comparison function: compare(ev1, ev2) returns true if ev1 should be processed first.
*
* @return std::pair<EV, size_t> with total weight and number of connected components.
*
* **Mandates:**
* - IELR must satisfy x_index_edgelist_range
* - OELR must satisfy x_index_edgelist_range with push_back() and reserve()
*
* **Preconditions:**
* - compare must define a strict weak ordering on edge values
*
* **Effects:**
* - Copies and sorts edge list using compare; input e is unchanged
* - Appends optimal spanning tree edges to output container t
*
* **Postconditions:**
* - t contains V-1 edges forming optimal spanning tree
* - Input edge list e is unchanged
*
* **Returns:**
* - first: Total weight of the spanning tree/forest (EV)
* - second: Number of connected components (size_t)
*
* **Throws:**
* - std::bad_alloc if internal containers cannot allocate memory
* - May propagate exceptions from comparison operator
* - Exception guarantee: Basic. Input e unchanged; output t may be partially written.
*
* **Complexity:**
* - Time: O(E log E) — dominated by edge sorting
* - Space: O(E + V) for edge copy and disjoint-set structure
*
* ## Example Usage
*
* ```cpp
* // Maximum spanning tree
* std::vector<edge_descriptor<uint32_t, int>> max_st;
* auto [total_weight, components] = kruskal(edges, max_st, std::greater<int>{});
* ```
*/
template <x_index_edgelist_range IELR, x_index_edgelist_range OELR, class CompareOp>
auto kruskal(IELR&& e, // graph
OELR&& t, // tree
CompareOp compare // edge value comparator
) {
using edge_data = range_value_t<IELR>;
using VId = remove_const_t<typename edge_data::source_id_type>;
using EV = typename edge_data::value_type;
// Handle empty input
if (std::ranges::empty(e)) {
t.clear();
return std::pair<EV, size_t>{EV{}, 0};
}
// Copy edges to allow sorting without modifying input
std::vector<tuple<VId, VId, EV>> e_copy;
std::ranges::transform(e, back_inserter(e_copy),
[](auto&& ed) { return std::make_tuple(ed.source_id, ed.target_id, ed.value); });
// Find maximum vertex ID while sorting edges by weight
VId N = 0; // Will hold the maximum vertex ID in the graph
auto outer_compare = [&](auto&& i, auto&& j) {
// Track maximum vertex ID (needed to size disjoint-set structure)
if (get<0>(i) > N) {
N = get<0>(i); // Check source vertex
}
if (get<1>(i) > N) {
N = get<1>(i); // Check target vertex
}
return compare(get<2>(i), get<2>(j)); // Compare edge weights
};
std::ranges::sort(e_copy, outer_compare);
// Initialize disjoint-set: each vertex starts in its own set
disjoint_vector<VId> subsets(N + 1); // Size N+1 to accommodate vertices 0 through N
for (VId uid = 0; uid <= N; ++uid) {
subsets[uid].id = uid; // Each vertex is its own parent (root)
subsets[uid].count = 0; // Initial rank is 0
}
// MST has exactly N-1 edges for a connected graph (or fewer for disconnected)
t.reserve(N); // Pre-allocate space for efficiency
// Track MST statistics
EV total_weight = EV{}; // Sum of edge weights in MST
size_t num_components = N + 1; // Initially each vertex is its own component
// Process edges in sorted order (by weight)
for (auto&& [uid, vid, val] : e_copy) {
// Try to add edge: succeeds if endpoints are in different components
if (disjoint_union_find(subsets, uid, vid)) {
// Edge connects different components - add to MST
t.push_back(range_value_t<OELR>());
t.back().source_id = uid;
t.back().target_id = vid;
t.back().value = val;
total_weight += val; // Accumulate MST weight
--num_components; // Merge reduces component count
}
// else: edge would create cycle - skip it
}
return std::pair<EV, size_t>{total_weight, num_components};
}
/**
* @ingroup graph_algorithms
* @brief Find the minimum weight spanning tree using Kruskal's algorithm, sorting input in place.
*
* Memory-efficient variant that sorts the input edge list directly instead of copying.
*
* @tparam IELR Input edge list range type (must be permutable).
* @tparam OELR Output edge list range type.
*
* @param e Input edge list (will be sorted by edge weight).
* @param t Output edge list for MST edges.
*
* @return std::pair<EV, size_t> with total MST weight and number of connected components.
*
* **Mandates:**
* - IELR must satisfy x_index_edgelist_range and std::permutable<iterator_t<IELR>>
* - OELR must satisfy x_index_edgelist_range with push_back() and reserve()
*
* **Preconditions:**
* - e must be a mutable range (not const)
* - Edge values must be comparable with operator<
*
* **Effects:**
* - Sorts input e by edge weight (destructive — modifies input)
* - Appends MST edges to output container t
*
* **Postconditions:**
* - e is sorted by edge weight (ascending)
* - t contains V-1 edges forming minimum spanning tree
*
* **Returns:**
* - first: Total weight of the minimum spanning tree/forest (EV)
* - second: Number of connected components (size_t)
*
* **Throws:**
* - std::bad_alloc if internal containers cannot allocate memory
* - Exception guarantee: Basic. Input e may be partially sorted; output t may be partial.
*
* **Complexity:**
* - Time: O(E log E) — dominated by edge sorting
* - Space: O(V) for disjoint-set structure (no edge copy)
*
* **Remarks:**
* - Use when input edge list is no longer needed in original order
*/
template <x_index_edgelist_range IELR, x_index_edgelist_range OELR>
requires std::permutable<iterator_t<IELR>>
auto inplace_kruskal(IELR&& e, OELR&& t) {
return inplace_kruskal(e, t, [](auto&& i, auto&& j) { return i < j; });
}
/**
* @ingroup graph_algorithms
* @brief Find spanning tree using Kruskal's algorithm with custom comparison, sorting input in place.
*
* Memory-efficient variant with custom comparison function. Sorts input edge list directly.
*
* @tparam IELR Input edge list range type (must be permutable).
* @tparam OELR Output edge list range type.
* @tparam CompareOp Comparison operator type.
*
* @param e Input edge list (will be sorted by comparison function).
* @param t Output edge list for spanning tree edges.
* @param compare Comparison function for edge values.
*
* @return std::pair<EV, size_t> with total weight and number of connected components.
*
* **Mandates:**
* - IELR must satisfy x_index_edgelist_range and std::permutable<iterator_t<IELR>>
* - OELR must satisfy x_index_edgelist_range with push_back() and reserve()
*
* **Preconditions:**
* - e must be a mutable range
* - compare must define a strict weak ordering
*
* **Effects:**
* - Sorts input e using compare (destructive — modifies input)
* - Appends optimal spanning tree edges to output container t
*
* **Postconditions:**
* - e is sorted according to compare function
* - t contains V-1 edges forming optimal spanning tree
*
* **Returns:**
* - first: Total weight of the spanning tree/forest (EV)
* - second: Number of connected components (size_t)
*
* **Throws:**
* - std::bad_alloc if internal containers cannot allocate memory
* - May propagate exceptions from comparison operator
* - Exception guarantee: Basic. Input e may be partially sorted; output t may be partial.
*
* **Complexity:**
* - Time: O(E log E) — dominated by edge sorting
* - Space: O(V) for disjoint-set structure (no edge copy)
*/
template <x_index_edgelist_range IELR, x_index_edgelist_range OELR, class CompareOp>
requires std::permutable<iterator_t<IELR>>
auto inplace_kruskal(IELR&& e, // graph
OELR&& t, // tree
CompareOp compare // edge value comparator
) {
using edge_data = range_value_t<IELR>;
using VId = remove_const_t<typename edge_data::source_id_type>;
using EV = typename edge_data::value_type;
// Handle empty input
if (std::ranges::empty(e)) {
t.clear();
return std::pair<EV, size_t>{EV{}, 0};
}
// Find maximum vertex ID while sorting edges by weight (modifies input!)
VId N = 0; // Will hold the maximum vertex ID in the graph
auto outer_compare = [&](auto&& i, auto&& j) {
// Track maximum vertex ID (needed to size disjoint-set structure)
if (i.source_id > N) {
N = i.source_id; // Check source vertex
}
if (i.target_id > N) {
N = i.target_id; // Check target vertex
}
return compare(i.value, j.value); // Compare edge weights
};
std::ranges::sort(e, outer_compare); // ⚠️ Modifies input edge list!
// Empty graph edge case
if (N == 0) {
t.clear();
return std::pair<EV, size_t>{EV{}, 0};
}
// Initialize disjoint-set: each vertex starts in its own set
disjoint_vector<VId> subsets(N + 1); // Size N+1 to accommodate vertices 0 through N
for (VId uid = 0; uid <= N; ++uid) {
subsets[uid].id = uid; // Each vertex is its own parent (root)
subsets[uid].count = 0; // Initial rank is 0
}
// MST has exactly N-1 edges for a connected graph (or fewer for disconnected)
t.reserve(N); // Pre-allocate space for efficiency
// Track MST statistics
EV total_weight = EV{}; // Sum of edge weights in MST
size_t num_components = N + 1; // Initially each vertex is its own component
// Process edges in sorted order (by weight)
for (auto&& [uid, vid, val] : e) {
// Try to add edge: succeeds if endpoints are in different components
if (disjoint_union_find(subsets, uid, vid)) {
// Edge connects different components - add to MST
t.push_back(range_value_t<OELR>());
t.back().source_id = uid;
t.back().target_id = vid;
t.back().value = val;
total_weight += val; // Accumulate MST weight
--num_components; // Merge reduces component count
}
// else: edge would create cycle - skip it
}
return std::pair<EV, size_t>{total_weight, num_components};
}
/**
* @ingroup graph_algorithms
* @brief Find the minimum weight spanning tree using Prim's algorithm starting from a seed vertex.
*
* Grows a minimum spanning tree from a seed vertex by repeatedly adding the minimum-weight
* edge connecting a tree vertex to a non-tree vertex. Delegates to dijkstra_shortest_paths
* with a projecting combine function.
*
* @tparam G Graph type satisfying adjacency_list.
* @tparam PredecessorFn Function type returning lvalue ref to predecessor for a vertex.
* @tparam WeightFn Function type returning lvalue ref to edge weight for a vertex.
* @tparam WF Edge weight function type. Defaults to edge_value(g, uv).
* @tparam CompareOp Comparison operator type. Defaults to less<>.
*
* @param g The graph to process.
* @param seed Starting vertex for MST growth.
* @param weight Function weight(g, uid) -> edge_weight&: returns lvalue ref to edge weight for vertex uid.
* @param predecessor Function predecessor(g, uid) -> vertex_id&: returns lvalue ref to predecessor for vertex uid.
* @param weight_fn Edge weight function (default: edge_value).
* @param compare Comparison for edge weights (default: less<>).
*
* @return Total weight of the spanning tree.
*
* **Mandates:**
* - G must satisfy adjacency_list
* - WeightFn must satisfy distance_fn_for<WeightFn, G>
* - PredecessorFn must satisfy predecessor_fn_for<PredecessorFn, G>
* - WF must satisfy basic_edge_weight_function
*
* **Preconditions:**
* - seed must be a valid vertex in the graph
* - predecessor and weight must be initialized (use init_shortest_paths)
* - compare must define a strict weak ordering
*
* **Effects:**
* - Writes MST structure via predecessor and weight functions
* - Does not modify graph g
*
* **Postconditions:**
* - predecessor(g, seed) == seed
* - For reachable vertices: predecessor(g, v) points to parent in MST
* - For unreachable vertices: predecessor(g, v) is unchanged
* - weight(g, v) contains edge weight from predecessor(g, v) to v
*
* **Returns:**
* - Total weight of the spanning tree (edge_value_type)
* - For disconnected graphs: weight of tree in seed's component only
*
* **Throws:**
* - std::out_of_range if seed vertex ID is out of range
* - Exception guarantee: Basic. Graph g unchanged; predecessor/weight may be partial.
*
* **Complexity:**
* - Time: O(E log V) with binary heap priority queue
* - Space: O(V) for distance array and priority queue
*
* **Remarks:**
* - Only produces MST for the connected component containing seed
* - For disconnected graphs, call multiple times with different seeds
*
* ## Example Usage
*
* ```cpp
* std::vector<uint32_t> pred(num_vertices(g));
* std::vector<int> wt(num_vertices(g));
* init_shortest_paths(g, wt, pred);
* auto total_weight = prim(g, 0,
* [&wt](const auto& g, auto uid) -> auto& { return wt[uid]; },
* [&pred](const auto& g, auto uid) -> auto& { return pred[uid]; });
* ```
*/
template <adjacency_list G,
class WeightFn,
class PredecessorFn,
class WF = function<distance_fn_value_t<WeightFn, G>(const std::remove_reference_t<G>&, const edge_t<G>&)>,
class CompareOp = less<distance_fn_value_t<WeightFn, G>>>
requires distance_fn_for<WeightFn, G> &&
is_arithmetic_v<distance_fn_value_t<WeightFn, G>> &&
predecessor_fn_for<PredecessorFn, G> &&
basic_edge_weight_function<G, WF, distance_fn_value_t<WeightFn, G>, CompareOp, plus<distance_fn_value_t<WeightFn, G>>>
auto prim(G&& g, // graph
const vertex_id_t<G>& seed, // seed vtx
WeightFn&& weight, // out: weight(g, uid) -> edge weight& from tree edge uid to predecessor
PredecessorFn&& predecessor, // out: predecessor(g, uid) -> predecessor& of uid in tree
WF&& weight_fn =
[](const auto& gr, const edge_t<G>& uv) {
return edge_value(gr, uv);
}, // default weight_fn(g, uv) -> edge_value(g, uv)
CompareOp compare = less<distance_fn_value_t<WeightFn, G>>() // edge value comparator
) {
using edge_value_type = distance_fn_value_t<WeightFn, G>;
// Prim's combine: ignore accumulated distance, use edge weight directly.
// This transforms Dijkstra's relaxation check from compare(d_u + w, d_v)
// to compare(w, d_v), which is exactly Prim's criterion.
auto prim_combine = [](edge_value_type /*d_u*/, edge_value_type w_uv) -> edge_value_type { return w_uv; };
dijkstra_shortest_paths(g, seed,
std::forward<WeightFn>(weight),
std::forward<PredecessorFn>(predecessor),
std::forward<WF>(weight_fn), empty_visitor(),
std::forward<CompareOp>(compare), prim_combine);
// Calculate total MST weight by summing edge weights
edge_value_type total_weight = edge_value_type{};
for (auto [vid] : views::basic_vertexlist(g)) {
if (vid != seed && predecessor(g, vid) != vid) {
total_weight += weight(g, vid);
}
}
return total_weight;
}
} // namespace graph
#endif //GRAPH_MST_HPP