Link-Cut-Tree

4 篇文章 0 订阅
1 篇文章 0 订阅

What is Link-Cut-Tree?

动态树(Link-Cut-Tree),也称LCT,是一种可以维护一个有根树森林改变形态、合并、分离、查询等操作的数据结构。由Robert Tarjan(他可以说是算法大师了,前面的文章中我是提过他的)为首的团队提出。在学习动态树前,要先明白splay的原理。

在下面的一段中,会详细介绍splay的rotate与splay两个操作,请大家暂时忘掉有根树森林之类的。(下面这部分的树是指splay树)对splay十分熟悉的同学可以直接跳过去。

Splay & Rotate


(一)rotate(请忽略上图的x与y,αβγ可以看图)

左旋x:使x的右儿子成为x这棵子树的根。换言之,以前x的位置,现在放的是x的右儿子。

右旋x:使x的左儿子成为x这棵子树的根。换言之,以前x的位置,现在放的是x的左儿子。

观察这两句话。这两种方向的旋转的共性就是:把x这棵子树拿出来,把x的某个儿子旋上去,即换成根(x自然就被旋下去了),再给它接回去。

于是定义rotate函数。rotate(x)表示:在fa[x]的这棵子树中,把根换成x。

首先取y为x的父亲,z为x的爷爷(这两个节点是不一定存在的)。重申一遍,我们要做的就是对y的子树进行换根。

如果z存在,那就先解决子树外的事情。令z的儿子为x(即,以前y是z的哪个儿子,现在x就是z的哪个儿子),x的父亲为z。

然后看上面的图。左旋(左到右时),只有β改变了相对位置。右旋(右到左时),也是只有β改变了。α和γ的父亲都还是原来的父亲。这就是旋转的第二个共性。

于是我们就考虑维护一下β的信息。β究竟是谁呢?继续看图,可知:若x是y的左儿子,那β就是x的左儿子。相反地,若x是y的右儿子,那β就是x的左儿子。(恰好是反过来的)

那β去哪了呢?

继续继续看图:若x是y的左儿子,那β就做y的左儿子。若x是y的右儿子,那β做y的右儿子。(恰好是正着的)

可以这么记:反找(反着找β)正改(正着把β改为y的儿子)。

最后,如果x是y的左儿子,旋后y就是x的右儿子。如果x是y的右儿子,旋后y就是x的左儿子。(中序不变)

既然是平衡树,就不要忘记,更改了形态,最后的最后要update。可知,只有x与y需要更新信息。先update儿子,也就是y(现在y是儿子了)。再update父亲x。

其时间复杂度为O(1)。

wh(x)是一个返回值的函数,若x是父亲的左儿子,返回0,否则返回1。(因为ch[x][0]表示x的左儿子,ch[x][1]表示x的右儿子)

inline void rotate(int x) {
	int y = fa[x], z = fa[y], c = wh(x);
	if(!isr(y)) ch[z][wh(y)] = x; fa[x] = z;
	ch[y][c] = ch[x][c^1]; fa[ch[y][c]] = y;
	ch[x][c^1] = y; fa[y] = x;
	update(y); update(x);
}

(二)splay

splay(x)表示,把x转为x所在的这一整棵splay的根

经过上面我们对rotate的研究,rotate(x)也可以理解为,把x往上升一层。

在这里,就不证明为什么要双旋而不是单旋(spaly)了(这篇文章是LCT的讲解)。

首先pd(x),意思是,从现在的根节点,一直到x,顺次pushdown(pushdown要从上往下,update要从下往上)。

然后就可以肆无忌惮的转了。转的次数的期望是logN级的。

循环,若x不是根就做。循环中分为两步。

第一步:若x的父亲pa不是根节点(这个时候转pa才有意义,否则我可以直接只转x)就进行这一步,否则直接第二部。

若pa的父亲、pa、x呈一条没有拐弯的链(即,pa是pa父亲的左儿子,x也是pa的左儿子,或pa是pa父亲的右儿子,x也是pa的右儿子),在这种情况下我们如果转x,是无法改变讨厌的链结构的(数据结构最讨厌的就是退化成链)。所以就要转pa。刚刚说过“反找正改”,x是正,所以不会被改变与pa的相对位置,而实际上pa上升了一层,x也就随着上升了一层。这样既破坏了链结构,又向上升了一层,十分的巧妙。当然,若原来不是个链,就直接旋x就好。

