あまりに短い夏だけで何を残しているのかな?|

LHLeisus

园龄:1年5个月粉丝:11关注:26

浅谈树链剖分—轻重链剖分

闲话

似乎会有很多种树剖,什么长链剖分之类的,但是暂时只会轻重链剖分(可怜)。
以前的版本在这里,但是感觉写的太粗糙了,所以决定重写一篇(我也不知道为什么要一直写树剖而不写点别的)。


正文

引入

一些树上问题并不是很好解决,因为并不是对连续的区间进行处理。所以,今天的主角——轻重链剖分,应运而生。有了它,我们就可以把树变成一条条链进行处理,非常厉害,简直是黑科技(捧读)。

algorithm

定义

在介绍具体的算法流程前,我们需要明确一些树上的定义:

  • 重儿子:对于每一个非叶子节点,它的子节点中,子树大小最大的那个子节点被称为重儿子。
  • 轻儿子:对于每一个非叶子节点,除了重儿子以外的子节点都是它的轻儿子。
  • 重边:任意两个重儿子或重儿子和其父节点之间的边称为重边。
  • 轻边:除去重边剩下的边。
  • 重链:重边相连组成的链即为重链,且重链一定以轻儿子为起点。
  • 此外,若一个叶子节点是轻儿子,那么它也可以视为一条重链。

这些就比较形式化了。具体一些:

\(1\) 号点的重儿子是 \(4\),因为 \(2,4,7\)\(4\) 的子树大小最大,以此类推得到所有的重儿子,即可划分出重边与重链。
图中黄色的边即为重边,粉色的边为轻边。那么 \(1-4-5-8-9\) 即为一条重链。


