欢迎光临
我们一直在努力

PLCT检查是什么LCT——Link Cut Tree

运用实链剖分,对于一个父亲来说,只有一个儿子对应实边,其它的对应虚边,一堆的实边连在一起就变成了实链,我们用 维护。

其中实边指的是一条连通父亲儿子的双向边,而虚边则指的是儿子到父亲的单向边。在 中,实边组成的实链被 储存,而虚边则是 的根节点连到父亲的边,所以 其实可以看做一堆 通过虚边相连。

性质:

  1. 任何结点包含且仅包含于一个 中
  2. 中的结点的深度在原树中按中序遍历递增

注意 维护的是森林。

)

接下来讲 的基本操作。

最重要的操作,将 到树根结点之间拉一条实链,以下记 为上一次操作的 的根, 初始为 。

首先我们把 转到它所处的 的根,然后把它的右儿子( 深度大,由操作二替换到右儿子)改为 ,然后 一下 ,最后令 。

这样子起到了什么效果呢?显然原来的右实儿子变成了虚儿子,而右儿子的地方被替换为了上一个 的根,并且连的是实边。我们的目标是将最初的 与根结点之间打通实链,那么 的过程相当于在不断地将两个 连接起来,形成实链。

放几个图:

先是

然后

然后

最后

如此 的路径便在一个 里了。

inline void access(int x) {
    for (int y = 0; x; y = x, x = tr[x].p) 
        splay(x), tr[x].s[1] = y, pushup(x); 
}

将 的根换为 。

首先 ,然后 ,最后翻转一整棵 。

由于 一直是往右儿子挂,所以最后 会在整棵 的最右下(即中序遍历最后的点)。所以 后 没有右儿子,这时再将整棵 翻转,这样 的左子树就去了右边, 就成了深度最小的点。

inline void reverse(int x) {
    swap(tr[x].s[0], tr[x].s[1]);
    tr[x].flag ^= 1;
}
inline void makeroot(int x) {
    access(x); splay(x); pushdown(x);
}

代码中的 就是文艺平衡树中的懒标记下传。

找 所在树的根节点。

同样 ,然后 ,接着找到最左边的,它就是 所在树的根节点。

为了防止卡链,最后还要再 。

inline void pushdown(int x) 
    tr[x].flag = 0; // 这里一定是等于 0,不能 ^1
}
inline int findroot(int x) {
    access(x); splay(x);
    while (tr[x].s[0]) {
        pushdown(x); // 记得下传懒标记
        x = tr[x].s[0];
    }
    splay(x);
    return x;
}

将 和 连边。

首先 ,然后判断一下 如果等于 就不连,否则将 的父亲置为 连一条虚边。

时不连是因为如果别人已经在一条链上了,你再连就会出现双向边或环。

inline void link(int x, int y) 

砍掉 的边。

首先还是 ,接着考虑什么时候连边不合法。明显当 时不合法,说明两者不在一棵 中。那在同一棵树中的情况呢?

你想, 维护的是中序遍历,并且我们已经使 成为了根,所以如果 还有左子结点,则二者一定在原树上不相邻。同时,我们在 中已经 ,所以如果 与 在同一 中却还没有连边,这条路径上一定还会有其它的点,所以如果 也是不合法的。

判完后令 的右儿子和 的父亲置为 。

最后因为记得 。

为什么 不需要 ?因为它连的是虚边。

inline void cut(int x, int y) 

一个很重要的点:在使用 时无论边是否一定合法都不能直接去掉判断行,因为 操作会对 进行操作,去掉答案会发生错误!!!!

把 拆成一棵方便操作的 。

因为 会使得 到根节点拉出一条实链,所以只要事先 就可以拉出一条 的实链。

最后为了方便瞎搞,可以 把 转到根节点,原因是这样子 只有左儿子。

inline void split(int x, int y) {
    makeroot(x); access(y); splay(y);
}

由于 引入了虚实边的概念,所以 的代码也要稍加变化。

在 中,由于 可能是 的根,这样 就是虚边了,所以旋转时 只能单向连边。

在 中,由于每次都是转到根, 中就只传入一个变量,循环的判断条件也要改变。

