动态树概述
一、适用问题
动态树主要用于解决操作中带有加边、删边、换根的一系列问题,即树结构发生变化的问题,理论上来说,树链剖分的问题都能用 进行解决。
二、函数解析
本质是上对树进行实链剖分,实链剖分的意思就是将一棵树分成多条链,链中的边称为实边,链与链之间的边则称为虚边,每条链都是一个 ,在 中进行中序遍历即可还原原来的树结构。而 就是不断进行虚边、实边转换的一个算法。
中一共有 、 、 、 、 、 、 、 、 、 、 、 等函数,下面大致介绍一下每个函数的具体作用以及一些坑点,更多的是提纲挈领的作用,想要从最基础的地方开始学的话,推荐 oiwiki。
简单函数(仅操作单个 的函数)
- :清除一个点的信息,如父亲、左右儿子、标记、维护信息等信息。
- :由左右儿子的信息更新父节点的信息,与线段树的 函数没有太大差别。
- :将当前节点的标记下放到儿子节点,如加、减、翻转等标记。
- :一直递归到根节点,然后把标记信息不断下放,没有涉及任何虚实边的转换。
void update(int p){ //递归地从上到下pushDown信息
if(!isRoot(p)) update(f[p]);
pushDown(p);
}
- :将当前节点向上旋转一层,可以自己模拟一下。此处改变了 的内部结构,即子节点发生了改变,因此需要进行 ,但是仍然没有进行任何虚实边的转换。
inline void rotate(int x){ //将x向上旋转一层的操作
int y = f[x], z = f[y], k = Get(x);
if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
ch[x][!k] = y, f[y] = x, f[x] = z;
pushUp(y); //要先pushUp(y)
pushUp(x);
}
- :将当前点旋转到 的根节点, 到根节点作用在于不需要在向上进行更新。比如你现在要修改 的点权,但是每个节点还要维护子树 的信息,如果 不是其所在 的根节点,那么修改 的点权势必影响到其祖先节点的 信息,因此需要将 旋转为其所在 的根后再进行单点修改。注意 函数也没有进行实边和虚边的转换。
inline void splay(int x){ //把x旋转到当前splay的根
update(x); //将上面的标记完全下放
for(int fa; fa = f[x], !isRoot(x); rotate(x)){
if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
}
}
以上函数都属于 函数中的简单函数,因为这些函数都只是在单个 中进行操作,不涉及任何虚实边的转换。
复杂函数(涉及多个 的操作,进行虚实边转换)
- :将点 到根的路径经过的点放入同一个 中,且这个 中仅包含从 到根路径上经过的点。具体操作即是将 点不断转成其所在 的根,然后再进行虚实边转换一直到根。此处 函数有返回值,返回值为最后构成的 的根节点。
inline int access(int x){ //把从根到x的所有点放在一条实链里, 返回这个splay的根
int p; //每次改变右儿子的值,因为整棵树是中序遍历,放入右儿子才能保证先遍历父亲再遍历儿子
for(p = 0; x; p = x, x = f[x]){
splay(x), ch[x][1] = p, pushUp(x);
}
return p;
}
- :换根操作,将点 变成当前树的根。具体过程为先 点 ,然后再将点 旋转为其 所在根,然后将所有节点的左右儿子翻转即可。
inline void makeRoot(int p){ //使x点成为整棵树的根
access(p); splay(p);
swap(ch[p][0],ch[p][1]); //把整条链反向
rev[p] ^= 1;
}
- :从树中拎出 的路径,返回该路径的 根节点,可以查询路径最大值、点权和、边权和等信息。
inline int split(int x,int y){
makeRoot(x);
return access(y);
}
- :即返回点 所在树的根节点,不是所在 中的根节点,用于判断两点是否连通。
inline int find(int p){ //找到x所在树的根节点编号
access(p), splay(p);
while(ls) pushDown(p), p = ls;
return p;
}
- :连接树中 、 两点之间的边,无边变虚边,如果题目中没有保证操作一定合法,则需要自行判断 、 是否已经连通。
inline void link(int x,int y){ //在x、y两点间连一条边,连接了虚边
if (find(x) != find(y)) makeRoot(x), f[x] = y;
}
- :断开树中 、 两点之间的实边,两个点同时断开即可。
inline void cut(int x,int p){ //把x、y两点间边删掉,此处删除的是实边,注意实边和虚边的区别
makeRoot(x), access(p), splay(p);
if (ls == x && !rs) ls = f[x] = 0;
}
三、具体细节
单点修改
由于 中维护了多个 ,因此单点修改需要把该点修改的信息不断上传,所以我们需要先将点 旋转到 的根或者整棵树的根,然后再进行单点修改。
如果题中只需要维护实链信息,则只需要旋转到 的根,如果需要同时维护实链和虚链信息,即整棵子树的信息的话,则需要令该点为树根,即调用 函数。
维护边权
由于 是不断地进行虚边、实边转换,因此没有固定的边结构,所以直接维护边权十分困难,因此我们将边转成点,边 成为一个点 , 、 即可。
维护子树信息
普通 只能维护具有可减性的子树信息,比如子树大小,子树贡献等,而子树 、 等问题则不具有可减性,难以维护。
维护子树信息主要在于维护实边信息和虚边信息,而进行实虚转换的函数只有 、 、 三个函数,只需要在该三个函数进行一定的修改即可,下面习题中包含了该问题可供参考。
动态树习题
1. [国家集训队] Tree II(模板题)
题意:
个点一棵树,支持四种操作。
- ,将 到 的路径上的点的权值都加上 。
- ,将树中原有的边 删除,加入一条新边 ,保证操作完之后仍然是一颗树。
- \ ,将 到 的路径上的点的权值都乘上 。
- / ,询问 到 的路径上的点的权值和,答案 。
思路:
三个涉及到路径的操作,都是先把
变成树根,然后
,即拉起一条
到
的路径,使得
到
路径上的点都在一个
中,然后获得这个
的根节点,即可对根节点打标记完成。
删边则是令 为根,拉起 到 的路径,将 旋转成 的根,然后儿子与父亲双向断开联系。加边则是令 为根,然后使 的父亲变成 。
总结:
这题应该算是
的模板题,涉及的操作都是基础操作,没有太多思维上的难点。
代码:
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
using namespace std;
#define int long long
const int N = 100010;
const int mod = 51061;
int n, q, u, v, c;
char op;
struct LCT{
#define ls ch[p][0]
#define rs ch[p][1]
#define Get(p) (ch[f[p]][1] == p)
int ch[N][2], f[N], sum[N], val[N], siz[N], rev[N], add[N], mul[N];
inline void clear(int p){ //清除这个点的信息
ch[p][0] = ch[p][1] = f[p] = siz[p] = val[p] = sum[p] = rev[p] = add[p] = 0;
mul[p] = 1;
}
inline int isRoot(int p){
clear(0);
return ch[f[p]][0] != p && ch[f[p]][1] != p;
}
inline void pushUp(int p){
clear(0);
siz[p] = (siz[ls] + 1 + siz[rs]) % mod;
sum[p] = (sum[ls] + val[p] + sum[rs]) % mod;
}
inline void pushDown(int p){
clear(0);
if(mul[p] != 1){ //乘法
if(ls){ //左儿子
mul[ls] = (mul[ls] * mul[p]) % mod;
val[ls] = (val[ls] * mul[p]) % mod;
sum[ls] = (sum[ls] * mul[p]) % mod;
add[ls] = (add[ls] * mul[p]) % mod;
}
if(rs){ //右儿子
mul[rs] = (mul[rs] * mul[p]) % mod;
val[rs] = (val[rs] * mul[p]) % mod;
sum[rs] = (sum[rs] * mul[p]) % mod;
add[rs] = (add[rs] * mul[p]) % mod;
}
mul[p] = 1;
}
if(add[p]){
if(ls){
add[ls] = (add[ls] + add[p]) % mod;
val[ls] = (val[ls] + add[p]) % mod;
sum[ls] = (sum[ls] + add[p] * siz[ls] % mod) % mod;
}
if(rs){
add[rs] = (add[rs] + add[p]) % mod;
val[rs] = (val[rs] + add[p]) % mod;
sum[rs] = (sum[rs] + add[p] * siz[rs] % mod) % mod;
}
add[p] = 0;
}
if(rev[p]){
if(ls) rev[ls] ^= 1, swap(ch[ls][0],ch[ls][1]);
if(rs) rev[rs] ^= 1, swap(ch[rs][0],ch[rs][1]);
rev[p] = 0;
}
}
void update(int p){ //递归地从上到下pushDown信息
//没有将实边变成虚边
if(!isRoot(p)) update(f[p]);
pushDown(p);
}
inline void rotate(int x){ //将x向上旋转一层的操作
//没有将实边变成虚边
int y = f[x], z = f[y], k = Get(x);
if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
ch[x][!k] = y, f[y] = x, f[x] = z;
pushUp(y); //要先pushUp(y)
pushUp(x);
}
inline void splay(int x){ //把x旋转到当前splay的根
//没有将实边变成虚边
update(x); //将上面的标记完全下放
for(int fa; fa = f[x], !isRoot(x); rotate(x)){
if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
}
}
inline int access(int x){ //把从根到x的所有点放在一条实链里, 返回这个splay的根
//进行了边的虚实变换
int p; //每次改变右儿子的值,因为整棵树是中序遍历,放入右儿子才能保证先遍历父亲再遍历儿子
for(p = 0; x; p = x, x = f[x]){
splay(x), ch[x][1] = p, pushUp(x);
}
return p;
}
inline void makeRoot(int p){ //使x点成为整棵树的根
access(p); splay(p);
swap(ch[p][0],ch[p][1]); //把整条链反向
rev[p] ^= 1;
}
inline void link(int x,int y){ //在x、y两点间连一条边,连接了虚边
if (find(x) != find(y)) makeRoot(x), f[x] = y;
}
inline void cut(int x,int p){ //把x、y两点间边删掉,此处删除的是实边,注意实边和虚边的区别
makeRoot(x), access(p), splay(p);
if (ls == x && !rs) ls = f[x] = 0;
}
inline int find(int p){ //找到x所在树的根节点编号
access(p), splay(p);
while(ls) pushDown(p), p = ls;
return p;
}
//中序遍历即可还原树结构
void print(int p){
if(!p) return;
pushDown(p);
print(ls);
printf("%lld ",p);
print(rs);
}
}st;
signed main() {
scanf("%lld%lld", &n, &q);
for (int i = 1; i <= n; i++) st.val[i] = 1;
for (int i = 1; i < n; i++) {
scanf("%lld%lld", &u, &v);
st.link(u,v);
}
while (q--) {
scanf(" %c%lld%lld", &op, &u, &v);
if (op == '+') { //+ u v c, u到v的路径上的点权值+c
scanf("%lld", &c);
//u变成树根,拉起v到u的链,把v旋到splay的根
st.makeRoot(u); v = st.access(v);
st.val[v] = (st.val[v] + c) % mod;
st.sum[v] = (st.sum[v] + st.siz[v] * c % mod) % mod;
st.add[v] = (st.add[v] + c) % mod;
}
if (op == '-') { //- u1 v1 u2 v2, 删除(u1,v1), 加上(u2,v2)
st.cut(u,v);
scanf("%lld%lld", &u, &v);
st.link(u,v);
}
if (op == '*') { //* u v c, u到v的路径乘上c
scanf("%lld", &c);
st.makeRoot(u); v = st.access(v);
st.val[v] = st.val[v] * c % mod;
st.sum[v] = st.sum[v] * c % mod;
st.mul[v] = st.mul[v] * c % mod;
}
if (op == '/'){ //u v, 询问u到v路径权值和
st.makeRoot(u); v = st.access(v);
printf("%lld\n", st.sum[v]);
}
}
return 0;
}
2. Query on a tree
题意:
个点一棵树,支持两种操作。
- ,将第 条边的边权改为 。
- ,查询树中点 到点 的路径中的边权最大值。
思路:
边权
,需要对于每一条边建一个节点,即树中一共有
个节点,每个边节点与上下两个节点连边。
建边需要先确定每个节点的边权之后再进行 ,因为点修改会对该节点的祖先节点产生影响,需要将该点旋至 端点后才能进行修改。
总结:
总结一下构建
构建的关键,构建
需要先对各个顶点赋值然后再进行
操作,若是
之后再赋值相当于点修改,而点修改需要将点旋为
根之后才能进行更改。
代码:
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
using namespace std;
const int N = 20000+10;
int n,val[N];
struct LCT{
#define ls ch[p][0]
#define rs ch[p][1]
#define Get(p) (ch[f[p]][1] == p)
int ch[N][2], f[N], maxn[N], val[N], siz[N], rev[N];
inline void clear(int p){ //清除这个点的信息
ch[p][0] = ch[p][1] = f[p] = siz[p] = val[p] = maxn[p] = 0;
}
inline int isRoot(int p){
clear(0);
return ch[f[p]][0] != p && ch[f[p]][1] != p;
}
inline void pushUp(int p){
clear(0);
siz[p] = siz[ls] + 1 + siz[rs];
maxn[p] = max(val[p],max(maxn[ls],maxn[rs]));
}
inline void pushDown(int p){
clear(0);
if(rev[p]){
if(ls) rev[ls] ^= 1, swap(ch[ls][0],ch[ls][1]);
if(rs) rev[rs] ^= 1, swap(ch[rs][0],ch[rs][1]);
rev[p] = 0;
}
}
void update(int p){ //递归地从上到下pushDown信息
if(!isRoot(p)) update(f[p]);
pushDown(p);
}
inline void rotate(int x){ //将x向上旋转一层的操作
int y = f[x], z = f[y], k = Get(x);
if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
ch[x][!k] = y, f[y] = x, f[x] = z;
pushUp(y); //要先pushUp(y)
pushUp(x);
}
inline void splay(int x){ //把x旋转到当前splay的根
update(x); //将上面的标记完全下放
for(int fa; fa = f[x], !isRoot(x); rotate(x)){
if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
}
}
inline int access(int x){ //把从根到x的所有点放在一条实链里, 返回这个splay的根
int p;
for(p = 0; x; p = x, x = f[x]){
splay(x), ch[x][1] = p, pushUp(x);
}
return p;
}
inline void makeRoot(int p){ //使x点成为整棵树的根
access(p); splay(p);
swap(ch[p][0],ch[p][1]); //把整条链反向
rev[p] ^= 1;
}
inline void link(int x,int y){ //在x、y两点间连一条边
makeRoot(x), f[x] = y; //dfs建树, 每条边都是有效的, 因此不需要判断是否有效
}
}st;
int main(){
int _; scanf("%d",&_);
while(_--){
scanf("%d",&n);
rep(i,0,2*n) st.clear(i);
rep(i,1,n-1){
int a,b,c; scanf("%d%d%d",&a,&b,&c);
st.val[i+n] = c;
st.link(a,i+n);
st.link(i+n,b);
}
while(1){
char s[20]; scanf("%s",s);
if(s[0] == 'D') break;
else if(s[0] == 'C'){
int a,b; scanf("%d%d",&a,&b);
st.splay(a+n); //先转为splay根节点
st.val[a+n] = b;
}
else{
int a,b; scanf("%d%d",&a,&b);
st.makeRoot(a);
b = st.access(b);
printf("%d\n",st.maxn[b]);
}
}
}
return 0;
}
3. Can you answer these queries VII
题意:
个点一棵树,每个节点有一个值,支持两种操作。
- ,查询树中点 到点 的路径中最大连续和
- ,将树中点 到点 路径中所有点的值改为
思路:
对每一个点维护一个
、
、
表示点
子树中左连续的最大值、右连续的最大值以及整棵子树中的最大连续值。
需要注意一点,交换左右儿子的时候,还需要把每个节点的 和 进行交换,其余细节见代码。
代码:
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
using namespace std;
typedef long long ll;
const ll inf = 1e9+100;
const int N = 1e5+10;
int n,Q;
struct LCT{
#define ls ch[p][0]
#define rs ch[p][1]
#define Get(p) (ch[f[p]][1] == p)
int ch[N][2], f[N];
ll maxn[N], sum[N], lc[N], rc[N], val[N], siz[N], lazy[N];
bool rev[N];
inline void clear(int p){ //清除这个点的信息
ch[p][0] = ch[p][1] = f[p] = val[p] = maxn[p] = lc[p] = sum[p] = rc[p] = siz[p] = 0;
}
inline int isRoot(int p){
return ch[f[p]][0] != p && ch[f[p]][1] != p;
}
inline void pushUp(int p){
siz[p] = siz[ls] + 1 + siz[rs];
sum[p] = val[p] + sum[ls] + sum[rs]; //ls、rs可能为0
maxn[p] = max(maxn[ls],max(maxn[rs],rc[ls]+lc[rs]+val[p]));
lc[p] = max(lc[ls],sum[ls]+val[p]+lc[rs]);
rc[p] = max(rc[rs],sum[rs]+val[p]+rc[ls]);
}
inline void pushDown(int p){
if(rev[p]){
if(ls) rev[ls] ^= 1, swap(ch[ls][0],ch[ls][1]);
if(rs) rev[rs] ^= 1, swap(ch[rs][0],ch[rs][1]);
swap(lc[ls],rc[ls]); swap(lc[rs],rc[rs]); //交换左右儿子时还要交换左右连续最大值
rev[p] = 0;
}
if(lazy[p] != -inf){
if(ls){
sum[ls] = siz[ls]*lazy[p];
val[ls] = lazy[ls] = lazy[p];
lc[ls] = rc[ls] = maxn[ls] = lazy[p] > 0 ? sum[ls]:0;
}
if(rs){
sum[rs] = siz[rs]*lazy[p];
val[rs] = lazy[rs] = lazy[p];
lc[rs] = rc[rs] = maxn[rs] = lazy[p] > 0 ? sum[rs]:0;
}
lazy[p] = -inf;
}
}
void update(int p){ //递归地从上到下pushDown信息
if(!isRoot(p)) update(f[p]);
pushDown(p);
}
inline void rotate(int x){ //将x向上旋转一层的操作
int y = f[x], z = f[y], k = Get(x);
if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
ch[x][!k] = y, f[y] = x, f[x] = z;
pushUp(y); //要先pushUp(y)
pushUp(x);
}
inline void splay(int x){ //把x旋转到当前splay的根
update(x); //将上面的标记完全下放
for(int fa; fa = f[x], !isRoot(x); rotate(x)){
if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
}
}
inline int access(int x){ //把从根到x的所有点放在一条实链里, 返回这个splay的根
int p;
for(p = 0; x; p = x, x = f[x]){
splay(x), ch[x][1] = p, pushUp(x);
}
return p;
}
inline void makeRoot(int p){ //使x点成为整棵树的根
access(p);
splay(p);
swap(ch[p][0],ch[p][1]); //把整条链反向
rev[p] ^= 1;
}
inline void link(int x,int y){ //在x、y两点间连一条边
makeRoot(x), f[x] = y; //dfs建树, 每条边都是有效的, 因此不需要判断是否有效
}
}st;
int main(){
scanf("%d",&n);
rep(i,1,n){
scanf("%lld",&st.val[i]);
st.siz[i] = 1; st.sum[i] = st.val[i];
st.lazy[i] = -inf;
st.lc[i] = st.rc[i] = st.maxn[i] = st.val[i] > 0 ? st.val[i]:0;
}
rep(i,1,n-1){
int a,b; scanf("%d%d",&a,&b);
st.link(a,b);
}
scanf("%d",&Q);
while(Q--){
int op; scanf("%d",&op);
if(op == 1){ //a->b max
int a,b; scanf("%d%d",&a,&b);
st.makeRoot(a);
b = st.access(b);
printf("%lld\n",st.maxn[b]);
}
else{ //a->b to c
int a,b; ll c; scanf("%d%d%lld",&a,&b,&c);
st.makeRoot(a);
b = st.access(b);
st.val[b] = st.lazy[b] = c;
st.sum[b] = st.siz[b]*c;
st.lc[b] = st.rc[b] = st.maxn[b] = c > 0 ? st.sum[b]:0;
}
}
return 0;
}
4. [ZJOI2008] 树的统计 Count
题意:
一棵树上有
个节点,每个节点都有一个权值
。支持三种操作:
:把结点
的权值改为
;
:询问从点
到点
的路径上的节点的最大权值;
:询问从点
到点
的路径上的节点的权值和。
思路:
单点查询 + 路径最大值 + 路径
和,非常裸的题目,纯当练习。
代码:
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
using namespace std;
typedef long long ll;
const int inf = 1e9+100;
const int N = 40000+10;
int n,Q,A[N],B[N];
struct LCT{
#define ls ch[p][0]
#define rs ch[p][1]
#define Get(p) (ch[f[p]][1] == p)
int ch[N][2], f[N];
int maxn[N], sum[N], val[N];
bool rev[N];
inline void clear(int p){ //清除这个点的信息
ch[p][0] = ch[p][1] = f[p] = val[p] = maxn[p] = 0;
}
inline int isRoot(int p){
return ch[f[p]][0] != p && ch[f[p]][1] != p;
}
inline void pushUp(int p){
sum[p] = val[p] + sum[ls] + sum[rs]; //ls、rs可能为0
maxn[p] = max(val[p],max(maxn[ls],maxn[rs]));
}
inline void pushDown(int p){
if(rev[p]){
if(ls) rev[ls] ^= 1, swap(ch[ls][0],ch[ls][1]);
if(rs) rev[rs] ^= 1, swap(ch[rs][0],ch[rs][1]);
rev[p] = 0;
}
}
void update(int p){ //递归地从上到下pushDown信息
if(!isRoot(p)) update(f[p]);
pushDown(p);
}
inline void rotate(int x){ //将x向上旋转一层的操作
int y = f[x], z = f[y], k = Get(x);
if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
ch[x][!k] = y, f[y] = x, f[x] = z;
pushUp(y); //要先pushUp(y)
pushUp(x);
}
inline void splay(int x){ //把x旋转到当前splay的根
update(x); //将上面的标记完全下放
for(int fa; fa = f[x], !isRoot(x); rotate(x)){
if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
}
}
inline int access(int x){ //把从根到x的所有点放在一条实链里, 返回这个splay的根
int p;
for(p = 0; x; p = x, x = f[x]){
splay(x), ch[x][1] = p, pushUp(x);
}
return p;
}
inline void makeRoot(int p){ //使x点成为整棵树的根
access(p);
splay(p);
swap(ch[p][0],ch[p][1]); //把整条链反向
rev[p] ^= 1;
}
inline void link(int x,int y){ //在x、y两点间连一条边
makeRoot(x), f[x] = y; //dfs建树, 每条边都是有效的, 因此不需要判断是否有效
}
}st;
int main(){
scanf("%d",&n);
st.maxn[0] = -inf;
rep(i,1,n-1) scanf("%d%d",&A[i],&B[i]);
rep(i,1,n){
int hp; scanf("%d",&hp);
st.val[i] = st.maxn[i] = st.sum[i] = hp;
}
rep(i,1,n-1) st.link(A[i],B[i]);
scanf("%d",&Q);
while(Q--){
char s[20]; int u,v;
scanf("%s%d%d",s,&u,&v);
if(s[0] == 'C'){
st.splay(u);
st.val[u] = v;
st.pushUp(u);
}
else if(s[1] == 'M'){
st.makeRoot(u);
v = st.access(v);
printf("%d\n",st.maxn[v]);
}
else{
st.makeRoot(u);
v = st.access(v);
printf("%d\n",st.sum[v]);
}
}
return 0;
}
5. 最小差值生成树
题意:
个点,
条边的一个无向图,求边权最大值与最小值的差值最小的生成树。
思路:
关于这类特殊生成树问题,一般考虑用
动态维护树结构然后更新答案。
此题也可以这样考虑。将边按边权从小到大排序,如果 两点不连通,则加上该边,如果 两点连通,则将 路径上边权最小的边去除,然后连上当前的边。维护过程不断更新最大值与最小值的差值,不断取 即可。
因此只需要维护一个边权 ,并且维护路径最小值以及最小值点的编号,然后动态加边删边即可。还需要对在树中的边打上标记,去除的时候删去标记,用于查找整棵树中的最小边权。
代码:
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
using namespace std;
const int N = 2e5+5e4+100;
const int M = 2e5+10;
int n, m, vis[N], pos = 1, num, ans = 1e9;
struct Edge{
int a,b,w;
bool operator < (Edge xx) const {
return w < xx.w;
}
}e[N];
void dbg() {cout << "\n";}
template<typename T, typename... A> void dbg(T a, A... x) {cout << a << ' '; dbg(x...);}
#define logs(x...) {cout << #x << " -> "; dbg(x);}
struct LCT{
#define ls ch[p][0]
#define rs ch[p][1]
#define Get(p) (ch[f[p]][1] == p)
int ch[N][2], f[N], val[N], minn[N], mpos[N], rev[N];
inline void clear(int p){ //清除这个点的信息
ch[p][0] = ch[p][1] = f[p] = val[p] = mpos[p] = minn[p] = 0;
}
inline int isRoot(int p){
return ch[f[p]][0] != p && ch[f[p]][1] != p;
}
inline void pushUp(int p){
minn[p] = val[p]; mpos[p] = p;
if(ls && minn[ls] < minn[p]) minn[p] = minn[ls], mpos[p] = mpos[ls];
if(rs && minn[rs] < minn[p]) minn[p] = minn[rs], mpos[p] = mpos[rs];
}
inline void pushDown(int p){
if(rev[p]){
if(ls) rev[ls] ^= 1, swap(ch[ls][0],ch[ls][1]);
if(rs) rev[rs] ^= 1, swap(ch[rs][0],ch[rs][1]);
rev[p] = 0;
}
}
void update(int p){ //递归地从上到下pushDown信息
if(!isRoot(p)) update(f[p]);
pushDown(p);
}
inline void rotate(int x){ //将x向上旋转一层的操作
int y = f[x], z = f[y], k = Get(x);
if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
ch[x][!k] = y, f[y] = x, f[x] = z;
pushUp(y); //要先pushUp(y)
pushUp(x);
}
inline void splay(int x){ //把x旋转到当前splay的根
update(x); //将上面的标记完全下放
for(int fa; fa = f[x], !isRoot(x); rotate(x)){
if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
}
}
inline int access(int x){ //把从根到x的所有点放在一条实链里, 返回这个splay的根
int p = 0;
for(p = 0; x; p = x, x = f[x]){
splay(x), ch[x][1] = p, pushUp(x);
}
return p;
}
inline void makeRoot(int p){ //使x点成为整棵树的根
access(p); splay(p);
swap(ch[p][0],ch[p][1]); //把整条链反向
rev[p] ^= 1;
}
inline void link(int x,int y){ //在x、y两点间连一条边
// if (find(x) != find(y))
makeRoot(x), f[x] = y;
}
inline void cut(int x,int p){ //把x、y两点间边删掉
makeRoot(x), access(p), splay(p);
if (ls == x && !rs) ls = f[x] = 0;
}
inline int find(int p){ //找到x所在树的根节点编号
access(p), splay(p);
while(ls) pushDown(p), p = ls;
return p;
}
}st;
signed main() {
scanf("%d%d", &n, &m);
rep(i,0,n) st.val[i] = st.minn[i] = 1e5;
rep(i,1,m) scanf("%d%d%d",&e[i].a,&e[i].b,&e[i].w);
sort(e+1,e+1+m);
rep(i,1,m) st.val[i+n] = st.minn[i+n] = e[i].w, st.mpos[i+n] = i+n;
rep(i,1,m){
if(e[i].a == e[i].b) continue;
if(st.find(e[i].a) != st.find(e[i].b)){
st.link(e[i].a,i+n); st.link(i+n,e[i].b);
num++; vis[i] = 1;
while(!vis[pos]) pos++;
if(num == n-1) ans = min(ans,e[i].w-e[pos].w);
}
else{
st.makeRoot(e[i].a);
int p1 = st.access(e[i].b);
p1 = st.mpos[p1];
st.cut(e[p1-n].a,p1); st.cut(p1,e[p1-n].b); vis[p1-n] = 0;
st.link(e[i].a,i+n); st.link(i+n,e[i].b); vis[i] = 1;
while(!vis[pos]) pos++;
if(num == n-1) ans = min(ans,e[i].w-e[pos].w);
}
}
printf("%d\n",ans);
return 0;
}
6. [BJOI2014] 大融合
题意:
个点,一共
次操作。一共有两种操作类型,
表示连通
,保证操作合法,且始终是棵森林。
表示查询去除
边之后,
所在树的节点数
所在树的节点数。
思路:
我们一般遇到的都是维护链上节点个数的问题,而此题要求这颗树上的节点个数,因此我们需要同时维护虚边和实边的信息。
我们令 表示节点 子树中节点个数, 表示节点 虚儿子的节点个数和。因此 ,而这也正是 函数。
因此我们只需要维护 即可,然后观察哪些函数会改变 的值,不难发现,只有 、 、 、 会改变边的虚实关系,其中 主要修改在于调用了 函数,而 只是删除实边不会修改虚边,因此真正关键的函数即为 和 函数,具体操作见代码,不难思考。
这里主要讲解 中修改 信息时为什么需要将节点 ,原因在于单点修改之后,其祖先节点维护的信息都会发生变化,因此一般的问题需要 ,因为一般问题只需要维护实链信息。然后在该问题中还维护了虚链信息,因此需要 而不是 。
代码:
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
using namespace std;
typedef long long ll;
const int N = 100010;
int n, q;
struct LCT{
#define ls ch[p][0]
#define rs ch[p][1]
#define Get(p) (ch[f[p]][1] == p)
int ch[N][2], f[N], siz[N], siz2[N], rev[N];
inline void clear(int p){ //清除这个点的信息
ch[p][0] = ch[p][1] = f[p] = siz[p] = siz2[p] = rev[p] = 0;
}
inline int isRoot(int p){
clear(0);
return ch[f[p]][0] != p && ch[f[p]][1] != p;
}
inline void pushUp(int p){
clear(0);
siz[p] = siz[ls] + 1 + siz[rs] + siz2[p];
}
inline void pushDown(int p){
clear(0);
if(rev[p]){
if(ls) rev[ls] ^= 1, swap(ch[ls][0],ch[ls][1]);
if(rs) rev[rs] ^= 1, swap(ch[rs][0],ch[rs][1]);
rev[p] = 0;
}
}
void update(int p){ //递归地从上到下pushDown信息
if(!isRoot(p)) update(f[p]);
pushDown(p);
}
inline void rotate(int x){ //将x向上旋转一层的操作
int y = f[x], z = f[y], k = Get(x);
if(!isRoot(y)) ch[z][ch[z][1] == y] = x;
ch[y][k] = ch[x][!k], f[ch[y][k]] = y;
ch[x][!k] = y, f[y] = x, f[x] = z;
pushUp(y); //要先pushUp(y)
pushUp(x);
}
inline void splay(int x){ //把x旋转到当前splay的根
update(x); //将上面的标记完全下放
for(int fa; fa = f[x], !isRoot(x); rotate(x)){
if(!isRoot(fa)) rotate(Get(fa) == Get(x) ? fa : x);
}
}
inline int access(int x){ //把从根到x的所有点放在一条实链里, 返回这个splay的根
int p; //每次改变右儿子的值,因为整棵树是中序遍历,放入右儿子才能保证先遍历父亲再遍历儿子
for(p = 0; x; p = x, x = f[x]){
splay(x), siz2[x] += siz[ch[x][1]]-siz[p], ch[x][1] = p, pushUp(x);
}
return p;
}
inline void makeRoot(int p){ //使x点成为整棵树的根
access(p); splay(p);
swap(ch[p][0],ch[p][1]); //把整条链反向
rev[p] ^= 1;
}
inline void link(int x,int y){ //在x、y两点间连一条边
//makeRoot(x)的作用是使得x无父亲
//makeRoot(y)的作用是使得y无父亲,因此可以修改y的信息,不用去更新y的祖先
makeRoot(x), makeRoot(y), f[x] = y, siz2[y] += siz[x]; pushUp(y);
}
inline void cut(int x,int p){ //把x、y两点间边删掉,此处删除的是实边,注意实边和虚边的区别
makeRoot(x), access(p), splay(p);
if (ls == x && !rs) ls = f[x] = 0;
}
inline int find(int p){ //找到x所在树的根节点编号
access(p), splay(p);
while(ls) pushDown(p), p = ls;
return p;
}
}st;
signed main() {
scanf("%d%d", &n, &q);
rep(i,1,n) st.siz[i] = 1;
while (q--) {
char op[10]; int x,y; scanf("%s%d%d",op,&x,&y);
if(op[0] == 'A') st.link(x,y);
else{
st.cut(x,y);
st.makeRoot(x); st.splay(x);
int a1 = st.siz[x];
st.makeRoot(y); st.splay(y);
int a2 = st.siz[y];
st.link(x,y);
printf("%lld\n",(ll)a1*(ll)a2);
}
}
return 0;
}
7. Sone1
题意:
支持
种操作,包括链
、
、
,子树
、
、
,换根,换边,子树和链的修改与加值。
思路:
典型例题,主要思路是对于每个点维护了一个
,详情看
的题解。
代码:
贴上
的代码。
/*
Toptree即为可以维护子树信息的lct升级版
*/
#include<cstdio>
#define N 200010
const int inf=~0U>>1;
inline void swap(int&a,int&b){int c=a;a=b;b=c;}
inline int max(int a,int b){return a>b?a:b;}
inline int min(int a,int b){return a<b?a:b;}
inline void read(int&a){
char c;bool f=0;a=0;
while(!((((c=getchar())>='0')&&(c<='9'))||(c=='-')));
if(c!='-')a=c-'0';else f=1;
while(((c=getchar())>='0')&&(c<='9'))(a*=10)+=c-'0';
if(f)a=-a;
}
struct tag{
int a,b;//ax+b
tag(){a=1,b=0;}
tag(int x,int y){a=x,b=y;}
inline bool ex(){return a!=1||b;}
inline tag operator+(const tag&x){return tag(a*x.a,b*x.a+x.b);}
};
inline int atag(int x,tag y){return x*y.a+y.b;}
struct data{
int sum,minv,maxv,size;
data(){sum=size=0,minv=inf,maxv=-inf;}
data(int x){sum=minv=maxv=x,size=1;}
data(int a,int b,int c,int d){sum=a,minv=b,maxv=c,size=d;}
inline data operator+(const data&x){return data(sum+x.sum,min(minv,x.minv),max(maxv,x.maxv),size+x.size);}
};
inline data operator+(const data&a,const tag&b){return a.size?data(a.sum*b.a+a.size*b.b,atag(a.minv,b),atag(a.maxv,b),a.size):a;}
//son:0-1:重链儿子,2-3:AAA树儿子
int f[N],son[N][4],a[N],tot,rt,rub,ru[N];bool rev[N],in[N];
int val[N];
data csum[N],tsum[N],asum[N];
tag ctag[N],ttag[N];
inline bool isroot(int x,int t){
if(t)return !f[x]||!in[f[x]]||!in[x];
return !f[x]||(son[f[x]][0]!=x&&son[f[x]][1]!=x)||in[f[x]]||in[x];
}
inline void rev1(int x){
if(!x)return;
swap(son[x][0],son[x][1]);rev[x]^=1;
}
inline void tagchain(int x,tag p){
if(!x)return;
csum[x]=csum[x]+p;
asum[x]=csum[x]+tsum[x];
val[x]=atag(val[x],p);
ctag[x]=ctag[x]+p;
}
inline void tagtree(int x,tag p,bool t){
if(!x)return;
tsum[x]=tsum[x]+p;
ttag[x]=ttag[x]+p;
if(!in[x]&&t)tagchain(x,p);else asum[x]=csum[x]+tsum[x];
}
inline void pb(int x){
if(!x)return;
if(rev[x])rev1(son[x][0]),rev1(son[x][1]),rev[x]=0;
if(!in[x]&&ctag[x].ex())tagchain(son[x][0],ctag[x]),tagchain(son[x][1],ctag[x]),ctag[x]=tag();
if(ttag[x].ex()){
tagtree(son[x][0],ttag[x],0),tagtree(son[x][1],ttag[x],0);
tagtree(son[x][2],ttag[x],1),tagtree(son[x][3],ttag[x],1);
ttag[x]=tag();
}
}
inline void up(int x){
tsum[x]=data();
for(int i=0;i<2;i++)if(son[x][i])tsum[x]=tsum[x]+tsum[son[x][i]];
for(int i=2;i<4;i++)if(son[x][i])tsum[x]=tsum[x]+asum[son[x][i]];
if(in[x]){
csum[x]=data();
asum[x]=tsum[x];
}else{
csum[x]=data(val[x]);
for(int i=0;i<2;i++)if(son[x][i])csum[x]=csum[x]+csum[son[x][i]];
asum[x]=csum[x]+tsum[x];
}
}
inline int child(int x,int t){pb(son[x][t]);return son[x][t];}
inline void rotate(int x,int t){
int y=f[x],w=(son[y][t+1]==x)+t;
son[y][w]=son[x][w^1];
if(son[x][w^1])f[son[x][w^1]]=y;
if(f[y])for(int z=f[y],i=0;i<4;i++)if(son[z][i]==y)son[z][i]=x;
f[x]=f[y];f[y]=x;son[x][w^1]=y;up(y);
}
inline void splay(int x,int t=0){
int s=1,i=x,y;a[1]=i;
while(!isroot(i,t))a[++s]=i=f[i];
while(s)pb(a[s--]);
while(!isroot(x,t)){
y=f[x];
if(!isroot(y,t)){if((son[f[y]][t]==y)^(son[y][t]==x))rotate(x,t);else rotate(y,t);}
rotate(x,t);
}
up(x);
}
inline int newnode(){
int x=rub?ru[rub--]:++tot;
son[x][2]=son[x][3]=0;in[x]=1;
return x;
}
inline void setson(int x,int t,int y){son[x][t]=y;f[y]=x;}
inline int pos(int x){for(int i=0;i<4;i++)if(son[f[x]][i]==x)return i;return 4;}
inline void add(int x,int y){//从x连出一条虚边到y
if(!y)return;
pb(x);
for(int i=2;i<4;i++)if(!son[x][i]){
setson(x,i,y);
return;
}
while(son[x][2]&&in[son[x][2]])x=child(x,2);
int z=newnode();
setson(z,2,son[x][2]);
setson(z,3,y);
setson(x,2,z);
splay(z,2);
}
inline void del(int x){//将x与其虚边上的父亲断开
if(!x)return;
splay(x);
if(!f[x])return;
int y=f[x];
if(in[y]){
int s=1,i=y,z=f[y];a[1]=i;
while(!isroot(i,2))a[++s]=i=f[i];
while(s)pb(a[s--]);
if(z){
setson(z,pos(y),child(y,pos(x)^1));
splay(z,2);
}
ru[++rub]=y;
}else{
son[y][pos(x)]=0;
splay(y);
}
f[x]=0;
}
inline int fa(int x){//x通过虚边的父亲
splay(x);
if(!f[x])return 0;
if(!in[f[x]])return f[x];
int t=f[x];
splay(t,2);
return f[t];
}
inline int access(int x){
int y=0;
for(;x;y=x,x=fa(x)){
splay(x);
del(y);
add(x,son[x][1]);
setson(x,1,y);
up(x);
}
return y;
}
inline int lca(int x,int y){
access(x);
return access(y);
}
inline int root(int x){
access(x);
splay(x);
while(son[x][0])x=son[x][0];
return x;
}
inline void makeroot(int x){
access(x);
splay(x);
rev1(x);
}
inline void link(int x,int y){
makeroot(x);
add(y,x);
access(x);
}
inline void cut(int x){
access(x);
splay(x);
f[son[x][0]]=0;
son[x][0]=0;
up(x);
}
inline void changechain(int x,int y,tag p){
makeroot(x);
access(y);
splay(y);
tagchain(y,p);
}
inline data askchain(int x,int y){
makeroot(x);
access(y);
splay(y);
return csum[y];
}
inline void changetree(int x,tag p){
access(x);
splay(x);
val[x]=atag(val[x],p);
for(int i=2;i<4;i++)if(son[x][i])tagtree(son[x][i],p,1);
up(x);
splay(x);
}
inline data asktree(int x){
access(x);
splay(x);
data t=data(val[x]);
for(int i=2;i<4;i++)if(son[x][i])t=t+asum[son[x][i]];
return t;
}
int n,m,x,y,z,k,i,ed[N][2];
int main(){
read(n);read(m);
tot=n;
for(i=1;i<n;i++)read(ed[i][0]),read(ed[i][1]); //连边
for(i=1;i<=n;i++)read(val[i]),up(i); //先赋点权,再连边
for(i=1;i<n;i++)link(ed[i][0],ed[i][1]); //每个点的权值
read(rt); //给出根
makeroot(rt);
while(m--){
read(k);
if(k==1){//换根,x变成根
read(rt);
makeroot(rt);
}
if(k==9){//x的父亲变成y,x父亲换成y
read(x),read(y);
if(lca(x,y)==x)continue;
cut(x);
link(y,x);
makeroot(rt);
}
if(k==0){//子树赋值,以x为根的子树点权值改为y
read(x),read(y);
changetree(x,tag(0,y));
}
if(k==5){//子树加,x为根子树点权值加上y
read(x),read(y);
changetree(x,tag(1,y));
}
if(k==3){//子树最小值,x为根子树中点权值求min
read(x);
printf("%d\n",asktree(x).minv);
}
if(k==4){//子树最大值,x为根子树中点权值求max
read(x);
printf("%d\n",asktree(x).maxv);
}
if(k==11){//子树和,x为根子树中点权sum
read(x);
printf("%d\n",asktree(x).sum);
}
if(k==2){//链赋值,x-y路径上点权值改为z
read(x),read(y),read(z);
changechain(x,y,tag(0,z));
makeroot(rt);
}
if(k==6){//链加,x-y路径上点权值加上z
read(x),read(y),read(z);
changechain(x,y,tag(1,z));
makeroot(rt);
}
if(k==7){//链最小值,x-y路径上点权值求min
read(x),read(y);
printf("%d\n",askchain(x,y).minv);
makeroot(rt);
}
if(k==8){//链最大值,x-y路径上点权值求max
read(x),read(y);
printf("%d\n",askchain(x,y).maxv);
makeroot(rt);
}
if(k==10){//链和,x-y路径上点权值求sum
read(x),read(y);
printf("%d\n",askchain(x,y).sum);
makeroot(rt);
}
}
return 0;
}
后记
本篇博客到这里就结束了,祝大家 愉快,一起爱上 把!(๑•̀ㅂ•́)و✧