\(\color{#FF69B4}\texttt{明确了以上的定义,就可以正式开始剖了!(撒花✿✿✿)}\)

核心思路

  • 我们的目的是把树变成便于处理的连续区间,要怎么做呢?

    • 似乎可以利用 dfs 序,但是正常 dfs 的时候每一次选择进入的子节点是随机的(其实是按照输入的顺序),并不能很好地解决问题。
  • 这时,就可以利用上文提到的重儿子与重链了。

    • 我们可以在 dfs 时选择优先选择重儿子,并且记录每一个点所在的重链的链顶,这样就把一棵树剖成了许多的重链,并且每条重链上的 dfs 序是连续的。
    • 重链的维护可以使用数据结构,每两条重链之间的轻边就直接处理。
  • 具体到代码中,我们需要进行两次 dfs ,第一次要求出重儿子,顺带求出每个点的父节点、深度和子树大小。

  • 第二次按照先重儿子再轻儿子的顺序进行 dfs,记录每个点的 dfs 序和所在链顶。

    • 如果要用到线段树等数据结构维护,还需要记录当前点的 dfs 序所对应的点的权值(这地方文字写出来很绕,可以看代码 \(18\) 行),记录编号也可以,可以理解为将原来的权值数组(假如是 \(a\))按照 dfs 序重新排列得到 \(a'\)。因为数据结构要维护的是 dfs 序对应的序列,也就是 \(a'\),而不是 \(a\)
  • 时间复杂度 \(O(n+m)\)

code

void dfs1(int u,int f){
int max_son=-1;//当前重儿子的子树大小
dep[u]=dep[f]+1;fa[u]=f;
sz[u]=1;//sz:子树大小
for(int i=head[u];i;i=edge[i].nex){
int v=edge[i].to;
if(v==f) continue;
dfs1(v,u);
sz[u]+=sz[v];
if(sz[v]>max_son){
son[u]=v;
max_son=sz[v];
}//找出重儿子
}
}
void dfs2(int u,int topx){//topx为当前节点所在链顶
dfn[u]=++cnt;//记录dfs序
pos[cnt]=a[u];//dfs序为cnt的点的值为a[u]
top[u]=topx;//链顶
if(!son[u]) return ;
dfs2(son[u],topx);//优先处理重儿子
for(int i=head[u];i;i=edge[i].nex){//处理剩下的轻儿子
int v=edge[i].to;
if(v==fa[u]||v==son[u]) continue;
dfs2(v,v);
}
}

简单运用

其实你可以认为上面是整个轻重链剖分的全部,因为它确实剖分了。不过也可以把上面当成一种预处理,即处理出 \(fa,dep,sz,top,pos,dfn\) 等数组,方便后续的处理。

洛谷 P3384 【模板】重链剖分/树链剖分

洛谷 P3384 【模板】重链剖分/树链剖分

  • 两次 dfs 后,利用线段树维护重链上的信息即可。
  • 在前两个操作中,每一次选择 \(x,y\) 链顶深度较深的那个点(假设为 \(x\)),在线段树上维护 \([dfn[top[x]],dfn[x]]\) 这个区间(也就是 \(x\) 所在的重链),再将 \(x\) 跳到下一条链的链底 \(fa[top[x]]\),重复这个步骤。直到 \(x,y\) 都在一条链上了,就维护 \(x,y\) 之间的部分。
  • 后两个操作更为简单,因为子树里的 dfs 序是连续的,直接维护就好了。

code

void add_1(int x,int y,ll k){
k%=p;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
modify(1,1,n,dfn[top[x]],dfn[x],k);
x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
modify(1,1,n,dfn[x],dfn[y],k);
}
ll query_2(int x,int y){
ll res=0;
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
res+=query(1,1,n,dfn[top[x]],dfn[x]);
x=fa[top[x]];
}
if(dep[x]>dep[y]) swap(x,y);
res+=query(1,1,n,dfn[x],dfn[y]);
return res%p;
}//代码很重复有没有
void add_3(int x,ll k){
k%=p;
modify(1,1,n,dfn[x],dfn[x]+sz[x]-1,k);
}
ll query_4(int x){
return query(1,1,n,dfn[x],dfn[x]+sz[x]-1);
}//不要忘记-1,sz[x]包括x自己

模板就是模板,这种树上修改查询的题感觉一共也就这些操作种类了,题目间的差别主要在于线段树维护的信息不同。


求解 \(\texttt{LCA}\)

目录上求解跑到LCA后面去了,怪事

  • \(\texttt{LCA}\) 的过程实际上跟模板题里面 \(1,2\) 操作很像。每一次选择 \(x,y\) 链顶深度较深的那个点(假设为 \(x\)),将 \(x\) 跳到下一条链的链底 \(fa[top[x]]\),重复这个步骤。直到 \(x,y\) 都在一条链上了,\(\texttt{LCA}\) 就是 \(x,y\) 中深度更浅的那一个。
  • 时间复杂度 \(O(n+m\log n)\)\(O(n)\) 是预处理,查询 \(m\) 次,单次 \(O(\log n)\))。

code

int LCA(int x,int y){
while(top[x]!=top[y]){
if(dep[top[x]]<dep[top[y]]) swap(x,y);
x=fa[top[x]];
}
return dep[x]>dep[y]?y:x;
}

现在你可以抛弃倍增了,毕竟树剖常数小,还可以求出其他信息比如子树大小,万一有用呢

  • 关于单次查询的时间复杂度:一个点到根节点的轻边数是 \(\log n\) 级别的,因为如果一个点是轻儿子,它的父节点所有儿子里,一定会有一个重儿子的 \(sz\geq\) 它的 \(sz\),所以跳一次轻边就至少将当前子树中节点数\(\times 2\)。故最多跳 \(\log n\) 次,也就是循环 \(\log n\) 次,所以单次查找为 \(O(\log n)\)

一些练习题

板子

[ZJOI2008] 树的统计
[NOI2015] 软件包管理器
[HAOI2015] 树上操作
[SHOI2012] 魔法树

问:CCF什么时候再出一次模板题?

洛谷 P4114 Qtree1

洛谷 P4114 Qtree1

