Zip and Skip Tries
Loading...
Searching...
No Matches
ZipTrie.hpp
Go to the documentation of this file.
1
11#pragma once
12
13#include "BitString.cuh"
14
15#include <compare> // For std::strong_ordering
16#include <limits> // For std::numeric_limits
17#include <vector> // For std::vector
18#include <cmath> // For std::log2
19#include <random> // For random rank generation
20#include <fstream> // For file output (to_dot)
21#include <optional> // For std::optional
22#include <string> // For std::string
23#include <type_traits> // For std::conditional_t
24#include <ostream> // For std::ostream
25#include <algorithm> // For std::max, std::min
26#include <iostream> // For std::cerr (used in to_dot error handling)
27
37{
42
48 auto operator<=>(const MemoryEfficientLCP&) const = default;
49
54 unsigned value() const noexcept;
55
62};
63
73{
78
84 auto operator<=>(const GeometricRank&) const = default;
85
94 static GeometricRank get_random();
95};
96
124template <typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T = GeometricRank, unsigned CHAR_SIZE_BITS = sizeof(CHAR_T) * 8>
126{
127public:
132 using LCP_T = std::conditional_t<MEMORY_EFFICIENT, MemoryEfficientLCP, unsigned>;
138
148 ZipTrie(unsigned max_size, unsigned max_lcp_length) noexcept;
149
158 int get_depth(const KEY_T* key) const noexcept;
159
167
175 double get_average_height() const noexcept;
176
183 unsigned size() const noexcept;
184
191 bool contains(const KEY_T* key) const noexcept;
192
204
223 void insert(const KEY_T* key) noexcept;
224
232 // bool remove(const KEY_T& key) noexcept;
233
239 const RANK_T& get_root_rank() const noexcept;
240
254 void set(const KEY_T* key, RANK_T rank, unsigned left, unsigned right, LCP_T predecessor_lcp, LCP_T successor_lcp) noexcept;
255
264 void set_root_index(unsigned root_index) noexcept;
265
286
297 virtual SearchResults search(const KEY_T* key) const noexcept;
298
308 void to_dot(const std::string& file_path) const noexcept;
309
310protected:
312 unsigned _root_index;
317 unsigned _max_lcp_length; // TODO: Review if this is actively used or if _log_max_size suffices.
324
326 static constexpr unsigned NULLPTR = std::numeric_limits<unsigned>::max();
327
339 {
341 LCP_T predecessor = {}; // Default initialize (0 for unsigned, {0,0} for MemoryEfficientLCP)
343 LCP_T successor = {}; // Default initialize
344 };
345
351 struct Bucket
352 {
358 const KEY_T* key;
362 unsigned left = NULLPTR;
364 unsigned right = NULLPTR;
371 AncestorLCPs ancestor_lcps = {};
372 };
373
379 std::vector<Bucket> _buckets;
380
391 inline MemoryEfficientLCP get_memory_efficient_lcp(size_t num) const noexcept;
392
406 template <typename CompareFunction>
407 SearchResults search_recursive(const KEY_T* key, CompareFunction compare_func) const noexcept;
408
432 template<typename CompareFunction>
433 unsigned insert_recursive(Bucket* x, unsigned x_index, unsigned v_index, AncestorLCPs ancestor_lcps, CompareFunction compare_func) noexcept;
434
435// Protected Member Variables and Helper Structures/Functions used internally
436
437protected:
446 {
448 std::strong_ordering comparison;
455 };
456
478 std::optional<ComparisonResult> k_compare_prefix_check(
479 const Bucket& v,
480 const AncestorLCPs& ancestor_lcps,
481 size_t& out_start_lcp_val) const noexcept;
482
492 inline LCP_T convert_lcp(size_t val) const noexcept;
493
494private:
495 // Private helper methods used internally by public or protected methods.
496
518 inline ComparisonResult k_compare(const KEY_T* x, const Bucket& v, const AncestorLCPs& ancestor_lcps) const noexcept;
519
527 int height(unsigned node_index) const noexcept;
528
538 uint64_t get_total_depth(unsigned node_index, uint64_t depth) const noexcept;
539
549 void to_dot_recursive(std::ofstream& os, unsigned node_index) const noexcept;
550};
551
552//-----------------------------------------------------------------------------
553// ZipTrie Method Implementations
554// (Definitions are usually placed in the header for templates)
555//-----------------------------------------------------------------------------
556
557template<typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
559 : _root_index(NULLPTR),
560 _max_lcp_length(max_lcp_length),
561 // Calculate log2(max_size) safely, handling max_size = 0 case.
562 _log_max_size(max_size > 0 ? static_cast<uint8_t>(std::log2(static_cast<double>(max_size))) : 0)
563{
564 // Reserve space in the vector to potentially avoid reallocations if max_size is a good estimate.
565 _buckets.reserve(max_size);
566}
567
568template<typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
570{
571 // Delegate the actual search logic to the search method.
572 return search(key).contains;
573}
574
575template<typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
577{
578 // Delegate the actual search logic to the search method.
579 return search(key).max_lcp;
580}
581
582template<typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
583std::optional<typename ZipTrie<CHAR_T, MEMORY_EFFICIENT, RANK_T, CHAR_SIZE_BITS>::ComparisonResult> ZipTrie<CHAR_T, MEMORY_EFFICIENT, RANK_T, CHAR_SIZE_BITS>::k_compare_prefix_check(
584 const Bucket& v,
585 const AncestorLCPs& ancestor_lcps,
586 size_t& out_start_lcp_val) const noexcept
587{
588 // Get the LCPs from the current search path.
589 auto predecessor_lcp = ancestor_lcps.predecessor;
590 auto successor_lcp = ancestor_lcps.successor;
591
592 // Determine the maximum LCP encountered on the current search path.
593 auto x_max_lcp = std::max(predecessor_lcp, successor_lcp);
594 // Determine the corresponding LCP stored in node 'v' (from the path when 'v' was inserted).
595 // If the search path's max LCP came from the predecessor side, use v's predecessor LCP, otherwise use v's successor LCP.
596 auto corr_v_lcp = (predecessor_lcp > successor_lcp) ? v.ancestor_lcps.predecessor : v.ancestor_lcps.successor;
597
598 // If the maximum LCP on the current path (x_max_lcp) differs from the
599 // corresponding LCP recorded when node 'v' was inserted (corr_v_lcp)...
600 if (x_max_lcp != corr_v_lcp)
601 {
602 // ...we can determine the order based on which LCP is larger and which path it came from.
603 // The logic determines if x should be less or greater than v.
604 // `(x_max_lcp > corr_v_lcp)` is true if the current path's LCP is longer.
605 // `(predecessor_lcp > successor_lcp)` is true if the current path's max LCP came from the predecessor side.
606 // If these booleans match, x is less; otherwise, x is greater.
607 std::strong_ordering comparison_result =
608 ((x_max_lcp > corr_v_lcp) == (predecessor_lcp > successor_lcp)) ? std::strong_ordering::less : std::strong_ordering::greater;
609
610 // The resulting LCP for this comparison is the minimum of the two differing LCPs.
612
613 // Return the determined comparison result and LCP. No need for full key comparison.
615 }
616
617 // If the LCPs matched (x_max_lcp == corr_v_lcp), we cannot decide the order yet.
618 // We need to perform a full key comparison, but we know the keys match up to `x_max_lcp`.
619 // Set the starting offset for the key comparison.
620 if constexpr (MEMORY_EFFICIENT)
621 {
622 // If using approximate LCPs, get the actual value.
624 }
625 else
626 {
627 // If using exact LCPs (unsigned), the value is directly usable.
629 }
630
631 // Indicate that a full key comparison should proceed by returning nullopt.
632 return std::nullopt;
633}
634
635template<typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
637{
638 if constexpr (MEMORY_EFFICIENT) {
639 // If memory efficiency is enabled, call the approximation function.
640 return get_memory_efficient_lcp(val);
641 } else {
642 // Otherwise, just cast the exact value to the LCP type (unsigned).
643 // Ensure the value fits; consider potential truncation if LCP_T were smaller than size_t.
644 return static_cast<LCP_T>(val);
645 }
646}
647
648template<typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
650{
651 size_t start_lcp_val = 0; // Initialize starting bit offset for comparison.
652
653 // Attempt to determine the result based on ancestor LCPs first.
654 if (auto prefix_result = k_compare_prefix_check(v, ancestor_lcps, start_lcp_val))
655 {
656 // If k_compare_prefix_check returned a result, use it directly.
657 return *prefix_result;
658 }
659
660 // If prefix check didn't yield a result, perform sequential key comparison
661 // starting from the calculated bit offset 'start_lcp_val'.
662 // Assumes KEY_T has a method `seq_k_compare(const KEY_T& other, size_t start_offset)`
663 // that returns a struct/pair like {std::strong_ordering, size_t actual_lcp}.
664 auto [comparison, actual_lcp_val] = x->seq_k_compare(*v.key, start_lcp_val);
665
666 // Convert the resulting exact LCP length to the appropriate LCP_T format.
667 return { comparison, convert_lcp(actual_lcp_val) };
668}
669
670template<typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
672{
673 // Define a lambda function that encapsulates the call to the standard k_compare method.
674 // This lambda matches the signature required by search_recursive.
675 auto sequential_compare = [&](const KEY_T* k, const Bucket& v, const AncestorLCPs& ancestor_lcps)
676 {
677 // Call the member function k_compare for the actual comparison logic.
678 return this->k_compare(k, v, ancestor_lcps);
679 };
680
681 // Call the recursive search helper, passing the key and the comparison lambda.
682 return search_recursive(key, sequential_compare);
683}
684
685template<typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
686template<typename CompareFunction>
688{
689 // Handle empty trie case.
690 if (_root_index == NULLPTR)
691 {
692 // Return not found, default LCP, depth -1.
693 return { false, {}, -1 };
694 }
695
696 unsigned v_index = _root_index; // Start traversal at the root.
697 AncestorLCPs ancestor_lcps = {}; // Initialize ancestor LCPs to zero/default at the root.
698 int depth = 0; // Initialize depth counter.
699
700 // Iterate down the trie until a null pointer is reached or the key is found.
701 while (v_index != NULLPTR)
702 {
703 // Get a reference to the current node.
704 const Bucket& v_node = _buckets[v_index];
705
706 // Perform the comparison using the provided comparison function.
707 auto [ comparison, lcp ] = compare_func(key, v_node, ancestor_lcps);
708
709 // Check if the key is an exact match.
710 if (comparison == std::strong_ordering::equal)
711 {
712 // Key found. Return true, the final LCP, and the current depth.
713 return { true, lcp, depth };
714 }
715
716 // If not equal, decide whether to go left or right.
717 if (comparison == std::strong_ordering::less)
718 {
719 // Go left. Update the successor LCP for the path: it's the maximum of the
720 // current successor LCP and the LCP computed in this step.
721 ancestor_lcps.successor = std::max(ancestor_lcps.successor, lcp);
722 // Move to the left child.
723 v_index = v_node.left;
724 }
725 else // comparison == std::strong_ordering::greater
726 {
727 // Go right. Update the predecessor LCP similarly.
728 ancestor_lcps.predecessor = std::max(ancestor_lcps.predecessor, lcp);
729 // Move to the right child.
730 v_index = v_node.right;
731 }
732
733 // Increment depth for the next level.
734 ++depth;
735 }
736
737 // Key not found after traversing the path.
738 // Return false, the maximum LCP encountered along the path, and depth -1.
739 return { false, std::max(ancestor_lcps.predecessor, ancestor_lcps.successor), -1 };
740}
741
742template <typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
744{
745 // Add a new bucket to the vector. Initialize with key and a new random rank.
746 // Children and ancestor LCPs will be set during the recursive insertion.
747 _buckets.push_back({ key, RANK_T::get_random() });
748 // Get the index of the newly added bucket.
749 unsigned new_node_index = _buckets.size() - 1;
750
751 // Define the comparison function lambda, similar to the search method.
752 auto sequential_compare = [&](const KEY_T* k, const Bucket& v, const AncestorLCPs& ancestor_lcps)
753 {
754 return this->k_compare(k, v, ancestor_lcps); // Use the standard k_compare
755 };
756
757 // Call the recursive insert helper to place the new node and update the tree structure.
758 // Start recursion from the root (_root_index), passing the new node's pointer and index,
759 // initial empty ancestor LCPs, and the comparison function.
760 // Update the _root_index with the result, as the root might change due to the zip operation.
761 _root_index = insert_recursive(&_buckets[new_node_index], new_node_index, _root_index, {}, sequential_compare);
762}
763
764template <typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
765template<typename CompareFunction>
767{
768 // Base case: Reached a null pointer, meaning this is the insertion spot.
769 if (v_index == NULLPTR)
770 {
771 // Set the ancestor LCPs for the new node 'x' based on the path taken.
772 x->ancestor_lcps = ancestor_lcps;
773 // Return the index of the new node 'x', indicating it's the root of this (now non-empty) subtree.
774 return x_index;
775 }
776
777 // Get a reference to the current node 'v'.
778 Bucket& v_node = _buckets[v_index];
779
780 // Compare the new key 'x->key' with the current node's key 'v_node.key'.
781 auto [ comparison, lcp ] = compare_func(x->key, v_node, ancestor_lcps);
782
783 // If keys are equal, the key already exists. Do not insert duplicates.
784 // Return the current node's index, leaving the subtree unchanged.
785 if (comparison == std::strong_ordering::equal)
786 {
787 // Note: Could potentially update value associated with key here if storing values.
788 // Since we only store keys, we just return.
789 // Also, need to handle the bucket pushed back in `insert`. A robust implementation
790 // might check for existence *before* adding to _buckets or remove the unused bucket here.
791 // Current implementation leaves the unused bucket if key exists.
792 // We should pop the unused bucket here.
793 if (x_index == _buckets.size() - 1) { // Check if it's the last one added
794 _buckets.pop_back(); // Remove the unused bucket
795 } // Else, if not the last, it's harder to remove without messing up indices.
796
797 return v_index;
798 }
799
800 // Navigate left or right based on the comparison result.
801 if (comparison == std::strong_ordering::less)
802 {
803 // Go left. Recursively insert into the left subtree.
804 // Update the ancestor LCPs for the recursive call: predecessor stays the same,
805 // successor becomes the max of the current path's successor and the LCP just computed.
806 unsigned subroot_index = insert_recursive(x, x_index, v_node.left, { ancestor_lcps.predecessor, std::max(ancestor_lcps.successor, lcp) }, compare_func);
807
808 // If the recursive call returned an index other than the original left child,
809 // it means the structure below changed.
810 if (subroot_index == x_index && x->rank >= v_node.rank)
811 {
812 v_node.left = x->right; // v adopts x's right child as its left child.
813 x->right = v_index; // x takes v as its right child.
814 // Update LCPs stored *in the nodes* due to the structural change.
815 // v's predecessor LCP is now the LCP computed between x and v.
816 v_node.ancestor_lcps.predecessor = lcp;
817 // x's ancestor LCPs become those passed into this level (ancestor_lcps).
818 x->ancestor_lcps = ancestor_lcps;
819 // Return x_index as the new root of this subtree.
820 return x_index;
821 }
822 else
823 {
824 // Simply update v's left child pointer to the returned subtree root.
825 v_node.left = subroot_index;
826 }
827 // If subroot_index == v_node.left, the left subtree structure didn't change relative to v.
828 }
829 else // comparison == std::strong_ordering::greater
830 {
831 // Go right. Recursively insert into the right subtree.
832 // Update ancestor LCPs: successor stays same, predecessor updates.
833 unsigned subroot_index = insert_recursive(x, x_index, v_node.right, { std::max(ancestor_lcps.predecessor, lcp), ancestor_lcps.successor }, compare_func);
834
835 // If the recursive call returned an index other than the original right child.
836 if (subroot_index == x_index && x->rank >= v_node.rank)
837 {
838 v_node.right = x->left; // v adopts x's left child as its right child.
839 x->left = v_index; // x takes v as its left child.
840 // Update LCPs stored in the nodes.
841 v_node.ancestor_lcps.successor = lcp; // v's successor LCP is now the computed LCP.
842 x->ancestor_lcps = ancestor_lcps; // x inherits parent's LCPs.
843 // Return x_index as the new root.
844 return x_index;
845 }
846 else
847 {
848 v_node.right = subroot_index;
849 }
850 // If subroot_index == v_node.right, the right subtree structure didn't change relative to v.
851 }
852
853 return v_index;
854}
855
856template<typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
858{
859 // Simply return the current size of the underlying vector storing the nodes.
860 // Note: This assumes no gaps or marked-deleted nodes if removal were implemented differently.
861 // If duplicate keys caused buckets to be added but not used in `insert_recursive`,
862 // this size might be slightly inflated compared to unique keys. Consider adjusting if duplicates are handled differently.
863 return _buckets.size();
864}
865
866template <typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
868{
869 // Delegate to the recursive helper function starting at the root index.
870 return height(_root_index);
871}
872
873template <typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
875{
876 // Base case: An empty subtree (represented by NULLPTR) has a height of -1.
877 if (node_index == NULLPTR)
878 {
879 return -1;
880 }
881
882 // Recursive step: The height of a non-empty subtree is 1 (for the current node)
883 // plus the maximum of the heights of its left and right subtrees.
884 return std::max(height(_buckets[node_index].left), height(_buckets[node_index].right)) + 1;
885}
886
887template <typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
889{
890 // Perform a standard search for the key and return the depth field from the results.
891 // If the key is not found, search returns depth -1, which is correctly propagated here.
892 return search(key).depth;
893}
894
895template <typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
897{
898 // Get the total number of nodes.
899 unsigned current_size = size();
900 // Handle the case of an empty trie to avoid division by zero.
901 if (current_size == 0)
902 {
903 return 0.0; // Average depth of an empty trie is 0.
904 }
905 // Calculate the sum of depths of all nodes using the recursive helper, starting at the root (depth 0).
906 uint64_t total_depth_sum = get_total_depth(_root_index, 0);
907 // Compute the average by dividing the total depth sum by the number of nodes.
908 return static_cast<double>(total_depth_sum) / current_size;
909}
910
911template <typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
913{
914 // Base case: An empty subtree contributes 0 to the total depth sum.
915 if (node_index == NULLPTR)
916 {
917 return 0;
918 }
919
920 // Recursive step: The total depth sum for the subtree rooted at node_index is:
921 // - The depth of the current node (`depth`)
922 // - Plus the total depth sum of the left subtree (nodes in the left subtree are one level deeper)
923 // - Plus the total depth sum of the right subtree (nodes in the right subtree are one level deeper)
924 return depth + get_total_depth(_buckets[node_index].left, depth + 1) + get_total_depth(_buckets[node_index].right, depth + 1);
925}
926
927template <typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
929{
930 // Handle LCPs smaller than the log of max size directly.
931 if (num < _log_max_size)
932 {
933 // Store the exact value in 'multiple', with exponent 0.
934 return { 0, static_cast<uint8_t>(num) };
935 }
936
937 // Otherwise, calculate exponent and multiple for approximation.
939 lcp.exp_of_2 = std::log2(num / _log_max_size);
940 lcp.multiple = num / (1u << lcp.exp_of_2);
941
942 return lcp;
943}
944
945template <typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
947{
948 // Attempt to open the specified file path for writing.
949 std::ofstream os(file_path);
950 if (!os) {
951 // If the file cannot be opened, print an error message to standard error.
952 std::cerr << "Error: Could not open file " << file_path << " for writing." << std::endl;
953 return; // Exit the function if file opening failed.
954 }
955 // Write the standard DOT graph header.
956 os << "digraph G {\n";
957 // Define default node attributes (shape=record allows complex labels).
958 os << "\tnode [shape=record, fontname=\"Arial\"];\n";
959 // Define default edge attributes.
960 os << "\tedge [fontname=\"Arial\"];\n";
961 // Call the recursive helper function to write the nodes and edges, starting from the root.
962 if (_root_index != NULLPTR) {
963 to_dot_recursive(os, _root_index);
964 } else {
965 os << "\tlabel=\"Empty Trie\";\n"; // Indicate if the trie is empty
966 }
967 // Write the closing brace for the DOT graph definition.
968 os << "}\n";
969 // File stream `os` is automatically closed when it goes out of scope.
970}
971
972template <typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
974{
975 // Base case: If the node index represents a null pointer, do nothing further down this path.
976 if (node_index == NULLPTR)
977 {
978 return;
979 }
980
981 // Get a constant reference to the current node's data.
982 const Bucket& node = _buckets[node_index];
983
984 // Define the DOT representation for the current node.
985 // Use a unique ID "node_<index>" for each node.
986 // The label uses DOT's record shape syntax: { field1 | field2 | ... }
987 // Display the Index, Key, Rank (geometric, uniform), and stored Ancestor LCPs (predecessor, successor).
988 // Note: Assumes KEY_T and LCP_T have overloaded operator<< for printing.
989 // Note: Explicitly cast uint8_t ranks to unsigned for printing numeric values.
990 os << "\tnode_" << node_index << " [label=\"{Idx: " << node_index
991 << "|Key: " << *node.key // Assumes KEY_T is printable
992 << "|Rank: (" << static_cast<unsigned>(node.rank.geometric_rank) << "," << static_cast<unsigned>(node.rank.uniform_rank) << ")"
993 << "|LCPs: (" << node.ancestor_lcps.predecessor << ", " << node.ancestor_lcps.successor << ")}\"];\n";
994
995 // Process the left child.
996 if (node.left != NULLPTR)
997 {
998 // Draw an edge from the current node to the left child node.
999 // Label the edge " L" for clarity.
1000 os << "\tnode_" << node_index << " -> node_" << node.left << " [label=\" L\"];\n";
1001 // Recursively call the function for the left child.
1002 to_dot_recursive(os, node.left);
1003 }
1004 else
1005 {
1006 // Optional: Explicitly draw null pointers for visualization.
1007 // Create a small, invisible point node for the null left child.
1008 // os << "\tnull_left_" << node_index << " [shape=point, style=invis];\n";
1009 // Draw an edge to the null point.
1010 // os << "\tnode_" << node_index << " -> null_left_" << node_index << " [label=\" L\", style=dashed];\n";
1011 }
1012
1013 // Process the right child.
1014 if (node.right != NULLPTR)
1015 {
1016 // Draw an edge from the current node to the right child node.
1017 // Label the edge " R".
1018 os << "\tnode_" << node_index << " -> node_" << node.right << " [label=\" R\"];\n";
1019 // Recursively call the function for the right child.
1020 to_dot_recursive(os, node.right);
1021 }
1022 else
1023 {
1024 // Optional: Explicitly draw null pointers for visualization.
1025 // os << "\tnull_right_" << node_index << " [shape=point, style=invis];\n";
1026 // os << "\tnode_" << node_index << " -> null_right_" << node_index << " [label=\" R\", style=dashed];\n";
1027 }
1028}
1029
1030template <typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
1032{
1033 // Assert or check for empty trie might be useful here in a debug build.
1034 // Example: assert(_root_index != NULLPTR && "Cannot get rank of root in an empty trie.");
1035 return _buckets[_root_index].rank;
1036}
1037
1038template <typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
1040{
1041 _buckets.push_back({ key, rank, left, right, {predecessor_lcp, successor_lcp} });
1042}
1043
1044template <typename CHAR_T, bool MEMORY_EFFICIENT, typename RANK_T, unsigned CHAR_SIZE_BITS>
1049
1050
1051//-----------------------------------------------------------------------------
1052// Standalone Function Implementations (related to structs defined earlier)
1053//-----------------------------------------------------------------------------
1054
1056{
1057 // Calculate the value by left-shifting 1 by exp_of_2 and multiplying by multiple.
1058 // Ensure intermediate shift doesn't overflow if exp_of_2 is large, though unlikely for uint8_t.
1059 return (1u << exp_of_2) * multiple;
1060}
1061
1062inline std::ostream& MemoryEfficientLCP::print(std::ostream& os) const noexcept
1063{
1064 // Cast uint8_t to unsigned for correct stream output as numbers.
1065 return os << "2^" << static_cast<unsigned>(exp_of_2) << "*" << static_cast<unsigned>(multiple);
1066}
1067
1077inline std::ostream& operator<<(std::ostream& os, const MemoryEfficientLCP& lcp)
1078{
1079 // Delegate the actual printing logic to the struct's print method.
1080 return lcp.print(os);
1081}
1082
1084{
1085 // Static generators ensure the sequence is consistent across calls within a single thread,
1086 // but also mean it's not thread-safe without external locking.
1087 static std::random_device rd; // Obtain a random seed from the hardware device
1088 static std::default_random_engine generator(rd()); // Seed the default random engine
1089 // static std::default_random_engine generator(1); // Use for deterministic testing (fixed seed)
1090 static std::geometric_distribution<uint8_t> g_dist(0.5); // p=0.5: ~50% chance of 0, ~25% of 1, etc.
1091 static std::uniform_int_distribution<uint8_t> u_dist(0, std::numeric_limits<uint8_t>::max()); // Uniform distribution over all possible uint8_t values
1092
1093 // Generate and return a rank with both components
1094 return { g_dist(generator), u_dist(generator) };
1095}
1096
1097
1107inline std::ostream& operator<<(std::ostream& os, const GeometricRank& rank)
1108{
1109 // Cast uint8_t to unsigned for correct stream output as numbers.
1110 return os << "(" << static_cast<unsigned>(rank.geometric_rank) << ", "
1111 << static_cast<unsigned>(rank.uniform_rank) << ")";
1112}
Defines the BitString class for compact character string storage and comparison.
A class to store strings compactly into integers, packing multiple characters into a single word.
Definition BitString.cuh:58
typename ZT::Bucket Bucket
Alias for the Bucket (node) struct from the base class ZipTrie.
typename ZT::SearchResults SearchResults
Removes a node with a given key from the zip tree. (Not implemented)
typename ZT::LCP_T LCP_T
Alias for the LCP type used, inherited from the base class (ZipTrie::LCP_T).
typename ZT::KEY_T KEY_T
Alias for the Key type used, inherited from the base class (ZipTrie::KEY_T).
typename ZT::AncestorLCPs AncestorLCPs
Alias for the AncestorLCPs struct from the base class ZipTrie.
Implements a zip trie, a data structure combining properties of tries and zip trees for storing strin...
Definition ZipTrie.hpp:126
ZipTrie(unsigned max_size, unsigned max_lcp_length) noexcept
Constructs an empty ZipTrie.
Definition ZipTrie.hpp:558
const RANK_T & get_root_rank() const noexcept
Removes a node with a given key from the zip tree. (Not implemented)
Definition ZipTrie.hpp:1031
int height() const noexcept
Calculates the height of the trie.
Definition ZipTrie.hpp:867
std::optional< ComparisonResult > k_compare_prefix_check(const Bucket &v, const AncestorLCPs &ancestor_lcps, size_t &out_start_lcp_val) const noexcept
Performs an initial check based on ancestor LCPs before full key comparison (k-compare optimization).
Definition ZipTrie.hpp:583
void set_root_index(unsigned root_index) noexcept
Directly sets the root index of the trie.
Definition ZipTrie.hpp:1045
unsigned insert_recursive(Bucket *x, unsigned x_index, unsigned v_index, AncestorLCPs ancestor_lcps, CompareFunction compare_func) noexcept
Recursive helper function for inserting a new node x.
Definition ZipTrie.hpp:766
void to_dot_recursive(std::ofstream &os, unsigned node_index) const noexcept
Recursive helper function to generate the DOT language representation for a subtree.
Definition ZipTrie.hpp:973
ComparisonResult k_compare(const KEY_T *x, const Bucket &v, const AncestorLCPs &ancestor_lcps) const noexcept
Compares a key x with the key in a node v, utilizing ancestor LCPs for optimization (k-compare).
Definition ZipTrie.hpp:649
int height(unsigned node_index) const noexcept
Recursive helper function to calculate the height of the subtree rooted at node_index.
Definition ZipTrie.hpp:874
LCP_T convert_lcp(size_t val) const noexcept
Converts an exact LCP length (e.g., number of matching characters/bits) to the LCP_T type.
Definition ZipTrie.hpp:636
std::vector< Bucket > _buckets
A contiguous vector storing all the nodes (Buckets) of the trie.
Definition ZipTrie.hpp:379
void insert(const KEY_T *key) noexcept
Inserts a key into the zip trie.
Definition ZipTrie.hpp:743
bool contains(const KEY_T *key) const noexcept
Checks if a key exists within the trie.
Definition ZipTrie.hpp:569
MemoryEfficientLCP get_memory_efficient_lcp(size_t num) const noexcept
Converts a standard LCP length (size_t) into the MemoryEfficientLCP format.
Definition ZipTrie.hpp:928
void set(const KEY_T *key, RANK_T rank, unsigned left, unsigned right, LCP_T predecessor_lcp, LCP_T successor_lcp) noexcept
Directly adds a pre-constructed bucket to the internal storage vector.
Definition ZipTrie.hpp:1039
void to_dot(const std::string &file_path) const noexcept
Generates a DOT language representation of the trie structure for visualization.
Definition ZipTrie.hpp:946
uint8_t _log_max_size
Stores the approximate log base 2 of the max_size provided during construction.
Definition ZipTrie.hpp:323
unsigned _root_index
Index of the root node within the _buckets vector. Initialized to NULLPTR for an empty trie.
Definition ZipTrie.hpp:312
std::conditional_t< MEMORY_EFFICIENT, MemoryEfficientLCP, unsigned > LCP_T
The type used to store Longest Common Prefix values.
Definition ZipTrie.hpp:132
unsigned _max_lcp_length
Stores the maximum possible LCP length provided during construction.
Definition ZipTrie.hpp:317
SearchResults search_recursive(const KEY_T *key, CompareFunction compare_func) const noexcept
Recursive helper function for the public search method.
Definition ZipTrie.hpp:687
uint64_t get_total_depth(unsigned node_index, uint64_t depth) const noexcept
Recursive helper function to calculate the sum of the depths of all nodes within a subtree.
Definition ZipTrie.hpp:912
int get_depth(const KEY_T *key) const noexcept
Calculates the depth of a given key in the trie.
Definition ZipTrie.hpp:888
LCP_T lcp(const KEY_T *key) const noexcept
Finds the Longest Common Prefix (LCP) between the search key and the path taken in the trie.
Definition ZipTrie.hpp:576
double get_average_height() const noexcept
Calculates the average depth of all nodes currently in the trie.
Definition ZipTrie.hpp:896
virtual SearchResults search(const KEY_T *key) const noexcept
Performs a search for a key within the ZipTrie.
Definition ZipTrie.hpp:671
unsigned size() const noexcept
Returns the number of nodes (keys) currently stored in the trie.
Definition ZipTrie.hpp:857
T * alloc_to_device(size_t size)
Allocates memory on the CUDA device.
Represents a rank used in zip-zip trees, typically generated randomly for balancing.
Definition ZipTrie.hpp:73
uint8_t geometric_rank
Rank component following a geometric distribution (primary rank). Higher values are less likely.
Definition ZipTrie.hpp:75
std::ostream & operator<<(std::ostream &os, const GeometricRank &rank)
Overloaded stream insertion operator (<<) for GeometricRank.
Definition ZipTrie.hpp:1107
static GeometricRank get_random()
Generates a random rank using static random number generators.
Definition ZipTrie.hpp:1083
auto operator<=>(const GeometricRank &) const =default
Default comparison operator.
uint8_t uniform_rank
Rank component following a uniform distribution (used for tie-breaking).
Definition ZipTrie.hpp:77
Represents an approximate Longest Common Prefix (LCP) length in a memory-efficient format.
Definition ZipTrie.hpp:37
auto operator<=>(const MemoryEfficientLCP &) const =default
Default comparison operator.
std::ostream & operator<<(std::ostream &os, const MemoryEfficientLCP &lcp)
Overloaded stream insertion operator (<<) for MemoryEfficientLCP.
Definition ZipTrie.hpp:1077
std::ostream & print(std::ostream &os) const noexcept
Prints the LCP representation (e.g., "2^3 * 5") to an output stream.
Definition ZipTrie.hpp:1062
uint8_t multiple
The multiple (mantissa) part of the LCP representation.
Definition ZipTrie.hpp:41
uint8_t exp_of_2
The exponent part of the LCP representation (base 2).
Definition ZipTrie.hpp:39
unsigned value() const noexcept
Calculates the approximate actual LCP value represented by this struct.
Definition ZipTrie.hpp:1055
Stores the Longest Common Prefix (LCP) values associated with the path from the root down to a node's...
Definition ZipTrie.hpp:339
LCP_T successor
LCP accumulated along the path corresponding to the inorder successor branch.
Definition ZipTrie.hpp:343
LCP_T predecessor
LCP accumulated along the path corresponding to the inorder predecessor branch.
Definition ZipTrie.hpp:341
Represents a single node (bucket) within the ZipTrie's internal storage.
Definition ZipTrie.hpp:352
const KEY_T * key
Pointer to the key associated with this node.
Definition ZipTrie.hpp:358
AncestorLCPs ancestor_lcps
Stores the AncestorLCPs passed down from the parent during this node's insertion.
Definition ZipTrie.hpp:371
unsigned left
Index of the left child in the _buckets vector, or NULLPTR if no left child.
Definition ZipTrie.hpp:362
RANK_T rank
The rank associated with this node, used for maintaining the Zip Tree (max-heap) property.
Definition ZipTrie.hpp:360
unsigned right
Index of the right child in the _buckets vector, or NULLPTR if no right child.
Definition ZipTrie.hpp:364
Holds the result of comparing a search/insert key x with a node v's key, considering the context of a...
Definition ZipTrie.hpp:446
LCP_T lcp
The Longest Common Prefix (LCP) calculated between x and v during this specific comparison.
Definition ZipTrie.hpp:454
std::strong_ordering comparison
The relative ordering of x compared to v (std::strong_ordering::less, greater, or equal).
Definition ZipTrie.hpp:448
Holds the results returned by a search operation (search method).
Definition ZipTrie.hpp:271
LCP_T max_lcp
The Longest Common Prefix (LCP) calculated during the search.
Definition ZipTrie.hpp:279
int depth
The depth at which the key was found (number of edges from the root, root is depth 0).
Definition ZipTrie.hpp:284
bool contains
True if a node with the exact key was found in the trie, false otherwise.
Definition ZipTrie.hpp:273