inline void rotate(int x) 

inline void splay(int x) 
        rotate(x);
    }
}

瞎扯了那么多,现在放个模板。

// 洛谷 P3690
#include <bits/stdc++.h>
#define rep(i, a, b) for (int i = (a); i <= (b); i ++)
#define fro(i, a, b) for (int i = (a); i >= b; i --)
#define INF 0x3f3f3f3f
#define eps 1e-6
#define lowbit(x) (x & (-x))
#define initrand srand((unsigned)time(0))
#define random(x) ((LL)rand() * rand() % (x))
#define eb emplace_back
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<double, int> PDI;
inline int read() 
    while (ch >= '0' && ch <= '9') 
    return x * f;
}

const int N = 100010;
int n, m;

struct Splay {
    int s[2], p, val, res, flag;
} tr[N];

inline bool ntroot(int x) {
    return tr[tr[x].p].s[0] == x || tr[tr[x].p].s[1] == x;    
}

inline void reverse(int x) {
    swap(tr[x].s[0], tr[x].s[1]);
    tr[x].flag ^= 1;
}

inline void pushdown(int x) 
    tr[x].flag = 0;
}

inline void pushall(int x) 

inline void pushup(int x) {
    tr[x].res = tr[tr[x].s[0]].res ^ tr[tr[x].s[1]].res ^ tr[x].val;
}

inline void rotate(int x) 

inline void splay(int x) 
        rotate(x);
    }
}

inline void access(int x) {
    for (int y = 0; x; y = x, x = tr[x].p) 
        splay(x), tr[x].s[1] = y, pushup(x); 
}

inline void makeroot(int x) {
    access(x); splay(x); reverse(x);
}

inline int findroot(int x) {
    access(x); splay(x);
    while (tr[x].s[0]) {
        pushdown(x);
        x = tr[x].s[0];
    }
    splay(x);
    return x;
}

inline void link(int x, int y) 

inline void cut(int x, int y) 

inline void split(int x, int y) {
    makeroot(x); access(y); splay(y);
}

int main()  else if (op == 1) {
            link(x, y);
        } else if (op == 2) {
            cut(x, y);
        } else {
            splay(x);
            tr[x].val = y;
        }
    } 
    return 0;
}

代码在上面

考虑运用线段树打懒标记的思路,对于每个 结点维护 ,其中 为结点的值, 为乘的懒标记, 为加的懒标记, 为子树的和, 是翻转的懒标记, 是子树的大小。

维护懒标记时先 再 ,原因可以自己列个式子算一算。

代码

自我感觉马蜂挺良好的?

操作就是 , 操作我们可以维护每个子树的 , 后答案就是 ,这样就做完了,吗?

由于 有虚边和实边的分别,所以我们不能单纯地维护实边的 ,我们还要加上虚边,所以我们可以再维护一个 代表虚边的大小。然后 要修改,并且会改变边的虚实关系的 和 中都要维护 。

注意到 中进行了修改增加了 ,因为如果直接连 的祖先就爆了。

inline void pushup(int x) {
    tr[x].sz = tr[tr[x].s[0]].sz + tr[tr[x].s[1]].sz + 1 + tr[x].sz2;
}
inline void access(int x) {
    for (int y = 0; x; y = x, x = tr[x].p) 
        splay(x), tr[x].sz2 += tr[tr[x].s[1]].sz - tr[y].sz, tr[x].s[1] = y, pushup(x); 
}
inline void link(int x, int y) {
    makeroot(x); makeroot(y);
    tr[y].sz2 += tr[x].sz, tr[y].sz += tr[x].sz;
    tr[x].p = y;
}

其实解决这个问题的方式还有一种就是 当不需要判断数据合法性时还有另一种写法,直接 然后这时 相当于一个在链首,一个在链尾,这时候直接搞就不会出问题了。

inline void link(int x, int y) {
    split(x, y);
    tr[x].p = y;
}

完整代码

赞(0)
未经允许不得转载:上海聚慕医疗器械有限公司 » PLCT检查是什么LCT——Link Cut Tree

登录

找回密码

注册