单点修改,区间最大值,但是给出的是边权。显然边权是没法直接维护的,我们需要点权。而除了根节点,每个节点又正好对应一条边,那么就可以把每条边的权值赋给深度较深的那个端点,根节点赋为 \(0\),查询时忽略 \(\texttt{LCA}\) 即可。
有可能出现 \(x\)\(y\) 最终跳到一个点的情况,而我们要忽略 \(\texttt{LCA}\),修改的是 \([dfn[x]+1,dfn[y]]\),会出现左端点大于右端点的情况。可以特判排除这种情况,但其实线段树上并不能找到这样的区间,所以不会对最终答案造成影响。

code

void dfs1(int u,int f){//边权转点权
sz[u]=1;
fa[u]=f;
dep[u]=dep[f]+1;
int max_son=-1;
for(int i=head[u];i;i=edge[i].nex){
int v=edge[i].to,id=edge[i].id;
if(v==f) continue;
mapi[id]=v;//记录每条边对应的节点
val[v]=edge[i].w;
dfs1(v,u);
sz[u]+=sz[v];
if(sz[v]>max_son){
son[u]=v;
max_son=sz[v];
}
}
}
//主函数中
if(s[0]=='Q'){
while(top[u]!=top[v]) {
if(dep[top[u]]<dep[top[v]]) swap(u,v);
res=max(res,Max(1,1,n,dfn[top[u]],dfn[u]));
u=fa[top[u]];
}
if(dep[u]>dep[v]) swap(u,v);
res=max(res,Max(1,1,n,dfn[u]+1,dfn[v]));//加1为了排除LCA
}

其余代码都跟模板类似,就不贴了。

洛谷 P4315 月下“毛景树”

洛谷 P4315 月下“毛景树”

  • 这道题可以说是上一题的升级版,同样是边权转点权,还涉及了单点修改,区间加,区间覆盖以及查询区间最值,细节问题很多。
  • 边权转点权的处理还是同样,赋值给深度较深的点。
  • 对于线段树的部分,我们有两个标记,一个是区间加的,记为 \(add\),一个是区间覆盖的,记为 \(cov\)。一定要注意这两个的顺序。每一次区间加,正常处理 \(add\) 即可;区间覆盖时,要把当前的 \(add\) 清空。这样可以保证当前的 \(cov\) 一定是在 \(add\) 之前操作的,故 \(\operatorname{pushdown}\) 的时候要先处理 \(cov\) 再处理 \(add\)
  • 别忘记跳重链时最后要忽略 \(\texttt{LCA}\)

code

