树——DFS和dfn相关 学习笔记
上课写的,跟着老师的思路走,可能会没有那么循序渐进,请体谅。
Pt.1 一次DFS可以做什么
其实一次DFS可以预处理出很多东西,给大家列举一些吧。
-
所有\(fa[u]\),即每个节点的父亲。这个好办,在 DFS 的时候顺带记录一下即可,至于根的父亲则看题目要求,不然就默认置为0。
-
所有\(dep[u]\),即每个节点的深度。将根节点的虚拟父亲的深度设置为零,然后对于每个节点其深度在往下递归之前调整为为父亲深度加一即可。
-
所有\(sz[u]\),即以每个节点为根的子树的大小。参考树上 DP ,先给每个节点的 sz 初始化为1,然后将所有儿子节点的 sz 加起来即可。
-
所有\(dfn[u]\),即每个节点的时间戳,具体来讲就是维护一个变量\(tim\)代表当前时间,当我们往下搜的时候时间++(注意回溯不算时间,你可以理解为回溯用时 0 ,往下走用时 1 ),每当我们走到一个节点就把这个节点的时间打上一个标记记录当前时间,这就是时间戳,用\(dfn\)数组保存这个标记。
Pt.2 树上的区间问题
对于树上的一些问题,比如给出一个$ u \(,求\) u $所在的子树里的点权之和。
暴力很好想,直接 DFS 即可。但是当我们的询问次数躲起来,子树大小大起来,那么我们这个暴力的效率问题就会逐渐明显,导致出现一系列的时间问题说白了就是你会T。
合理联想可以用一些数据结构去维护这个东西。可是这是个树,很多数据结构比如线段树,树状数组都是维护一维的线性的数列的,可是树差不多算是个二维的东西,这些数据结构不起作用,怎么办呢?
要不这样,我们给这些节点打上编号,然后把他们的权值扯下来变成一维的,然后去维护不就好了吗?
问题又来了,怎么打上编号呢?
合理联想一手,注意到时间戳正是给每个节点打上编号的操作,而且因为一个节点所在的子树里的时间戳一定是连续的(大家可以模拟一下 DFS 的操作加深理解,很显然),所以我们可以给每个节点打上时间戳然后用线段树或者什么东西来维护这个,这样我们就可以实现子树修改/求和的操作。
Pt.3 dfn的其他应用
我们引入一个数组\(out\),表示一个节点搜索完回溯时的时间戳。与\(dfn\)配合可以解决很多问题。下面给出一个例子。
Example 1
题目描述
给定一棵以 1 为根的树。
有 q 次询问,每次给出两个点 u v,请判断 u 是否是 v 的祖先。特别地,一个点也是它自己的祖先。
输入格式
第一行两个整数 n q。
接下来 n - 1 行,每行两个整数 u v,表示树上的一条无向边。
接下来 q 行,每行两个整数 u v,表示一次询问。
输出格式
对于每次询问,如果 u 是 v 的祖先,输出 YES,否则输出 NO。
样例
输入
5 4
1 2
1 3
2 4
2 5
1 5
2 3
2 2
4 5
输出
YES
NO
YES
NO
数据范围
对于全部数据,1 <= n, q <= 50000。
数据特征
对于前 20% 的数据,n,q≤20,包含单点、自身祖先询问和短链。
对于前 45% 的数据,n≤500,包含链、菊花、二叉树和随机树。
对于前 70% 的数据,n,q≤5000,询问包含根到叶、兄弟节点和反向点对。
对于前 90% 的数据,n,q≤50000,包含深链和大规模随机树。
对于 100% 的数据,满足完整数据范围。
Solution
这题我写了两个做法。
首先看见祖先和儿子容易想到 LCA ,如果节点\(u,v\)的 LCA 为\(u\),则\(u\)为\(v\)的祖先。
基于这个原理可以直接写出代码
AC code 1
#include<bits/stdc++.h>
using namespace std;
int n,m,s,dep[500005],f[500005][21];
vector<vector<int> >g;
void dfs(int u,int fa){
dep[u]=dep[fa]+1;
f[u][0]=fa;
for(int i=1;i<=20;i++)f[u][i]=f[f[u][i-1]][i-1];
for(int i=0;i<g[u].size();i++){
if(g[u][i]!=fa)dfs(g[u][i],u);
}
}
int lca(int x,int y){
if(dep[x]<dep[y])swap(x,y);
for(int i=20;i>=0;i--){
if(dep[f[x][i]]>=dep[y]){
x=f[x][i];
}
}
if(x==y){return y;}
for(int i=20;i>=0;i--){
if(f[x][i]!=f[y][i]){
x=f[x][i];
y=f[y][i];
}
}
return f[x][0];
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>n>>m;
g.resize(n+1);
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1,0);
while(m--){
int x,y;
cin>>x>>y;
if(lca(x,y)==x)cout<<"YES\n";
else cout<<"NO\n";
}
return 0;
}
其次我们可以注意到,如果\(v\)是\(u\)的后代,那么\(v\)的\(dfn\)应当在\(u\)的\(dfn\)与\(out\)之间。还是那句话,大家可以模拟一下,很容易理解。
AC code 2
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=50005;
int n,dfn[N],idx,q,out[N];
vector<vector<int> >g;
inline void dfs(int now,int f){
dfn[now]=++idx;
for(int i=0;i<g[now].size();i++){
int v=g[now][i];
if(v==f)continue;
dfs(v,now);
}
out[now]=idx;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>q;
g.resize(n+1);
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1,0);
while(q--){
int u,v;
cin>>u>>v;
if(dfn[u]<=dfn[v]&&out[u]>=dfn[v])cout<<"YES\n";
else cout<<"NO\n";
}
return 0;
}
dfn 还可以解决别的问题,比如我们刚刚说的子树求和。
Example 2
这里给出题面是方便大家理解代码。
题目描述
给定一棵以 1 为根的树,每个点有一个初始权值。
有 q 次操作,操作分为两种:
1 u x:把以 u 为根的子树中所有点的权值都加上 x;
2 u:查询以 u 为根的子树中所有点的权值和。
请依次回答所有查询操作。
输入格式
第一行两个整数 n q。
第二行 n 个整数 a[1], a[2], ..., a[n],表示初始权值。
接下来 n - 1 行,每行两个整数 u v,表示树上的一条无向边。
接下来 q 行,每行表示一次操作,格式如题目描述。
输出格式
对每个 2 u 操作,输出一行一个整数,表示对应子树的权值和。
样例
输入
5 5
1 2 3 4 5
1 2
1 3
2 4
2 5
2 2
1 2 10
2 2
1 1 -3
2 1
输出
11
41
30
数据范围
对于全部数据,1 <= n, q <= 50000,-10^9 <= a[i], x <= 10^9。
数据特征
对于前 20% 的数据,n,q≤25,包含单点、短链和少量正负修改。
对于前 45% 的数据,n≤300,q≤500,树形包含链、菊花、二叉树和随机树。
对于前 70% 的数据,n,q≤5000,操作会覆盖根节点、叶子节点和中间子树。
对于前 90% 的数据,n,q≤50000,包含深链、大菊花形树和接近上限的操作数。
对于 100% 的数据,权值和修改量可能达到正负边界,满足原题完整数据范围。
AC code
这里我们用线段树维护因为我是把我树剖的代码拷过来用的所以写的是线段树,树状数组貌似也可以但我没试。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=100000;
int n,q,rt=1;
int sz[N+5],val[N+5],dfn[N+5],idx;
vector<vector<int> >g;
int tree[4*N+5],tag[4*N+5],a[N+5];
int ls(int p){return p<<1;}
int rs(int p){return p<<1|1;}
void push_up(int p){tree[p]=tree[ls(p)]+tree[rs(p)];}
void build(int p,int pl,int pr){
if(pl==pr){
tree[p]=a[pl];
return;
}
int mid=pl+(pr-pl)/2;
build(ls(p),pl,mid);
build(rs(p),mid+1,pr);
push_up(p);
}
void addtag(int p,int pl,int pr,int d){
tag[p]+=d;
tree[p]+=(pr-pl+1)*d;
}
void push_down(int p,int pl,int pr){
if(tag[p]){
int mid=pl+(pr-pl)/2;
addtag(ls(p),pl,mid,tag[p]);
addtag(rs(p),mid+1,pr,tag[p]);
tag[p]=0;
}
}
void update(int l,int r,int p,int pl,int pr,int d){
if(l<=pl&&pr<=r){
addtag(p,pl,pr,d);
return;
}
push_down(p,pl,pr);
int mid=pl+(pr-pl)/2;
if(l<=mid)update(l,r,ls(p),pl,mid,d);
if(r>mid)update(l,r,rs(p),mid+1,pr,d);
push_up(p);
}
int query(int l,int r,int p,int pl,int pr){
if(l<=pl&&pr<=r)return tree[p];
push_down(p,pl,pr);
int res=0,mid=pl+(pr-pl)/2;
if(l<=mid)res+=query(l,r,ls(p),pl,mid);
if(r>mid)res+=query(l,r,rs(p),mid+1,pr);
return res;
}
void t_init(int now,int fa){
dfn[now]=++idx;
sz[now]=1;
for(int i=0;i<g[now].size();i++){
if(g[now][i]==fa)continue;
int to=g[now][i];
t_init(to,now);
sz[now]+=sz[to];
}
}
int query_subtree(int x){return query(dfn[x],dfn[x]+sz[x]-1/*区间终点,具体为什么参照我前面的解释理解*/,1,1,n);}
void update_subtree(int x,int by){update(dfn[x],dfn[x]+sz[x]-1,1,1,n,by);}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>q;
for(int i=1;i<=n;i++)cin>>val[i];
g.resize(n+1);
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
t_init(rt,0);
for(int i=1;i<=n;i++)a[dfn[i]]=val[i];
build(1,1,n);
while(q--){
int opt,u;
cin>>opt>>u;
if(opt==1){
int x;
cin>>x;
update_subtree(u,x);
}
else cout<<query_subtree(u)<<'\n';
}
return 0;
}
THE END
感谢您的阅读
虽然看起来不长但是那两个example展开的篇幅还是蛮大的

浙公网安备 33010602011771号