【数据结构】树的DFS序&欧拉序



写在前面 - 本篇用到的输入和存图方法

输入方式:

  第一行两个数 n 和 root ,表示树共有 n 个节点,其中以编号为 root 的作为根节点

  接下来 n-1 行,每行两个整数 a b ,表示节点 a 与节点 b 相连

const int MAXN=1e4+50;

int dfs_order[MAXN];
int euler_order1[MAXN];
int euler_order2[MAXN];

bool vis[MAXN]; //访问标记
vector<int> G[MAXN]; //邻接表存图
int pos;
int n,root,a,b;
scanf("%d%d",&n,&root);
for(int i=1;i<n;i++)
{
    scanf("%d%d",&a,&b);
    G[a].push_back(b);
    G[b].push_back(a);//双向存边
}



DFS序

顾名思义,DFS序就代表着树从根节点开始dfs的访问顺序

也可以当作访问每个节点的时间戳

PIC1

以这棵树为例,根节点为 1

按照从左到右的顺序搜索,则它的搜索顺序便是

PIC2

在图中的顺序表示为

PIC3



DFS序的搜索代码

void dfs(int p)
{
    dfs_order[++pos]=p; //访问到节点p时,++pos作为访问到的时间
    vis[p]=true;//标记访问
    for(int i:G[p])//再搜索未访问过的与p相邻的节点
        if(!vis[i])
            dfs(i);
}



欧拉序

欧拉序长得跟dfs序相差无几

储存的则是从根节点开始,按照dfs的顺序经过所有点再绕回原点的路径

共存在两种欧拉序



欧拉序 1

这一种欧拉序相当于是在dfs的时候,如果储存节点的栈变化一次,就把栈顶的节点编号记录下来

也就是说,每当访问完一个节点的子树,则需要返回一次该节点,再继续搜索该节点的其余子树

在树上的移动过程为

PIC4

它的搜索顺序便是

PIC5



欧拉序 1 的搜索代码

void euler_dfs1(int p)
{
    euler_order1[++pos]=p;
    vis[p]=true;
    for(int i:G[p])
        if(!vis[i])
        {
            euler_dfs1(i);
            euler_order1[++pos]=p; //与dfs序的差别,在搜索完一棵子树后就折返一次自己
        }
} //数组需要开2倍n大



欧拉序 2

这一种欧拉序相当于是在dfs的时候,如果某个节点入栈,就把这个节点记录下来,直到后面的操作中这个节点出栈,再记录一次这个节点

也就是说,每个节点严格会在记录中出现两次,第一次是搜索到它的时候,第二次是它的子树完全被搜索完的时候

除根节点外,每个节点严格两个入度两个出度

在树上的移动过程为

PIC6

它的搜索顺序便是

PIC7

可以发现,某个节点在顺序中出现的两次所围成的区间,就表示这个节点与它的子树



欧拉序 2 的搜索代码

void euler_dfs2(int p)
{
    euler_order2[++pos]=p;
    vis[p]=true;
    for(int i:G[p])
        if(!vis[i])
            euler_dfs2(i);
    euler_order2[++pos]=p; //与dfs序的差别,在所有子树搜索完后再折返自己
} //数组需要开2倍n大



样例输入 & 程序


Sample Input :

9 1
1 2
1 3
2 4
2 5
3 6
4 7
4 8
4 9

Sample Output :

DFS Order :
1 2 4 7 8 9 5 3 6
Euler Order 1 :
1 2 4 7 4 8 4 9 4 2 5 2 1 3 6 3 1
Euler Order 2 :
1 2 4 7 7 8 8 9 9 4 5 5 2 3 6 6 3 1


模板程序:

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e4+50;

int dfs_order[MAXN],euler_order1[MAXN*2],euler_order2[MAXN*2];

bool vis[MAXN];
vector<int> G[MAXN];
int pos;

void dfs(int p)
{
    dfs_order[++pos]=p;
    vis[p]=true;
    for(int i:G[p])
        if(!vis[i])
            dfs(i);
}

void euler_dfs1(int p)
{
    euler_order1[++pos]=p;
    vis[p]=true;
    for(int i:G[p])
        if(!vis[i])
        {
            euler_dfs1(i);
            euler_order1[++pos]=p;
        }
}