点击查看代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<string>
#include<utility>
#include<vector>
#include<queue>
#include<bitset>
#include<map>
#define int long long
#define FOR(i,a,b) for(register int i=a;i<=b;i++)
#define ROF(i,a,b) for(register int i=a;i>=b;i--)
#define mp(a,b) make_pair(a,b)
#define pll pair<long long,long long>
#define pii pair<int,int>
#define fi first
#define se second
using namespace std;
inline int read();
typedef long long ll;
const int N=1e5+5;
const int INF=0x3f3f3f3f;
int n,m,k;
struct E{
int to,nex,w,id;
}edge[N<<1];
int head[N],cnt=0;
void add(int u,int v,int w,int id){
edge[++cnt]=(E){v,head[u],w,id};
head[u]=cnt;
}
int mapi[N],pos[N],dfn[N],top[N],sz[N],son[N],fa[N],dep[N],val[N];
void dfs1(int u,int f){
sz[u]=1;
fa[u]=f;
dep[u]=dep[f]+1;
int max_son=-1;
for(int i=head[u];i;i=edge[i].nex){
int v=edge[i].to;
if(v==f) continue;
mapi[edge[i].id]=v;
val[v]=edge[i].w;
dfs1(v,u);
sz[u]+=sz[v];
if(sz[v]>max_son){
son[u]=v;
max_son=sz[v];
}
}
}
int Cnt=0;
void dfs2(int u,int topx){
dfn[u]=++Cnt;
pos[Cnt]=val[u];
top[u]=topx;
if(!son[u]) return ;
dfs2(son[u],topx);
for(int i=head[u];i;i=edge[i].nex){
int v=edge[i].to;
if(v==fa[u]||v==son[u]) continue;
dfs2(v,v);
}
}
int lazy[N<<2],maxn[N<<2],cov[N<<2];
void pushup(int u){
maxn[u]=max(maxn[u<<1],maxn[u<<1|1]);
}
void build(int u,int l,int r){
cov[u]=-INF;
if(l==r){
maxn[u]=pos[l];
return ;
}
int mid=l+r>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
pushup(u);
}
void pushdown(int u,int l,int r){
int mid=l+r>>1;
int ls=u<<1,rs=u<<1|1,Ll=mid-l+1,Lr=r-mid;
if(cov[u]!=-INF){
cov[ls]=cov[rs]=cov[u];
lazy[ls]=lazy[rs]=0;
maxn[ls]=maxn[rs]=cov[u];
cov[u]=-INF;
}
if(lazy[u]){
lazy[ls]+=lazy[u];
lazy[rs]+=lazy[u];
maxn[ls]+=lazy[u];
maxn[rs]+=lazy[u];
lazy[u]=0;
}
}
void change(int u,int l,int r,int x,int k){
if(l==r&&l==x){
maxn[u]=k;
return ;
}
pushdown(u,l,r);
int mid=l+r>>1;
if(x<=mid) change(u<<1,l,mid,x,k);
else change(u<<1|1,mid+1,r,x,k);
pushup(u);
}
void cover(int u,int l,int r,int L,int R,int k){
if(L<=l&&r<=R){
maxn[u]=k;
cov[u]=k;
lazy[u]=0;
return ;
}
pushdown(u,l,r);
int mid=l+r>>1;
if(L<=mid) cover(u<<1,l,mid,L,R,k);
if(R>mid) cover(u<<1|1,mid+1,r,L,R,k);
pushup(u);
}
void Add(int u,int l,int r,int L,int R,int k){
if(L<=l&&r<=R){
maxn[u]+=k;
lazy[u]+=k;
return ;
}
pushdown(u,l,r);
int mid=l+r>>1;
if(L<=mid) Add(u<<1,l,mid,L,R,k);
if(R>mid) Add(u<<1|1,mid+1,r,L,R,k);
pushup(u);
}
int Max(int u,int l,int r,int L,int R){
if(L<=l&&r<=R){
return maxn[u];
}
pushdown(u,l,r);
int mid=l+r>>1;
int res=0;
if(L<=mid) res=max(Max(u<<1,l,mid,L,R),res);
if(R>mid) res=max(Max(u<<1|1,mid+1,r,L,R),res);
return res;
}
signed main()
{
n=read();
FOR(i,1,n-1){
int u=read(),v=read(),w=read();
add(u,v,w,i);
add(v,u,w,i);
}
dfs1(1,0);
dfs2(1,1);
build(1,1,Cnt);
char s[10];
while(~scanf("%s",s)){
if(s[0]=='S') break;
if(s[0]=='M'){
int u=read(),v=read();
int res=0;
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]]) swap(u,v);
res=max(res,Max(1,1,n,dfn[top[u]],dfn[u]));
u=fa[top[u]];
}
if(dep[u]>dep[v]) swap(u,v);
res=max(res,Max(1,1,n,dfn[u]+1,dfn[v]));
printf("%lld\n",res);
}
else if(s[0]=='A'){
int u=read(),v=read();
k=read();
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]]) swap(u,v);
Add(1,1,n,dfn[top[u]],dfn[u],k);
u=fa[top[u]];
}
if(dep[u]>dep[v]) swap(u,v);
if(u!=v)
Add(1,1,n,dfn[u]+1,dfn[v],k);
}
else if(s[1]=='h'){
k=read();
int z=read();
change(1,1,n,dfn[mapi[k]],z);
}
else{
int u=read(),v=read();
k=read();
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]]) swap(u,v);
cover(1,1,n,dfn[top[u]],dfn[u],k);
u=fa[top[u]];
}
if(dep[u]>dep[v]) swap(u,v);
if(u!=v)
cover(1,1,n,dfn[u]+1,dfn[v],k);
}
}
return 0;
}
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
return f*x;
}

