Splay && LCT
Splay && LCT
Splay维护平衡树
写在前面
splay是一种维护二叉搜索树的数据结构,其精髓主要是转根的操作,通过不断的变换\(\text{root}\)节点,保证二叉搜索树的平衡,经过证明,\(\text{splay}\)旋转的次数越多,这一颗\(\text{splay}\)所维护的二叉搜索树就会越平衡
先思考一下二叉树的操作
- 我们设定一个 \(\text{root}\) 节点, 我们比 \(\text{root}\) 节点大的点都加到它的右边,比 \(\text{root}\) 小的点都放在它在左边
- 这时候考虑一组数据,满足单调增,那么我们会发现,我们插入会一直向右边插入,查询的时候会一向右边查询,然后二叉树所具有的优秀性质会被扼杀掉,于是便引出了平衡这个概念
平衡: \(\text{root}\) 节点的 左儿子和右儿子数目差不多相等
- 考虑如何实现, 我们会发现二叉搜索树的中序遍历其实就是从小到大的一次遍历,如果我们把根替换,二叉树的中序遍历是不会改变的
转跟后两个图的中序遍历都是
\(3 \to 5 \to 6 \to 7 \to 8 \to 10 \to 12\)
这是一个很重要的性质,与LCT和文艺平衡树的有重大联系
考虑如何转根
定义
左旋和右旋
左旋:顾名思义,向左旋转,就是把右儿子转到\(\text{root}\)
右旋: 同理,向右旋转,就是把左儿子转到\(\text{root}\)
但是如果只进行单旋的话很显然还是会被 \(X\) 所以引出了双旋这个概念
- 如果当前节点 \(\text{x}\) 的父节点是根节点,直接左旋或者右旋,把\(\text{x}\)转上去
- 如果当前节点 \(\text{x}\) 的父亲不是根节点,但是 父节点 和 当前节点 \(\text{x}\) 都分别是其父节点的左儿子(或者右儿子)就是两个点的类型一样,那么就先转父亲再转儿子
- 如果两个点的类型不同,那就直接转两次儿子,给一个很形象的图
代码实现:
void rootate(int now_) {
int fa = fath[now_], fg = fath[fa],whichson = Whichson(now_) ;
if(fg) son[fg][Whichson(fa)] = now_;
fath[now_] = fg;
son[fa][whichson] = son[now_][whichson ^ 1];
fath[son[fa][whichson]] = fa;
son[now_][whichson ^ 1] = fa;
fath[fa] = now_;
push_up(fa),push_up(now_);
}
void splay(int now_) {
for(;fath[now_] != 0; rootate(now_)) {
if(fath[fath[now_]]) rootate(Whichson(now_) == Whichson(fath[now_]) ? fath[now_] : now_);
}
root = now_;
}
- 求第 \(\text{rank}\) 名权值的操作,类似主席树的那种操作,就是统计每个点的\(siz\)(儿子数) , 看看 \(\text{rank}\) 和 \(\text{siz}\) 的关系即可
- 求权值为 \(\text{val}\) 的排名,找 \(val\) 就很显然了,比 \(\text{val}\) 大的就往左找反之向右找
Q: 关于为啥要求的时候转根呢?
A: 为了让平衡树更加平衡
int queryRank(int val_) {
Insert(root, 0, val_);
int ret = siz[son[root][0]] + 1;
del(val_);
return ret;
}
int queryval(int rk_) {
int now_ = root;
while(true) {
if(!now_) return -1;
if(ls && siz[ls] >= rk_) {
now_ = ls;
} else {
rk_ -= ls ? siz[ls] : 0;
if(rk_ <= cnt[now_]) {
splay(now_) ;
return val[now_];
}
rk_ -= cnt[now_];
now_ =rs;
}
}
}
插入: 递归插入就好,大就向右递归反之向左递归
里面有个垃圾桶就是记录删除的点的加点时候是优取用垃圾桶里面的点的
int build_new_point(int fa,int val_) {
int now_ = top ? bin[top--] : ++node_num;
fath[now_] = fa, val[now_] = val_, siz[now_] = cnt[now_] = 1;
return now_;
}
void Insert(int now_,int fa, int val_) {
if(now_ && val[now_] != val_) {
Insert(son[now_][val[now_] < val_],now_,val_);
return ;
}
if(val[now_] == val_) ++cnt[now_];
if(!now_) {
now_ = build_new_point(fa, val_);
if(fath[now_]) son[fath[now_]][val[fath[now_]] < val_] = now_;
}
push_up(now_), push_up(fath[now_]), splay(now_);
}
关于删除,这个操作有两种很神笔的写法,一种就是模拟考虑 5 种情况(懒得写了)无非就是
- 只有左儿子
- 只有右儿子
- 左右儿子都有
- 没有儿子
- 是这个权值的点有多个
没什么可以细扯的
还有一种就是很巧妙的写法, 依赖前驱和后继,其实本质是运用了文艺平衡树的思想, 把前驱先变为 \(\text{root}\) ,然后把后继转到前驱的下面,找到后继的左儿子然后删除
下面这个 \(\text{spaly}\) 函数能实现的功能是把 \(\text{x}\) 转到 \(\text{goal}\) 的下面
void splay(int x,int goal) {
while(tree[x].fa != goal) {
int fa = tree[x].fa;
int fg = tree[fa].fa;
if(fg != goal) {
(tree[fg].son[0] == fa) ^ (tree[fa].son[0] == x) ? rootate(x) : rootate(fa);
}
rootate(x);
}
if(goal == 0) root = x;
}
void del(int x) {
int p_fa = pre(x) ;
int n_fa = net(x) ;
splay(p_fa,0),splay(n_fa,p_fa);
int lsn = tree[n_fa].son[0];
if(tree[lsn].cnt > 1) {
tree[lsn].cnt--;
splay(lsn,0);
} else tree[n_fa].son[0] = 0;
}
5 种情况的写法
void clear(int now_) {
fath[now_] = ls = rs = val[now_] = cnt[now_] = siz[now_] = 0;
bin[++top] = now_;
}
void del(int val_) {
if(!Find(root, val_)) return;
if(cnt[root] > 1) {
--cnt[root];
push_up(root);
return ;
}
int oldroot = root;
if(!son[root][0] && !son[root][1]) {
root = 0;
} else if (!son[root][0]) {
root = son[root][1] , fath[root] = 0;
} else if (!son[root][1]) {
root = son[root][0] ,fath[root] = 0;
} else if (son[root][0] && son[root][1]) {
int lsmax = son[root][0];
while(son[lsmax][1]) lsmax = son[lsmax][1];
splay(lsmax);
son[root][1] = son[oldroot][1] , fath[son[root][1]] = root;
}
clear(oldroot), push_up(root);
}
找前驱和后继……就是把这个点先转到根,然后找左子树中最右边的点(前驱),和找右子树中最左边的点(后继)
int querypre(int val_) {
Insert(root, 0, val_);
int now_ = son[root][0];
while(rs) now_ = rs;
del(val_);
return val[now_];
}
int querynet(int val_) {
Insert(root, 0, val_);
int now_ = son[root][1];
while(ls) now_ =ls;
del(val_);
return val[now_];
}
我们是吧 \(val\) 先加进去从而使它到了 \(\text{root}\) 其实可以想到一个东西,那就是为啥不直接 \(\text{splay}\) 它,
因为我们不知道它的点号……,如果重新开一个桶来记录的话理论上是可以的
/*
Author:Imy_isLy
知识点:
*/
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#define ll long long
using namespace std;
const int N = 1e5+100;
const int inf = 1e9;
//=============================================================
//=============================================================
int read() {
int s = 0 , f = 0 ; char ch = getchar() ;
while(!isdigit(ch)) f |= (ch == '-') , ch = getchar();
while(isdigit(ch)) s = s * 10 + (ch ^ 48) , ch = getchar();
return f ? -s : s;
}
namespace Splay{
#define ls son[now_][0]
#define rs son[now_][1]
int root, node_num, fath[N], son[N][2];
int val[N], cnt[N], siz[N];
int top, bin[N];
void push_up(int now_) {
if(!now_) return ;
siz[now_] = cnt[now_];
if(ls) siz[now_] += siz[ls];
if(rs) siz[now_] += siz[rs];
}
int Whichson(int now_) {
return now_ == son[fath[now_]][1];
}
void clear(int now_) {
fath[now_] = ls = rs = val[now_] = cnt[now_] = siz[now_] = 0;
bin[++top] = now_;
}
int build_new_point(int fa,int val_) {
int now_ = top ? bin[top--] : ++node_num;
fath[now_] = fa, val[now_] = val_, siz[now_] = cnt[now_] = 1;
return now_;
}
void rootate(int now_) {
int fa = fath[now_], fg = fath[fa],whichson = Whichson(now_) ;
if(fg) son[fg][Whichson(fa)] = now_;
fath[now_] = fg;
son[fa][whichson] = son[now_][whichson ^ 1];
fath[son[fa][whichson]] = fa;
son[now_][whichson ^ 1] = fa;
fath[fa] = now_;
push_up(fa),push_up(now_);
}
void splay(int now_) {
for(;fath[now_] != 0; rootate(now_)) {
if(fath[fath[now_]]) rootate(Whichson(now_) == Whichson(fath[now_]) ? fath[now_] : now_);
}
root = now_;
}
void Insert(int now_,int fa, int val_) {
if(now_ && val[now_] != val_) {
Insert(son[now_][val[now_] < val_],now_,val_);
return ;
}
if(val[now_] == val_) ++cnt[now_];
if(!now_) {
now_ = build_new_point(fa, val_);
if(fath[now_]) son[fath[now_]][val[fath[now_]] < val_] = now_;
}
push_up(now_), push_up(fath[now_]), splay(now_);
}
int Find(int now_,int val_) {
if(!now_) return false;
if(val_ < val[now_]) return Find(ls, val_);
if(val_ == val[now_]) {
splay(now_);
return true;
}
return Find(rs, val_);
}
void del(int val_) {
if(!Find(root, val_)) return;
if(cnt[root] > 1) {
--cnt[root];
push_up(root);
return ;
}
int oldroot = root;
if(!son[root][0] && !son[root][1]) {
root = 0;
} else if (!son[root][0]) {
root = son[root][1] , fath[root] = 0;
} else if (!son[root][1]) {
root = son[root][0] ,fath[root] = 0;
} else if (son[root][0] && son[root][1]) {
int lsmax = son[root][0];
while(son[lsmax][1]) lsmax = son[lsmax][1];
splay(lsmax);
son[root][1] = son[oldroot][1] , fath[son[root][1]] = root;
}
clear(oldroot), push_up(root);
}
int queryRank(int val_) {
Insert(root, 0, val_);
int ret = siz[son[root][0]] + 1;
del(val_);
return ret;
}
int queryval(int rk_) {
int now_ = root;
while(true) {
if(!now_) return -1;
if(ls && siz[ls] >= rk_) {
now_ = ls;
} else {
rk_ -= ls ? siz[ls] : 0;
if(rk_ <= cnt[now_]) {
splay(now_) ;
return val[now_];
}
rk_ -= cnt[now_];
now_ =rs;
}
}
}
int querypre(int val_) {
Insert(root, 0, val_);
int now_ = son[root][0];
while(rs) now_ = rs;
del(val_);
return val[now_];
}
int querynet(int val_) {
Insert(root, 0, val_);
int now_ = son[root][1];
while(ls) now_ =ls;
del(val_);
return val[now_];
}
}
int main() {
int n = read();
while (n --) {
int opt = read(), x = read();
if (opt == 1) {
Splay::Insert(Splay::root, 0, x);
} else if (opt == 2) {
Splay::del(x);
} else if (opt == 3) {
printf("%d\n", Splay::queryRank(x));
} else if (opt == 4) {
printf("%d\n", Splay::queryval(x));
} else if (opt == 5) {
printf("%d\n", Splay::querypre(x));
} else if (opt == 6) {
printf("%d\n", Splay::querynet(x));
}
}
system("pause");
return 0;
}
考虑区间翻转的操作,把研究成果都画在一张图中了
/*
Author:Imy_isLy
知识点:
*/
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#define ll long long
using namespace std;
const int N = 1e5+100;
const int inf = 0x3f3f3f3f;
//=============================================================
int n, m;
//=============================================================
int read() {
int s = 0 , f = 0 ; char ch = getchar() ;
while(!isdigit(ch)) f |= (ch == '-') , ch = getchar();
while(isdigit(ch)) s = s * 10 + (ch ^ 48) , ch = getchar();
return f ? -s : s;
}
namespace Splay{
#define ls son[now_][0]
#define rs son[now_][1]
int root, node_num, fa[N], son[N][2], val[N], siz[N], cnt[N], fath[N];
bool tag[N];
int top, bin[N];
void push_up(int now_) {
if(!now_) return ;
siz[now_] = cnt[now_];
if(ls) siz[now_] += siz[ls];
if(rs) siz[now_] += siz[rs];
}
void push_down(int now_) {
if(!now_ || !tag[now_]) return ;
tag[ls] ^= 1, tag[rs] ^= 1;
swap(ls, rs);
tag[now_] = false;
}
void clear(int now_) {
fath[now_] = ls = rs = val[now_] = siz[now_] = cnt[now_] = 0;
bin[++top] = now_;
}
int build_new_point(int fa, int val_) {
int now_ = top ? bin[top--] : ++node_num;
fath[now_] = fa, val[now_] = val_, cnt[now_] = siz[now_] = 1;
return now_;
}
int Whichson(int now_) {
return son[fath[now_]][1] == now_;
}
void rootate(int now_) {
int fa = fath[now_], whichson = Whichson(now_);
if(fath[fa]) son[fath[fa]][Whichson(fa)] = now_;
fath[now_] = fath[fa];
son[fa][whichson] = son[now_][whichson ^ 1];
fath[son[now_][whichson ^ 1]] = fa;
son[now_][whichson ^ 1] = fa;
fath[fa] = now_;
push_up(fa), push_up(now_);
}
void splay(int now_,int gool){
for (; fath[now_] != gool; rootate(now_)) {
if(fath[fath[now_]] != gool )
rootate(Whichson(now_) == Whichson(fath[now_]) ? fath[now_] : now_);
}
if(!gool) root = now_;
}
void build(int &now_,int fa, int L, int R) {
// cout << L <<" " << R <<"\n" ;
if(L > R) return ;
int mid = ((L + R) >> 1);
now_ = build_new_point(fa, mid);
// cout << now_ <<"\n";
// system("pause");
build(ls, now_, L, mid - 1), build(rs, now_, mid + 1, R);
push_up(now_);
}
int query_val_id(int now_,int rank) {
push_down(now_);
// cout << now_ <<" "<< rank <<"\n";
// system("pause");
if(ls && rank <= siz[ls]) return query_val_id(ls, rank);
if(rank <= siz[ls] + cnt[now_]) return now_;
return query_val_id(rs, rank - siz[ls] - cnt[now_]);
}
void Modify(int L, int R) {
int x = query_val_id(root, L), y = query_val_id(root, R + 2);
// cout << x <<" " << y <<"\n";
splay(x, 0), splay(y, x);
tag[son[son[root][1]][0]] ^= 1;
}
void print(int now_) {
push_down(now_);
if(ls) print(ls);
if(val[now_] && val[now_] <= n) printf("%d ", val[now_]);
if(rs) print(rs);
}
}
int main() {
n = read(), m = read();
Splay::build(Splay::root, 0, 0, n + 1);
while(m --) {
int l = read(), r = read();
Splay::Modify(l, r);
}
Splay::print(Splay::root);
system("pause");
return 0;
}
lct :
基础的不再进行整理,下面是一些自己学的时候的误区
可能以后有空会补充一下基础,主要是基础没啥可以讲的OI wike讲的就很nice
相对于 树剖,LCT 灵活性更高,但是一些操作是更复杂的(能洗树剖写树剖,树剖常数小而且容易调试)
自己其实是考虑多了很多东西,会很疑惑
比如 \(\text{link}\) 操作连接的是一条轻边,这合理吗??
轻边很多东西不能干啊,那怎么处理???
其实很简单 \(\text{access}\) 操作之后会让 \(\text{root}\) 与 \(\text{x}\) 之间连接一条实边,然后我们就可以在这条实链上进行我们的的操作, 通过二叉树的一些性质从而实现我们的想法
其实本质删上与树剖是一致的,只不过树剖的链是静止的,无法改变的, 但是 \(\text{LCT}\) 不一样, 它是灵活多变的,并且我们的 \(\text{root}\) 也是不固定的,假如我们想查询 \(x \to y\) 这条链上的信息,那么很简单,我们先 \(\text{access(x)}\) 让 \(\text{x}\) 与 \(\text{root}\) 进入到同一颗 \(\text{splay}\) 然后 \(\text{splay(x)}\) 就能把 \(\text{x}\) 转到 \(\text{root}\) ,然后 \(\text{access(y)}\) , \(x \to y\) 这样就会让 \(x \to y\) 连一条边,其实看到这里……我才意识到我的 \(\text{splay}\) 其实是变味了的,我的 \(\text{splay}\) 维护是一颗树, \(\text{LCT}\) 维护的其实是这个 \(\text{splay}\) 森林,也就说 \(\text{LCT}\) 是图论啊,跟平衡树屁关系没一点
LCT 的一些性质
LCT 满足 在原树中,中序遍历深度从小到大,每一个Splay维护的是一条从上到下按在原树中深度严格递增的路径,且中序遍历Splay得到的每个点的深度序列严格递增
- 因为轻边认父不认子的性质: 我们要从子节点进行跳跃,向其父节点进行跳跃
- 一开始 我们一个点就是一颗 \(\text{splay}\) 森林
- \(\text{LCT}\) 中 \(\text{link}\) 操作的连边是 连接了一条轻边
- 在access的时候我们的节点 \(\text{now}\) 会先转到自己所在splay的根
然后跳到另一棵 \(\text{splay}\) 中;
我们如何保证这个点的深度 一定是小于所指向的点的?
我们有 \(\text{meke_root}\)的操作,我们把这个点变成整颗 \(\text{LCT}\) 的树根,然后就可以了
我们的深度是自己定义的,所以很多操作是在这的基础上来实现的
其实深度这个定义没有啥用……
/*
Author:Imy_isLy
知识点:
*/
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#define ll long long
using namespace std;
const int N = 3e5+110;
const int inf = 0x3f3f3f3f;
//=============================================================
//=============================================================
int read() {
int s = 0 , f = 0 ; char ch = getchar() ;
while(!isdigit(ch)) f |= (ch == '-') , ch = getchar();
while(isdigit(ch)) s = s * 10 + (ch ^ 48) , ch = getchar();
return f ? -s : s;
}
namespace LCT{
#define ls son[now_][0]
#define rs son[now_][1]
int fath[N], son[N][2], val[N];
int xr[N];bool tag[N];
void push_up(int now_) {
xr[now_] = xr[ls] ^ xr[rs] ^ val[now_];
return ;
}
void push_reverse(int now_) {
swap(ls, rs);
tag[now_] ^= 1;
}
void push_down(int now_) {
if(!tag[now_]) return ;
if(ls) push_reverse(ls);
if(rs) push_reverse(rs);
tag[now_] = false;
}
bool Isroot(int now_) {
return son[fath[now_]][0] != now_ && son[fath[now_]][1] != now_;
}
int Whichson(int now_) {
return now_ == son[fath[now_]][1];
}
void rootate(int now_) {
int fa = fath[now_], whichson = Whichson(now_);
if(!Isroot(fa)) son[fath[fa]][Whichson(fa)] = now_;
fath[now_] = fath[fa];
son[fa][whichson] = son[now_][whichson ^ 1];
fath[son[now_][whichson ^ 1]] = fa;
son[now_][whichson ^ 1] = fa;
fath[fa] = now_;
push_up(fa), push_up(now_);
return ;
}
void updata(int now_) {
if(!Isroot(now_)) updata(fath[now_]);
push_down(now_);
}
void splay(int now_) {
updata(now_);
for (; !Isroot(now_); rootate(now_)) {
if(!Isroot(fath[now_]))
rootate(Whichson(fath[now_]) == Whichson(now_) ? fath[now_] : now_);
}
}
void access(int now_) {
for (int last_ = 0; now_; last_ = now_, now_ = fath[now_]) {
splay(now_), rs = last_;
push_up(now_);
}
}
void make_root(int now_) {
access(now_);
splay(now_);
push_reverse(now_);
}
int Find(int now_) {
access(now_);
splay(now_);
while(ls) push_down(now_), now_ = ls;
splay(now_);
return now_;
}
int split(int x, int y) {
make_root(x);
access(y);
splay(y);
return y;
}
void link(int x, int y) {
make_root(x);
if(Find(y) != x) fath[x] = y;
}
void cut(int x, int y) {
make_root(x);
if(Find(y) != x || fath[y] != x || son[y][0]) return ;
son[x][1] = fath[y] = 0;
push_up(x);
}
void Modify(int now_, int val_) {
splay(now_);
val[now_] = val_;
push_up(now_);
}
}
int main() {
int n = read(), m = read();
for (int i = 1; i <= n; ++i) LCT::val[i] = LCT::xr[i] = read();
while(m--) {
int opt = read(), x = read(), y = read();
if(opt == 0) printf("%d\n", LCT::xr[LCT::split(x, y)]);
if(opt == 1) LCT::link(x, y);
if(opt == 2) LCT::cut(x, y);
if(opt == 3) LCT::Modify(x, y);
}
// system("pause");
return 0;
}
通过这道题我终于体会到了 \(\text{LCT}\)的真正用处
LCT 是处理动态树问题的有力武器
题意: 给定一颗树
- \(u \to v\) 路径上的点进行区间加
- \(u \to v\) 路径上的点进行区间乘
- 断边连边
- \(u \to v\) 区间求和并取模
solution:
不考虑 3 我们可以通过树剖来解决,并且是裸的树剖套线段树
那 \(\text{LCT}\) 如何处理操作 \(3\) 通过 \(\text{LCT}\) 所特有的 \(\text{link} ~~ \text{cut}\) 操作完成, 然后就和线段树二, 一毛一一样打上懒惰标记即可,那如何懒惰标记呢处理呢?
很简单, 首先抽出 \(u \to v\) 这一条链,我是给\(\text{v}\) 打上懒标记,在更换链之前,先把所有的懒标记都下传下去如果一直换链常数大的一批 ,因为一个点只有一个实边所以不会传错
/*
Author:Imy_isLy
知识点:
*/
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#define int long long
using namespace std;
const int N = 1e5+100;
const int inf = 0x3f3f3f3f;
const int mod = 51061;
//=============================================================
int n, q;
struct Edge {
int from, to, net;
}e[N << 1];
int fath[N], son[N][2];
int siz[N], add[N], mul[N], val[N], sum[N];
bool Rev[N];
//=============================================================
int read() {
int s = 0 , f = 0 ; char ch = getchar() ;
while(!isdigit(ch)) f |= (ch == '-') , ch = getchar();
while(isdigit(ch)) s = s * 10 + (ch ^ 48) , ch = getchar();
return f ? -s : s;
}
namespace LCT{
#define ls son[now_][0]
#define rs son[now_][1]
void push_up(int now_) {
sum[now_] = (sum[ls] + sum[rs] + val[now_]) % mod;
siz[now_] = siz[ls] + siz[rs] + 1;
}
void push_reverse(int now_) {
if(!now_) return ;
swap(ls, rs);
Rev[now_] ^= 1;
}
void push_add(int now_, int val_) {
if(!now_) return ;
val[now_] = (val[now_] + val_) % mod;
sum[now_] = (sum[now_] + siz[now_] * val_ % mod) % mod;
add[now_] = (add[now_] + val_) % mod;
}
void push_mul(int now_, int val_) {
if(!now_) return ;
val[now_] = val[now_] * val_ % mod;
sum[now_] = sum[now_] * val_ % mod;
add[now_] = add[now_] * val_ % mod;
mul[now_] = mul[now_] * val_ % mod;
}
void push_down(int now_) {
if(mul[now_] != 1) push_mul(ls, mul[now_]),push_mul(rs ,mul[now_]);
if(add[now_]) push_add(ls, add[now_]),push_add(rs, add[now_]);
if(Rev[now_]) push_reverse(ls), push_reverse(rs);
mul[now_] = 1, add[now_] = Rev[now_] = 0;
}
bool Isroot(int now_) {
return son[fath[now_]][0] != now_ && son[fath[now_]][1] != now_;
}
int Whichson(int now_) {
return now_ == son[fath[now_]][1];
}
void rootate(int now_) {
int fa = fath[now_], w = Whichson(now_);
if (!Isroot(fath[now_])) son[fath[fath[now_]]][Whichson(fath[now_])] = now_;
fath[now_] = fath[fath[now_]];
son[fa][w] = son[now_][w ^ 1];
fath[son[fa][w]] = fa;
son[now_][w ^ 1] = fa;
fath[fa] = now_;
push_up(fa), push_up(now_);
}
void update(int now_) {
if(!Isroot(now_)) update(fath[now_]);
push_down(now_);
}
void splay(int now_) {
update(now_);
for (; !Isroot(now_); rootate(now_)) {
if(!Isroot(fath[now_]))
rootate(Whichson(fath[now_]) == Whichson(now_) ? fath[now_] : now_);
}
}
void access(int now_) {
for (int last_ = 0; now_;last_ = now_, now_ = fath[now_]) {
splay(now_), rs = last_;
push_up(now_);
}
}
void make_root(int now_) {
access(now_), splay(now_);
push_reverse(now_);
}
int Find(int now_) {
access(now_), splay(now_);
while(ls) push_down(now_) ,now_ = ls;
splay(now_);
return now_;
}
void split(int x, int y) {
make_root(x);
access(y);
splay(y);
}
void link(int x,int y) {
make_root(x);
if(Find(y) != x) fath[x] = y;
}
void cut(int x, int y) {
make_root(x);
if(Find(y) != x || fath[y] != x || son[y][0]) return ;
fath[y] = son[x][1] = 0;
push_up(x);
}
void Modify(int x, int y, int val_, int tpy) {
split(x, y);
if(!tpy) push_add(y, val_);
else push_mul(y, val_);
}
int query(int x, int y) {
split(x, y);
return sum[y];
}
#undef ls
#undef rs
}
char opt[10];
signed main() {
n = read(), q = read();
for (int i = 1; i <= n; ++i) {
val[i] = mul[i] = siz[i] = 1;
}
for (int i = 1, u, v; i < n; ++i) {
u = read(), v = read();
LCT::link(u, v);
}
while(q--) {
cin >> opt;
int u = read(), v = read(), x;
if(opt[0] == '+') x = read(), LCT::Modify(u, v, x, 0);
if(opt[0] == '*') x = read(), LCT::Modify(u, v, x, 1);
if(opt[0] == '/') printf("%lld\n", LCT::query(u, v));
if(opt[0] == '-') {
LCT::cut(u, v), u = read(), v = read(), LCT::link(u, v);
}
}
system("pause");
return 0;
}
- 做题是加深对知识点理解的最好方法
题目要求:
- 对于操作 \(1\) 就是简单的\(\text{link}\)
- 对于操作\(2\) 就是求 \(u \to v\) 简单路径的个数
如何解决操作 \(2\) 统计以 $u $ 为根的子树的虚儿子的个数, 然后统计以
\(v\) 为 根的虚儿子的个数, 然后做乘法原理即可
扯一句
今天突然就是意识到昨天做的一个题为啥错了, 就是认为简单的判断是否连通需要 \(\text{reverse}\), 没有想到 \(\text{splay}\) 转根的时候会造成翻转,左右儿子要翻转
而且为啥要使用轻边?目的就是为了转根,而且是为了转根的时候不会造成原树的结构的改变,保证答案的正确
solution:
考虑如何统计子树中点的个数
开两个数组 \(\text{siz}\) 和 \(\text{s}\)
\(\text{siz} \to 虚儿子的数目\)
\(\text{s} \to 全部的儿子\)
\(\text{s}\) 好维护 只需要 更改的时候 \(\text{push_up}\) 即可
考虑如何维护 \(\text{siz}\) 我们 \(\text{access}\) 的时候断边连边才会引起 \(\text{siz}\) 的改变,那就考虑断边和连边影响, 我们断了一条实边, 那我们就减去这条实边所贡献的虚点的价值,然后加上新连的边的所贡献的虚点的价值,顺便进行一次 \(\text{push_up}\) 维护 \(\text{s}\)
code:
/*
Author:Imy_isLy
知识点:
*/
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#define int long long
using namespace std;
const int N = 1e5+100;
const int inf = 0x3f3f3f3f;
//=============================================================
int n, m;
int son[N][2], fath[N], siz[N], s[N];bool Rev[N];
//=============================================================
int read() {
int s = 0 , f = 0 ; char ch = getchar() ;
while(!isdigit(ch)) f |= (ch == '-') , ch = getchar();
while(isdigit(ch)) s = s * 10 + (ch ^ 48) , ch = getchar();
return f ? -s : s;
}
namespace LCT{
#define ls son[now_][0]
#define rs son[now_][1]
int Isroot(int now_) {
return now_ != son[fath[now_]][0] && now_ != son[fath[now_]][1];
}
void push_up(int now_) {
s[now_] = s[ls] + s[rs] + siz[now_] + 1;
return ;
}
void push_down(int now_) {
if(Rev[now_]) {
swap(ls, rs);
Rev[ls] ^= 1, Rev[rs] ^= 1, Rev[now_] = 0;
}
}
void updata(int now_) {
if(!Isroot(now_)) updata(fath[now_]) ;
push_down(now_);
}
int Whichson(int now_) {
return now_ == son[fath[now_]][1];
}
void rootate(int now_) {
int fa = fath[now_], w = Whichson(now_);
if (!Isroot(fath[now_])) son[fath[fa]][Whichson(fa)] = now_;
fath[now_] = fath[fa];
son[fa][w] = son[now_][w ^ 1];
fath[son[fa][w]] = fa;
son[now_][w ^ 1] = fa;
fath[fa] = now_;
push_up(fa), push_up(now_);
return ;
}
void splay(int now_) {
updata(now_);
for (; !Isroot(now_); rootate(now_)) {
if(!Isroot(fath[now_]))
rootate(Whichson(fath[now_]) == Whichson(now_) ? fath[now_] : now_);
}
// while(!Isroot(now_)) rootate(now_);
// push_up(now_);
}
void access(int now_) {
for (int last_ = 0; now_; last_ = now_, now_ = fath[now_]) {
splay(now_) ,siz[now_] += s[rs] ;
siz[now_] -= s[rs = last_];
// push_up(now_);
}
}
void make_root(int now_) {
access(now_);
splay(now_);
Rev[now_] ^= 1;
}
void split(int x, int y) {
make_root(x);
access(y);
splay(y);
}
void link(int x, int y) {
split(x, y);
siz[fath[x] = y] += s[x];
push_up(y);
}
}
signed main() {
n = read(), m = read();
for (int i = 1; i <= n; ++i) s[i] = 1;
while(m --) {
char opt ;cin >> opt;
int x = read(), y = read();
if(opt == 'A') LCT::link(x, y);
else {
LCT::split(x, y);
cout << (siz[x] + 1ll) * (siz[y] + 1ll) << "\n";
}
}
system("pause");
return 0;
}
简化题意:
每一个点都有一个点权 \(\text{val}\),可到达自身点号 + \(\text{val}\) 的点
- 从点 \(\text{x}\) 开始最少经过几个点才能到达超过点号大于 \(\text{n}\)的点
- 单点修改
solution:
我们可以建立一个虚点 \(\text{n + 1}\) 表示到达 \(\text{n + 1}\) 号点就不能再走了,然后统计一个 \(\text{siz}\) , \(\text{siz[x]}\) 表示在 \(\text{splay}\) 中以 \(\text{x}\) 为根有几个点
对于操作 \(1\) 我们之间在点 \(\text{n+1}\) 和点 \(\text{x}\) 之间拉起一条实链输出 \(\text{siz[n + 1]}\)
对于操作 \(2\) 我们可以先断边 修改点权, 然后再重新连边不要被链式前向星给带偏了
code:
/*
Author:Imy_isLy
知识点:
*/
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <string>
#define ll long long
using namespace std;
const int N = 2e5+100;
const int inf = 0x3f3f3f3f;
//=============================================================
int n;
int k[N];
int son[N][2], fath[N], Rev[N], siz[N];
//=============================================================
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while (isdigit(ch)) s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
namespace LCT {
#define ls son[now_][0]
#define rs son[now_][1]
void push_up(int now_) {
siz[now_] = siz[ls] + siz[rs] + 1;
return;
}
void push_reverse(int now_) {
if(!now_) return ;
swap(ls, rs);
Rev[now_] ^= 1;
}
void push_down(int now_) {
if (!Rev[now_]) return;
if (ls) push_reverse(ls);
if (rs) push_reverse(rs);
Rev[now_] = 0;
}
int Isroot(int now_) {
return now_ != son[fath[now_]][1] && now_ != son[fath[now_]][0];
}
int Whichson(int now_) {
return now_ == son[fath[now_]][1];
}
void rootate(int now_) {
int fa = fath[now_], w = Whichson(now_);
if (!Isroot(fa)) son[fath[fa]][Whichson(fa)] = now_;
fath[now_] = fath[fa];
son[fa][w] = son[now_][w ^ 1];
fath[son[now_][w ^ 1]] = fa;
son[now_][w ^ 1] = fa;
fath[fa] = now_;
push_up(fa), push_up(now_);
}
void update(int now_) {
if (!Isroot(now_)) update(fath[now_]);
push_down(now_);
}
void splay(int now_) {
update(now_);
for (; !Isroot(now_); rootate(now_)) {
if(!Isroot(fath[now_]))
rootate(Whichson(fath[now_]) == Whichson(now_) ? fath[now_] : now_);
// puts("LKP AK IOI");
}
}
void access(int now_) {
for (int last_ = 0; now_; now_ = fath[last_ = now_]) {
// puts("LKP AK IOI");
splay(now_), rs = last_;
push_up(now_);
}
}
void make_root(int now_) {
access(now_);
splay(now_);
// puts("LKP AK IOI");
push_reverse(now_);
}
int Find(int now_) {
access(now_);
splay(now_);
while (ls) push_down(now_), now_ = ls;
splay(now_);
return now_;
}
void split(int x, int y) {
make_root(x);
// puts("LKP AK IOI");
access(y);
splay(y);
return;
}
void link(int x, int y) {
make_root(x);
fath[x] = y;
}
void cut(int x, int y) {
split(x, y);
fath[x] = son[y][0] = 0;
push_up(y);
}
} // namespace LCT
int main() {
n = read();
for (int i = 1; i <= n; ++i) siz[i] = 1;
for (int i = 1; i <= n; ++i)
k[i] = read(), LCT::link(i, i + k[i] > n ? n + 1 : i + k[i]);
int m = read();
for (int i = 1; i <= m; ++i) {
int opt = read(), x = read() + 1;
if (opt == 1) {
LCT::split(x, n + 1), printf("%d\n", siz[n + 1] - 1);
}
else {
LCT::cut(x, x + k[x] > n ? n + 1 : x + k[x]),
k[x] = read(),
LCT::link(x, x + k[x] > n ? n + 1 : x + k[x] );
}
}
system("pause");
return 0;
}
写在前面:
建 \(\text{m}\) 条边, 输入的时候打成了 \(\text{n}\) ,以为 \(\text{LCT}\) 挂了调了两晚上 ,我两晚上干啥不好呢
**solution: **
\(\text{c <= 10}\)
考虑每一种颜色都建一颗 \(\text{LCT}\) 进行维护每一种颜色间的数据,便于查询同种颜色直接一条链上的最大值,改颜色就开个 \(\text{map}\) 记录一下暴力记录一下并判断,删边重连即可
/*
Author:Imy_isLy
知识点:
*/
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <string>
#define ll long long
using namespace std;
const int N = 1e5 + 100;
const int inf = 0x3f3f3f3f;
//=============================================================
int n, m, c, k;
int val[N];
//=============================================================
int read() {
int s = 0, f = 0;
char ch = getchar();
while (!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while (isdigit(ch)) s = s * 10 + (ch ^ 48), ch = getchar();
return f ? -s : s;
}
struct Link_Cut_Tree{
#define ls son[now_][0]
#define rs son[now_][1]
int fath[N], maxx[N], Rev[N], sta[N], son[N][2], cnt[N];
int q[N], top;
Link_Cut_Tree () {}
void push_up(int now_) {
maxx[now_] = val[now_];
if(ls) maxx[now_] = max(maxx[now_], maxx[ls]);
if(rs) maxx[now_] = max(maxx[now_], maxx[rs]);
return ;
}
void push_down(int now_) {
if(Rev[now_]) {
Rev[ls] ^= 1;
Rev[rs] ^= 1;
Rev[now_] ^= 1;
swap(ls, rs);
}
}
int Isroot(int now_){
return son[fath[now_]][1] != now_
&& son[fath[now_]][0] != now_;
}
int Whichson(int now_) {
return son[fath[now_]][1] == now_;
}
void rootate(int now_) {
int fa = fath[now_], w = Whichson(now_);
if(!Isroot(fa)) son[fath[fa]][Whichson(fa)] = now_;
fath[now_] = fath[fa];
son[fa][w] = son[now_][w ^ 1];
fath[son[now_][w ^ 1]] = fa;
son[now_][w ^ 1] = fa;
fath[fa] = now_;
push_up(fa), push_up(now_);
}
void update(int now_) {
if(!Isroot(now_)) update(fath[now_]);
push_down(now_);
}
void splay(int now_) {
update(now_);
for (; !Isroot(now_); rootate(now_)) {
if(!Isroot(fath[now_]))
rootate(Whichson(fath[now_]) == Whichson(now_) ? fath[now_] : now_);
}
}
void access(int now_) {
for (int last_ = 0; now_; now_ = fath[last_ = now_]) {
splay(now_), rs = last_,push_up(now_);
}
}
void make_root(int now_) {
access(now_);
splay(now_);
Rev[now_] ^= 1;
return ;
}
void link(int x, int y) {
cnt[x] ++, cnt[y]++;
make_root(x);
fath[x] = y;
splay(x);
}
void split(int x, int y) {
make_root(x);
access(y);
splay(y);
}
void cut(int x, int y) {
split(x, y);
cnt[x]--, cnt[y]--;
son[y][0] = fath[x] = 0;
push_up(y);
}
int Find(int now_) {
access(now_);
splay(now_);
while(ls) push_down(now_), now_ = ls;
splay(now_);
return now_;
}
int query_max(int x, int y) {
split(x, y);
return maxx[y];
}
}LCT[15];
struct Edge {
int u, v;
bool operator < (const Edge& b) const {
if (u != b.u)
return u < b.u;
else
return v < b.v;
}
};
map<Edge, int> mp;
int main() {
n = read(), m = read(), c = read(), k = read();
for (int i = 1; i <= n; ++i) val[i] = read();
for (int i = 1, u, v, w; i <= m; ++i) {
u = read(), v = read(), w = read() + 1;
Edge e1 = (Edge){u, v}, e2 = (Edge){v, u};
mp[e1] = mp[e2] = w;
LCT[w].link(u, v);
}
for (int i = 1 ; i <= k; ++i){
// cout<< "id:" << i <<"\n";
int opt = read();
if (opt == 0) {
int x = read(), w = read();
val[x] = w;
for (int i = 1; i <= c; ++i) LCT[i].splay(x);
} else if (opt == 1) {
int u = read(), v = read(), w = read() + 1;
Edge a = (Edge){u, v}, b = (Edge){v, u};
if (!mp.count(a)) {
puts("No such edge.");
continue;
}
int flag = mp[a];
if (flag == w) {
puts("Success.");
continue;
}
if (LCT[w].cnt[u] >= 2 || LCT[w].cnt[v] >= 2) {
puts("Error 1.");
continue;
}
if (LCT[w].Find(u) == LCT[w].Find(v)) {
puts("Error 2.");
continue;
}
puts("Success.");
LCT[flag].cut(u, v);
LCT[w].link(u, v);
mp[a] = mp[b] = w;
} else {
int w = read() + 1, u = read(), v = read();
if (LCT[w].Find(u) != LCT[w].Find(v)) {
puts("-1");
continue;
}
printf("%d\n", LCT[w].query_max(u, v));
}
}
system("pause");
return 0;
}
我们发现删边特别难处理,那么怎样转化一下呢?
倒着处理所有询问,于是删边变成了加边。然后查询所有路径上最大值的最小值,
解释一下这里 : 就是首先把操作都记下来,把要删的边先都\(\text{cut}\),然后倒着处理,都重新 \(\text{link}\)
不难发现就是要维护这个图的最小生成树,然后就可以直接查询树路径上的最大值了,也可以用 \(\text{LCT}\)做到。那么问题转化为了动态维护最小生成树。
查询 \(u \to v\) 链上最大边权\(\text{max}\)
比较新加的边权 \(\text{w}\) 和 \(\text{w}\) 的大小关系,如果 \(\text{w>m}\),则不做任何操作;否则删去边权为\(\text{max}\)的边 \(\text{cut}\),加上 \(u \to v\)这条边 \(\text{link}\)。
notice:
在保存 \(\text{max}\) 的时候需要存的是边的编号,因为到时加边的时候需要用到。
\(\text{LCT}\) 似乎只能处理链上最大点权而无法保存边权。怎么办呢?我们可以考虑 把边看成点 ,加一条边 \(u \to v\),编号为 \(\text{id}\),则 \(\text{link(u, id)}\); \(\text{link(v, id)}\) ;删边同理。
在处理询问的时候需要找到某条边的编号,可以用 \(\text{STL}\) 开一个 \(\text{map}\) 记录边的编号。
貌似 \(\text{LCT}\) 的题经常会用到 \(\text{map}\) ,会起到很有意思的作用,而且 \(\text{map}\) 的下标貌似啥都行, 昨天写了个结构体,今天用了个 \(\text{pair}\) 就挺有意思的
/*
Author:Imy_isLy
知识点:
*/
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#include <map>
#define pr pair
#define mp make_pair
#define ll long long
#define Orz puts("LKP AK IOI")
using namespace std;
const int N = 5e5+100;
const int inf = 0x3f3f3f3f;
//=============================================================
int n, m;
int u[N], v[N], w[N];
map <pr<int, int>, int>del, id;
int q_num, q[N][3], ans[N];
//=============================================================
int read() {
int s = 0 , f = 0 ; char ch = getchar() ;
while(!isdigit(ch)) f |= (ch == '-') , ch = getchar();
while(isdigit(ch)) s = s * 10 + (ch ^ 48) , ch = getchar();
return f ? -s : s;
}
namespace LCT {
#define f fa[now_]
#define ls son[now_][0]
#define rs son[now_][1]
const int kMaxNode = N;
int fa[kMaxNode], son[kMaxNode][2], val[kMaxNode];
int maxval[kMaxNode], node[kMaxNode];
bool tagrev[kMaxNode];
void Pushup(int now_) {
maxval[now_] = val[now_], node[now_] = now_;
if (ls && maxval[ls] > maxval[now_]) {
maxval[now_] = maxval[ls], node[now_] = node[ls];
}
if (rs && maxval[rs] > maxval[now_]) {
maxval[now_] = maxval[rs], node[now_] = node[rs];
}
}
void PushReverse(int now_) {
if (!now_) return;
std::swap(ls, rs);
tagrev[now_] ^= 1;
}
void Pushdown(int now_) {
if (tagrev[now_]) PushReverse(ls), PushReverse(rs);
tagrev[now_] = 0;
}
bool IsRoot(int now_) {
return son[f][0] != now_ && son[f][1] != now_;
}
bool WhichSon(int now_) {
return son[f][1] == now_;
}
void Rotate(int now_) {
int fa_ = f, w = WhichSon(now_);
if (!IsRoot(f)) son[fa[f]][WhichSon(f)] = now_;
f = fa[f];
son[fa_][w] = son[now_][w ^ 1];
fa[son[fa_][w]] = fa_;
son[now_][w ^ 1] = fa_;
fa[fa_] = now_;
Pushup(fa_), Pushup(now_);
}
void Update(int now_) {
if (!IsRoot(now_)) Update(f);
Pushdown(now_);
}
void Splay(int now_) {
Update(now_);
for (; !IsRoot(now_); Rotate(now_)) {
if (!IsRoot(f)) Rotate(WhichSon(f) == WhichSon(now_) ? f : now_);
}
}
void Access(int now_) {
for (int last_ = 0; now_; last_ = now_, now_ = f) {
Splay(now_), rs = last_;
Pushup(now_);
}
}
void MakeRoot(int now_) {
Access(now_);
Splay(now_);
PushReverse(now_);
}
int Find(int now_) {
Access(now_);
Splay(now_);
while (ls) Pushdown(now_), now_ = ls;
Splay(now_);
return now_;
}
void Split(int x_, int y_) {
MakeRoot(x_);
Access(y_);
Splay(y_);
}
void Link(int x_, int y_) {
MakeRoot(x_);
if (Find(y_) != x_) fa[x_] = y_;
}
void Cut(int x_, int y_) {
MakeRoot(x_);
if (Find(y_) != x_ || fa[y_] != x_ || son[y_][0]) return ;
fa[y_] = son[x_][1] = 0;
Pushup(x_);
}
int Query(int x_, int y_) {
Split(x_, y_);
return maxval[y_];
}
}
int main() {
n = read(), m = read(), q_num = read();
for (int i = 1; i <= n; ++i) LCT::val[i] = LCT::maxval[i] = 0;
for (int i = 1; i <= m; ++i) {
u[i] = read(), v[i] = read(), w[i] = read();
LCT::val[n + i] = LCT::maxval[n + i] = w[i];
LCT::node[n + i] = n + i;
if(u[i] > v[i]) swap(u[i], v[i]);
id[mp(u[i], v[i])] = i;
}
for (int i = 1, opt, u_, v_; i <= q_num; ++i) {
opt = read(), u_ = read(), v_ = read();
if(u_ > v_) swap(u_, v_);
q[i][0] = opt, q[i][1] = u_, q[i][2] = v_;
if(opt == 2) del[mp(u_, v_)] = true;
}
for (int i = 1; i <= m; ++i) {
int u_ = u[i], v_ = v[i], w_ = w[i];
if(del[mp(u_, v_)]) continue;
if(LCT::Find(u_) != LCT::Find(v_)) {
LCT::Link(u_, n + i), LCT::Link(v_, n + i);
} else {
LCT::Split(u_, v_);
int max_val = LCT::maxval[v_], node_ = LCT::node[v_];
if(max_val > w_) {
LCT::Cut(u[node_ - n], node_), LCT::Cut(v[node_ - n], node_);
LCT::Link(u_, n + i), LCT::Link(v_, n + i);
}
}
}
for(int i = q_num; i; --i){
int opt = q[i][0], u_ = q[i][1], v_ = q[i][2];
int id_ = id[mp(u_, v_)], w_ = w[id_];
if(opt == 1) {
ans[i] = LCT::Query(u_, v_);
} else if(opt == 2) {
if(LCT::Find(u_) != LCT::Find(v_)) {
LCT::Link(u_, n + id_), LCT::Link(v_, n + id_);
} else {
LCT::Split(u_, v_);
int max_val = LCT::maxval[v_], node_ = LCT::node[v_];
if(max_val > w_) {
LCT::Cut(u[node_ - n], node_), LCT::Cut(v[node_ - n], node_);
LCT::Link(u_, n + id_), LCT::Link(v_, n + id_);
}
}
}
}
for (int i = 1; i <= q_num; ++i) {
if(q[i][0] == 1) cout << ans[i] <<"\n";
}
return 0;
}