DFS序及其应用
DFS序及其应用
定义
按照深度优先搜索(DFS)的访问顺序对树节点进行排序,即得到树的dfs序。
性质
在dfs序中,每个节点及其整个子树的节点的dfs序是一段数值连续的编号
因此对于子树的操作,就可以转化成对于一段区间的操作。可以与 线段树 和 树状数组 结合使用。
包括后续的 树链剖分。
基本模板
vector<int> e[N];
int n;
int ln[N],rn[N],id[N],tot;
//ln[i]表示i的dfs序,rn[i]表示i的子树中最后访问到的节点dfs序。
//因此节点i的子树对应的区间为 [ln[i],rn[i]]。
//id[i]表示dfs序中i对应的原节点编号
void dfs(int u,int fa){
ln[u]=++tot;
id[tot]=u;
for(auto v:e[u]){
if(v==fa) continue;
dfs(v,u);
}
rn[u]=tot;
}
例题
P2982 [USACO10FEB] Slowing down G DFS序+区间修改+单点查询
思路:
容易发现,每头奶牛经过在自己前面的父亲节点就会放慢一次速度。反过来,每头奶牛会让后面的所有子树节点的奶牛都放慢一次速度。使用DFS序后,问题转变成为 区间修改+单点查询 问题。使用线段树或树状数组即可解决。树状数组代码更易实现。
代码:
#include<bits/stdc++.h>
using namespace std;
using i64=long long;
const int N = 1e5 + 10;
vector<int> e[N];
int n;
int ln[N],rn[N],id[N],tot;
int tr[N];
void dfs(int u,int fa){
ln[u]=++tot;
id[tot]=u;
for(auto v:e[u]){
if(v==fa) continue;
dfs(v,u);
}
rn[u]=tot;
}
void add(int x,int c){
for(;x<=n+1;x+=x&-x) tr[x]+=c;
}
int ask(int x){
int ret=0;
for(;x;x-=x&-x) ret+=tr[x];
return ret;
}
void Showball(){
cin>>n;
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs(1,-1);
for(int i=1;i<=n;i++){
int x;
cin>>x;
cout<<ask(ln[x])<<"\n";
add(ln[x],1);
add(rn[x]+1,-1);
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t=1;
//cin>>t;
while(t--){
Showball();
}
return 0;
}
打击传销 DFS序+静态区间排名
给你一棵树,查询每个节点权值在其子树中的排名(从小到大排,若有相同,该节点视为最小的一个)。
思路:
首先根据DFS序,问题转换为静态区间排名问题。解决方法很多,可以使用 可持久化线段树,平衡树来解决。
但是代码量较大。由于本题为静态区间,参考 pzr 大佬的这篇 博客 的思路。
我们将所有询问离线,表示为一个五元组 \((x,y,k,op,id)\)。正常的询问 \(op=1\)。表示第 \(id\) 个查询,查询 \([x,y]\) 中所有大于等于 \(k\) 的数有多少个。
对于每个询问,我们额外增加一个 \(op=0\) 的询问。将所有询问进行排序(按照 \(k\) 为第一关键字,从大到小。\(op\) 为第二关键字,从小到大。)
解决用树状数组处理询问。
\(op=0\),将 \(x\) 进行单点修改,增加一。
\(op=1\),查询区间 \([x,y]\) 的和即为答案。
这是因为再查询之前,我们已经将所有大于等于 \(k\) 的元素全部加入到了树状数组中,因此区间和直接为所求。
本题求排名,只需要用子树大小减去区间和即可。
代码:
#include<bits/stdc++.h>
using namespace std;
using i64=long long;
const int N = 1e5 + 10;
vector<int> e[N];
int n;
int a[N];
int ln[N],rn[N],id[N],tot;
int tr[N];
struct node{
int x,y,k,op,id;
bool operator<(const node &u)const{
if(k!=u.k) return k>u.k;
return op<u.op;
}
};
void dfs(int u,int fa){
ln[u]=++tot;
id[tot]=u;
for(auto v:e[u]){
if(v==fa) continue;
dfs(v,u);
}
rn[u]=tot;
}
void add(int x,int c){
for(;x<=n;x+=x&-x) tr[x]+=c;
}
int ask(int x){
int ret=0;
for(;x;x-=x&-x) ret+=tr[x];
return ret;
}
void Showball(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs(1,-1);
vector<node> q;
for(int i=1;i<=n;i++){
q.push_back(node{ln[i],rn[i],a[i],1,i});
q.push_back(node{ln[i],rn[i],a[i],0,i});
}
sort(q.begin(),q.end());
vector<int> ans(n+1);
for(auto it:q){
if(!it.op) add(it.x,1);
else{
int siz=rn[it.id]-ln[it.id]+1;
int cur=ask(it.y)-ask(it.x-1);
ans[it.id]=siz-cur+1;
}
}
for(int i=1;i<=n;i++) cout<<ans[i]<<" ";
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t=1;
//cin>>t;
while(t--){
Showball();
}
return 0;
}
复杂的树上操作 DFS序+单点修改+区间最值+换根
给定一棵大小为 n 的有根点权树,支持以下操作:
• 换根
• 修改点权
• 查询子树最小值
思路:
对于修改点权和查询子树最小值。使用DFS序和线段树即可解决。
考虑如何解决 换根。
容易发现换根对于路径上的问题没有影响,对于子树有影响。
容易发现,对于新根子树中的节点,一定没有影响。对于不是新根祖先的节点,一定也没有影响。
影响的只有新根的祖先节点。容易发现,换根之后,祖先的覆盖范围就变成了全树抛去新根到祖先路径上距离祖先距离最近的节点的子树大小,设这个点为 \(u\) , 那么 \(dfs\) 序中的范围就是 \([1,ln[u]-1] \cup[rn[u]+1,n]\) 。
这个节点可以通过树上倍增快速求得。
代码:
#include<bits/stdc++.h>
using namespace std;
using i64=long long;
const int N = 1e5 + 10;
vector<int> e[N];
int n,q;
int a[N];
int ln[N],rn[N],id[N],tot;
int dep[N],f[N][20];
int root=1;
void dfs(int u,int fa){
ln[u]=++tot;
id[tot]=u;
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(auto v:e[u]){
if(v==fa) continue;
dfs(v,u);
}
rn[u]=tot;
}
int get_fa(int u,int k){
for(int i=0;i<20;i++){
if(k>>i&1) u=f[u][i];
}
return u;
}
//线段树
struct node{
int l,r;
int mn;
}tr[N*4];
#define ls u<<1
#define rs u<<1|1
void pushup(node &u,node &l,node &r){
u.mn=min(l.mn,r.mn);
}
void pushup(int u){
pushup(tr[u],tr[ls],tr[rs]);
}
void build(int u,int l,int r){
tr[u].l=l,tr[u].r=r;
if(l==r){
tr[u].mn=a[id[r]];
return;
}
int mid=l+r>>1;
build(ls,l,mid),build(rs,mid+1,r);
pushup(u);
}
void modify(int u,int x,int v){//单点修改
if(tr[u].l==x&&tr[u].r==x) tr[u].mn=v;
else{
int mid=tr[u].l+tr[u].r>>1;
if(x<=mid) modify(ls,x,v);
else modify(rs,x,v);
pushup(u);
}
}
int query(int u,int l,int r){//区间查询
if(l<=tr[u].l&&tr[u].r<=r){
return tr[u].mn;
}
int mid=tr[u].l+tr[u].r>>1;
int mn=1e9;
if(l<=mid) mn=min(mn,query(ls,l,r));
if(r>mid) mn=min(mn,query(rs,l,r));
return mn;
}
void Showball(){
cin>>n>>q;
for(int i=1;i<=n;i++){
int v;
cin>>v>>a[i];
if(!v) continue;
e[v].push_back(i);
e[i].push_back(v);
}
dfs(1,-1);
build(1,1,n);
while(q--){
char op;
int x,y;
cin>>op>>x;
if(op=='V'){
cin>>y;
modify(1,ln[x],y);
}else if(op=='E'){
root=x;
}else{
if(x==root){
cout<<tr[1].mn<<"\n";
}else if(ln[x]<=ln[root]&&rn[root]<=rn[x]){
int u=get_fa(root,dep[root]-dep[x]-1);
cout<<min(query(1,1,ln[u]-1),query(1,rn[u]+1,n))<<"\n";
}else{
cout<<query(1,ln[x],rn[x])<<"\n";
}
}
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t=1;
//cin>>t;
while(t--){
Showball();
}
return 0;
}
P6584重拳出击 贪心+DFS序+区间修改+区间最值+换根
思路:
贪心,我们只要保证距离小Z 最远的 \(youyou\) 的距离都不大于 \(k\) 。
容易发现,从 \(x\) 节点走到 \(y\) 节点,那么以 \(x\) 为根下 \(y\) 的子树内所有的 \(youyou\) 的距离都会减 \(2\) 。其他位置不变。
那么,我们每次就朝着距离最大值最大的子树走。但是若存在多个子树距离最大值相同,如何解决?
那么可以 以逸待劳。此时呆着不动两个回合,就可以使得所有子树内的距离都减 \(2\) 。
需要修改子树值和维护子树最值。只用DFS序后,转化为区间修改+区间最值。线段树即可解决。
注意如果 \(x\) 需要向祖先节点走一步。那么换根后就会影响子树答案。采用之前的方法分割即可。
代码:
#include<bits/stdc++.h>
using namespace std;
using i64=long long;
const int N = 1e6 + 10;
vector<int> e[N];
int a[N];
int ln[N],rn[N],id[N],tot;
int dep[N],f[N];
void dfs(int u,int fa){
ln[u]=++tot;
id[tot]=u;
dep[u]=dep[fa]+1;
f[u]=fa;
for(auto v:e[u]){
if(v==fa) continue;
dfs(v,u);
}
rn[u]=tot;
}
//线段树
struct node{
int l,r;
int mx;
int tag;
}tr[N*4];
#define ls u<<1
#define rs u<<1|1
void pushup(node &u,node &l,node &r){
u.mx=max(l.mx,r.mx);
}
void pushup(int u){
pushup(tr[u],tr[ls],tr[rs]);
}
void addtag(node &u,int tag){
u.mx=max(u.mx+tag,0);
u.tag+=tag;
}
void pushdown(int u){
addtag(tr[ls],tr[u].tag);
addtag(tr[rs],tr[u].tag);
tr[u].tag=0;
}
void build(int u,int l,int r){
tr[u].l=l,tr[u].r=r;
if(l==r){
tr[u].mx=a[id[r]]*dep[id[r]];
return;
}
int mid=l+r>>1;
build(ls,l,mid),build(rs,mid+1,r);
pushup(u);
}
void modify(int u,int l,int r,int tag){//区间修改
if(l<=tr[u].l&&tr[u].r<=r) {
addtag(tr[u],tag);
return;
}
int mid=tr[u].l+tr[u].r>>1;
pushdown(u);
if(l<=mid) modify(ls,l,r,tag);
if(r>mid) modify(rs,l,r,tag);
pushup(u);
}
int query(int u,int l,int r){//区间查询
if(l<=tr[u].l&&tr[u].r<=r){
return tr[u].mx;
}
pushdown(u);
int mid=tr[u].l+tr[u].r>>1;
int mx=0;
if(l<=mid) mx=max(mx,query(ls,l,r));
if(r>mid) mx=max(mx,query(rs,l,r));
return mx;
}
void Showball(){
int n;
cin>>n;
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
int m;
cin>>m;
while(m--){
int x;
cin>>x;
a[x]=1;
}
int k,rt;
cin>>k>>rt;
dep[0]=-1;
dfs(rt,0);
build(1,1,n);
int cur=rt,cnt=0;
while(1){
cnt++;
int mx=0,pos=0;
bool flg=false;
//枚举cur相邻位置
for(auto v:e[cur]){
if(v==f[cur]) continue;
int tmp=query(1,ln[v],rn[v]);
if(tmp>mx){
mx=tmp;
pos=v;
flg=false;
}else if(tmp==mx){
flg=true;
}
}
//cur父亲节点
int tmp=max(query(1,1,ln[cur]-1),query(1,rn[cur]+1,n));
if(tmp>mx){
mx=tmp;
pos=f[cur];
flg=false;
}else if(tmp==mx){
flg=true;
}
if(mx<=k) break;
if(flg){//原地等待
modify(1,1,n,-1);
continue;
}
if(pos==f[cur]){
modify(1,1,ln[cur]-1,-2);
modify(1,rn[cur]+1,n,-2);
}else{
modify(1,ln[pos],rn[pos],-2);
}
cur=pos;
}
cout<<cnt<<"\n";
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t=1;
//cin>>t;
while(t--){
Showball();
}
return 0;
}

浙公网安备 33010602011771号