第二步:旋x。(是的,即使前一步是旋x,这一步也要再旋一次,原因就不解释了)

inline void splay(int x) {
	pd(x);
	for(; !isr(x); rotate(x))
		if(!isr(pa)) rotate(wh(pa) == wh(x) ? pa : x);
}

splay讲解完毕,撒花!!!✿✿ヽ(°▽°)ノ✿

LCT

LCT是如何运作的呢?

对于一棵树,把它像树链剖分那样做成好多个部分(还是Prefered Son那套东西),用好多棵Splay去维护(这个splay的中序遍历是以在树中的深度为关键字的)。只不过这次的Prefered Son,不听子树大小,而是听我的!

我现在给你两种新工具。

access(x):打通root到x这条路,即用一个splay去维护这条路径的所有点。(注意,是只维护这条路径,x的下面就不算这条路径了)(在access之后,这条路径的每一个节点都变成了其父亲的Prefered Son,而x并没有Prefered Son)

makeroot(x):把x所在的这棵树的根换成x(这是对实树的操作,而对splay的换根操作叫做splay(x))。毕竟,一棵树换根之后,还是不改变相对位置的。

得到这两种新工具,我现在让你查询u到v这段路径的信息。你肯定要精确恰好的得到这条路径的那个splay咯。怎么做呢?

先makeroot(u),再access(v)。此时u->v就恰好在一棵splay上了。但这棵splay的根并不确定。于是直接splay(v)或splay(u)就好了。(当然,你要顺着u或v找根我也是没意见的。那样可能会常数大一点)

目前来看,这两种新工具的用途极大。如何实现这两个函数呢?

为了方便书写代码,宏定义一下。

#define lc ch[x][0]
#define rc ch[x][1]
#define pa fa[x]

(一)access

就一行,画个图理解一下吧(其实是我懒)。

inline void access(int x) {
	for(int y = 0; x; y = x, x = pa) splay(x), rc = y, update(x);
}

(二)makeroot

由于splay是按深度维护,并且这两个是等价的(并没有其他东西去记载谁是根),所以直接在splay里改就可以。

先access(x),此时x在整棵splay中深度是最大的,处于最右端的位置。

然后给整棵splay(同理,如果你不splay(x),你根本不知道谁是根)打个翻转标记,x就在最左端了,于是就ok了。

(个人习惯:打懒标记时把这个节点要修改的都改过来,下放的时候只改两个儿子的信息。)

inline void maker(int x) {
	access(x); splay(x); rever(x);
}

(三)衍生操作

把x与y连一条边。(makeroot之后,x就是根节点了,就不必再splay了)(若x、y已经连通就不用link了)

inline void link(int x, int y) {
	maker(x); fa[x] = y;
}

把x与y的边断开。(前提是二者之间要有边,连通并不难说明有边,详情请看注释)

inline void cut(int x, int y) {
	maker(x); access(y); splay(y);
	if(ch[y][0] != x && !ch[ch[y][0]][1]) return ; //理论上,x是实树的根,y是splay的根。若想x与y间有边,只能是y在第2层。而由于x的Prefered Son只有一个y,所以y在splay中,应该只有x在它左边(深度小的在左边),这样才合法。不止要y的lson是x,还要使x没有rson(即y的左子树只有一个节点)。
	fa[x] = ch[y][0] = 0; update(y);
}

找x所在实树的根是谁。(很少使用)一直往左找就可以了。如果没有删边,用这个判连通实在是不如并查集。

inline int findr(int x) {
	access(x); splay(x);
	while(lc) pushdn(x), x = lc; return x;
}

LCT讲解完毕,撒花!!!✿✿ヽ(°▽°)ノ✿

Extend

目前的LCT只能解决路径问题。如果要查询子树,要在现有基础上加一些东西。也称之为Top-Tree。(真正的高级数据结构了,码量极大,大概要600+行,不是太推荐)

Example

LuoguP3690(模板)

