Detailed Link-Cut-Tree (LCT)

Pre-knowledge

basic definition

LCT is a way to solve the dynamic tree problem, by tarjan(Why him again)Proposed in 1982, the most original paper is here . In the paper, in addition to introducing the LCT with amortized complexity \(O(log^2n)\) , tarjan also introduced a strict \(O(log ^2n)\) algorithm, but I am too weak to understand what tarjan said, and did not find other learning materials, so we will not discuss this algorithm

problems that can be solved

  1. Seeking LCA
  2. Find the minimum spanning tree
  3. Maintain on-chain information (max min, on-chain sum, etc.)
  4. Maintain connectivity
  5. Maintain subtree information (need to cooperate with AAA tree)
  6. Optimize the simpliciality algorithm to find the maximum flow under the complexity of \(O(mnlog(n))\) (these two are mentioned by tarjan at the beginning of his paper, but I see in a book except I haven't seen any information about it, except for the relevant introduction (no code),)

structure

When solving problems on trees, there is a very common operation called chain division

Chain segmentation is generally divided into three types

  • Heavy chain division, also known as "tree division", the principle of division is to regard the son with the largest number of nodes as the heavy son

  • Long-chain segmentation is not very common. You can find \(k\) -level ancestors by \(O(1)\) , the principle is to regard the son with the deepest depth as the heavy (long) son

  • Real chain division, that is, the division method used by LCT, here is the focus

When the real chain is divided, one child is selected as the heavy child, the edge connecting these two nodes is regarded as the heavy edge, and the edge connecting other children is regarded as the light edge.

Different from the first two divisions, the heavy son in the real chain division can change constantly, so the heavy chain and light chain in the whole tree are also changing constantly, which requires us to use more flexible data structure to maintain.

To maintain a chain, the first thing we can think of is a linked list, but if a linked list is used to maintain the sum of the weights on the tree, the time complexity must be \(O(n^2)\) , even a block linked list can only do it \(O(n \sqrt{n})\)

Therefore, we use the balanced tree to maintain the information on the chain, so what is the balanced tree used? In theory, both splay and fhq treap are possible, but treap is not, because the flip information cannot be maintained. Here I recommend splay, because most of the online questions about LCT are written in splay, so the learning difficulty will also be reduced. It is smaller, and the constant of my self-test splay is much smaller than that of fhq treap

We use splay to maintain each real path (a path composed only of real edges), because each real path corresponds to a chain starting from the root node, so the depth of each node on the path is different, so in splay , we use the depth as the key, for a node, the depth of the original node corresponding to its left child is smaller than it, and the depth of the original node corresponding to the right child is larger than it

(Insert a gossip here, the reason why LCT is difficult to understand is largely because beginners can’t distinguish the original tree/splay, so I hope that in the process of continuing to read, you must clearly distinguish the concepts I am talking about. Refers to the original tree or the splay tree)

But in this case, although each node is included in the splay, each splay is independent, so we need to consider how to establish a connection in each splay.
For a node, suppose it has three sons, of which the most There is a node that can be in a splay with him, and the other two sons belong to different splays. There is a very clever property here - the other two sons must have the smallest depth in the subtree they are in, so we can make the root node in each splay point to the smallest in-order traversal number in the splay (that is, the original tree). The father of the node with the smallest depth in the middle is a bit confusing, I hope you can understand it well

For one such tree
(Image credit: FlashHu's blog)

Its splay tree may look like this

Basic operation of LCT

\(access(x)\)

The path from the root node to the \(x\) point becomes a real path, and the edges between \(x\) and its children become imaginary edges.

First consider what the purpose of this operation is. With this operation, we can put the path from the root node to \(x\) in the same splay, so that the path information can be easily obtained by marking the splay.

How to achieve it?

Still the picture above

If we were to execute \(access(N)\) ,

We can process from bottom to top. First, we need to make the edge \(NO\) a light edge, because in splay, the depth of the node is unique, so we first put \(N\) , go to The root node, so that the node on its right must be \(O\) , and then directly set the right child of the \(N\) node to \(0\)

Moving on, we need to make the edge \(IK\) a light edge, and make the edge \(IL\) a heavy edge

Consider how to make \(IK\) a light edge. The same idea as above, we first make \(I\) go to the root node of the splay, so that its right son must be \(K\) , and then we only need to disconnect \(IK\) this edge Just

Consider how to make \(IL\) a heavy edge. This is so easy, just set the right son of \(I\) to \(L\)

At this time , the father of the splay where \(I\) is located points to \(H\)

The idea of ​​continuing to go up is the same. According to the same routine, make \(HJ\) become a light edge, make \(HI\) become a heavy edge,

At this point \(H\) points to \(A\) , then let \(A\) go to the root node of the splay where it is located, and set the right child to \ (H\)

In this way, we have realized the \(access\) operation. According to the above process, it is not difficult for us to summarize the algorithm framework.

  1. Go to the root of the node to be processed
  2. change right son
  3. update tag

In terms of code, first of all, let's talk about various definitions

struct node {
    int f, ch[2], s;
    //f:父亲
    //ch[0]/[1]:左/右儿子
    //s:标记,依题目而变
    bool r;//是否翻转
}T[MAXN];

access

void access(int x) {//访问x节点 
    for(int y = 0; x; x = fa(y = x))
        splay(x), rs(x) = y, update(x);
    //首先把x splay到所在平衡树的根,这样可以保证它的右孩子就是在原树中对应的重链(右孩子深度比它大)
    //y是splay中x的儿子,把x的右儿子改成y,也就是把x和y之间的边变成实边
    //更改了节点顺序,需要update 
}

\ (makeroot (x) \)

Change \(x\) to the root of the original tree

Consider the significance of this operation. For most tree queries, it is to query a path through the root node, which is very troublesome to deal with due to the construction of LCT. But when one end of the query is at the root node, it is much easier to handle, so the meaning of this operation is to help us handle the query better

Here is a very clever implementation idea:

First we need \(access(x)\) , so we put the root node and \(x\) in the same splay

At this time \(x\) does not contain the right son (there is no point with a depth greater than him)

Then we need \(splay(x)\) , at this time \(x\) will become the root node of splay, and \(x\) does not contain the right child.

But in the splay where the root node is located, the root node has no left child (no node with a depth smaller than him)

what should we do? ?

There is a saucy operation here, we directly flip the left and right subtrees of \(x\) !

Would n't \(x\) have no left son? 233

It turns out it's right

Code:

void makeroot(int x) {//把x改为原树的根节点 
    access(x);
    splay(x);
    T[x].r ^= 1;//更新翻转标记
}

\(findroot(x)\)

Find the root of the original tree where \(x\) is located

At the beginning, you may be as stupid as me and think: Isn't the root of the original tree where \(x\) is located the root node?

but. .

One thing to note: LCT maintains forests, that is, many trees, so the root nodes of the original tree where each node is located are often different

Consider the implications of this operation:

Have you noticed that this operation is very similar to the operation of finding ancestors in a union search? Yes, the purpose of this operation is to judge the connectivity of two nodes (whether they are on the same tree)

If implemented, we take advantage of a property: the root node of a tree must be the point with the smallest depth

In this way, we first \(accexx(x)\) , then \(splay(x)\) , and then keep going left, so that we can find the root node of the tree where \(x\) is located!

Notice! When you go to the left, be sure to download the mark, otherwise you will be miserable in some questions!

code

int findroot(int x) {//找到x在原树中的根节点 
    access(x);splay(x);
    pushdown(x);
    while(ls(x)) pushdown(x = ls(x));//找到深度最小的点即为根节点 
    return x;
}

\(split(x,y)\)

Get the path corresponding to \(xy\) ,

The meaning of this operation is very obvious

The implementation is also very good, let's go directly to the code

In this case , information about the path can be obtained by visiting the \(y\) node

void split(int x, int y) {
    makeroot(x);//首先把x置为根节点 
    access(y); splay(y);
    //然后访问一下y,再把y转到根节点,这样y维护的就是x - y 路径上的信息 
}

\(link(x,y)\)

Connect \(x,y\)

The meaning of this operation is also obvious

First we need \(makeroot(x)\) , then \(access(y),splay(y)\) , and finally point the father of \(x\) to \(y\)

Note that in some cancer problems, it is not guaranteed that \(x,y\) is not connected, so it is necessary to judge the connectivity

void link(int x, int y) {
    makeroot(x);//把x置为根节点 
    if(findroot(y) != x ) fa(x) = y;
    //如果x与y不在同一个splay中,就把y置为x的父亲 
    //findroot(y)的时候已经执行了access(y),splay(y)
}

\(cut(x,y)\)

Disconnect \(xy\) this edge

The meaning is also very obvious

It seems easier to implement

We need \(split(x,y)\) , at this time the father of \ (x\) is \(y\) , and \(y\) is the deepest point, so its left son is \(x \) ,

Then sever the father-son relationship

but! ! , what if the question maker does not guarantee that \(xy\) is connected? ?

If you still change it like this, the whole tree will be messed up.

Therefore, we have to add a judgment

  1. First \(x,y\) must be in a tree
  2. The father of \ (x\) is \(y\) , and the left son of \(y\) is \ (x\).
    These two comparisons are obvious

  3. \(x\) has no right son

This may not be so obvious

Let's think about it carefully, the right child of \(x\) represents a node with a greater depth than him, and the left child of \(y\) represents a node with a smaller depth than him

What if this happens?

Then we GG,

Or you can try this set of data

5 6 1 2 3 4 5
1 1 2
1 2 3
1 3 4
1 4 5
2 1 5
0 1 5

Therefore, we need to add this judgment

Code:

void cut(int x, int y) {
    makeroot(x);
    if(findroot(y) == x && fa(x) == y && ls(y) == x && !rs(x)) { 
        fa(x) = T[y].ch[0] = 0;
        update(y);
    }
}

time complexity

The amortized complexity of the above operations are all \(O(log^2N)\)

The specific proof can refer to YangZhe's paper

summary

There are so many operations of LCT.

However, LCT has a lot of expansion, and there are many types of questions, most of which I haven't done yet.

I'll sort it out later

Luogu's board question code:

The splay and rotate are my own, not very mainstream, but they run faster

// luogu-judger-enable-o2
#include<cstdio>
#include<cstring>
#include<algorithm>
#define swap(x,y) x ^= y, y ^= x, x ^= y
const int MAXN=3 * 1e5 + 10;
inline int read()
{
    char c = getchar();int x = 0,f = 1;
    while(c < '0' || c > '9'){if(c == '-')f = -1;c = getchar();}
    while(c >= '0' && c <= '9'){x = x * 10 + c - '0',c = getchar();}
    return x * f;
}
#define fa(x) T[x].f
#define ls(x) T[x].ch[0]
#define rs(x) T[x].ch[1]
int v[MAXN];
struct node {
    int f, ch[2], s;
    bool r;
}T[MAXN];
int ident(int x) {
    return T[fa(x)].ch[0] == x ? 0 : 1;//判断该节点是父亲的哪个儿子 
}
int connect(int x,int fa,int how) {
    T[x].f=fa;
    T[fa].ch[how]=x;//连接 
}
inline bool IsRoot(int x) {//若为splay中的根则返回1 否则返回0 
    return ls( fa(x) ) != x && rs( fa(x) ) != x;
    //用到了两个性质
    //1.若x与fa(x)之间的边是虚边,那么它的父亲的孩子中不会有他(不在同一个splay内)
    //2. splay的根节点与其父亲之间的边是虚边 
}
void update(int x) {
    T[x].s = T[ls(x)].s ^ T[rs(x)].s ^ v[x];//维护路径上的异或和 
}
void pushdown(int x) {
    if(T[x].r) {
        swap(ls(x),rs(x));
        T[ls(x)].r ^= 1;
        T[rs(x)].r ^= 1;
        T[x].r = 0;//标记下传 
    }
}
void rotate(int x) {
    int Y = T[x].f, R = T[Y].f, Yson = ident(x), Rson = ident(Y);
    int B = T[x].ch[Yson ^ 1];
    T[x].f = R;
    if(!IsRoot(Y))
        connect(x, R, Rson);
    //这里如果不判断y是否根节点,那么当y是根节点的时候,0节点的儿子就会被更新为x
    //这样x就永远不能被判断为根节点,也就会无限循环下去了
    //但是这里不更新x的父亲的话就会出现无限递归的情况 
    connect(B, Y, Yson);
    connect(Y, x, Yson ^ 1);
    update(Y); update(x);
}
int st[MAXN];
void splay(int x) {
    int y = x, top = 0;
    st[++top] = y;
    while(!IsRoot(y)) st[++top] = y = fa(y);
    //用一个栈维护所有的标记
    while(top) pushdown(st[top--]);
    //因为在旋转的时候不会处理标记,所以splay之前应该下传所有标记 
    for(int y = fa(x); !IsRoot(x); rotate(x), y = fa(x))//只要不是根就转 
        if(!IsRoot(y)) 
            rotate( ident(x) == ident(y) ? x : y );
}
void access(int x) {//访问x节点 
    for(int y = 0; x; x = fa(y = x))
        splay(x), rs(x) = y, update(x);
}
void makeroot(int x) {//把x改为原树的根节点 
    access(x);
    splay(x);
    T[x].r ^= 1;
    pushdown(x);
}
int findroot(int x) {//找到x在原树中的根节点 
    access(x);splay(x);
    pushdown(x);
    while(ls(x)) pushdown(x = ls(x));//找到深度最小的点即为根节点 
    return x;
}
void split(int x, int y) {
    makeroot(x);//首先把x置为根节点 
    access(y); splay(y);
}
void link(int x, int y) {
    makeroot(x);//把x置为根节点 
    if(findroot(y) != x ) fa(x) = y;
}
void cut(int x, int y) {
    makeroot(x);
    if(findroot(y) == x && fa(x) == y && ls(y) == x && !rs(x)) { 
        fa(x) = T[y].ch[0] = 0;
        update(y);
    }
}
int main()
{
    #ifdef WIN32
    freopen("a.in","r",stdin);
    //freopen("a.out","w",stdout);
    #else
    #endif
    int N = read(), M = read();
    for(int i = 1; i <= N; i++) v[i] = read();
    for(int i = 1; i <= M; i++) {
        int opt = read(), x = read(), y = read();
        if(opt == 0) split(x, y), printf("%d\n",T[y].s);
        else if(opt == 1) link(x, y);
        else if(opt == 2) cut(x, y);
        else if(opt == 3) splay(x), v[x] = y;
    }
    return 0;
}

References

FlashHu's Blog

Some Researches on YangZhe-QTREE Solutions

Advanced Data Structures - Lin Houcong

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325104372&siteId=291194637