Recent from talks
Nothing was collected or created yet.
Link/cut tree
View on WikipediaThis article includes a list of references, related reading, or external links, but its sources remain unclear because it lacks inline citations. (October 2015) |
| Link/cut tree | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Type | Tree | |||||||||||||||
| Invented | 1982 | |||||||||||||||
| Invented by | ||||||||||||||||
| Time complexity in big O notation | ||||||||||||||||
| ||||||||||||||||
A link/cut tree is a data structure for representing a forest, a set of rooted trees, and offers the following operations:
- Add a tree consisting of a single node to the forest.
- Given a node in one of the trees, disconnect it (and its subtree) from the tree of which it is part.
- Attach a node to another node as its child.
- Given a node, find the root of the tree to which it belongs. By doing this operation on two distinct nodes, one can check whether they belong to the same tree.
The represented forest may consist of very deep trees, so if we represent the forest as a plain collection of parent pointer trees, it might take us a long time to find the root of a given node. However, if we represent each tree in the forest as a link/cut tree, we can find which tree an element belongs to in O(log(n)) amortized time. Moreover, we can quickly adjust the collection of link/cut trees to changes in the represented forest. In particular, we can adjust it to merge (link) and split (cut) in O(log(n)) amortized time.
Link/cut trees divide each tree in the represented forest into vertex-disjoint paths, where each path is represented by an auxiliary data structure (often splay trees, though the original paper predates splay trees and thus uses biased binary search trees). The nodes in the auxiliary data structure are ordered by their depth in the corresponding represented tree. In one variation, Naive Partitioning, the paths are determined by the most recently accessed paths and nodes, similar to Tango Trees. In Partitioning by Size paths are determined by the heaviest child (child with the most children) of the given node. This gives a more complicated structure, but reduces the cost of the operations from amortized O(log n) to worst case O(log n). It has uses in solving a variety of network flow problems and to jive data sets.
In the original publication, Sleator and Tarjan referred to link/cut trees as "dynamic trees", or "dynamic dyno trees".
Structure
[edit]We take a tree where each node has an arbitrary degree of unordered nodes and split it into paths. We call this the represented tree. These paths are represented internally by auxiliary trees (here we will use splay trees), where the nodes from left to right represent the path from root to the last node on the path. Nodes that are connected in the represented tree that are not on the same preferred path (and therefore not in the same auxiliary tree) are connected via a path-parent pointer. This pointer is stored in the root of the auxiliary tree representing the path.

Preferred paths
[edit]When an access to a node v is made on the represented tree, the path that is taken becomes the preferred path. The preferred child of a node is the last child that was on the access path, or null if the last access was to v or if no accesses were made to this particular branch of the tree. A preferred edge is the edge that connects the preferred child to v.
In an alternate version, preferred paths are determined by the heaviest child.

Operations
[edit]The operations we are interested in are FindRoot(Node v), Cut(Node v), Link(Node v, Node w), and Path(Node v).
Every operation is implemented using the Access(Node v) subroutine. When we access a vertex v, the preferred path of the represented tree is changed to a path from the root R of the represented tree to the node v. If a node on
the access path previously had a preferred child u, and the path now goes to child w, the old preferred edge
is deleted (changed to a path-parent pointer), and the new path now goes through w.
Access
[edit]After performing an access to node v, it will no longer have any preferred children, and will be at the end of the path. Since nodes in the auxiliary tree are keyed by depth, this means that any nodes to the right of v in the auxiliary tree must be disconnected. In a splay tree this is a relatively simple procedure; we splay at v, which brings v to the root of the auxiliary tree. We then disconnect the right subtree of v, which is every node that came below it on the previous preferred path. The root of the disconnected tree will have a path-parent pointer, which we point to v.
We now walk up the represented tree to the root R, breaking and resetting the preferred path where necessary. To do this we follow the path-parent pointer from v (since v is now the root, we have direct access to the path-parent pointer). If the path that v is on already contains the root R (since the nodes are keyed by depth, it would be the left most node in the auxiliary tree), the path-parent pointer will be null, and we are done the access. Otherwise we follow the pointer to some node on another path w. We want to break the old preferred path of w and reconnect it to the path v is on. To do this we splay at w, and disconnect its right subtree, setting its path-parent pointer to w. Since all nodes are keyed by depth, and every node in the path of v is deeper than every node in the path of w (since they are children of w in the represented tree), we simply connect the tree of v as the right child of w. We splay at v again, which, since v is a child of the root w, simply rotates v to root. We repeat this entire process until the path-parent pointer of v is null, at which point it is on the same preferred path as the root of the represented tree R.