#include <cstdio>
#include <algorithm>
#define N 300010
#define lc ch[x][0]
#define rc ch[x][1]
#define pa fa[x]
using namespace std;
inline char gc() {
	static char now[1<<16], *S, *T;
	if(S == T) {T = (S = now) + fread(now, 1, 1<<16, stdin); if(S == T) return EOF;}
	return *S++;
}
inline int read() {
	int x = 0; char c = gc();
	while(c < '0' || c > '9') c = gc();
	while(c >= '0' && c <= '9') {x = x * 10 + c - 48; c = gc();}
	return x;
}
namespace lct {
	int ch[N][2], fa[N], rev[N], sum[N], w[N];
	inline int wh(int x) {return ch[pa][1] == x;}
	inline int isr(int x) {return ch[pa][0] != x && ch[pa][1] != x;}
	inline void update(int x) {sum[x] = sum[lc] ^ sum[rc] ^ w[x];}
	inline void rever(int x) {rev[x]^= 1; swap(lc, rc);}
	inline void pushdn(int x) {
		if(rev[x]) {
			if(lc) rever(lc);
			if(rc) rever(rc);
			rev[x] = 0;
		}
	}
	void pd(int x) {if(!isr(x)) pd(pa); pushdn(x);}
	inline void rotate(int x) {
		int y = fa[x], z = fa[y], c = wh(x);
		if(!isr(y)) ch[z][wh(y)] = x; fa[x] = z;
		ch[y][c] = ch[x][c^1]; fa[ch[y][c]] = y;
		ch[x][c^1] = y; fa[y] = x;
		update(y); update(x);
	}
	inline void splay(int x) {
		pd(x);
		for(; !isr(x); rotate(x))
			if(!isr(pa)) rotate(wh(pa) == wh(x) ? pa : x);
	}
	inline void access(int x) {
		for(int y = 0; x; y = x, x = pa) splay(x), rc = y, update(x);
	}
	inline void maker(int x) {
		access(x); splay(x); rever(x);
	}
	inline int findr(int x) {
		access(x); splay(x);
		while(lc) pushdn(x), x = lc; return x;
	}
	inline void link(int x, int y) {
		maker(x); fa[x] = y;
	}
	inline void cut(int x, int y) {
		maker(x); access(y); splay(y);
		if(ch[y][0] != x && !ch[ch[y][0]][1]) return ; //理论上,x是实树的根,y是splay的根。若想x与y间有边,只能是y在第2层。而由于x的Prefered Son只有一个y,所以y在splay中,应该只有x在它左边(深度小的在左边),这样才合法。不止要y的lson是x,还要使x没有rson(即y的左子树只有一个节点)。
		fa[x] = ch[y][0] = 0; update(y);
	}
	inline void split(int x, int y) {
		maker(x); access(y); splay(y); //makeroot(x)之后,x到y这棵splay未必是以x为根的,所以splay(x)还是splay(y)都可以 
	}
}
int n, Q;
int main() {
	n = read(); Q = read();
	for(int i = 1; i <= n; ++i) lct::w[i] = read();
	for(int i = 1; i <= Q; ++i) {
		int opt = read(), x = read(), y = read();
		if(opt == 0) lct::split(x, y), printf("%d\n", lct::sum[y]);
		if(opt == 1) if(lct::findr(x) != lct::findr(y)) lct::link(x, y); //link可以直接看是否连通 
		if(opt == 2) lct::cut(x, y); //如果x、y连通,不代表一定直接有边将二者连起来,操作未必合法 
		if(opt == 3) lct::w[x] = y, lct::splay(x);
	}
	return 0;
}

[HNOI2010]弹飞绵羊(代码在前一个基础上改的,就是直接维护一个Size,我代码写的比较多余,不推荐看)

