广义的dfs序是指在树上dfs时各种巧妙记录留下的一个线性序列 从而把树上问题转化为序列问题便于解决
由于问题的类型各异 dfs序的种类也有很多 比如 入栈序、入栈出栈序、 出栈序、 树链剖分序等。
先贴个代码镇住场面
int id,a[N],ord[N],fa[N],sum[N],deep[N];
void dfs(int x)
{
//a[++id]=x; // 这行代码表示把当前点加入序列中 根据问题实际操作
//ord[x]=id; // ord[i]表示i号节点出现在dfs序的位置 根据问题可能用多个数组分情况存
sum[x]=1; //初始化子树大小
for(int i=last[x];i;i=nxt[i])
{int y=ver[i];
if(y==fa[x]) return;
fa[y]=x; deep[y]=deep[x]+1;
dfs(y);
sum[x]+=sum[y];
//a[++id]=x; //ord[x]=id;
}
//a[++id]=x; //ord[x]=id;
}
是不是顿时觉得dfs序是个很好学的东西?
dfs序的重要性质 一个点和他所有的子节点会被存储在连续的区间之中。
证明:因为是dfs,所以显然,命题得证;
第一部分 dfs序
dfs序能干啥?
1.dfs序+数据结构
dfs序+数据结构强无敌之一 POJ3321 Apple Tree
给定一棵n个点的有根苹果树,每个点上最多只有一个苹果,现在有Q次操作:
(1).修改一个点上的苹果个数。
(2).求某个子树内有几个苹果。
n,Q<=10^5.
题解:裸题 单点修改区间查询 用树状数组维护就好。
dfs序+数据结构强无敌之二 HDU5692 Snacks
给定一棵n个点的有根树,每个点有一个点权。根节点为0,节点标号为0~n-1。
定义最大路径为:从根出发走到某个点,点权和最大的路径。
现在有Q次操作,每种是以下两种之一:
(1).将点x的点权变成v。
(2).求经过某一个点的最大路径的点权和。
题解:
设w[x]为节点x的权值
dis[x]为从根节点到达当前节点的权值,可以通过一遍dfs全部求出来。
题目询问的是经过一个点的最大路径的点权和,那么其实就是到达这个节点或者其子孙节点的最大dis[x], 其实就是查询整个子树的max(dis[x])。
修改其实就是修改整个子树的最大值!对于0 x y, 其实就是子树x的所有节点都增加y-w[x]!那么最大值也增加y-w[x]。
问题就变成了一个询问区间最大值和区间修改的问题了。
于是,我们就可以用线段树来维护。
https://blog.csdn.net/qq_39670434/article/details/78425125
dfs序+数据结构强无敌之三
对节点X到Y的最短路上所有点权都加一个数W, 查询某个点的权值
题解:
树上差分 路径修改转化为单点修改 单点查询转化为子树和查询 之后显然
dfs序+数据结构强无敌之四
对节点X到Y的最短路上所有点权都加一个数W, 查询某个点子树的权值之和
题解 :
树上差分 路径修改转化为4个点修改
当修改某个节点A, 查询另一节点B时
只有A在B的子树内, 子树权值和会增加W×(depth[A]−depth[B]+1)=W×(depth[A]+1)−W×depth[B]
同样是用树状数组来查询子树, 修改 和 查询方法都要新增一个数组
第一个数组保存 W×(depth[A]+1) 第二个数组保存 W
每次查询结果为Sum(ed[B])−Sum(st[B]−1)−(Sum1(ed[B])−Sum1(st[B]−1))×depth[B]
dfs序+数据结构强无敌之五
对某个点X权值加上一个数W, 查询X到Y路径上所有点权之和
题解:
求X到Y路径上所有的点权之和, 和前面X到Y路径上所有点权加一个数相似
这个问题转化为
X到根节点的和 + Y到根节点的和 −LCA(x,y)到根节点的和 −fa(LCA(x,y))到根节点的和
于是只要支持单点修改, 区间查询即可
为了保证在该节点子树内的能加到这个W, 不在该点子树内的无影响
我们要dfs求出入栈出栈序 在该点开始出现位置加W, 在结束位置减W
2.dfs序+DP
dfs序+DP强无敌之一
O(nm)构造树背包
那么有没有可能在O(nm)的时间复杂度内构造树背包呢?
有可能!
我们可以求出树的后序遍历,将树上问题转化为序列问题。
f[i][0~m]代表的不再是“以i为根的子树的背包”,而是“后序遍历序列的前i个节点上的物品所构成的背包”。
对于序列上第i个点,令其所代表的节点为k。节点k上的物品只有选和不选之分,且只有选了它才可能选它的子树。
因为k所在的子树在后序遍历序列上是连续的一段,所以在更新f[i][0~m]之前先将f[i-size[k]][0~m]的值copy过来,这表示着不选k点的答案;然后在f[i-1][0~m]这个背包上添加k点上的物品来更新f[i][0~m],表示选择k点的答案。
for(j=0;j<=m;++j) f[i][j]=f[i-size[k]][j];
for(j=w[k];j<=m;++j) f[i][j]=max(f[i][j],f[i-1][j-w[k]]+v[k]);
这就是计算大基佬zP1nG退役后的遗作 现在让我这个蒟蒻借鉴一番
第二部分 欧拉序及其重要性质
https://www.cnblogs.com/pealicx/p/6859901.html
欧拉序貌似有两个版本
1.就是从根结点出发,按dfs的顺序在绕回原点所经过所有点的顺序
dfs到加进,dfs回加进,总共加度数遍;
void dfs(int x,int dep)
{que[++tot]=x;
first[x]=tot;
deep[tot]=dep;
for(int i=last[x];i;i=nxt[i])
{dfs(v[i],dep+1);
que[++tot]=x;
deep[tot]=dep;
}
que[++tot]=x;
deep[tot]=dep;
}
2.入栈出栈序
欧拉序求lca
两个点的 对应第一次在欧拉序中出现的位置 所夹的在deep数组闭区间的最小值 对应的 点
然后就可以用ST表维护 ST回答询问为O(1)
欧拉序lca适合询问数大的情况
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
short int n,last[1005],nxt[1005],v[1005],num,l2[2005];
short int que[2005],tot,first[1005],deep[2005],f[2005][12];
int fang[12]={1,2,4,8,16,32,64,128,256,512,1024,2048},ans[1005];
bool fa[1005];
void dfs(int x,int dep)
{que[++tot]=x;
first[x]=tot;
deep[tot]=dep;
for(int i=last[x];i;i=nxt[i])
{dfs(v[i],dep+1);
que[++tot]=x;
deep[tot]=dep;
}
}
int main()
{ int p,q,l,r,m;
scanf("%d",&n);
for(int i=1;i<n;i++)
{scanf("%d%d",&p,&q);
nxt[++num]=last[p];
last[p]=num;
v[num]=q; fa[q]=1;
}
for(int i=1;i<=n;i++) if(!fa[i]) dfs(i,1);
for(int i=1;i<=tot;i++) f[i][0]=i;
int k=1; l2[1]=0;
for(int i=2;i<=tot;i++)
{l2[i]=k;if(fang[k+1]==i) k++;}
for(int j=1;fang[j]<=tot;j++)
for(int i=1;i+fang[j]-1<=tot;i++)
if(deep[f[i][j-1]]<deep[f[i+fang[j-1]][j-1]]) f[i][j]=f[i][j-1];
else f[i][j]=f[i+fang[j-1]][j-1];
scanf("%d",&m);
for(int i=1;i<=m;i++)
{scanf("%d%d",&p,&q);
l=first[p]; r=first[q]; if(l>r) swap(l,r);
p=l2[r-l+1];
if(deep[f[l][p]]<deep[f[r-fang[p]+1][p]]) ans[que[f[l][p]]]++;
else ans[que[f[r-fang[p]+1][p]]]++;
}
for(int i=1;i<=n;i++)
if(ans[i]>0) printf("%d:%d\n",i,ans[i]);
return 0;
}
根据求欧拉序lca的算法
我们可以得出一个结论
对于多个点求lca 相当于入栈序最小的点和入栈序最大的点求lca
欧拉游览树 动态维护欧拉序