关于 d f s dfs dfs序是访问一棵树时将其按照 d f s dfs dfs时访问的先后顺序打上序号,一般维护两个数组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,当某个结点回溯回来并且没其它结点这个结点的出时间戳一定和最后一个子树的最后一个访问结点的出时间戳一样。于是,通过 O ( n ) O(n) O(n)的扫一遍树上的每个点就可以把非线性的树形结构转成线性的区间结构。
d f s dfs dfs序能够把树上的问题转化为区间问题,用像线段树等维护区间的数据结构来对树上进行询问、更新
u结点包含v结点(或称u结点是v结点的祖先)当且仅当v的时间戳介于u的时间戳之间,即 i n [ u ] < i n [ v ] 且 o u t [ u ] > = o u t [ v ] in[u]<in[v]且out[u]>=out[v] in[u]<in[v]且out[u]>=out[v];或者当且仅当v的序列是u序列中连续的一段。
注:seq[in[u]]=u(映射关系)
int in[maxn], out[maxn], id;
int seq[maxn];
void dfs(int cur, int parent) {
in[cur] = ++id;
seq[id] = cur; //新生成的线性序列
for(int i = 0; i < E[cur].size(); i++) {
int v = E[cur][i];
if (v == parent) continue;
dfs(v, cur);
}
out[cur] = id;
}
欧拉序其实是一种广义的dfs序,基于dfs的很多生成的序列都可以称为欧拉序,这里特指访问一个结点是第一次进来记一次,回溯回来也记一次。也有那种只记录时间戳的,总之遍历生成的序列很多种,但其目的都是要把树形结构转为线性结构方便处理。
和dfs序很类似,形成的序列是有子树则必有多一个的回溯的标记(例如A有子树B,则形成的欧拉序:ABA,假如A没有子树,则形成的序列为:A,注意区别),然后还有记录时间戳的数组in和out,一般out不会用到,可以省略。形成的序列长度为 2 ∗ n − 1 2*n-1 2∗n−1个。
int in[maxn], out[maxn], id;
int seq[maxn*2];
void dfs(int cur, int parent) {
in[cur] = ++id; //可以理解称dfn
seq[id] = cur;
for(int i = 0; i < E[cur].size(); i++) {
int v = E[cur][i];
if (v == parent) continue;
dfs(v, cur);
seq[++id] = cur; //回溯一次加一次
}
out[cur] = id;
}
其他写法:(这个就是规规矩矩地进一次出一次而不是回溯多记一次)
int seq[maxn*2],id;
void dfs(int cur, int parent) {
seq[++id] = cur;
for(int i = 0; i < E[cur].size(); i++) {
int v = E[cur][i];
if (v == parent) continue;
dfs(v, cur);
}
seq[++id] = cur;
}
关于dfs序的几个问题有份很棒的博客,先贴一下以后学习,原文戳这里
dfs序七个经典问题
update-2018.07.23: 原文问题五思路描述有误,已更正。
参考自:《数据结构漫谈》-许昊然
dfs序是树在dfs先序遍历时的序列,将树形结构转化成序列问题处理。
dfs有一个很好的性质:一棵子树所在的位置处于一个连续区间中。
ps:deep[x]为x的深度,l[x]为dfs序中x的位置,r[x]为dfs序中x子树的结束位置
1.点修改,子树和查询
在dfs序中,子树处于一个连续区间中。所以这题可以转化为:点修改,区间查询。用树状数组或线段树即可。
2.树链修改,单点查询
将一条树链x,y上的所有点的权值加v。这个问题可以等价为:
1).x到根节点的链上所有节点权值加v。
2).y到根节点的链上所有节点权值加v。
3).lca(x,y)到根节点的链上所有节点权值和减v。
4).fa(lca(x,y))到根节点的链上所有节点权值和减v。
上面四个操作可以归结为:节点x到根节点链上所有节点的权值加减v。修改节点x权值,当且仅当y是x的祖先节点时,x对y的值有贡献。
所以节点y的权值可以转化为节点y的子树节点贡献和。从贡献和的角度想:这就是点修改,区间和查询问题。
修改树链x,y等价于add(l[x],v),add(l[y],v),add(l[lca(x,y)],-v),add(l[fa(lca(x,y))],-v)。
查询:get_sum(r[x])-get_sum(l[x]-1)
用树状数组或线段树即可。
3.树链修改,子树和查询
树链修改部分同上一问题。下面考虑子树和查询问题:前一问是从贡献的角度想,子树和同理。
对于节点y其到根节点的权值和,考虑其子节点x的贡献: w [ x ] ∗ ( d e e p [ x ] − d e e p [ y ] + 1 ) = w [ x ] ∗ ( d e e p [ x ] + 1 ) − w [ x ] ∗ d e e p [ y ] w[x]*(deep[x]-deep[y]+1) = w[x]*(deep[x]+1)-w[x]*deep[y] w[x]∗(deep[x]−deep[y]+1)=w[x]∗(deep[x]+1)−w[x]∗deep[y]
所以节点y的子树和为:
∑ i = l [ y ] r [ i ] w [ i ] ∗ ( d e e p [ i ] + 1 ) − d e e p [ y ] ∗ ∑ i = l [ y ] r [ i ] w [ i ] \sum_{i=l[y]}^{r[i]} w[i]*(deep[i]+1)-deep[y]*\sum_{i=l[y]}^{r[i]}w[i] ∑i=l[y]r[i]w[i]∗(deep[i]+1)−deep[y]∗∑i=l[y]r[i]w[i]
ps:公式中的v[i]为手误,应为w[i]。
所以用两个树状数组或线段树即可:
第一个维护∑w[i]*(deep[i]+1):支持操作单点修改,区间和查询。(这也就是问题2)
第二个维护∑ w[i]:支持操作单点修改,区间查询。(这其实也是问题2)
4.单点更新,树链和查询
树链和查询与树链修改类似,树链和(x,y)等于下面四个部分和相加:
1).x到根节点的链上所有节点权值加。
2).y到根节点的链上所有节点权值加。
3).lca(x,y)到根节点的链上所有节点权值和的-1倍。
4).fa(lca(x,y))到根节点的链上所有节点权值和的-1倍。
所以问题转化为:查询点x到根节点的链上的所有节点权值和。
修改节点x权值,当且仅当y是x的子孙节点时,x对y的值有贡献。
差分前缀和,y的权值等于dfs中[1,l[y]]的区间和。
单点修改:add(l[x],v),add(r[x]+1,-v);
5.子树修改,单点查询
修改节点x的子树权值,在dfs序上就是区间修改,单点权值查询就是单点查询。
区间修改,单点查询问题:树状数组或线段树即可;
6.子树修改,子树和查询
题目等价与区间修改,区间查询问题。用树状数组或线段树即可。
7.子树修改,树链查询
树链查询同上,等价为根节点到y节点的链上所有节点和问题。
修改节点x的子树权值,当且仅当y是x的子孙节点时(或y等于x),x对y的值有贡献。
x对根节点到y节点的链上所有节点和的贡献为: w [ x ] ∗ ( d e e p [ y ] − d e e p [ x ] + 1 ) = w [ x ] ∗ d e e p [ y ] − w [ x ] ∗ ( 1 − d e e p [ x ] ) w[x]*(deep[y]-deep[x]+1)=w[x]*deep[y]-w[x]*(1-deep[x]) w[x]∗(deep[y]−deep[x]+1)=w[x]∗deep[y]−w[x]∗(1−deep[x])
同问题三,用两个树状数组或线段树即可。
作者:weeping
出处:www.cnblogs.com/weeping/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。