#include <cstdio>
#include <algorithm>
#define N 200010
#define lc ch[x][0]
#define rc ch[x][1]
#define pa fa[x]
using namespace std;
inline char gc() {
    static char now[1<<16], *S, *T;
    if(S == T) {T = (S = now) + fread(now, 1, 1<<16, stdin); if(S == T) return EOF;}
    return *S++;
}
inline int read() {
    int x = 0; char c = gc();
    while(c < '0' || c > '9') c = gc();
    while(c >= '0' && c <= '9') {x = x * 10 + c - 48; c = gc();}
    return x;
}
int k[N];
namespace lct {
    int ch[N][2], fa[N], rev[N], sum[N], w[N];
    inline int wh(int x) {return ch[pa][1] == x;}
    inline int isr(int x) {return ch[pa][0] != x && ch[pa][1] != x;}
    inline void update(int x) {sum[x] = sum[lc] + sum[rc] + w[x];}
    inline void rever(int x) {rev[x]^= 1; swap(lc, rc);}
    inline void pushdn(int x) {
        if(rev[x]) {
            if(lc) rever(lc);
            if(rc) rever(rc);
            rev[x] = 0;
        }
    }
    void pd(int x) {if(!isr(x)) pd(pa); pushdn(x);}
    inline void rotate(int x) {
        int y = fa[x], z = fa[y], c = wh(x);
        if(!isr(y)) ch[z][wh(y)] = x; fa[x] = z;
        ch[y][c] = ch[x][c^1]; fa[ch[y][c]] = y;
        ch[x][c^1] = y; fa[y] = x;
        update(y); update(x);
    }
    inline void splay(int x) {
        pd(x);
        for(; !isr(x); rotate(x))
            if(!isr(pa)) rotate(wh(pa) == wh(x) ? pa : x);
    }
    inline void access(int x) {
        for(int y = 0; x; y = x, x = pa) splay(x), rc = y, update(x);
    }
    inline void maker(int x) {
        access(x); splay(x); rever(x);
    }
    inline int findr(int x) {
        access(x); splay(x);
        while(lc) pushdn(x), x = lc; return x;
    }
    inline void link(int x, int y) {
        maker(x); fa[x] = y;
    }
    inline void cut(int x, int y) {
        maker(x); access(y); splay(y);
        if(ch[y][0] != x && !ch[ch[y][0]][1]) return ; //理论上,x是实树的根,y是splay的根。若想x与y间有边,只能是y在第2层。而由于x的Prefered Son只有一个y,所以y在splay中,应该只有x在它左边(深度小的在左边),这样才合法。不止要y的lson是x,还要使x没有rson(即y的左子树只有一个节点)。
        fa[x] = ch[y][0] = 0; update(y);
    }
    inline void split(int x, int y) {
        maker(x); access(y); splay(y); //makeroot(x)之后,x到y这棵splay未必是以x为根的,所以splay(x)还是splay(y)都可以 
    }
}
int n, Q;
int main() {
    n = read();
    for(int i = 1; i <= n; ++i) {
        lct::w[i] = 1, k[i] = read();
        if(i + k[i] <= n) lct::link(i, i + k[i]);
        else lct::link(i, n + 1);
    }
    Q = read();
    for(int i = 1; i <= Q; ++i) {
        int opt = read(), x = read(); ++x;
        if(opt == 1) {
        	lct::maker(n + 1);
        	lct::access(x); lct::splay(x);
        	printf("%d\n", lct::sum[x]);
        }
        if(opt == 2) {
        	int y = read();
        	if(x + k[x] <= n) lct::cut(x, x + k[x]); else lct::cut(x, n + 1);
        	k[x] = y;
        	if(x + k[x] <= n) lct::link(x, x + k[x]);
            else lct::link(x, n + 1);
        }
    }
    return 0;
}

[NOI2014]魔法森林

动态维护最小生成树。

这题的思路特别有用。当有两个参数时,考虑按照其中一个进行排序(先把其中一维给搞有序了),另一个动态加入。