FindRoot
[edit]FindRoot refers to finding the root of the represented tree that contains the node v. Since the access subroutine puts v on the preferred path, we first execute an access. Now the node v is on the same preferred path, and thus the same auxiliary tree as the root R. Since the auxiliary trees are keyed by depth, the root R will be the leftmost node of the auxiliary tree. So we simply choose the left child of v recursively until we can go no further, and this node is the root R. The root may be linearly deep (which is worst case for a splay tree), we therefore splay it so that the next access will be quick.
Cut
[edit]Here we would like to cut the represented tree at node v. First we access v. This puts all the elements lower than v in the represented tree as the right child of v in the auxiliary tree. All the elements now in the left subtree of v are the nodes higher than v in the represented tree. We therefore disconnect the left child of v (which still maintains an attachment to the original represented tree through its path-parent pointer). Now v is the root of a represented tree. Accessing v breaks the preferred path below v as well, but that subtree maintains its connection to v through its path-parent pointer.
Link
[edit]If v is a tree root and w is a vertex in another tree, link the trees containing v and w by adding the edge(v, w), making w the parent of v. To do this we access both v and w in their respective trees, and make w the left child of v. Since v is the root, and nodes are keyed by depth in the auxiliary tree, accessing v means that v will have no left child in the auxiliary tree (since as root it is the minimum depth). Adding w as a left child effectively makes it the parent of v in the represented tree.
Path
[edit]For this operation we wish to do some aggregate function over all the nodes (or edges) on the path from root R to node v (such as "sum" or "min" or "max" or "increase", etc.). To do this we access v, which gives us an auxiliary tree with all the nodes on the path from root R to node v. The data structure can be augmented with data we wish to retrieve, such as min or max values, or the sum of the costs in the subtree, which can then be returned from a given path in constant time.
Pseudocode of operations
[edit]Switch-Preferred-Child(x, y):
if (right(x) is not null)
path-parent(right(x)) = x
right(x) = y
if (y is not null)
parent(y) = x
Access(v):
splay(v)
Switch-Preferred-Child(v, null)
if (path-parent(v) is not null)
w = path-parent(v)
splay(w)
Switch-Preferred-Child(w, v)
Access(v)
Link(v, w):
Access(v)
Access(w)
left(v) = w
parent(w) = v
Cut(v):
Access(v)
if (left(v) is not null)
path-parent(left(v)) = path-parent(v)
left(v) = null
path-parent(v) = null
Analysis
[edit]Cut and link have O(1) cost, plus that of the access. FindRoot has an O(log n) amortized upper bound, plus the cost of the access. The data structure can be augmented with additional information (such as the min or max valued node in its subtrees, or the sum), depending on the implementation. Thus Path can return this information in constant time plus the access bound.
So it remains to bound the access to find our running time.
Access makes use of splaying, which we know has an O(log n) amortized upper bound. So the remaining analysis deals with the number of times we need to splay. This is equal to the number of preferred child changes (the number of edges changed in the preferred path) as we traverse up the tree.
We bound access by using a technique called Heavy-Light Decomposition.
Heavy-light decomposition
[edit]This technique calls an edge heavy or light depending on the number of nodes in the subtree. represents the number of nodes in the subtree of v in the represented tree. An edge is called heavy if size(v) > 1⁄2 size(parent(v)). Thus we can see that each node can have at most 1 heavy edge. An edge that is not a heavy edge is referred to as a light edge.
The light-depth refers to the number of light edges on a given path from root to vertex v. Light-depth ≤ lg n because each time we traverse a light-edge we decrease the number of nodes by at least a factor of 2 (since it can have at most half the nodes of the parent).
So a given edge in the represented tree can be any of four possibilities: heavy-preferred, heavy-unpreferred, light-preferred or light-unpreferred.
First we prove an upper bound.
O(log 2 n) upper bound
[edit]The splay operation of the access gives us log n, so we need to bound the number of accesses to log n to prove the O(log 2 n) upper bound.
Every change of preferred edge results in a new preferred edge being formed. So we count the number of preferred edges formed. Since there are at most log n edges that are light on any given path, there are at most log n light edges changing to preferred.
The number of heavy edges becoming preferred can be for any given operation, but it is amortized. Over a series of executions we can have n-1 heavy edges become preferred (as there are at most n-1 heavy edges total in the represented tree), but from then on the number of heavy edges that become preferred is equal to the number of heavy edges that became unpreferred on a previous step. For every heavy edge that becomes unpreferred a light edge must become preferred. We have seen already that the number of light edges that can become preferred is at most log n. So the number of heavy edges that become preferred for m operations is . Over enough operations () this averages to .
Improving to O(log n) upper bound
[edit]We have bound the number of preferred child changes at , so if we can show that each preferred child change has cost O(1) amortized we can bound the access operation at . This is done using the potential method.
Let s(v) be the number of nodes under v in the tree of auxiliary trees. Then the potential function . We know that the amortized cost of splaying is bounded by:
We know that after splaying, v is the child of its path-parent node w. So we know that:
We use this inequality and the amortized cost of access to achieve a telescoping sum that is bounded by:
where R is the root of the represented tree, and we know the number of preferred child changes is . s(R) = n, so we have amortized.
Application
[edit]Link/cut trees can be used to solve the dynamic connectivity problem for acyclic graphs. Given two nodes x and y, they are connected if and only if FindRoot(x) = FindRoot(y). Another data structure that can be used for the same purpose is Euler tour tree.
In solving the maximum flow problem, link/cut trees can be used to improve the running time of Dinic's algorithm from to .
See also
[edit]Further reading
[edit]- Sleator, D. D.; Tarjan, R. E. (1983). "A Data Structure for Dynamic Trees". Proceedings of the thirteenth annual ACM symposium on Theory of computing - STOC '81 (PDF). pp. 114–122. doi:10.1145/800076.802464.
- Sleator, D. D.; Tarjan, R. E. (1985). "Self-Adjusting Binary Search Trees" (PDF). Journal of the ACM. 32 (3): 652. doi:10.1145/3828.3835.
- Goldberg, A. V.; Tarjan, R. E. (1989). "Finding minimum-cost circulations by canceling negative cycles". Journal of the ACM. 36 (4): 873. doi:10.1145/76359.76368. – Application to min-cost circulation
- Link-Cut trees in: lecture notes in advanced data structures, Spring 2012, lecture 19. Prof. Erik Demaine, Scribes: Scribes: Justin Holmgren (2012), Jing Jian (2012), Maksim Stepanenko (2012), Mashhood Ishaque (2007).
- https://jeffe.cs.illinois.edu/teaching/datastructures/2006/notes/07-linkcut.pdf
Link/cut tree
View on GrokipediaIntroduction
Definition and purpose
A link/cut tree (LCT), also known as a dynamic tree or ST-tree, is a data structure designed to represent and maintain a forest of rooted trees under dynamic updates, where the trees are partitioned into vertex-disjoint preferred paths represented by auxiliary splay trees.[1][2] This representation enables efficient handling of connectivity changes in the forest while supporting queries on paths between nodes.[1] The primary purpose of an LCT is to support operations such as linking two trees by adding an edge, cutting an edge to separate a subtree, finding the root of a tree containing a given node, and aggregating values (e.g., sum or minimum) along the path from a node to its root, all in dynamic environments where the forest structure frequently changes.[1][2] These capabilities make LCTs particularly useful for graph-theoretic problems, such as maintaining connectivity in undirected graphs subject to edge additions and deletions.[1][2] Key advantages of LCTs include achieving amortized O(log n) time per operation, which matches the complexity of Euler tour trees for dynamic path queries but leverages self-adjusting splay trees for amortized guarantees, and provides greater flexibility than heavy-light decomposition for handling frequent structural modifications without full tree rebuilds.[1][2][3] By leveraging splay trees for path representation, LCTs balance efficiency and adaptability in applications like network flow algorithms and dynamic minimum spanning trees.[1][2]Historical background
The link/cut tree data structure was developed in 1981 by Daniel D. Sleator and Robert E. Tarjan, initially termed "dynamic trees" to support efficient operations on collections of rooted forests, such as linking and cutting edges while maintaining path information.[1] The foundational work was presented at the 13th Annual ACM Symposium on Theory of Computing in 1981 and published in full in 1983 in the Journal of Computer and System Sciences.[4] This development was driven by needs in network design, including faster algorithms for maximum flow computations, and in computational geometry, such as finding constrained minimum spanning trees.[4] By 1985, Sleator and Tarjan had refined the approach in their paper on splay trees, extending the self-adjusting mechanism to dynamic trees and adopting the name "link/cut trees" to highlight the primitive link and cut operations at its core.[5] This evolution built directly on splay trees, another self-adjusting structure invented by the same authors, enabling amortized O(log n) time for path manipulations.[5] In the 1990s, link/cut trees were extended to handle more general path aggregates, such as minimum queries or sums along paths, through related structures like Euler tour trees and top trees that preserved the logarithmic efficiency. Extensions for parallel, batch, and unrooted operations have appeared in the 2010s and 2020s, though practical implementations have gained prominence in areas like competitive programming during the 2010s.[6][7]Data Structure Components
Splay trees as auxiliary structures
In link/cut trees, preferred paths are represented using splay trees as auxiliary data structures, where each path forms a binary tree in which the nodes are ordered according to their depth in the original forest, with left and right children denoting the predecessor and successor along the path, respectively.[1] This representation treats the path as a sequence from the rootward end to the leafward end, enabling efficient manipulation of path segments through standard binary tree operations adapted to splay trees.[1] The self-adjusting nature of splay trees is central to their role in link/cut trees: following an access to a node, the splay operation performs a series of rotations to bring that node to the root of its auxiliary tree, thereby restructuring the tree based on recent access patterns and amortizing the cost over a sequence of operations.[5] This dynamic reorganization ensures that frequently accessed nodes remain near the root, reducing the depth for subsequent operations on the same path.[5] Each node in the link/cut tree incorporates fields typical of splay tree nodes—such as left child, right child, and parent pointers—alongside additional path-parent pointers that link the auxiliary splay trees into the broader forest structure by connecting the rootward end of a child path to its parent node in the parent path.[1] These path-parent pointers distinguish the auxiliary trees from standalone splay trees, allowing the representation of the entire dynamic forest as a collection of interconnected paths.[1] Splay trees are employed in this context because they support amortized O(log n) time for accesses, searches, splits, and joins without requiring explicit balancing mechanisms, making them ideal for the frequent rotations needed to manipulate paths during link and cut operations.[5] Their ability to handle these operations efficiently stems from the potential method analysis, which bounds the total cost by the logarithmic potential of the tree structure.[5]Preferred paths and path partitioning
In link-cut trees, each non-root node selects one preferred child, initially chosen arbitrarily, which defines a preferred edge to that child and contributes to forming chains known as preferred paths that extend from leaves toward the roots of the trees in the forest.[1] These preferred paths result from the preferred child relation, where the preferred child of a node is the one leading toward the most recently accessed descendant in its subtree, ensuring that recent accesses influence the path structure dynamically.[8] The forest represented by link-cut trees is partitioned into a collection of these disjoint preferred paths, with non-preferred edges (dashed edges) connecting the paths through path-parent pointers that link the root of one path to a node in another path higher in the tree.[1] This decomposition allows the entire structure to be viewed as a set of vertex-disjoint paths, where each path captures a sequence of nodes connected by preferred edges, and the path-parent pointers maintain the overall tree connectivity without forming cycles.[8] The selection of preferred children evolves during operations such as access, where the preferred child pointers are updated along the accessed path to reflect the new most recent access, effectively rotating the partitioning to prioritize the accessed node.[1] In the naive partitioning approach, these changes occur dynamically based on access history without size constraints, leading to potentially long paths but amortized efficiency.[1] An alternative size-based partitioning, akin to heavy-light decomposition, designates a child as preferred if its subtree size is at least half the parent's subtree size, ensuring that any root-to-leaf path traverses at most preferred paths and providing worst-case logarithmic bounds.[1] Preferred paths are represented as chains of preferred edges, with the root of each path (the highest node in the chain) maintaining a path-parent pointer to the appropriate node in the parent path unless it is the root of the entire tree.[8] This representation facilitates efficient manipulation, as each path is typically stored using an auxiliary structure like a splay tree to handle local operations within the path.[1]Core Operations
Access operation
The Access operation in a link-cut tree restructures the auxiliary tree representation to expose the path from a specified vertex to the root of its underlying tree, making the root of its auxiliary splay tree through a sequence of rotations and updates to preferred child edges. This operation serves as the core mechanism for path manipulation, enabling efficient access to the path for queries or modifications.[1] The procedure initiates by splaying to the root of its current auxiliary tree, which brings to the forefront of that path's representation. If has a preferred child, this child is detached, and the subtree rooted at that child is reassigned a path-parent pointer to , effectively splitting the path at . While is not the overall tree root, the algorithm identifies the path-parent of 's auxiliary tree, splays to the root of its own auxiliary tree, detaches 's right subtree (if any) by setting its path-parent to , and then attaches 's auxiliary tree as 's new right subtree. This rotation extends the preferred path upward. To preserve the depth-based ordering in splay trees, path directions are reversed as needed during these attachments, ensuring left children represent shallower nodes (closer to the tree root) and right children deeper ones. The process repeats until the path-parent is null, indicating the tree root has been reached.[2] Upon completion, the entire path from the tree root to is consolidated into a single preferred path, represented as one auxiliary splay tree with at its root; with the tree root now the leftmost node in this auxiliary splay tree and the path-parent of set to null, any intersecting preferred paths are dismantled or reformed to maintain the vertex-disjoint partitioning. This reconfiguration destroys old preferred edges along the accessed path and establishes new ones, isolating subtrees via path-parent pointers. Preferred paths thus provide the structural backbone adjusted by this operation.[1] The Access operation runs in amortized time, where is the number of vertices, leveraging the amortized efficiency of splay trees for rotations and the potential method to bound the total cost across a sequence of operations.[2]Link and Cut operations
The link operation in a link/cut tree merges two distinct trees in the forest by adding an edge that connects a vertex from one tree as a child of a vertex from the other tree, with the precondition that is the root of its tree. To perform link(), the procedure first checks that and reside in different trees using the FindRoot operation on both vertices; if they share the same root, the operation is aborted to prevent cycles.[1] Assuming distinct trees and is a tree root, the Access operation is invoked on to expose the path from its tree root to . Then, attach 's auxiliary tree as the preferred (deeper) child of by setting 's path-parent to and updating the child pointer of accordingly (e.g., right() = in the splay tree), and the roots are updated to reflect the new connectivity, thereby combining the trees into a single structure while preserving acyclicity.[2][9] The cut operation disconnects a subtree from the rest of its tree by removing the edge between a vertex and its parent, resulting in two separate trees in the forest, with the precondition that is not the root of its tree. For cut(), the procedure starts with Access(), which restructures the preferred paths to isolate the path from the tree root to . Set the left child of to null in its auxiliary splay tree, detaching the path from the original tree root to 's parent and thereby isolating 's subtree as a new tree with as root; preferred edges are adjusted to complete the separation without altering other connections. This modifies the forest's tree connectivity by increasing the number of trees by one, maintaining the overall acyclicity without necessitating a complete rebuild.[1]Supporting Operations
FindRoot operation
The FindRoot operation in a link-cut tree, denoted as FindRoot(v), returns the root node of the tree containing a given node v within the underlying forest representation. This operation first invokes Access(v) to restructure the preferred paths such that the path from the true root to v becomes a single exposed preferred path, represented as a splay tree rooted at v. In this configuration, the true root appears as the leftmost node in the splay tree, since the path is ordered with shallower nodes to the left and deeper nodes to the right.[9] To identify this root, after Access(v), the algorithm traverses leftward from v by repeatedly setting v to its left child until reaching a node with no left child. This leftmost node is then splayed to the root of its auxiliary tree to optimize future accesses. The process concludes by returning this splayed node as the tree root. Although Access(v) internally relies on path-parent pointers to aggregate and expose the full path by linking intermediate path trees, the FindRoot traversal itself operates solely within the now-unified splay tree, without further path-parent navigation.[9] This operation serves to determine the root for connectivity checks prior to performing a Link, ensuring nodes belong to different trees, and facilitates root-relative queries, such as computing distances or aggregates from the root to v. The amortized time complexity is O(log n), where n is the number of nodes, primarily due to the cost of Access(v); the subsequent left traversal and splay contribute at most O(1) amortized time under the potential method analysis.[9]Path aggregation and evaluation
In link-cut trees, path aggregation enables the efficient computation of functions such as sums, minima, or custom operations over the values associated with nodes or edges along a path from a given vertex to the root of its tree. This is achieved by augmenting the auxiliary splay trees that represent preferred paths, where each node stores not only structural information but also the necessary aggregate values for its subtree. After performing the Access operation on a vertex , the preferred path from the root to is restructured such that becomes the root of its auxiliary splay tree, and the path itself forms the left spine of this tree. Aggregates can then be evaluated by traversing this left spine or, more efficiently, by directly accessing the precomputed aggregate value stored at the root , which represents the combination of values along the entire exposed path.[1][10] The supported aggregates include path sums, minimums, maximums, and other associative operations that can be maintained through subtree augmentations in the splay trees. Each node in the auxiliary tree holds the aggregate of its subtree, updated during splay operations via rotations that preserve the in-order traversal order corresponding to the path depth. For evaluation, the Access operation splays to the root and aggregates the left-path subtree, ensuring that the direction from to the root is correctly represented; if the reverse direction (root to ) is needed, the aggregate can be computed by adjusting the stored values or performing a reversal operation on the path. This mechanism allows queries in amortized time, leveraging the self-adjusting properties of splay trees to keep frequently accessed paths near the root. Custom functions can be supported by defining appropriate combination rules for the augmentations, as long as they are invertible or associative to handle path reversals.[1][11] Extensions to path aggregation accommodate edge weights or node labels by associating values directly with the edges (stored at child nodes) or nodes themselves, enabling aggregates over weighted paths in dynamic forests. For undirected graphs, the structure supports reversibility by allowing the Evert operation to flip path directions, ensuring aggregates can be queried bidirectionally without disrupting the overall tree representation. These features make link-cut trees particularly useful for applications requiring dynamic path queries, such as network flow optimizations or geometric searching, while maintaining the core amortized efficiency bounds.[1][10]Implementation Aspects
Pseudocode for Access, Link, and Cut
The pseudocode for the core operations of a link/cut tree—Access, Link, and Cut—is presented below, following common modern implementations. These incorporate a reversal flag (rev) to efficiently handle path reversals during operations, enabling support for unrooted trees or bidirectional traversals without explicit evert operations.[9] The pseudocode assumes a forest of rooted trees represented via preferred path decomposition, where each preferred path is stored in a splay tree auxiliary structure. The following describes a common modern implementation using splay trees with lazy reversal flags for path reversals, extending the original design.[1] The implementation includes supporting functions such as pushdown for propagating reversal flags, splay for restructuring the auxiliary tree, and make_root for changing the perceived root by reversing the path. The core operations are as follows (presented in C++-style pseudocode for clarity): Node structure:struct Node {
Node *ch[2], *fa;
bool rev;
Node() : fa(nullptr), rev(false) { ch[0] = ch[1] = nullptr; }
};
struct Node {
Node *ch[2], *fa;
bool rev;
Node() : fa(nullptr), rev(false) { ch[0] = ch[1] = nullptr; }
};
void pushdown(Node *x) {
if (x->rev) {
std::swap(x->ch[0], x->ch[1]);
if (x->ch[0]) x->ch[0]->rev ^= true;
if (x->ch[1]) x->ch[1]->rev ^= true;
x->rev = false;
}
}
bool get_dir(Node *x) { return x == x->fa->ch[1]; }
void rotate(Node *x) {
Node *y = x->fa, *z = y->fa;
int d = get_dir(x);
if (z) z->ch[get_dir(y)] = x;
x->fa = z;
y->ch[d] = x->ch[d ^ 1];
if (x->ch[d ^ 1]) x->ch[d ^ 1]->fa = y;
x->ch[d ^ 1] = y;
y->fa = x;
// pushup(y); pushup(x); if size/sum maintained
}
void splay(Node *x) {
// Push down from ancestors to x (stack-based for efficiency)
// ... (omitted for brevity; standard stack pushdown)
while (x->fa) {
Node *y = x->fa, *z = y->fa;
pushdown(z); pushdown(y); pushdown(x);
if (z) {
if (get_dir(x) == get_dir(y)) rotate(y);
else rotate(x);
}
rotate(x);
}
}
void pushdown(Node *x) {
if (x->rev) {
std::swap(x->ch[0], x->ch[1]);
if (x->ch[0]) x->ch[0]->rev ^= true;
if (x->ch[1]) x->ch[1]->rev ^= true;
x->rev = false;
}
}
bool get_dir(Node *x) { return x == x->fa->ch[1]; }
void rotate(Node *x) {
Node *y = x->fa, *z = y->fa;
int d = get_dir(x);
if (z) z->ch[get_dir(y)] = x;
x->fa = z;
y->ch[d] = x->ch[d ^ 1];
if (x->ch[d ^ 1]) x->ch[d ^ 1]->fa = y;
x->ch[d ^ 1] = y;
y->fa = x;
// pushup(y); pushup(x); if size/sum maintained
}
void splay(Node *x) {
// Push down from ancestors to x (stack-based for efficiency)
// ... (omitted for brevity; standard stack pushdown)
while (x->fa) {
Node *y = x->fa, *z = y->fa;
pushdown(z); pushdown(y); pushdown(x);
if (z) {
if (get_dir(x) == get_dir(y)) rotate(y);
else rotate(x);
}
rotate(x);
}
}
void access(Node *x) {
for (Node *y = nullptr; x; y = x, x = x->fa) {
splay(x);
x->ch[1] = y;
// pushup(x); if aggregates
}
}
void access(Node *x) {
for (Node *y = nullptr; x; y = x, x = x->fa) {
splay(x);
x->ch[1] = y;
// pushup(x); if aggregates
}
}
void make_root(Node *x) {
access(x);
splay(x);
x->rev ^= true;
}
void make_root(Node *x) {
access(x);
splay(x);
x->rev ^= true;
}
Node *find_root(Node *x) {
access(x);
splay(x);
pushdown(x);
while (x->ch[0]) {
x = x->ch[0];
pushdown(x);
}
splay(x);
return x;
}
Node *find_root(Node *x) {
access(x);
splay(x);
pushdown(x);
while (x->ch[0]) {
x = x->ch[0];
pushdown(x);
}
splay(x);
return x;
}
void link(Node *child, Node *parent) {
make_root(child);
if (find_root(parent) == child) return; // already connected
child->fa = parent;
}
void link(Node *child, Node *parent) {
make_root(child);
if (find_root(parent) == child) return; // already connected
child->fa = parent;
}
void cut(Node *child, Node *parent) {
make_root(child);
access(parent);
splay(parent);
if (parent->ch[0] != child || child->ch[0] || child->ch[1]) return; // not direct or invalid
parent->ch[0] = nullptr;
child->fa = nullptr;
// pushup(parent);
}
void cut(Node *child, Node *parent) {
make_root(child);
access(parent);
splay(parent);
if (parent->ch[0] != child || child->ch[0] || child->ch[1]) return; // not direct or invalid
parent->ch[0] = nullptr;
child->fa = nullptr;
// pushup(parent);
}
Node Fields and Conventions
Each node in the link/cut tree has the following fields:left(v)andright(v): Pointers to left and right children in the splay tree.parent(v): Pointer to the parent in the splay tree.path_parent(v): Pointer to the parent of the root of 's auxiliary tree in the overall forest (null if is on the root path).rev(v): Boolean flag indicating whether the subtree rooted at is reversed (initially false).
rev flag before rotations or traversals:
pushdown(v):
if rev(v):
swap(left(v), right(v))
if left(v) ≠ null: rev(left(v)) ← not rev(left(v))
if right(v) ≠ null: rev(right(v)) ← not rev(right(v))
rev(v) ← false
pushdown(v):
if rev(v):
swap(left(v), right(v))
if left(v) ≠ null: rev(left(v)) ← not rev(left(v))
if right(v) ≠ null: rev(right(v)) ← not rev(right(v))
rev(v) ← false
splay(v):
pushdown(v)
while parent(v) ≠ null:
pushdown(parent(parent(v)))
pushdown(parent(v))
pushdown(v)
if parent(parent(v)) = null:
// zig: single rotation
rotate(v)
else if parent(v) is left child of grandparent and v is left child of parent:
// zig-zig
rotate(parent(v))
rotate(v)
else if parent(v) is right child of grandparent and v is right child of parent:
// zig-zig
rotate(parent(v))
rotate(v)
else:
// zig-zag
rotate(v)
rotate(v)
// After splay, v is root of its auxiliary tree
splay(v):
pushdown(v)
while parent(v) ≠ null:
pushdown(parent(parent(v)))
pushdown(parent(v))
pushdown(v)
if parent(parent(v)) = null:
// zig: single rotation
rotate(v)
else if parent(v) is left child of grandparent and v is left child of parent:
// zig-zig
rotate(parent(v))
rotate(v)
else if parent(v) is right child of grandparent and v is right child of parent:
// zig-zig
rotate(parent(v))
rotate(v)
else:
// zig-zag
rotate(v)
rotate(v)
// After splay, v is root of its auxiliary tree
rotate function performs a single rotation, updating left, right, and parent pointers accordingly (details omitted for brevity, as they follow standard splay tree rotations). Post-rotation, if the auxiliary tree root changes during splay, the path_parent is transferred from the old root to the new root .[1]
Access Operation
The Access operation on a node restructures the preferred paths so that the path from the tree root to becomes a single preferred path represented by one auxiliary splay tree, with splayed to its root. It uses a loop to iteratively splay and attach subpaths, handling path-parent pointers and detaching right subtrees (deeper portions). Therev flag is pushed down during splays to maintain correct order.
access(v):
splay(v) // v becomes root of its auxiliary tree (includes pushdown)
// Detach right subtree (deeper portion below v) and set its path_parent to v
if right(v) ≠ null:
path_parent(right(v)) ← v
parent(right(v)) ← null
right(v) ← null
current ← v
while path_parent(current) ≠ null:
w ← path_parent(current)
splay(w) // w becomes root of its auxiliary tree
// Detach w's right subtree (deeper below w)
if right(w) ≠ null:
path_parent(right(w)) ← w
parent(right(w)) ← null
right(w) ← null
// Attach current's auxiliary tree as right child of w
right(w) ← current
parent(current) ← w
path_parent(current) ← null
current ← w
splay(v) // Splay v to root of the full path auxiliary tree
access(v):
splay(v) // v becomes root of its auxiliary tree (includes pushdown)
// Detach right subtree (deeper portion below v) and set its path_parent to v
if right(v) ≠ null:
path_parent(right(v)) ← v
parent(right(v)) ← null
right(v) ← null
current ← v
while path_parent(current) ≠ null:
w ← path_parent(current)
splay(w) // w becomes root of its auxiliary tree
// Detach w's right subtree (deeper below w)
if right(w) ≠ null:
path_parent(right(w)) ← w
parent(right(w)) ← null
right(w) ← null
// Attach current's auxiliary tree as right child of w
right(w) ← current
parent(current) ← w
path_parent(current) ← null
current ← w
splay(v) // Splay v to root of the full path auxiliary tree
Link Operation
The Link operation connects two nodes and from different trees by adding an edge with as parent of , assuming is the root of its tree (verified via FindRoot). It first accesses both nodes to expose their paths, then attaches 's tree as the right (deeper) child of .link(u, v):
// Assume FindRoot(u) ≠ FindRoot(v); otherwise, operation fails
access(u)
access(v)
// After access(v), right(v) = null; attach u as deeper child
right(v) ← u
parent(u) ← v
path_parent(u) ← null
// Update sizes or aggregates if maintained (e.g., size(v) += size(u))
link(u, v):
// Assume FindRoot(u) ≠ FindRoot(v); otherwise, operation fails
access(u)
access(v)
// After access(v), right(v) = null; attach u as deeper child
right(v) ← u
parent(u) ← v
path_parent(u) ← null
// Update sizes or aggregates if maintained (e.g., size(v) += size(u))
Cut Operation
The Cut operation severs the edge between and its parent in the represented tree, detaching the subtree rooted at as a new tree. It accesses to expose the path, then nullifies the connection to the left child (the shallower portion above , which becomes part of the original tree).cut(v):
access(v) // Exposes path to v; v is now splay root
if left(v) ≠ null:
// Detach left subtree (path above v)
path_parent(left(v)) ← null
parent(left(v)) ← null
left(v) ← null
// The left subtree now heads the remaining original tree
// v remains root of its new tree (v plus descendant subtrees via path-parents)
cut(v):
access(v) // Exposes path to v; v is now splay root
if left(v) ≠ null:
// Detach left subtree (path above v)
path_parent(left(v)) ← null
parent(left(v)) ← null
left(v) ← null
// The left subtree now heads the remaining original tree
// v remains root of its new tree (v plus descendant subtrees via path-parents)
