树的dfs序
二叉树
对于一颗二叉树,它有三种遍历方式:前序、中序、后序
前序(根左右),中序(左根右),后序(左右根)
这三种顺序其实都有使用的场景(主要是在分治算法中考虑是递归前,递归进行中,递归后)的区别。
多叉树
在多叉树里面,常用的dfs序其实也有三种:dfs序、扩展dfs序、欧拉序
dfs序类似二叉树的先序遍历
扩展dfs序类似二叉树的先序+中序
欧拉序类似二叉树的先序+后序
在树上问题中dfs序列往往是为了构建“树”与“序列”的桥梁。绝大部分的“子树”操作类问题,“树链”操作问题,其实本质来讲都是利用了dfs序列将树结构整理成数组。
树的dfs序
对于一颗多叉树,发现将其利用dfs序重标号后,每一颗子树的dfs序全都是连续的。
定义id[x]数组表示在dfs遍历的过程中x点dfn值,也就是遍历顺序。
定义index[x]数组表示在dfs遍历过程中dfn值为x的节点。
定义L[x]数组表示在dfs遍历的过程中x所覆盖子树的dfn最小值。
定义R[x]数组表示在dfs遍历的过程中x所覆盖子树的dfn最大值。
有了LR数组,子树的维护问题就转化成了数组区间的维护问题
注意
id(L),R,idx这三个数组,我们接下来在学习轻重树链剖分,长链剖分,dsu on tree的时候还会继续用到它们。
实际上,所有关于树的dfs序列的算法你都可以预处理出这几个数组。
扩展dfs序
扩展dfs序是一种“类中序”的处理。
一般来讲对于多叉树是用不到这种类中序的,因为多叉树的多个子树不像二叉树有左右的顺序关系,但是扩展dfs序被巧妙的使用在了一个黑科技LCA的模板中。
该模板支持
O(NlogN)时间预处理。
O(1)在线查询任意两点的LCA。
听上去好像没有卵用,但是要看使用场景。
该模板的使用场景为:
树的尺寸N和LCA的查询数目M不对等,并且在数量级上至少有
N<M*10的关系,并且题目需要强制在线。
这个时候就可以掏出这个非常牛逼的模板了。
其实也不是啥黑科技,就是一个普通的ST实现的RMQ。
实现
与之前做的dfs序一样,数组的含义没变,同样是id(L),R,idx这些。
不一样的是,我们要做一个“类中序”,即在每个子树遍历前做一个对于当前节点的额外遍历,额外遍历不是真正的初次遍历,不需要维护id(L)数组。
在遍历的过程中,维护一个pair类型的ST表,piar<deep,id>ST。
要查询任何两点的lca(x,y),就等于选中的两个点在idex数组表示的区间的deep数组rmq的最小值所对应的index数组的节点编号
具体如下图所示
要查询lca(4,6),在id查4和6所对应index值得到区间[5,9],并在deep数组里找到对应最小值为2,2所对应的index值为2,所以lca(4,6)=2
特殊
很有趣,因为利用扩展dfs序,LCA问题竟然转化成了RMQ问题。
(反过来是不是也可以转化呢)
确实可以,利用笛卡尔树,可以将任意的RMQ问题转化成LCA问题。
拓展--笛卡尔树
性质
- 有两个关键字 ( index , val )组成
- 如果只看第一个关键字index的时候它满足二叉搜索树(二叉查找树的性质)
即:中序遍历为原来的序列,并且\(index_{左儿子}< index_i<index_{右儿子}\) - 如果看第二个关键字的话它满足小根堆的性质。即:节点的权值 val,大于两个儿子的
- 任意两个点的LCA的权值就是它们之间的RMQ
建树
对于我们要满足二叉搜索树的性质的时候,我们肯定按顺序加入数字,并且每次加入的时候往最右边加入,这样肯定能实现对二叉搜索树的条件
然后再进行分类讨论
1.再右边找不到比这个点更大的权值,那么就把这个点作为根,原来的根节点作为这个点的左边节点
2.找到了这个点,那么就把这个点的右儿子移到新加入的这个点的左儿子上
例题9.F - Pre-order and In-order
笛卡尔树将rmq问题转化为lca问题
如下图
求[1,3]区间的最大值,就是lca(4,5)
笛卡尔树的dfs序所得到的L[i]和R[i]数组就是把i当做最大值的区间范围[ L[i],R[i] ]
笛卡尔树模型可以将和区间RMQ有关的问题(例如和RMQ问题有关的动态规划方程,区间DP等)转化成树上问题,然后可以利用树形DP求解。
扩展dfs序:LCA问题->RMQ问题
笛卡尔树:RMQ问题->LCA问题
所以你证明了,静态查询中LCA问题和RMQ问题是本质等价的。
欧拉序
欧拉序是一种“类前后序”,原本我们处理LR数组的时候,一般来讲只对L进行dfn++的操作,换句话说就是其实并没有为回溯的时候分配dfn。
一般认为只有分配了dfn才能够称得上是一次“遍历”不然只不过是在函数调用过程中路过了一下。
欧拉序和dfs的区别只在要不要对于R(回溯访问)分配dfn,但是其实你不分配也可以记录回溯时的dfn,除非它有用。
如下图
一般来讲,回溯时分配dfn的作用有两点:
一是为了“抵消”前后的影响,例如树链上莫队,树链求亦或和。
例如找3和4的路径,在index中(绿色框出的区间)路径上的点出现1次(lca不在区间内单独考虑),其他出现2次或0次(偶数次)的点都不是路径上的点
(当然要找路径2端点有一个端点是lca,lca也出现在区间内)
树链异或和就转化成了区间的异或和,因为不在链上的点会出现偶数次,就不会对答案产生影响
树上莫队也类似
二是利用欧拉序列维护信息,例如ETT(欧拉环游树)