具体题解就不写了,这不是本文目的。(还是因为我懒

这题并查集就比findroot快的多!!!

#include <cstdio>
#include <algorithm>
#define N 50010
#define M 100010
#define lc ch[x][0]
#define rc ch[x][1]
#define pa fa[x]
using namespace std;
inline char gc() {
    static char now[1<<16], *S, *T;
    if(S == T) {T = (S = now) + fread(now, 1, 1<<16, stdin); if(S == T) return EOF;}
    return *S++;
}
inline int read() {
    int x = 0; char c = gc();
    while(c < '0' || c > '9') c = gc();
    while(c >= '0' && c <= '9') {x = x * 10 + c - 48; c = gc();}
    return x;
}
struct edge {int fr, to, a, b;}e[M]; int n, m;
inline bool cmp1(edge A, edge B) {return (A.a == B.a) ? (A.b < B.b) : (A.a < B.a);}
namespace lct {
    int ch[N+M][2], fa[N+M], rev[N+M], mx[N+M], w[N+M], from[N+M];
    inline int wh(int x) {return ch[pa][1] == x;}
    inline int isr(int x) {return ch[pa][0] != x && ch[pa][1] != x;}
    inline void update(int x) {
        mx[x] = w[x]; from[x] = x;
        if(mx[lc] > mx[x]) {mx[x] = mx[lc]; from[x] = from[lc];}
        if(mx[rc] > mx[x]) {mx[x] = mx[rc];	from[x] = from[rc];}
    }
    inline void rever(int x) {rev[x]^= 1; swap(lc, rc);}
    inline void pushdn(int x) {
        if(rev[x]) {
            if(lc) rever(lc);
            if(rc) rever(rc);
            rev[x] = 0;
        }
    }
    void pd(int x) {if(!isr(x)) pd(pa); pushdn(x);}
    inline void rotate(int x) {
        int y = fa[x], z = fa[y], c = wh(x);
        if(!isr(y)) ch[z][wh(y)] = x; fa[x] = z;
        ch[y][c] = ch[x][c^1]; fa[ch[y][c]] = y;
        ch[x][c^1] = y; fa[y] = x;
        update(y); update(x);
    }
    inline void splay(int x) {
        pd(x);
        for(; !isr(x); rotate(x))
            if(!isr(pa)) rotate(wh(pa) == wh(x) ? pa : x);
    }
    inline void access(int x) {
        for(int y = 0; x; y = x, x = pa) splay(x), rc = y, update(x);
    }
    inline void maker(int x) {
        access(x); splay(x); rever(x);
    }
    inline void link(int x, int y) {
        maker(x); fa[x] = y;
    }
    inline void cut(int x, int y) {
        maker(x); access(y); splay(y);
        fa[x] = ch[y][0] = 0; update(y);
    }
    inline void split(int x, int y) {
        maker(x); access(y); splay(y);
    }
    inline int query(int x, int y) {
        split(x, y);
        return mx[y];
    }
}
int FA[N];
int findr(int x) {return (FA[x] == x) ? x : (FA[x] = findr(FA[x]));}
int main() {
    n = read(); m = read();
    for(int i = 1; i <= n; ++i) lct::w[i] = lct::mx[i] = 0, FA[i] = i;
    for(int i = 1; i <= m; ++i) {
        e[i].fr = read();
        e[i].to = read();
        e[i].a = read();
        e[i].b = read();
    }
    sort(e+1, e+m+1, cmp1);
    for(int i = 1; i <= n + m; ++i) lct::from[i] = i;
    for(int i = 1; i <= m; ++i) lct::w[i + n] = lct::mx[i + n] = e[i].b;
    int ans = 0x3f3f3f3f;
    for(int i = 1; i <= m; ++i) {
        int x = e[i].fr, y = e[i].to, now = i + n;
        if(x == y) continue; int fx = findr(x), fy = findr(y);
        if(fx != fy) {lct::link(x, now); lct::link(now, y);  FA[fx] = fy;}
        else {
            lct::split(x, y);
            if(lct::mx[y] <= e[i].b) continue;
            int pre = lct::from[y];
            lct::cut(e[pre - n].fr, pre); lct::cut(pre, e[pre - n].to);
            lct::link(x, now); lct::link(now, y);
        }
        if(findr(1) == findr(n)) ans = min(ans, e[i].a + lct::query(1, n));
    }
    if(ans == 0x3f3f3f3f) ans = -1; printf("%d", ans);
    return 0;
}

这道题中,起初我update是这么写的。

inline void update(int x) {
    if(mx[lc] > mx[rc] && mx[lc] > w[x]) {
        mx[x] = mx[lc];
        from[x] = from[lc];
    }else if(mx[rc] > mx[lc] && mx[rc] > w[x]) {
        mx[x] = mx[rc];
        from[x] = from[rc];
    }else {
        mx[x] = w[x];
        from[x] = x;
    }
}

但是这样写,第19个点会WA(原因是有一阵会查询出0)。但其他点都ok,包括uoj的extend test。

有没有小伙伴可以看出,为什么这样会错呀?【期待脸】

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值