LCT 学习笔记

引子

在古老且美妙的数据结构王国,一次,一个巨大的怪兽出现在了这个国家,这个怪兽是一棵树,打败这个怪兽只需要能快速求出这个怪兽任意一条路径上的和就可以了,可是他灵活多变,自己的手脚可以调换位置,或拿下来(边可以断掉或连上)身上的每一寸肌肤都可改变其硬度(点可以修改值)

树链剖分找到了\(splay\),他们决定联手,树链剖分将这个怪兽分成一条条的链,让\(splay\)去套在链上,无论怪兽怎样变化,\(splay\)都可以去维护这条链,怪兽就这样被打败了

这是怎么做到的呢??

\(aqz180321\) 今天比较疯狂,决定研究一下

我初三写的好幼稚, 不过确实是这样

基本算法

用来维护树森林

我们先对每一棵树进行实链剖分


对于一个点连向它所有儿子的边,我们自己任意选择一条边进行剖分,我们称被选择的边为实边,其他边则为虚边。

对于实边,我们称它所连接的儿子为实儿子。对于一条由实边组成的链,我们同样称之为实链。

请记住我们选择实链剖分的最重要的原因:它是我们选择的,灵活且可变。正是它的这种灵活可变性,我们采用 \(Splay Tree\) 来维护这些实链。

\(--\) oi-wiki


实链有 :

  • \(A-B-C\)

  • \(C-E\)

  • \(F\)

我们对于每一颗实链都建一棵\(splay\)

\(splay\)树中通过深度进行的划分,也就是说 \(lson\)的深度 \(<\) 当前节点的深度 \(<\) \(rson\) 的深度

不难发现,这颗\(splay\)树的中序遍历的结果,相当于从上到下遍历这条链

然后将这些\(splay\)树的根指向在原树中 这条链 的父亲节点(即链最顶端的点(即深度最小的点)的父亲节点)

如图

我们将这样构成的树叫做辅助树

任意一颗辅助树都能还原成原树, 我们通过对辅助树森林的操作来实现原树的变化

然后这就是他的初步,是不是感觉挺简单的

函数

前提:变量名

#define lson(x) a[x].son[0]
#define rson(x) a[x].son[1]
#define fa(x) a[x].fa
struct Node {
	int fa, son[2];//左右儿子, 父亲
	int val, sum;//你要维护的子树信息
	bool revflag;//splay 反转标记
} a[N];

因为对于的树不同,所以会出现一种情况:

它的辅助树可能长这样:

isroot

其中\(fa[D] = B\),但是\(son[B][0] = A\) \(son[B][1] = E\)均不是\(D\)

说人话就是,任何儿子认父亲,但是父亲只认实儿子(因为实儿子和父亲在一条实链中即在一棵\(splay\)中)

所以可以根据这个特性去判断当前节点是否为\(splay\)的根

bool notroot (int pos) {
	return (lson(fa(pos)) == pos || rson(fa(pos)) == pos);
}

update

维护子树信息的函数

void update (int pos) {
	a[pos].sum = a[lson(pos)].sum + a[rson(pos)].sum + a[pos].val; 
}

reverse

void reverse (int pos) {
	std::swap(lson(pos), rson(pos));
	a[pos].revflag ^= 1;
}

push

下放反转标记

void push (int pos) {
	if (!a[pos].revflag) return ;
	if (lson(pos)) reverse(lson(pos));
	if (rson(pos)) reverse(rson(pos));
	a[pos].revflag = 0;
}

rotate

\(spaly\)中的旋转

int get (int pos) {
	return rson(fa(pos)) == pos;
}
void rotate (int pos) {
	int father = fa(pos), ffather = fa(father);
	int d = get(pos), dd = get(father);
	if (notroot (father)) a[ffather].son[dd] = pos;
	a[father].son[d] = a[pos].son[d ^ 1];
	if (a[pos].son[d ^ 1]) fa (a[pos].son[d ^ 1]) = father;
	a[pos].son[d ^ 1] = father, fa (father) = pos;
	fa (pos) = ffather;
	update (father), update(pos);
}

旋转一定不能超过当前的\(splay\)即超过根

splay

\(spaly\)中的旋转到根

int sta[N], top;
void splay (int pos) {
	int now = sta[top = 1] = pos;
	while (notroot (now)) 
		sta[++top] = now = fa (now);
	while (top) push(sta[top--]);
	for (; notroot(pos); rotate(pos))
		if (notroot(fa(pos))) 
			rotate(get(pos) == get(fa(pos)) ? fa(pos) : pos);
}

反转前下方懒标记