void euler_dfs2(int p)
{
    euler_order2[++pos]=p;
    vis[p]=true;
    for(int i:G[p])
        if(!vis[i])
            euler_dfs2(i);
    euler_order2[++pos]=p;
}

int main()
{
    int n,root,a,b;
    scanf("%d%d",&n,&root);
    for(int i=1;i<n;i++)
    {
        scanf("%d%d",&a,&b);
        G[a].push_back(b);
        G[b].push_back(a);
    }

    puts("DFS Order :");
    memset(vis,false,n+5);
    pos=0;
    dfs(root);
    for(int i=1;i<=pos;i++)
        printf("%d ",dfs_order[i]);
    putchar('
');

    puts("Euler Order 1 :");
    memset(vis,false,n+5);
    pos=0;
    euler_dfs1(root);
    for(int i=1;i<=pos;i++)
        printf("%d ",euler_order1[i]);
    putchar('
');

    puts("Euler Order 2 :");
    memset(vis,false,n+5);
    pos=0;
    euler_dfs2(root);
    for(int i=1;i<=pos;i++)
        printf("%d ",euler_order2[i]);
    putchar('
');

    return 0;
}



应用 - 例题


1 - Codeforces 1006E

1006E. Military Problem

Prob-1-1
Prob-1-2
Prob-1-3

dfs序 的裸题

一棵 n 个节点的树,每条边都是单向边,第二行 n-1 个数分别对应 2,3,4... 节点的父节点编号

q 次询问,每次询问包含两个数 u k ,要求输出在dfs序中从 u 开始往后数到第 k 个所表示的节点,如果这个节点不存在于 u 的子树中,输出 -1

记录下处理dfs序时某个节点入栈时间 in 、出栈时间 out 以及dfs序实际表示的节点 即可

如果 in[u]+k-1 > out[u] 说明这个节点不包含于 u 的子树中

否则输出 dfs_order[in[u]+k-1]

#include<bits/stdc++.h>
using namespace std;

vector<int> G[200050];
int dfs_order[200050],in[200050],out[200050],pos;

void dfs(int p)
{
    in[p]=++pos;
    dfs_order[pos]=p;
    for(int it:G[p])
        dfs(it);
    out[p]=pos;
}

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    int n,q,d,a,b;
    cin>>n>>q;
    for(int i=1;i<n;i++)
    {
        cin>>d;
        G[d].push_back(i);
    }
    pos=0;
    dfs(1);
    while(q--)
    {
        cin>>a>>b;
        if(in[a]+b-1>out[a])
            cout<<"-1
";
        else
            cout<<dfs_order[in[a]+b-1]<<'
';
    }
    return 0;
}



2 - LibreOJ #144

#144. DFS 序 1

Prob-2

对树的权值进行 点修改子树和查询

可以以 dfs序 的时间戳作为索引来维护一个树状数组

将每个节点入栈的时间 in 和出栈的时间 out 记录下来

则这个节点与它的子树就在区间 [ in , out ] 中

查询操作只要取 sum(out) - sum(in-1) 作为答案即可

修改操作只要修改入栈时间 in 那个点的值即可

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

int n,ar[1000050],in[1000050],out[1000050],pos=0;
bool vis[1000050];
vector<int> G[1000050];
ll tree[1000050];

int lowbit(int x)
{
    return x&(-x);
}
void add(int p,ll d)
{
    while(p<=n)
    {
        tree[p]+=d;
        p+=lowbit(p);
    }
}
ll sum(int p)
{
    ll r=0;
    while(p>0)
    {
        r+=tree[p];
        p-=lowbit(p);
    }
    return r;
}

void dfs(int p)
{
    in[p]=++pos; //入栈时间
    vis[p]=true;
    for(int it:G[p])
        if(!vis[it])
            dfs(it);
    out[p]=pos; //出栈时间
}

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    int m,r,a,b,kd;
    cin>>n>>m>>r;
    for(int i=1;i<=n;i++)
        cin>>ar[i];
    for(int i=1;i<n;i++)
    {
        cin>>a>>b;
        G[a].push_back(b);
        G[b].push_back(a);
    }
    dfs(r);
    for(int i=1;i<=n;i++)
        add(in[i],ar[i]); //以时间作为索引建立树状数组
    while(m--)
    {
        cin>>kd;
        if(kd==1)
        {
            cin>>a>>b;
            add(in[a],b);
        }
        else
        {
            cin>>a;
            cout<<sum(out[a])-sum(in[a]-1)<<'
';
        }
    }
    return 0;
}



3 - LibreOJ #145

#145. DFS 序 2

Prob-3

对树的权值进行 全子树修改子树和查询

与上一题相差无几

可以以 dfs序 的时间戳作为索引来维护一棵线段树

依然按照入栈时间 in 和出栈时间 out 作为索引

然后套上区间修改和区间求和的线段树即可

又因为线段树的某个节点维护的是一段区间,也就是一段时间

所以在建树的时候要注意,再引入一个 anti_in 数组记录与 in 数组相反的数据

具体用法如下

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN=1e6+50;

int n,ar[MAXN];
bool vis[MAXN];
vector<int> G[MAXN];
int in[MAXN],out[MAXN],pos=0,anti_in[MAXN];

struct node
{
	ll l,r,sum,lazy;
}tree[MAXN*4];

void push_up(int id)
{
	tree[id].sum=tree[id<<1].sum+tree[id<<1|1].sum;
}

void push_down(int id)
{
	if(tree[id].lazy)
	{
		int m=tree[id].r-tree[id].l+1;
		tree[id<<1].lazy+=tree[id].lazy;
		tree[id<<1|1].lazy+=tree[id].lazy;
		tree[id<<1].sum+=tree[id].lazy*(m-(m>>1));
		tree[id<<1|1].sum+=tree[id].lazy*(m>>1);
		tree[id].lazy=0;
	}
}

void buildTree(int id,int l,int r)
{
	tree[id].l=l;
	tree[id].r=r;
	tree[id].lazy=0;
	if(l==r)
	{
		tree[id].sum=ar[anti_in[l]]; //这里引用的是第l(或r)的时间访问到的节点id传给ar数组获取原有的权值
		return;
	}
	ll mid=(l+r)>>1;
	buildTree(id<<1,l,mid);
	buildTree(id<<1|1,mid+1,r);
	push_up(id);
}

void update(int id,int L,int R,ll val)
{
	if(L<=tree[id].l&&R>=tree[id].r)
	{
		tree[id].sum+=val*(tree[id].r-tree[id].l+1);
		tree[id].lazy+=val;
		return;
	}
	push_down(id);
	int mid=(tree[id].r+tree[id].l)>>1;
	if(L<=mid)
		update(id<<1,L,R,val);
	if(R>mid)
		update(id<<1|1,L,R,val);
	push_up(id);
}

ll query_sum(int id,int L,int R)
{
	if(L<=tree[id].l&&R>=tree[id].r)
		return tree[id].sum;
	push_down(id);
	int mid=(tree[id].r+tree[id].l)>>1;
	ll ans=0;
	if(L<=mid)
		ans+=query_sum(id<<1,L,R);
	if(R>mid)
		ans+=query_sum(id<<1|1,L,R);
	return ans;
}

void dfs(int p)
{
	in[p]=++pos; //in记录节点p入栈的时间
	anti_in[pos]=p; //anti_in记录在某个时间访问到的节点的id
	vis[p]=true;
	for(int it:G[p])
		if(!vis[it])
			dfs(it);
	out[p]=pos;
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	int m,r,a,b,kd;
	cin>>n>>m>>r;
	for(int i=1;i<=n;i++)
		cin>>ar[i];
	for(int i=1;i<n;i++)
	{
		cin>>a>>b;
		G[a].push_back(b);
		G[b].push_back(a);
	}
	dfs(r);
	buildTree(1,1,n); //建立线段树
	while(m--)
	{
		cin>>kd;
		if(kd==1)
		{
			cin>>a>>b;
			update(1,in[a],out[a],b);
		}
		else
		{
			cin>>a;
			cout<<query_sum(1,in[a],out[a])<<'
';
		}
	}
	return 0;
}
原文地址:https://www.cnblogs.com/stelayuri/p/12702684.html