dfs序
dfs序学习
构造: d f s dfs dfs 序是通过按照 d f s dfs dfs 时访问结点的先后顺序产生的序,一般维护两个数组 i n , o u t in,out in,out ,用 id 表示时间戳。每次新访问一个新结点 i n [ u ] = + + i d in[u]=++id in[u]=++id,访问结束后 o u t [ u ] = i d out[u]=id out[u]=id。
用处: d f s dfs dfs 序可以把树上问题转化为区间问题,然后用一些线段树,树状数组之类的数据结构维护。
性质: 若 v v v 是 u u u 的子树,则 v 的时间戳一定在 u 的时间戳之间,即: i n [ u ] < i n [ v ] , o u t [ v ] < = o u t [ u ] in[u]<in[v],out[v]<=out[u] in[u]<in[v],out[v]<=out[u] ;并且 v 的序列是 u 的序列中的连续一段。更具体的:以 u 为根的子树所有结点是 d f s dfs dfs 序中连续的一段,在 [ i n [ u ] , o u t [ u ] ] [in[u],out[u]] [in[u],out[u]] 内
映射关系: s e q [ i n [ u ] ] = u seq[in[u]]=u seq[in[u]]=u
void dfs(int u,int fa){
in[u]=++id;
seq[id]=u;
for(int i=vex[u];i;i=e[i].next){
int v=e[i].v;
if(v==fa)continue;
dfs(v,u);
}
out[u]=id;
}
例题练习
例题1:树上单点修改,询问某点子树点权和
- 问题分析: 线段树模板即可,单点修改 i n [ u ] in[u] in[u],区间查询 [ i n [ u ] , o u t [ u ] ] [in[u],out[u]] [in[u],out[u]]
例题2:树上路径修改,查询某个点的权值
- 问题分析: 路径 ( x , y ) + v (x,y)+v (x,y)+v,相当于路径 ( r o o t , x ) + v , ( r o o t , y ) + v , ( r o o t , l c a ) − v , ( r o o t , f a ( l c a ) ) − v (root,x)+v,(root,y)+v,(root,lca)-v,(root,fa(lca))-v (root,x)+v,(root,y)+v,(root,lca)−v,(root,fa(lca))−v。那么问题就变成了:修改根到某个点的路径上所有点的值。假设需要修改路径 ( r o o t , x ) + v (root,x)+v (root,x)+v ,考虑这次修改只对子树中包含点 x 的点有贡献,不妨将问题转化为:单点修改 x 点的值,查询某点子树点权和。
例题3:树上路径修改,查询某点子树点权和
例题4:树上单点修改,查询路径点权和
例题5:树上某点子树所有点修改,查询路径点权和
例题6:树上某点子树所有点修改,查询某个点点权
例题7:树上某点子树所有点修改,查询某点子树点权和
欧拉序
欧拉序学习
定义: 欧拉序同样是通过按照 d f s dfs dfs 时访问结点的先后顺序产生的序,但是他对结点时间戳的记录与 d f s dfs dfs 序不同,同时它自身也有多种记录时间戳的方式,不同的记录方式能解决的问题不同。下面根据例题来学习几种欧拉序方式。
欧拉序构造1:每次访问一个结点时记录一次
构造方式:
Code:
void dfs(int u,int fa){
E[++cnt]=u;
in[u]=cnt;
for(int i=vex[u];i;i=e[i].next){
int v=e[i].v;
if(v==fa)continue;
dfs(v,u);
E[++cnt]=u;
}
out[u]=cnt;
}
性质: 任意两个点的路径必然是欧拉序中的连续一段,并且是 [ i n [ x ] , i n [ y ] ] [in[x],in[y]] [in[x],in[y]]
例题1:欧拉序求 lca
- 问题分析: 由于我们知道了任意两个点之间的路径一定是欧拉序中的连续一段 [ i n [ x ] , i n [ y ] ] [in[x],in[y]] [in[x],in[y]] ,那么这一段中,dep最小的点,就一定是 lca 了。相当于查询区间最小值,这一点刚好可以用 st 表实现。
void dfs(int u,int fa) {
E[++cnt]=u;
in[u]=cnt;
dep[u]=dep[fa]+1;
for(int i=vex[u]; i; i=e[i].next) {
int v=e[i].v;
if(v==fa)continue;
dfs(v,u);
E[++cnt]=u;
}
out[u]=cnt;
}
void init_ST() {
for(int i=1; i<=cnt; i++)f[i][0]=E[i];//点编号
for(int p=1; (1<<p)<=cnt; p++) {
for(int i=1; i+(1<<p)-1<=cnt; i++) {
dep[f[i][p-1]]<dep[f[i+(1<<(p-1))][p-1]]?f[i][p]=f[i][p-1]:f[i][p]=f[i+(1<<(p-1))][p-1];
}
}
int t=0;//log2打表
for(int i=0; i<=cnt; i++) {
while((1<<(t+1))<=i)t++;
Log2[i]=t;
}
}
int query(int u,int v){
u=in[u],v=in[v];
if(u>v)swap(u,v);
int k=Log2[v-u+1];
return dep[f[u][k]]<dep[f[v-(1<<k)+1][k]]?f[u][k]:f[v-(1<<k)+1][k];
}
例题2:欧拉序快速求两点最短路
- 问题分析: 以任意一个点为根,构造出欧拉序,并预处理出 ST 表求 lca 。那么可以 O(1) 求 d i s ( x , y ) = d i s ( x ) + d i s ( y ) − 2 × d i s ( l c a ( x , y ) ) dis(x,y)=dis(x)+dis(y)-2\times dis(lca(x,y)) dis(x,y)=dis(x)+dis(y)−2×dis(lca(x,y))
重链剖分
重链剖分学习
重链剖分的预处理
-
需要完成的功能: 路径修改,路径查询,子树修改,子树查询
-
前言: 重链剖分也是一种通过 d f s dfs dfs 时访问结点的先后顺序产生的序。其通过先访问重儿子再访问轻儿子的方式,将大部分的树上路径修改,路径查询等问题,在 l o g 2 2 log_2^2 log22 内解决了。前面用 d f s dfs dfs 序和欧拉序的所有问题,重链剖分也都能解决。
-
原理: 通过 d f s dfs dfs 序访问结点的先后顺序产生一个序,并将该序放入线段树中进行维护。
-
预处理:
-
第一次 dfs,统计size,求重儿子son,求深度dep,求父节点 f
-
第二遍 dfs,根据先遍历重儿子,再遍历轻儿子的顺序,记录以下数据
-
i d [ i ] : id[i]: id[i]:i 节点在线段树维护的数组中所对应的下标
-
s e g [ i ] : seg[i]: seg[i]:线段树维护的数组中 i 数字所对应的节点
-
t o p [ i ] : top[i]: top[i]:该条重链的头
-
性质: 1.重链上的点编号是连续的;2.子树上的所有点编号也是连续的一段区间
int vex[N],k,son[N],f[N],dep[N],size[N],id[N],cnt,top[N],seg[N];
void dfs1(int u,int fa){//预处理出高度,父亲,子树大小,重儿子
f[u]=fa;
dep[u]=dep[fa]+1;
size[u]=1;
for(int i=vex[u];i;i=e[i].next){
int v=e[i].v;
if(v==fa)continue;
dfs1(v,u);
size[u]+=size[v];
if(size[v]>size[son[u]])son[u]=v;
}
}
void dfs2(int u,int fa,int root){//预处理所在重链的头,线段树的映射关系
top[u]=root;
seg[++cnt]=u;//seg[i]=u:线段树中第 i 个点是结点 u
id[u]=cnt;//id[u]: 线段树中结点 u 所在线段树位置
if(son[u]==0)return;
dfs2(son[u],u,root);
for(int i=vex[u];i;i=e[i].next){
int v=e[i].v;
if(v==fa||v==son[u])continue;
dfs2(v,u,v);
}
}
重链剖分的区间修改与区间查询
- 问题分析: 对于查询路径 ( x , y ) (x,y) (x,y),假设 x 为较深的点
- 情况1: 如果他们在一条重链上,由于重链上的编号是连续的,所以这条路径必然是线段树上 [ i n [ y ] , i n [ x ] ] [in[y],in[x]] [in[y],in[x]]
- 情况2: 如果他们不在一条重链上,显然他们的路径是若干条重链 + 情况1
- 方法: 反复让两个点中所在重链顶较深的点往上跳重链,维护重链顶到该点的路径,让点跳到重链顶的上一个结点,即下一条重链的某个点中。直到两个点在同一条重链中,最后再维护一下 [ i n [ y ] , i n [ x ] ] [in[y],in[x]] [in[y],in[x]]
int range(int x,int y){
int ans=0;
while(top[x]!=top[y]){//当两个点不在同一条链上
if(dep[top[x]]<dep[top[y]])swap(x,y);//令x为所在重链顶端的深度更深的那个点
ans+=query(1,1,n,id[top[x]],id[x]);//维护x点到x所在链顶端
//update(1,1,n,id[top[x]],id[x]);
x=fa[top[x]];//令x跳到x所在重链顶端的上面一个点
}
//直到两个点处于一条链上
if(dep[x]>dep[y])swap(x,y);//把x点深度更深的那个点
ans+=query(1,1,n,id[x],id[y]);//维护最后一段
//update(1,1,n,id[x],id[y]);
return ans;
}
重链剖分的子树查询
方法: u u u 的子树在线段树 [ i d [ u ] , i d [ u ] + s i z e [ u ] − 1 ] [id[u],id[u]+size[u]-1] [id[u],id[u]+size[u]−1]
int sontree(int u){
query(1,1,n,id[u],id[u]+size[u]-1);
//update(1,1,n,id[u],id[u]+size[u]-1);
}
重链剖分总模板
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
struct E{
int u,v,next;
}e[N*2];
int vex[N],k,son[N],f[N],dep[N],size[N],id[N],cnt,top[N],seg[N];
void dfs1(int u,int fa){//预处理出高度,父亲,子树大小,重儿子
f[u]=fa;
dep[u]=dep[fa]+1;
size[u]=1;
for(int i=vex[u];i;i=e[i].next){
int v=e[i].v;
if(v==fa)continue;
dfs1(v,u);
size[u]+=size[v];
if(size[v]>size[son[u]])son[u]=v;
}
}
void dfs2(int u,int fa,int root){//预处理所在重链的头,线段树的映射关系
top[u]=root;
seg[++cnt]=u;//seg[i]=u:线段树中第 i 个点是结点 u
id[u]=cnt;//id[u]: 线段树中结点 u 所在线段树位置
if(son[u]==0)return;
dfs2(son[u],u,root);
for(int i=vex[u];i;i=e[i].next){
int v=e[i].v;
if(v==fa||v==son[u])continue;
dfs2(v,u,v);
}
}
int range(int x,int y){
int ans=0;
while(top[x]!=top[y]){//当两个点不在同一条链上
if(dep[top[x]]<dep[top[y]])swap(x,y);//令x为所在重链顶端的深度更深的那个点
ans+=query(1,1,n,id[top[x]],id[x]);//维护x点到x所在链顶端
//update(1,1,n,id[top[x]],id[x]);
x=fa[top[x]];//令x跳到x所在重链顶端的上面一个点
}
//直到两个点处于一条链上
if(dep[x]>dep[y])swap(x,y);//把x点深度更深的那个点
ans+=query(1,1,n,id[x],id[y]);//维护最后一段
//update(1,1,n,id[x],id[y]);
return ans;//y为lca
}
int sontree(int u){
query(1,1,n,id[u],id[u]+size[u]-1);
//update(1,1,n,id[u],id[u]+size[u]-1);
}