exonware-xwnode 0.0.1.22__py3-none-any.whl → 0.0.1.23__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- exonware/__init__.py +1 -1
- exonware/xwnode/__init__.py +18 -5
- exonware/xwnode/add_strategy_types.py +165 -0
- exonware/xwnode/common/__init__.py +1 -1
- exonware/xwnode/common/graph/__init__.py +30 -0
- exonware/xwnode/common/graph/caching.py +131 -0
- exonware/xwnode/common/graph/contracts.py +100 -0
- exonware/xwnode/common/graph/errors.py +44 -0
- exonware/xwnode/common/graph/indexing.py +260 -0
- exonware/xwnode/common/graph/manager.py +568 -0
- exonware/xwnode/common/management/__init__.py +3 -5
- exonware/xwnode/common/management/manager.py +2 -2
- exonware/xwnode/common/management/migration.py +3 -3
- exonware/xwnode/common/monitoring/__init__.py +3 -5
- exonware/xwnode/common/monitoring/metrics.py +6 -2
- exonware/xwnode/common/monitoring/pattern_detector.py +1 -1
- exonware/xwnode/common/monitoring/performance_monitor.py +5 -1
- exonware/xwnode/common/patterns/__init__.py +3 -5
- exonware/xwnode/common/patterns/flyweight.py +5 -1
- exonware/xwnode/common/patterns/registry.py +202 -183
- exonware/xwnode/common/utils/__init__.py +25 -11
- exonware/xwnode/common/utils/simple.py +1 -1
- exonware/xwnode/config.py +3 -8
- exonware/xwnode/contracts.py +4 -105
- exonware/xwnode/defs.py +413 -159
- exonware/xwnode/edges/strategies/__init__.py +86 -4
- exonware/xwnode/edges/strategies/_base_edge.py +2 -2
- exonware/xwnode/edges/strategies/adj_list.py +287 -121
- exonware/xwnode/edges/strategies/adj_matrix.py +316 -222
- exonware/xwnode/edges/strategies/base.py +1 -1
- exonware/xwnode/edges/strategies/{edge_bidir_wrapper.py → bidir_wrapper.py} +45 -4
- exonware/xwnode/edges/strategies/bitemporal.py +520 -0
- exonware/xwnode/edges/strategies/{edge_block_adj_matrix.py → block_adj_matrix.py} +77 -6
- exonware/xwnode/edges/strategies/bv_graph.py +664 -0
- exonware/xwnode/edges/strategies/compressed_graph.py +217 -0
- exonware/xwnode/edges/strategies/{edge_coo.py → coo.py} +46 -4
- exonware/xwnode/edges/strategies/{edge_csc.py → csc.py} +45 -4
- exonware/xwnode/edges/strategies/{edge_csr.py → csr.py} +94 -12
- exonware/xwnode/edges/strategies/{edge_dynamic_adj_list.py → dynamic_adj_list.py} +46 -4
- exonware/xwnode/edges/strategies/edge_list.py +168 -0
- exonware/xwnode/edges/strategies/edge_property_store.py +2 -2
- exonware/xwnode/edges/strategies/euler_tour.py +560 -0
- exonware/xwnode/edges/strategies/{edge_flow_network.py → flow_network.py} +2 -2
- exonware/xwnode/edges/strategies/graphblas.py +449 -0
- exonware/xwnode/edges/strategies/hnsw.py +637 -0
- exonware/xwnode/edges/strategies/hop2_labels.py +467 -0
- exonware/xwnode/edges/strategies/{edge_hyperedge_set.py → hyperedge_set.py} +2 -2
- exonware/xwnode/edges/strategies/incidence_matrix.py +250 -0
- exonware/xwnode/edges/strategies/k2_tree.py +613 -0
- exonware/xwnode/edges/strategies/link_cut.py +626 -0
- exonware/xwnode/edges/strategies/multiplex.py +532 -0
- exonware/xwnode/edges/strategies/{edge_neural_graph.py → neural_graph.py} +2 -2
- exonware/xwnode/edges/strategies/{edge_octree.py → octree.py} +69 -11
- exonware/xwnode/edges/strategies/{edge_quadtree.py → quadtree.py} +66 -10
- exonware/xwnode/edges/strategies/roaring_adj.py +438 -0
- exonware/xwnode/edges/strategies/{edge_rtree.py → rtree.py} +43 -5
- exonware/xwnode/edges/strategies/{edge_temporal_edgeset.py → temporal_edgeset.py} +24 -5
- exonware/xwnode/edges/strategies/{edge_tree_graph_basic.py → tree_graph_basic.py} +78 -7
- exonware/xwnode/edges/strategies/{edge_weighted_graph.py → weighted_graph.py} +188 -10
- exonware/xwnode/errors.py +3 -6
- exonware/xwnode/facade.py +20 -20
- exonware/xwnode/nodes/strategies/__init__.py +29 -9
- exonware/xwnode/nodes/strategies/adjacency_list.py +650 -177
- exonware/xwnode/nodes/strategies/aho_corasick.py +358 -183
- exonware/xwnode/nodes/strategies/array_list.py +36 -3
- exonware/xwnode/nodes/strategies/art.py +581 -0
- exonware/xwnode/nodes/strategies/{node_avl_tree.py → avl_tree.py} +77 -6
- exonware/xwnode/nodes/strategies/{node_b_plus_tree.py → b_plus_tree.py} +81 -40
- exonware/xwnode/nodes/strategies/{node_btree.py → b_tree.py} +79 -9
- exonware/xwnode/nodes/strategies/base.py +469 -98
- exonware/xwnode/nodes/strategies/{node_bitmap.py → bitmap.py} +12 -12
- exonware/xwnode/nodes/strategies/{node_bitset_dynamic.py → bitset_dynamic.py} +11 -11
- exonware/xwnode/nodes/strategies/{node_bloom_filter.py → bloom_filter.py} +15 -2
- exonware/xwnode/nodes/strategies/bloomier_filter.py +519 -0
- exonware/xwnode/nodes/strategies/bw_tree.py +531 -0
- exonware/xwnode/nodes/strategies/contracts.py +1 -1
- exonware/xwnode/nodes/strategies/{node_count_min_sketch.py → count_min_sketch.py} +3 -2
- exonware/xwnode/nodes/strategies/{node_cow_tree.py → cow_tree.py} +135 -13
- exonware/xwnode/nodes/strategies/crdt_map.py +629 -0
- exonware/xwnode/nodes/strategies/{node_cuckoo_hash.py → cuckoo_hash.py} +2 -2
- exonware/xwnode/nodes/strategies/{node_xdata_optimized.py → data_interchange_optimized.py} +21 -4
- exonware/xwnode/nodes/strategies/dawg.py +876 -0
- exonware/xwnode/nodes/strategies/deque.py +321 -153
- exonware/xwnode/nodes/strategies/extendible_hash.py +93 -0
- exonware/xwnode/nodes/strategies/{node_fenwick_tree.py → fenwick_tree.py} +111 -19
- exonware/xwnode/nodes/strategies/hamt.py +403 -0
- exonware/xwnode/nodes/strategies/hash_map.py +354 -67
- exonware/xwnode/nodes/strategies/heap.py +105 -5
- exonware/xwnode/nodes/strategies/hopscotch_hash.py +525 -0
- exonware/xwnode/nodes/strategies/{node_hyperloglog.py → hyperloglog.py} +6 -5
- exonware/xwnode/nodes/strategies/interval_tree.py +742 -0
- exonware/xwnode/nodes/strategies/kd_tree.py +703 -0
- exonware/xwnode/nodes/strategies/learned_index.py +533 -0
- exonware/xwnode/nodes/strategies/linear_hash.py +93 -0
- exonware/xwnode/nodes/strategies/linked_list.py +316 -119
- exonware/xwnode/nodes/strategies/{node_lsm_tree.py → lsm_tree.py} +219 -15
- exonware/xwnode/nodes/strategies/masstree.py +130 -0
- exonware/xwnode/nodes/strategies/{node_persistent_tree.py → persistent_tree.py} +149 -9
- exonware/xwnode/nodes/strategies/priority_queue.py +544 -132
- exonware/xwnode/nodes/strategies/queue.py +249 -120
- exonware/xwnode/nodes/strategies/{node_red_black_tree.py → red_black_tree.py} +183 -72
- exonware/xwnode/nodes/strategies/{node_roaring_bitmap.py → roaring_bitmap.py} +19 -6
- exonware/xwnode/nodes/strategies/rope.py +717 -0
- exonware/xwnode/nodes/strategies/{node_segment_tree.py → segment_tree.py} +106 -106
- exonware/xwnode/nodes/strategies/{node_set_hash.py → set_hash.py} +30 -29
- exonware/xwnode/nodes/strategies/{node_skip_list.py → skip_list.py} +74 -6
- exonware/xwnode/nodes/strategies/sparse_matrix.py +427 -131
- exonware/xwnode/nodes/strategies/{node_splay_tree.py → splay_tree.py} +55 -6
- exonware/xwnode/nodes/strategies/stack.py +244 -112
- exonware/xwnode/nodes/strategies/{node_suffix_array.py → suffix_array.py} +5 -1
- exonware/xwnode/nodes/strategies/t_tree.py +94 -0
- exonware/xwnode/nodes/strategies/{node_treap.py → treap.py} +75 -6
- exonware/xwnode/nodes/strategies/{node_tree_graph_hybrid.py → tree_graph_hybrid.py} +46 -5
- exonware/xwnode/nodes/strategies/trie.py +153 -9
- exonware/xwnode/nodes/strategies/union_find.py +111 -5
- exonware/xwnode/nodes/strategies/veb_tree.py +856 -0
- exonware/xwnode/strategies/__init__.py +5 -51
- exonware/xwnode/version.py +3 -3
- {exonware_xwnode-0.0.1.22.dist-info → exonware_xwnode-0.0.1.23.dist-info}/METADATA +23 -3
- exonware_xwnode-0.0.1.23.dist-info/RECORD +130 -0
- exonware/xwnode/edges/strategies/edge_adj_list.py +0 -353
- exonware/xwnode/edges/strategies/edge_adj_matrix.py +0 -445
- exonware/xwnode/nodes/strategies/_base_node.py +0 -307
- exonware/xwnode/nodes/strategies/node_aho_corasick.py +0 -525
- exonware/xwnode/nodes/strategies/node_array_list.py +0 -179
- exonware/xwnode/nodes/strategies/node_hash_map.py +0 -273
- exonware/xwnode/nodes/strategies/node_heap.py +0 -196
- exonware/xwnode/nodes/strategies/node_linked_list.py +0 -413
- exonware/xwnode/nodes/strategies/node_trie.py +0 -257
- exonware/xwnode/nodes/strategies/node_union_find.py +0 -192
- exonware/xwnode/queries/executors/__init__.py +0 -47
- exonware/xwnode/queries/executors/advanced/__init__.py +0 -37
- exonware/xwnode/queries/executors/advanced/aggregate_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/ask_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/construct_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/describe_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/for_loop_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/foreach_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/join_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/let_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/mutation_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/options_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/pipe_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/subscribe_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/subscription_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/union_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/window_executor.py +0 -51
- exonware/xwnode/queries/executors/advanced/with_cte_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/__init__.py +0 -21
- exonware/xwnode/queries/executors/aggregation/avg_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/count_executor.py +0 -38
- exonware/xwnode/queries/executors/aggregation/distinct_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/group_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/having_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/max_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/min_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/sum_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/summarize_executor.py +0 -50
- exonware/xwnode/queries/executors/array/__init__.py +0 -9
- exonware/xwnode/queries/executors/array/indexing_executor.py +0 -51
- exonware/xwnode/queries/executors/array/slicing_executor.py +0 -51
- exonware/xwnode/queries/executors/base.py +0 -257
- exonware/xwnode/queries/executors/capability_checker.py +0 -204
- exonware/xwnode/queries/executors/contracts.py +0 -166
- exonware/xwnode/queries/executors/core/__init__.py +0 -17
- exonware/xwnode/queries/executors/core/create_executor.py +0 -96
- exonware/xwnode/queries/executors/core/delete_executor.py +0 -99
- exonware/xwnode/queries/executors/core/drop_executor.py +0 -100
- exonware/xwnode/queries/executors/core/insert_executor.py +0 -39
- exonware/xwnode/queries/executors/core/select_executor.py +0 -152
- exonware/xwnode/queries/executors/core/update_executor.py +0 -102
- exonware/xwnode/queries/executors/data/__init__.py +0 -13
- exonware/xwnode/queries/executors/data/alter_executor.py +0 -50
- exonware/xwnode/queries/executors/data/load_executor.py +0 -50
- exonware/xwnode/queries/executors/data/merge_executor.py +0 -50
- exonware/xwnode/queries/executors/data/store_executor.py +0 -50
- exonware/xwnode/queries/executors/defs.py +0 -93
- exonware/xwnode/queries/executors/engine.py +0 -221
- exonware/xwnode/queries/executors/errors.py +0 -68
- exonware/xwnode/queries/executors/filtering/__init__.py +0 -25
- exonware/xwnode/queries/executors/filtering/between_executor.py +0 -80
- exonware/xwnode/queries/executors/filtering/filter_executor.py +0 -79
- exonware/xwnode/queries/executors/filtering/has_executor.py +0 -70
- exonware/xwnode/queries/executors/filtering/in_executor.py +0 -70
- exonware/xwnode/queries/executors/filtering/like_executor.py +0 -76
- exonware/xwnode/queries/executors/filtering/optional_executor.py +0 -76
- exonware/xwnode/queries/executors/filtering/range_executor.py +0 -80
- exonware/xwnode/queries/executors/filtering/term_executor.py +0 -77
- exonware/xwnode/queries/executors/filtering/values_executor.py +0 -71
- exonware/xwnode/queries/executors/filtering/where_executor.py +0 -44
- exonware/xwnode/queries/executors/graph/__init__.py +0 -15
- exonware/xwnode/queries/executors/graph/in_traverse_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/match_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/out_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/path_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/return_executor.py +0 -51
- exonware/xwnode/queries/executors/ordering/__init__.py +0 -9
- exonware/xwnode/queries/executors/ordering/by_executor.py +0 -50
- exonware/xwnode/queries/executors/ordering/order_executor.py +0 -51
- exonware/xwnode/queries/executors/projection/__init__.py +0 -9
- exonware/xwnode/queries/executors/projection/extend_executor.py +0 -50
- exonware/xwnode/queries/executors/projection/project_executor.py +0 -50
- exonware/xwnode/queries/executors/registry.py +0 -173
- exonware/xwnode/queries/parsers/__init__.py +0 -26
- exonware/xwnode/queries/parsers/base.py +0 -86
- exonware/xwnode/queries/parsers/contracts.py +0 -46
- exonware/xwnode/queries/parsers/errors.py +0 -53
- exonware/xwnode/queries/parsers/sql_param_extractor.py +0 -318
- exonware/xwnode/queries/strategies/__init__.py +0 -24
- exonware/xwnode/queries/strategies/base.py +0 -236
- exonware/xwnode/queries/strategies/cql.py +0 -201
- exonware/xwnode/queries/strategies/cypher.py +0 -181
- exonware/xwnode/queries/strategies/datalog.py +0 -70
- exonware/xwnode/queries/strategies/elastic_dsl.py +0 -70
- exonware/xwnode/queries/strategies/eql.py +0 -70
- exonware/xwnode/queries/strategies/flux.py +0 -70
- exonware/xwnode/queries/strategies/gql.py +0 -70
- exonware/xwnode/queries/strategies/graphql.py +0 -240
- exonware/xwnode/queries/strategies/gremlin.py +0 -181
- exonware/xwnode/queries/strategies/hiveql.py +0 -214
- exonware/xwnode/queries/strategies/hql.py +0 -70
- exonware/xwnode/queries/strategies/jmespath.py +0 -219
- exonware/xwnode/queries/strategies/jq.py +0 -66
- exonware/xwnode/queries/strategies/json_query.py +0 -66
- exonware/xwnode/queries/strategies/jsoniq.py +0 -248
- exonware/xwnode/queries/strategies/kql.py +0 -70
- exonware/xwnode/queries/strategies/linq.py +0 -238
- exonware/xwnode/queries/strategies/logql.py +0 -70
- exonware/xwnode/queries/strategies/mql.py +0 -68
- exonware/xwnode/queries/strategies/n1ql.py +0 -210
- exonware/xwnode/queries/strategies/partiql.py +0 -70
- exonware/xwnode/queries/strategies/pig.py +0 -215
- exonware/xwnode/queries/strategies/promql.py +0 -70
- exonware/xwnode/queries/strategies/sparql.py +0 -220
- exonware/xwnode/queries/strategies/sql.py +0 -275
- exonware/xwnode/queries/strategies/xml_query.py +0 -66
- exonware/xwnode/queries/strategies/xpath.py +0 -223
- exonware/xwnode/queries/strategies/xquery.py +0 -258
- exonware/xwnode/queries/strategies/xwnode_executor.py +0 -332
- exonware/xwnode/queries/strategies/xwquery.py +0 -456
- exonware_xwnode-0.0.1.22.dist-info/RECORD +0 -214
- /exonware/xwnode/nodes/strategies/{node_ordered_map.py → ordered_map.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_ordered_map_balanced.py → ordered_map_balanced.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_patricia.py → patricia.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_radix_trie.py → radix_trie.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_set_tree.py → set_tree.py} +0 -0
- {exonware_xwnode-0.0.1.22.dist-info → exonware_xwnode-0.0.1.23.dist-info}/WHEEL +0 -0
- {exonware_xwnode-0.0.1.22.dist-info → exonware_xwnode-0.0.1.23.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,626 @@
|
|
1
|
+
"""
|
2
|
+
#exonware/xwnode/src/exonware/xwnode/edges/strategies/link_cut.py
|
3
|
+
|
4
|
+
Link-Cut Trees Edge Strategy Implementation
|
5
|
+
|
6
|
+
This module implements the LINK_CUT strategy for dynamic trees with
|
7
|
+
path queries and updates using splay-based structure.
|
8
|
+
|
9
|
+
Company: eXonware.com
|
10
|
+
Author: Eng. Muhammad AlShehri
|
11
|
+
Email: connect@exonware.com
|
12
|
+
Version: 0.0.1.23
|
13
|
+
Generation Date: 12-Oct-2025
|
14
|
+
"""
|
15
|
+
|
16
|
+
from typing import Any, Iterator, Dict, List, Set, Optional, Tuple
|
17
|
+
from collections import deque, defaultdict
|
18
|
+
from ._base_edge import AEdgeStrategy
|
19
|
+
from ...defs import EdgeMode, EdgeTrait
|
20
|
+
from ...errors import XWNodeError, XWNodeValueError
|
21
|
+
|
22
|
+
|
23
|
+
class LCNode:
|
24
|
+
"""
|
25
|
+
Node in link-cut tree.
|
26
|
+
|
27
|
+
WHY splay tree representation:
|
28
|
+
- Preferred paths stored in splay trees
|
29
|
+
- Access/expose operations bring nodes to root
|
30
|
+
- Amortized O(log n) complexity
|
31
|
+
"""
|
32
|
+
|
33
|
+
def __init__(self, vertex: str):
|
34
|
+
"""
|
35
|
+
Initialize link-cut node.
|
36
|
+
|
37
|
+
Args:
|
38
|
+
vertex: Vertex identifier
|
39
|
+
"""
|
40
|
+
self.vertex = vertex
|
41
|
+
self.parent: Optional['LCNode'] = None
|
42
|
+
self.left: Optional['LCNode'] = None
|
43
|
+
self.right: Optional['LCNode'] = None
|
44
|
+
|
45
|
+
# Path aggregate values (for path queries)
|
46
|
+
self.value: float = 0.0
|
47
|
+
self.path_min: float = 0.0
|
48
|
+
self.path_max: float = 0.0
|
49
|
+
self.path_sum: float = 0.0
|
50
|
+
|
51
|
+
# Lazy propagation flag
|
52
|
+
self.reversed = False
|
53
|
+
|
54
|
+
def is_root(self) -> bool:
|
55
|
+
"""Check if this is a root of preferred path."""
|
56
|
+
return self.parent is None or (
|
57
|
+
self.parent.left != self and self.parent.right != self
|
58
|
+
)
|
59
|
+
|
60
|
+
def push_down(self) -> None:
|
61
|
+
"""Push down lazy reverse flag."""
|
62
|
+
if self.reversed:
|
63
|
+
self.left, self.right = self.right, self.left
|
64
|
+
|
65
|
+
if self.left:
|
66
|
+
self.left.reversed = not self.left.reversed
|
67
|
+
if self.right:
|
68
|
+
self.right.reversed = not self.right.reversed
|
69
|
+
|
70
|
+
self.reversed = False
|
71
|
+
|
72
|
+
def update(self) -> None:
|
73
|
+
"""Update path aggregate values from children."""
|
74
|
+
self.path_min = self.value
|
75
|
+
self.path_max = self.value
|
76
|
+
self.path_sum = self.value
|
77
|
+
|
78
|
+
if self.left:
|
79
|
+
self.path_min = min(self.path_min, self.left.path_min)
|
80
|
+
self.path_max = max(self.path_max, self.left.path_max)
|
81
|
+
self.path_sum += self.left.path_sum
|
82
|
+
|
83
|
+
if self.right:
|
84
|
+
self.path_min = min(self.path_min, self.right.path_min)
|
85
|
+
self.path_max = max(self.path_max, self.right.path_max)
|
86
|
+
self.path_sum += self.right.path_sum
|
87
|
+
|
88
|
+
|
89
|
+
class LinkCutStrategy(AEdgeStrategy):
|
90
|
+
"""
|
91
|
+
Link-Cut Trees strategy for dynamic trees with path operations.
|
92
|
+
|
93
|
+
WHY Link-Cut Trees:
|
94
|
+
- More powerful than Euler Tour Trees (supports path queries)
|
95
|
+
- O(log n) amortized for link, cut, and path operations
|
96
|
+
- Enables dynamic minimum spanning tree
|
97
|
+
- Supports path aggregates (sum, min, max) efficiently
|
98
|
+
- Essential for network flow and matching algorithms
|
99
|
+
|
100
|
+
WHY this implementation:
|
101
|
+
- Splay trees for preferred path representation
|
102
|
+
- Access operation brings nodes to root
|
103
|
+
- Lazy reversal for efficient path direction changes
|
104
|
+
- Path aggregates maintained incrementally
|
105
|
+
- Simplified exposure operation for clarity
|
106
|
+
|
107
|
+
Time Complexity (all amortized):
|
108
|
+
- Link: O(log n)
|
109
|
+
- Cut: O(log n)
|
110
|
+
- Connected: O(log n)
|
111
|
+
- Find root: O(log n)
|
112
|
+
- Path aggregate (sum/min/max): O(log n)
|
113
|
+
- Make root: O(log n)
|
114
|
+
|
115
|
+
Space Complexity: O(n) for n vertices
|
116
|
+
|
117
|
+
Trade-offs:
|
118
|
+
- Advantage: Path queries in addition to connectivity
|
119
|
+
- Advantage: Fully dynamic (link/cut)
|
120
|
+
- Advantage: Amortized O(log n) guarantees
|
121
|
+
- Limitation: More complex than Euler Tour Trees
|
122
|
+
- Limitation: Amortized (not worst-case) bounds
|
123
|
+
- Limitation: Splay tree implementation complexity
|
124
|
+
- Compared to Euler Tour: More features, more complex
|
125
|
+
- Compared to Heavy-Light: Simpler, similar performance
|
126
|
+
|
127
|
+
Best for:
|
128
|
+
- Dynamic minimum spanning trees
|
129
|
+
- Network flow algorithms
|
130
|
+
- Dynamic graph matching
|
131
|
+
- Path aggregate queries on trees
|
132
|
+
- Root changes in dynamic trees
|
133
|
+
- Bipartite matching algorithms
|
134
|
+
|
135
|
+
Not recommended for:
|
136
|
+
- Static trees (use LCA preprocessing)
|
137
|
+
- Simple connectivity (use Union-Find or Euler Tour)
|
138
|
+
- When path queries not needed
|
139
|
+
- Directed acyclic graphs
|
140
|
+
- Fixed root scenarios
|
141
|
+
|
142
|
+
Following eXonware Priorities:
|
143
|
+
1. Security: Validates tree structure, prevents cycles
|
144
|
+
2. Usability: Clean link/cut/path_query API
|
145
|
+
3. Maintainability: Modular splay operations
|
146
|
+
4. Performance: O(log n) amortized operations
|
147
|
+
5. Extensibility: Easy to add more aggregates, lazy propagation
|
148
|
+
|
149
|
+
Industry Best Practices:
|
150
|
+
- Follows Sleator-Tarjan link-cut trees (1983)
|
151
|
+
- Uses splay trees for preferred paths
|
152
|
+
- Implements access/expose operations correctly
|
153
|
+
- Provides path aggregate queries
|
154
|
+
- Compatible with dynamic graph algorithms
|
155
|
+
"""
|
156
|
+
|
157
|
+
def __init__(self, traits: EdgeTrait = EdgeTrait.NONE, **options):
|
158
|
+
"""
|
159
|
+
Initialize link-cut tree strategy.
|
160
|
+
|
161
|
+
Args:
|
162
|
+
traits: Edge traits
|
163
|
+
**options: Additional options
|
164
|
+
"""
|
165
|
+
super().__init__(EdgeMode.LINK_CUT, traits, **options)
|
166
|
+
|
167
|
+
# Node storage
|
168
|
+
self._nodes: Dict[str, LCNode] = {}
|
169
|
+
|
170
|
+
# Edge tracking
|
171
|
+
self._edges: Set[Tuple[str, str]] = set()
|
172
|
+
|
173
|
+
# Vertices
|
174
|
+
self._vertices: Set[str] = set()
|
175
|
+
|
176
|
+
def get_supported_traits(self) -> EdgeTrait:
|
177
|
+
"""Get supported traits."""
|
178
|
+
return EdgeTrait.DIRECTED | EdgeTrait.SPARSE
|
179
|
+
|
180
|
+
# ============================================================================
|
181
|
+
# SPLAY OPERATIONS
|
182
|
+
# ============================================================================
|
183
|
+
|
184
|
+
def _rotate(self, node: LCNode) -> None:
|
185
|
+
"""Rotate node with parent."""
|
186
|
+
parent = node.parent
|
187
|
+
if parent is None:
|
188
|
+
return
|
189
|
+
|
190
|
+
grandparent = parent.parent
|
191
|
+
|
192
|
+
if parent.left == node:
|
193
|
+
# Right rotation
|
194
|
+
parent.left = node.right
|
195
|
+
if node.right:
|
196
|
+
node.right.parent = parent
|
197
|
+
node.right = parent
|
198
|
+
else:
|
199
|
+
# Left rotation
|
200
|
+
parent.right = node.left
|
201
|
+
if node.left:
|
202
|
+
node.left.parent = parent
|
203
|
+
node.left = parent
|
204
|
+
|
205
|
+
parent.parent = node
|
206
|
+
node.parent = grandparent
|
207
|
+
|
208
|
+
if grandparent:
|
209
|
+
if grandparent.left == parent:
|
210
|
+
grandparent.left = node
|
211
|
+
elif grandparent.right == parent:
|
212
|
+
grandparent.right = node
|
213
|
+
|
214
|
+
# Update aggregates
|
215
|
+
parent.update()
|
216
|
+
node.update()
|
217
|
+
|
218
|
+
def _splay(self, node: LCNode) -> None:
|
219
|
+
"""
|
220
|
+
Splay node to root of its tree.
|
221
|
+
|
222
|
+
Args:
|
223
|
+
node: Node to splay
|
224
|
+
|
225
|
+
WHY splaying:
|
226
|
+
- Brings frequently accessed nodes closer to root
|
227
|
+
- Amortizes access cost
|
228
|
+
- Critical for O(log n) amortized complexity
|
229
|
+
"""
|
230
|
+
while not node.is_root():
|
231
|
+
parent = node.parent
|
232
|
+
|
233
|
+
if parent.is_root():
|
234
|
+
# Zig step
|
235
|
+
self._rotate(node)
|
236
|
+
else:
|
237
|
+
grandparent = parent.parent
|
238
|
+
|
239
|
+
if (grandparent.left == parent) == (parent.left == node):
|
240
|
+
# Zig-zig
|
241
|
+
self._rotate(parent)
|
242
|
+
self._rotate(node)
|
243
|
+
else:
|
244
|
+
# Zig-zag
|
245
|
+
self._rotate(node)
|
246
|
+
self._rotate(node)
|
247
|
+
|
248
|
+
def _access(self, node: LCNode) -> None:
|
249
|
+
"""
|
250
|
+
Make path from node to root preferred.
|
251
|
+
|
252
|
+
Args:
|
253
|
+
node: Node to access
|
254
|
+
|
255
|
+
WHY access:
|
256
|
+
- Makes path to root represented by single splay tree
|
257
|
+
- Enables path queries
|
258
|
+
- Essential operation for link-cut trees
|
259
|
+
"""
|
260
|
+
self._splay(node)
|
261
|
+
if node.right:
|
262
|
+
node.right.parent = None
|
263
|
+
node.right = None
|
264
|
+
node.update()
|
265
|
+
|
266
|
+
while node.parent:
|
267
|
+
parent = node.parent
|
268
|
+
self._splay(parent)
|
269
|
+
if parent.right:
|
270
|
+
parent.right.parent = None
|
271
|
+
parent.right = node
|
272
|
+
parent.update()
|
273
|
+
self._splay(node)
|
274
|
+
|
275
|
+
# ============================================================================
|
276
|
+
# LINK-CUT OPERATIONS
|
277
|
+
# ============================================================================
|
278
|
+
|
279
|
+
def _get_or_create_node(self, vertex: str) -> LCNode:
|
280
|
+
"""Get or create LC node for vertex."""
|
281
|
+
if vertex not in self._nodes:
|
282
|
+
self._nodes[vertex] = LCNode(vertex)
|
283
|
+
self._vertices.add(vertex)
|
284
|
+
return self._nodes[vertex]
|
285
|
+
|
286
|
+
def _link(self, u: str, v: str) -> None:
|
287
|
+
"""
|
288
|
+
Link vertices u and v.
|
289
|
+
|
290
|
+
Args:
|
291
|
+
u: First vertex
|
292
|
+
v: Second vertex (becomes parent of u)
|
293
|
+
|
294
|
+
Raises:
|
295
|
+
XWNodeError: If would create cycle
|
296
|
+
"""
|
297
|
+
node_u = self._get_or_create_node(u)
|
298
|
+
node_v = self._get_or_create_node(v)
|
299
|
+
|
300
|
+
# Check if already connected
|
301
|
+
if self._find_root(node_u) == self._find_root(node_v):
|
302
|
+
raise XWNodeError(f"Link would create cycle: {u} and {v} already connected")
|
303
|
+
|
304
|
+
# Make u child of v
|
305
|
+
self._access(node_u)
|
306
|
+
self._access(node_v)
|
307
|
+
node_u.parent = node_v
|
308
|
+
|
309
|
+
def _cut(self, u: str, v: str) -> bool:
|
310
|
+
"""
|
311
|
+
Cut edge between u and v.
|
312
|
+
|
313
|
+
Args:
|
314
|
+
u: First vertex
|
315
|
+
v: Second vertex
|
316
|
+
|
317
|
+
Returns:
|
318
|
+
True if cut successful
|
319
|
+
"""
|
320
|
+
if u not in self._nodes or v not in self._nodes:
|
321
|
+
return False
|
322
|
+
|
323
|
+
node_u = self._nodes[u]
|
324
|
+
node_v = self._nodes[v]
|
325
|
+
|
326
|
+
# Make path from u to v preferred
|
327
|
+
self._access(node_u)
|
328
|
+
self._access(node_v)
|
329
|
+
|
330
|
+
# Check if v is parent of u
|
331
|
+
if node_u.parent == node_v:
|
332
|
+
node_u.parent = None
|
333
|
+
return True
|
334
|
+
|
335
|
+
# Check if u is parent of v
|
336
|
+
if node_v.parent == node_u:
|
337
|
+
node_v.parent = None
|
338
|
+
return True
|
339
|
+
|
340
|
+
return False
|
341
|
+
|
342
|
+
def _find_root(self, node: LCNode) -> LCNode:
|
343
|
+
"""
|
344
|
+
Find root of tree containing node.
|
345
|
+
|
346
|
+
Args:
|
347
|
+
node: Node
|
348
|
+
|
349
|
+
Returns:
|
350
|
+
Root node
|
351
|
+
"""
|
352
|
+
self._access(node)
|
353
|
+
|
354
|
+
# Move to leftmost node
|
355
|
+
while node.left:
|
356
|
+
node = node.left
|
357
|
+
|
358
|
+
self._splay(node)
|
359
|
+
return node
|
360
|
+
|
361
|
+
# ============================================================================
|
362
|
+
# PATH QUERIES
|
363
|
+
# ============================================================================
|
364
|
+
|
365
|
+
def path_aggregate(self, u: str, v: str, operation: str = "sum") -> float:
|
366
|
+
"""
|
367
|
+
Compute aggregate on path from u to v.
|
368
|
+
|
369
|
+
Args:
|
370
|
+
u: Start vertex
|
371
|
+
v: End vertex
|
372
|
+
operation: Aggregate operation (sum, min, max)
|
373
|
+
|
374
|
+
Returns:
|
375
|
+
Aggregate value
|
376
|
+
|
377
|
+
Raises:
|
378
|
+
XWNodeValueError: If vertices not in same tree
|
379
|
+
|
380
|
+
WHY path aggregates:
|
381
|
+
- Essential for network flow
|
382
|
+
- Enables dynamic programming on trees
|
383
|
+
- Maintained incrementally in O(log n)
|
384
|
+
"""
|
385
|
+
if u not in self._nodes or v not in self._nodes:
|
386
|
+
raise XWNodeValueError(f"Vertices not found: {u}, {v}")
|
387
|
+
|
388
|
+
node_u = self._nodes[u]
|
389
|
+
node_v = self._nodes[v]
|
390
|
+
|
391
|
+
# Check if in same tree
|
392
|
+
if self._find_root(node_u) != self._find_root(node_v):
|
393
|
+
raise XWNodeValueError(f"Vertices {u} and {v} not in same tree")
|
394
|
+
|
395
|
+
# Access path from u to v
|
396
|
+
self._access(node_u)
|
397
|
+
self._access(node_v)
|
398
|
+
|
399
|
+
# Get aggregate
|
400
|
+
if operation == "sum":
|
401
|
+
return node_v.path_sum
|
402
|
+
elif operation == "min":
|
403
|
+
return node_v.path_min
|
404
|
+
elif operation == "max":
|
405
|
+
return node_v.path_max
|
406
|
+
else:
|
407
|
+
raise XWNodeValueError(f"Unknown operation: {operation}")
|
408
|
+
|
409
|
+
# ============================================================================
|
410
|
+
# GRAPH OPERATIONS
|
411
|
+
# ============================================================================
|
412
|
+
|
413
|
+
def add_edge(self, source: str, target: str, edge_type: str = "default",
|
414
|
+
weight: float = 1.0, properties: Optional[Dict[str, Any]] = None,
|
415
|
+
is_bidirectional: bool = False, edge_id: Optional[str] = None) -> str:
|
416
|
+
"""
|
417
|
+
Link vertices.
|
418
|
+
|
419
|
+
Args:
|
420
|
+
source: Source vertex
|
421
|
+
target: Target vertex
|
422
|
+
edge_type: Edge type
|
423
|
+
weight: Edge weight
|
424
|
+
properties: Edge properties
|
425
|
+
is_bidirectional: Bidirectional flag
|
426
|
+
edge_id: Edge ID
|
427
|
+
|
428
|
+
Returns:
|
429
|
+
Edge ID
|
430
|
+
"""
|
431
|
+
self._link(source, target)
|
432
|
+
|
433
|
+
self._edges.add((source, target))
|
434
|
+
if is_bidirectional:
|
435
|
+
self._edges.add((target, source))
|
436
|
+
|
437
|
+
self._edge_count += 1
|
438
|
+
|
439
|
+
return edge_id or f"edge_{source}_{target}"
|
440
|
+
|
441
|
+
def remove_edge(self, source: str, target: str, edge_id: Optional[str] = None) -> bool:
|
442
|
+
"""Cut edge."""
|
443
|
+
if self._cut(source, target):
|
444
|
+
self._edges.discard((source, target))
|
445
|
+
self._edges.discard((target, source))
|
446
|
+
self._edge_count -= 1
|
447
|
+
return True
|
448
|
+
return False
|
449
|
+
|
450
|
+
def has_edge(self, source: str, target: str) -> bool:
|
451
|
+
"""Check if edge exists."""
|
452
|
+
return (source, target) in self._edges
|
453
|
+
|
454
|
+
def is_connected(self, source: str, target: str, edge_type: Optional[str] = None) -> bool:
|
455
|
+
"""Check if vertices connected."""
|
456
|
+
if source not in self._nodes or target not in self._nodes:
|
457
|
+
return False
|
458
|
+
|
459
|
+
return self._find_root(self._nodes[source]) == self._find_root(self._nodes[target])
|
460
|
+
|
461
|
+
def get_neighbors(self, node: str, edge_type: Optional[str] = None,
|
462
|
+
direction: str = "outgoing") -> List[str]:
|
463
|
+
"""Get neighbors."""
|
464
|
+
neighbors = set()
|
465
|
+
|
466
|
+
for edge in self._edges:
|
467
|
+
if edge[0] == node:
|
468
|
+
neighbors.add(edge[1])
|
469
|
+
elif edge[1] == node:
|
470
|
+
neighbors.add(edge[0])
|
471
|
+
|
472
|
+
return list(neighbors)
|
473
|
+
|
474
|
+
def neighbors(self, node: str) -> Iterator[Any]:
|
475
|
+
"""Get iterator over neighbors."""
|
476
|
+
return iter(self.get_neighbors(node))
|
477
|
+
|
478
|
+
def degree(self, node: str) -> int:
|
479
|
+
"""Get degree of node."""
|
480
|
+
return len(self.get_neighbors(node))
|
481
|
+
|
482
|
+
def edges(self) -> Iterator[Tuple[Any, Any, Dict[str, Any]]]:
|
483
|
+
"""Iterate over all edges with properties."""
|
484
|
+
for edge_dict in self.get_edges():
|
485
|
+
yield (edge_dict['source'], edge_dict['target'], {})
|
486
|
+
|
487
|
+
def vertices(self) -> Iterator[Any]:
|
488
|
+
"""Get iterator over all vertices."""
|
489
|
+
return iter(self._vertices)
|
490
|
+
|
491
|
+
def get_edges(self, edge_type: Optional[str] = None, direction: str = "both") -> List[Dict[str, Any]]:
|
492
|
+
"""Get all edges."""
|
493
|
+
seen = set()
|
494
|
+
edges = []
|
495
|
+
|
496
|
+
for u, v in self._edges:
|
497
|
+
if (u, v) not in seen and (v, u) not in seen:
|
498
|
+
seen.add((u, v))
|
499
|
+
edges.append({
|
500
|
+
'source': u,
|
501
|
+
'target': v,
|
502
|
+
'edge_type': edge_type or 'tree'
|
503
|
+
})
|
504
|
+
|
505
|
+
return edges
|
506
|
+
|
507
|
+
def get_edge_data(self, source: str, target: str, edge_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
508
|
+
"""Get edge data."""
|
509
|
+
if self.has_edge(source, target):
|
510
|
+
return {'source': source, 'target': target}
|
511
|
+
return None
|
512
|
+
|
513
|
+
# ============================================================================
|
514
|
+
# GRAPH ALGORITHMS
|
515
|
+
# ============================================================================
|
516
|
+
|
517
|
+
def shortest_path(self, source: str, target: str, edge_type: Optional[str] = None) -> List[str]:
|
518
|
+
"""Find path (unique in tree)."""
|
519
|
+
if not self.is_connected(source, target):
|
520
|
+
return []
|
521
|
+
|
522
|
+
# BFS for simplicity (O(n) but simple)
|
523
|
+
queue = deque([source])
|
524
|
+
visited = {source}
|
525
|
+
parent = {source: None}
|
526
|
+
|
527
|
+
while queue:
|
528
|
+
current = queue.popleft()
|
529
|
+
|
530
|
+
if current == target:
|
531
|
+
path = []
|
532
|
+
while current:
|
533
|
+
path.append(current)
|
534
|
+
current = parent[current]
|
535
|
+
return list(reversed(path))
|
536
|
+
|
537
|
+
for neighbor in self.get_neighbors(current):
|
538
|
+
if neighbor not in visited:
|
539
|
+
visited.add(neighbor)
|
540
|
+
parent[neighbor] = current
|
541
|
+
queue.append(neighbor)
|
542
|
+
|
543
|
+
return []
|
544
|
+
|
545
|
+
def find_cycles(self, start_node: str, edge_type: Optional[str] = None, max_depth: int = 10) -> List[List[str]]:
|
546
|
+
"""Find cycles (trees have no cycles)."""
|
547
|
+
return []
|
548
|
+
|
549
|
+
def traverse_graph(self, start_node: str, strategy: str = "bfs",
|
550
|
+
max_depth: int = 100, edge_type: Optional[str] = None) -> Iterator[str]:
|
551
|
+
"""Traverse tree."""
|
552
|
+
if start_node not in self._vertices:
|
553
|
+
return
|
554
|
+
|
555
|
+
visited = set()
|
556
|
+
queue = deque([start_node])
|
557
|
+
visited.add(start_node)
|
558
|
+
|
559
|
+
while queue:
|
560
|
+
current = queue.popleft()
|
561
|
+
yield current
|
562
|
+
|
563
|
+
for neighbor in self.get_neighbors(current):
|
564
|
+
if neighbor not in visited:
|
565
|
+
visited.add(neighbor)
|
566
|
+
queue.append(neighbor)
|
567
|
+
|
568
|
+
# ============================================================================
|
569
|
+
# STANDARD OPERATIONS
|
570
|
+
# ============================================================================
|
571
|
+
|
572
|
+
def __len__(self) -> int:
|
573
|
+
"""Get number of edges."""
|
574
|
+
return len(self._edges) // 2
|
575
|
+
|
576
|
+
def __iter__(self) -> Iterator[Dict[str, Any]]:
|
577
|
+
"""Iterate over edges."""
|
578
|
+
return iter(self.get_edges())
|
579
|
+
|
580
|
+
def to_native(self) -> Dict[str, Any]:
|
581
|
+
"""Convert to native representation."""
|
582
|
+
return {
|
583
|
+
'vertices': list(self._vertices),
|
584
|
+
'edges': self.get_edges()
|
585
|
+
}
|
586
|
+
|
587
|
+
# ============================================================================
|
588
|
+
# STATISTICS
|
589
|
+
# ============================================================================
|
590
|
+
|
591
|
+
def get_statistics(self) -> Dict[str, Any]:
|
592
|
+
"""Get link-cut tree statistics."""
|
593
|
+
# Count trees (connected components)
|
594
|
+
roots = set()
|
595
|
+
for vertex in self._nodes.values():
|
596
|
+
roots.add(self._find_root(vertex).vertex)
|
597
|
+
|
598
|
+
return {
|
599
|
+
'vertices': len(self._vertices),
|
600
|
+
'edges': len(self),
|
601
|
+
'trees': len(roots),
|
602
|
+
'avg_tree_size': len(self._vertices) / max(len(roots), 1)
|
603
|
+
}
|
604
|
+
|
605
|
+
# ============================================================================
|
606
|
+
# UTILITY METHODS
|
607
|
+
# ============================================================================
|
608
|
+
|
609
|
+
@property
|
610
|
+
def strategy_name(self) -> str:
|
611
|
+
"""Get strategy name."""
|
612
|
+
return "LINK_CUT"
|
613
|
+
|
614
|
+
@property
|
615
|
+
def supported_traits(self) -> List[EdgeTrait]:
|
616
|
+
"""Get supported traits."""
|
617
|
+
return [EdgeTrait.DIRECTED, EdgeTrait.SPARSE]
|
618
|
+
|
619
|
+
def get_backend_info(self) -> Dict[str, Any]:
|
620
|
+
"""Get backend information."""
|
621
|
+
return {
|
622
|
+
'strategy': 'Link-Cut Trees',
|
623
|
+
'description': 'Dynamic trees with path queries',
|
624
|
+
**self.get_statistics()
|
625
|
+
}
|
626
|
+
|