树的DFS序
基础概念
树的DFS序列就是说: 树的每一个节点在DFS中进出栈的时间序列。
具体来说就是对树从根开始进行深搜,按搜到的时间顺序把所有节点排队。
就比如
上面这棵树,它的一个DFS序就是:
1 4 6 6 3 9 9 3 4 7 7 2 5 5 8 8 2 1
注意两点:
-
一棵树的DFS序不唯一。
因为深搜的时候选择哪个子节点的顺序是不一样的。 -
对于一棵树进行DFS序,需要把回溯的时候的节点编号也记录一下,因此DFS序的长度是2N。
这就是为什么每个数字在DFS序中会出现两遍的原因。
性质
一个数字两次出现的位置所夹的区间,正好是以这个数为根的一个子树。
比如:
截取上面dfs序的2的两个位置的区间为:
2 8 8 5 5 2
那么2为根的子树是:2 8 5
经典用法:
配合树状数组进行区间修改,单点查询
代码实现
代码实现很简单,就是从根节点开始深搜,然后按顺序打标记就可以了。
但是打标记的地方可以有很多:
- id:DFS序列
- in: 以u为根节点的子树的在dfs序列的起始位置。
- out:以u为根节点的子树的在dfs序列的结束位置。
很明显,如果我们打了标记in和out,那么我们就能快速找到任何一棵子树。
因为
in[u]和out[u]的位置所夹的区间,正好是以这个数为根的一个子树。
代码:
vector<int> g[N];
int id[N],len;
int in[N],out[N];
void dfs(int u,int pre){
id[len++]=u;
in[u]=len;
for(int t:g[u]){
if(t!=pre)
dfs(t,u);
}
out[u]=len;
}
基础应用
Military Problem 模板题
题意:
给出n-1对关系(构成一棵树),q个询问,包含u,vu,v ,表示一个命令从u点下达,问在他的子树中第v个收到命令的节点编号
分析:
在这颗子树中,第v个收到命令的那就是dfs序为\(dfn[u]+v-1\)的节点
代码:
#include <vector>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
vector<int> g[100010];
int dfs_xu[200020], len;
int in[N], out[N], timestemp;
bool vis[N];
void dfs(int u)
{
// if(vis[u]) return;
// vis[u]=true;
dfs_xu[++len] = u;
in[u] = len;
for (int t : g[u])
{
dfs(t);
}
out[u] = len;
}
int main()
{
int n;
cin >> n;
int m;
cin >> m;
vector<int> head;
for (int i = 2; i <= n; i++)
{
int t;
cin >> t;
g[t].push_back(i);
}
dfs(1);
// for (int i = 1; i <= len; i++)
// {
// cout << dfs_xu[i] <<endl;
// }
for (int i = 0; i < m; i++)
{
int u, k;
cin >> u >> k;
if(k>out[u]-in[u]+1){
cout<<"-1"<<endl;
}
else cout<<dfs_xu[in[u]+k-1]<<endl;
}
}
选点 性质应用
题意:
有一棵n个节点的二叉树,1为根节点,每个节点有一个值wi。现在要选出尽量多的点。
对于任意一棵子树,都要满足:
- 如果选了根节点的话,在这棵子树内选的其他的点都要比根节点的值大;
- 如果在左子树选了一个点,在右子树中选的其他点要比它小。
分析:
如果我们把这颗树拉成一条链,根据规律左节点>右节点>根节点,相当于在这个序列上求出一个最长下降子序列。
代码:
#include <vector>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 10;
int dfs_xu[200020], len;
int in[N], out[N], timestemp;
int w[N];
int f[N];
int ch[2][N];
void dfs(int u)
{
if(!u) return ;
dfs_xu[++len] = w[u];
// in[u] = len;
dfs(ch[1][u]);
dfs(ch[0][u]);
// out[u] = len;
}
int main()
{
int n;cin>>n;
for(int i=1;i<=n;i++){
cin>>w[i];
}
for(int i=1;i<=n;i++){
int x,y;cin>>x>>y;
ch[0][i]=x;
ch[1][i]=y;
}
dfs(1);
int cnt=0;
f[++cnt]=dfs_xu[1];
for(int i=2;i<=n;i++){
if(dfs_xu[i]>f[cnt])
f[++cnt]=dfs_xu[i];
else {
int pos=lower_bound(f+1,f+cnt+1,dfs_xu[i])-f;
f[pos]=dfs_xu[i];
}
}
cout<<cnt<<endl;
return 0;
}
求和 树状数组
题意:
已知有 n 个节点,有 n-1条边,形成一个树的结构。
给定一个根节点 k,每个节点都有一个权值,节点i的权值为 \(v_i\)
给 m 个操作,操作有两种类型:
-
a x :表示将节点 a 的权值加上 x
-
a :表示求 a 节点的子树上所有节点的和(包括 a 节点本身)
分析:
我们就在dfs序上使用树状数组,
对于操作一,单点修改
对于操作二,区间查询\([dfn[u]\,\,,\,\,dfn[u]+siz[u]-1]\)
代码:
#include <bits/stdc++.h>
#define endl '\n'
#define int long long
using namespace std;
typedef pair<int, int> pii;
const int N = 1e6+10;
const int INF = 0x3f3f3f3f3f;
int n,m,k;
int val[N];
int tr[N];
vector<int> g[N];
int in[N],out[N],dfn[N],cnt;
int lowbit(int x){
return x&-x;
}
void add(int x,int c){
for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
int sum(int x){
int res=0;
for(int i=x;i;i-=lowbit(i)) res+=tr[i];
return res;
}
void get_dfs(int u,int fa){
dfn[++cnt]=val[u];
add(cnt,val[u]);
in[u]=cnt;
for(int v: g[u]){
if(v!=fa)
get_dfs(v,u);
}
out[u]=cnt;
}
void slove()
{
cin>>n>>m>>k;
for(int i=1;i<=n;i++){
cin>>val[i];
}
for(int i=1;i<n;i++){
int u,v;cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
// cout<<" sdff"<<endl;
get_dfs(k,0);
while(m--){
int kk;cin>>kk;
// cout<<kk<<endl;
if(kk==1){
int a,x;cin>>a>>x;
add(in[a],x);
// add(out[a],x);
}
else{
int a;cin>>a;
int res=sum(out[a])-sum(in[a]-1);
cout<<res<<endl;
}
}
}
signed main()
{
// int t;
// cin >> t;
// while (t--)
slove();
return 0;
}
原文