洛谷 P1505 [国家集训队] 旅游

洛谷 P1505 [国家集训队] 旅游

  • 这道题更加麻烦,还是先边权转点权。之后需要建线段树,单点修改、区间取反、区间和、区间最大值、最小值。
  • 取反的操作和单点修改并不冲突,也没有区间修改,只有一个取反的标记,所以直接下传就好了。下传时区间和,最大值和最小值都直接取反,再把最大值和最小值交换一下。每次取反将标记异或 \(1\)

code

点击查看代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<string>
#include<utility>
#include<vector>
#include<queue>
#include<bitset>
#include<map>
#define FOR(i,a,b) for(register int i=a;i<=b;i++)
#define ROF(i,a,b) for(register int i=a;i>=b;i--)
#define mp(a,b) make_pair(a,b)
#define pll pair<long long,long long>
#define pii pair<int,int>
#define fi first
#define se second
using namespace std;
inline int read();
typedef long long ll;
const int N=2e5+5;
const int INF=0x3f3f3f3f;
int n,m,k;
struct E{
int to,nex,w,id;
}edge[N<<1];
int head[N],cnt=0;
void Add(int u,int v,int w,int id){
edge[++cnt]=(E){v,head[u],w,id};
head[u]=cnt;
}
int fa[N],son[N],dfn[N],pos[N],val[N],mapi[N],top[N],dep[N],sz[N];
int tr[N<<2],maxn[N<<2],minn[N<<2],rev[N<<2];
void dfs1(int u,int f){
sz[u]=1;
dep[u]=dep[f]+1;
fa[u]=f;
int max_son=-1;
for(int i=head[u];i;i=edge[i].nex){
int v=edge[i].to,w=edge[i].w,id=edge[i].id;
if(v==f) continue;
mapi[id]=v;
val[v]=w;
dfs1(v,u);
sz[u]+=sz[v];
if(sz[v]>max_son){
max_son=sz[v];
son[u]=v;
}
}
}
int tot=0;
void dfs2(int u,int topx){
dfn[u]=++tot;
pos[tot]=val[u];
top[u]=topx;
if(!son[u]) return ;
dfs2(son[u],topx);
for(int i=head[u];i;i=edge[i].nex){
int v=edge[i].to;
if(v==fa[u]||v==son[u]) continue;
dfs2(v,v);
}
}
#define ls(u) u<<1
#define rs(u) u<<1|1
void pushup(int u){
tr[u]=tr[ls(u)]+tr[rs(u)];
maxn[u]=max(maxn[ls(u)],maxn[rs(u)]);
minn[u]=min(minn[ls(u)],minn[rs(u)]);
}
void build(int u,int l,int r){
if(l==r){
tr[u]=maxn[u]=minn[u]=pos[l];
return ;
}
int mid=l+r>>1;
build(u<<1,l,mid);
build(u<<1|1,mid+1,r);
pushup(u);
}
void pushdown(int u,int l,int r){
if(rev[u]){
rev[u]=0;
rev[ls(u)]^=1;
rev[rs(u)]^=1;
tr[ls(u)]=-tr[ls(u)];
tr[rs(u)]=-tr[rs(u)];
maxn[ls(u)]=-maxn[ls(u)];
minn[ls(u)]=-minn[ls(u)];
swap(maxn[ls(u)],minn[ls(u)]);
maxn[rs(u)]=-maxn[rs(u)];
minn[rs(u)]=-minn[rs(u)];
swap(maxn[rs(u)],minn[rs(u)]);
}
}
void modify(int u,int l,int r,int x,int k){
if(l==r&&l==x){
tr[u]=maxn[u]=minn[u]=k;
return ;
}
pushdown(u,l,r);
int mid=l+r>>1;
if(x<=mid) modify(ls(u),l,mid,x,k);
else modify(rs(u),mid+1,r,x,k);
pushup(u);
}
void Re(int u,int l,int r,int L,int R){
if(L<=l&&r<=R){
rev[u]^=1;
tr[u]=-tr[u];
maxn[u]=-maxn[u];
minn[u]=-minn[u];
swap(maxn[u],minn[u]);
return ;
}
pushdown(u,l,r);
int mid=l+r>>1;
if(L<=mid) Re(ls(u),l,mid,L,R);
if(R>mid) Re(rs(u),mid+1,r,L,R);
pushup(u);
}
int qsum(int u,int l,int r,int L,int R){
int res=0;
if(L<=l&&r<=R){
return tr[u];
}
pushdown(u,l,r);
int mid=l+r>>1;
if(L<=mid) res+=qsum(ls(u),l,mid,L,R);
if(R>mid) res+=qsum(rs(u),mid+1,r,L,R);
return res;
}
int qmax(int u,int l,int r,int L,int R){
int res=-2000;
if(L<=l&&r<=R){
return maxn[u];
}
pushdown(u,l,r);
int mid=l+r>>1;
if(L<=mid) res=max(res,qmax(ls(u),l,mid,L,R));
if(R>mid) res=max(res,qmax(rs(u),mid+1,r,L,R));
return res;
}
int qmin(int u,int l,int r,int L,int R){
int res=2000;
if(L<=l&&r<=R){
return minn[u];
}
pushdown(u,l,r);
int mid=l+r>>1;
if(L<=mid) res=min(res,qmin(ls(u),l,mid,L,R));
if(R>mid) res=min(res,qmin(rs(u),mid+1,r,L,R));
return res;
}
int main()
{
n=read();
FOR(i,1,n-1){
int u=read()+1,v=read()+1,w=read();
Add(u,v,w,i),Add(v,u,w,i);
}
m=read();
dfs1(1,0);
dfs2(1,1);
build(1,1,n);
while(m--){
char s[5];
scanf("%s",s);
int u=read()+1,v=read()+1;
if(s[0]=='C'){
modify(1,1,n,dfn[mapi[u-1]],v-1);
}
else if(s[0]=='N'){
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]]) swap(u,v);
Re(1,1,n,dfn[top[u]],dfn[u]);
u=fa[top[u]];
}
if(dep[u]>dep[v]) swap(u,v);
Re(1,1,n,dfn[u]+1,dfn[v]);
}
else if(s[0]=='S'){
int res=0;
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]]) swap(u,v);
res+=qsum(1,1,n,dfn[top[u]],dfn[u]);
u=fa[top[u]];
}
if(dep[u]>dep[v]) swap(u,v);
res+=qsum(1,1,n,dfn[u]+1,dfn[v]);
printf("%d\n",res);
}
else if(s[1]=='A'){
int res=-2000;
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]]) swap(u,v);
res=max(res,qmax(1,1,n,dfn[top[u]],dfn[u]));
u=fa[top[u]];
}
if(dep[u]>dep[v]) swap(u,v);
res=max(res,qmax(1,1,n,dfn[u]+1,dfn[v]));
printf("%d\n",res);
}
else{
int res=2000;
while(top[u]!=top[v]){
if(dep[top[u]]<dep[top[v]]) swap(u,v);
res=min(res,qmin(1,1,n,dfn[top[u]],dfn[u]));
u=fa[top[u]];
}
if(dep[u]>dep[v]) swap(u,v);
res=min(res,qmin(1,1,n,dfn[u]+1,dfn[v]));
printf("%d\n",res);
}
}
return 0;
}
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
return f*x;
}
$$\color{#FF69B4}\texttt{--------------}\operatorname{THE}\;\operatorname{END}\texttt{--------------}$$
posted @   LHLeisus  阅读(59)  评论(2编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
展开