OI 必学:DFS序

作者: | 更新日期:

OI 必学的数据结构

本文首发于公众号:天空的代码世界,微信号:tiankonguse

零、背景

在 OI 比赛中,我们经常会遇到各种关于树的修改和查询问题。

而我们日常使用的数据结构,如线段树、树状数组,通常都是维护连续区间。

如果我们能把树上的节点映射到一段连续区间上,这类问题往往就能转化为熟悉的区间问题,从而迎刃而解。

本文主要介绍一种常用的树转换方法:DFS 序,并通过几个典型应用,帮助大家建立起对这类问题的整体认识。

一、DFS 序的定义

DFS 序,就是在对一棵树进行深度优先搜索(DFS)时,按照访问顺序记录下来的节点序列。

我们把每次「第一次访问到某个节点」的时刻记录下来,就得到一个长度为 n 的序列,这个序列就叫做 DFS 序。

在实现时,通常会同时维护三类信息:每个节点的进入时间、离开时间,以及某个时间戳对应的是哪个节点。

vector<int> dfn;  // dfs 序
vector<int> in;   // 进入时间
vector<int> out;  // 离开时间
int t = 0;

void BuildDfn(const int u, const int p) {
  in[u] = t;
  dfn[t] = u;
  t++;
  for (int v : g[u]) {
    if (v == p) continue;
    BuildDfn(v, u);
  }
  out[u] = t;
}

观察 DFS 序,可以发现它有一个非常关键的性质:

1)dfn 是一个长度为 n 的序列,其中 n 是节点数。
2)对任意一个节点 u,它的整棵子树在 DFS 序中对应的是一段连续区间 [in[u], out[u]]

正是因为这个「子树对应连续区间」的性质,我们才能把树上的很多操作转化为区间上的操作,从而使用线段树、树状数组等经典数据结构来解决。

源码截图

二、DFS 序的应用

下面通过几个常见的模型,看看 DFS 序是如何发挥作用的。

应用1:单点修改,子树查询

问题:给定一棵树,每次修改一个节点,查询某个节点子树中所有节点的权值和。

做法:先用 DFS 序把树「摊平」到区间上,每个节点对应区间中的一个位置。然后用线段树或树状数组,在这个数组上做单点更新与区间求和。

此时,节点 u 的整棵子树对应区间就是 [in[u], out[u]],在这个区间上做一次区间查询即可得到子树权值和。

应用2:子树修改,子树查询

问题:给定一棵树,每次对某个节点的整棵子树做加减操作,查询某个节点子树中所有节点的权值和。

做法:同样先建立 DFS 序,把树转成连续区间,然后在 [in[u], out[u]] 这个区间上做区间加、区间求和。可以使用支持区间修改与区间查询的数据结构,例如带懒标记的线段树或差分 + 前缀和。

应用3:树链修改,单点查询

问题:给定一棵树,每次对一条树链 (u, v) 上所有节点的权值同时加上某个值 a,然后支持单点查询某个节点当前的权值。

思路:仍然先用 DFS 序把树转成区间,但这次不直接在树链上做操作,而是把「树链加」转化为若干次以根为起点的路径加。

具体可以这样做:

先对 u 增加 a,即 Add(u, a),含义是把从根到 u 这条路径上的节点权值都加上 a。
然后对 v 增加 a,即 Add(v, a),含义是把从根到 v 这条路径上的节点权值都加上 a。

此时,从根到 lca(u, v) 的这条公共前缀路径被加了两次,从根到 fa(lca(u, v)) 的路径也被间接多加一次,需要再把多加的部分抵消掉:

于是,再对 lca(u, v) 减去 a,即 Add(lca(u, v), -a),含义是让 (root, lca(u, v)) 这条路径整体减去 a。
最后,对 fa(lca(u, v)) 再减去 a,即 Add(fa(lca(u, v)), -a),含义是让 (root, fa(lca(u, v))) 这条路径整体减去 a。

源码截图

单点查询某个节点 u 的权值时,本质上就是把子树上所有会对它产生贡献的标记累加起来,这些标记通过子树区间和即可得到。

应用4:单点修改,树链查询

问题:给定一棵树,每次只修改一个节点的权值,查询树链 (u, v) 上所有节点权值之和。

这个模型和「树链修改,单点查询」刚好是相反的方向。

一种常见的做法是:让每个节点维护「从根到当前节点路径上的权值和」,也就是把树上的信息按根到节点的路径前缀和来存。

单点更新时,更新的节点会影响它所在子树中所有节点的根路径和,因此可以在 DFS 序对应的子树区间内做一次区间更新。
树链查询 (u, v) 时,可以先分别求出 (root, u)(root, v) 两条路径的权值和,再减去 (root, lca(u, v)) 的权值和,最后再减去 (root, fa(lca(u, v))) 的权值和,就得到了树链 (u, v) 上的答案。

源码截图

三、最后

只记录每个节点被第一次访问到的时间(进入时间),并据此得到的序列,一般就被称为 DFS 序。

如果同时记录进入时间和离开时间,就可以得到更完整的欧拉序。

在很多竞赛题里,欧拉序相关的技巧往往都可以通过 DFS 序配合合适的数据结构来实现,因此作为入门,先把 DFS 序这一工具掌握牢固就足够应付绝大部分常见问题了。

对于更复杂的「子树 + 树链」混合修改与查询问题,还可以在 DFS 序的基础上叠加重链剖分、差分、树上前缀和等更高级的技巧,这部分内容会在后续文章中单独展开。

《完》

-EOF-

本文公众号:天空的代码世界
个人微信号:tiankonguse
公众号 ID:tiankonguse-code

本文首发于公众号:天空的代码世界,微信号:tiankonguse
如果你想留言,可以在微信里面关注公众号进行留言。

关注公众号,接收最新消息

tiankonguse +
穿越