接下来是LCT的精华

access

将当前节点到根连一条实链,实链使我们规定的链,所以会很灵活

void access (int pos) {
	int last = 0;
	while (pos) {
		splay(pos);
		rson(pos) = last;
		update(pos);
		pos = fa(last = pos);
	}
}

什么,这么简单???但是很抽象

可以见\(oi-wiki\)的演示

传送门

这里就文字描述一下

从根到一个节点路径应该是唯一的,深度也是递增的

首先将\(x\)旋转到根,这时\(son[pos][0]\)就是深度比他小的,这些是要的,因为是从根到当前节点,这些节点肯定经过,所以 \(son[pos][1]\) 就可以不要了,\(update\)修改一下 ,然后\(pos = fa(pos)\)去到他的父亲,父亲也是深度比他大的,所以也要,但是父亲所在的\(splay\)中有不合法的,所以去搞他的父亲,搞完之后,将自己替换他不合法的右儿子\(rson(pos) = last\)连上,这颗平衡树深度大小关系也是满足的

它执行以后,辅助树改变了,但是原树没有改变,只是改变了原树的实链的划分

makeroot

成为根,原树是一棵无根树,所以根可以是任意一个

先从根节点到要成为根的节点变成实链,在将当前节点旋到最上面,因为当前节点是深度最小的,所有右儿子为空,翻转儿子,左儿子为空,就成为了深度最小的点,即根

void makeroot (int pos) {
	access(pos), splay(pos), reverse(pos);
}

findroot

找原树的根,先把根节点到要找根的节点变成实链,再将当前节点旋转到上边,最后一直向左找,最左边就是深度最小的,即是根

最后再把根旋转上来,保证复杂度

int findroot (int pos) {
	access(pos); splay(pos);
	while (lson(pos)) pos = lson(pos), push(pos);
	splay(pos); return pos;
}

split

将x到y的路径变成实链

先把x变成根,在从根向y连一条实链就可以了,最后将y旋转上去,方便以后查询

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

将x和y连边, 规定连的边为虚边, 这就会对原树造成影响了与上边的split有很大的区别

先将x变成根, 再将父亲指向y

void link (int x, int y) {
	makeroot(x), fa(x) = y;
}

cut

将x和y的边删除

要求在原树上有连边, \(split(x, y)\) 后实链长度只有 \(2\) , 所以断开是好断开的

void cut (int x, int y) {
	split(x, y); fa(x) = 0, lson(y) = 0; update(y);
} 

不过不保证有连边呢 \(?\)

判断一下实链 \(x, y\) 之间有没有其它点就可以了, 利用中序遍历

void cut (int x, int y) {
	split(x, y); 
	if (lson(y) == x && rson(x) == 0)
		fa(x) = 0, lson(y) = 0;
} 

封装起来的

namespace LCT {
    #define lson(x) a[x].son[0]
    #define rson(x) a[x].son[1]
    #define fa(x) a[x].fa
    struct Node {
        int fa, son[2];
        int val, sum;
        bool revflag;
    } a[N];
    void update (int pos) {
        a[pos].sum = a[lson(pos)].sum + a[rson(pos)].sum + a[pos].val; 
    }
    void reverse (int pos) {
        std::swap(lson(pos), rson(pos));
        a[pos].revflag ^= 1;
    }
    void push (int pos) {
        if (!a[pos].revflag) return ;
        if (lson(pos)) reverse(lson(pos));
        if (rson(pos)) reverse(rson(pos));
        a[pos].revflag = 0;
    }
    int get (int pos) {
        return rson(fa(pos)) == pos;
    }
    bool notroot (int pos) {
        return (lson(fa(pos)) == pos || rson(fa(pos)) == pos);
    }
    void rotate (int pos) {
        int father = fa(pos), ffather = fa(father);
        int d = get(pos), dd = get(father);
        if (notroot (father)) a[ffather].son[dd] = pos;
        a[father].son[d] = a[pos].son[d ^ 1];
        if (a[pos].son[d ^ 1]) fa (a[pos].son[d ^ 1]) = father;
        a[pos].son[d ^ 1] = father, fa (father) = pos;
        fa (pos) = ffather;
        update (father), update(pos);
    }
    int sta[N], top;
    void splay (int pos) {
        int now = sta[top = 1] = pos;
        while (notroot (now)) 
            sta[++top] = now = fa (now);
        while (top) push(sta[top--]);
        for (; notroot(pos); rotate(pos))
            if (notroot(fa(pos))) 
                rotate(get(pos) == get(fa(pos)) ? fa(pos) : pos);
    }
    void access (int pos) {
        int last = 0;
        while (pos) {
            splay(pos);
            rson(pos) = last;
            update(pos);
            pos = fa(last = pos);
        }
    }
    void makeroot (int pos) {
        access(pos), splay(pos), reverse(pos);
    }
    void split (int x, int y) {
        makeroot(x), access(y), splay(y);
    }
    void link (int x, int y) {
        makeroot(x), fa(x) = y;
    }
    void cut (int x, int y) {
        split(x, y); fa(x) = 0, lson(y) = 0; update(y);
    } 
    int findroot (int pos) {
        access(pos); splay(pos);
        while (lson(pos)) pos = lson(pos), push(pos);
        splay(pos); return pos;
    }
}

模板题

【模板】动态树(LCT)

[COI 2009] OTOCI

应用

LCT 与 MST

可以动态的维护 \(MST\) , 但是只支持不断加边

如果加入的边是 \((u, v)\) 先判断是否是联通的, 然后再找 \(u \rightarrow v\) 路径上的最大边, cut

LCT 是用来维护点的, 所以我们需要边转点, 直接 \(u \rightarrow v\) 变成 \(u \rightarrow w \rightarrow v\)

两维的, 先枚举处理 \(a\) 从小到大枚举边, 然后加边, 利用 \(b\) 的最小生成树解决 另一维

按照上边所说的处理就可以

套了线段树合并

撤销可以开栈, linkcut, cutlink

LCT 与 变化的图

连通块的点减边, 于是找到原图的一棵生成树, 然后计算边的数量, 区间询问可以贪心的选择, 发现这个可以用LCT来维护, 然后一次区间询问相当于查询右端点时维护的边的编号大于左端点的个数, 使用主席树搞一下就可以了

LCT与树剖

LCT 维护了实链 这与树剖维护重链是差不多的 所以可以用LCT做树剖的题

虽然复杂度分析是 \(O(n logn)\) 的 但是常数比较大 有时候还不如 \(O(n log^2n)\)的树剖

DDP

DDP 通常情况下用树剖做 但是也可以用LCT做 这好像是为数不多的LCT比树剖快的例子

如果我们要从一个链转移到顶上 我们只需要中序遍历 然后把它们的g数组按顺序乘起来 然后查询就可以了

然后我们查询的时候就可以直接access(1) splay(1) 查询1 就可以了

修改 我们肯定不想要修改过多维护 所以我们可以这样 access(x) splay(x) 只用修改x就可以了 因为他是根 而且不是任何节点的虚儿子

然后考虑怎么在access的过程中维护呢?

在断开与链接的操作中 记录 旧儿子 与 新儿子 然后暴力改变就可以了

其它

我们观察到操作1很像access操作

然后我们维护这个东西

发现在access操作构成中 会改变若干个子树的值 所以我们使用一棵线段树来维护子树加与子树求和

2操作可以差分 3操作可以在线段树上直接查询

因为LCT的灵活性 所以它用来维护这些东西更加的容易

进阶应用

上文已经说过, \(LCT\) 可以维护链的信息, 那么 \(LCT\) 可以维护子树信息吗, 是可以的

还能加减边, 这不吊打树剖

具体的, 我们维护实链上的点, 以及实链上挂着的虚链的子树和, 挂着的虚链体现在辅助树上就是所有的认父不认子, 我们改变虚实后进行修改就可以了

容易发现, 这样 makeroot(x) \(+\) splay(x) 后, \(x\) 维护的就是整棵树, 所以我们可以 cut(x, fa(x)) 后查询 \(x\) 实现查询子树信息

具体实现到代码上就是在 access 的时候修改

void del (int x, int y) {
	if (!y) return ;
	//
} 
void ins (int x, int y) {
	if (!y) return ;
	//
}
void access (int pos) {
	int last = 0;
	while (pos) {
		splay(pos);
		ins(pos, rson(pos));
		del(pos, last);
		rson(pos) = last;
		update(pos);
		pos = fa(last = pos);
	}
}

需要维护子树大小

和上文说的一样

有根树 LCT

问题在于 makerootsplit 不能使用了

也只能处理根到当前节点的链信息

对与 linkcut 函数做出一些修改

void cut (int x) { //删掉 (fa(x), x)边
	access(x); splay(x);
    fa(lson(x)) = 0, lson(x) = 0;
    update(x);
} 
void link (int x, int y) { //有向边 (y, x)
	access(x), splay(x), fa(x) = y;
}
  • BZOJ2759 一个动态树好题
posted @ 2023-12-14 19:37  d3genera7e  阅读(21)  评论(1)    收藏  举报