重談DFS序、時間戳和歐拉序
本篇隨筆復習總結一下算法競賽中的DFS序、時間戳、歐拉序的相關知識。
DFS序的部分抄的是本蒟蒻今年年初的博客,鏈接放在下面:
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序中會出現兩遍的原因。
很容易發現的是,樹的DFS序的長度是\(2N\)。
求DFS序的代碼實現
代碼實現很簡單,就是從根節點開始深搜,然后按順序打標記就可以了。
在下面的代碼中,\(id[]\)數組就是DFS序的數組,\(cnt\)就是計時變量,上傳參數的\(f\)表示當前節點\(x\)的父親。
#include<cstdio>
using namespace std;
const int maxn=1e5+10;
int n;
int tot,to[maxn<<1],nxt[maxn<<1],head[maxn];
int id[maxn],cnt;
void add(int x,int y)
{
to[++tot]=y;
nxt[tot]=head[x];
head[x]=tot;
}
void dfs(int x,int f)
{
id[++cnt]=x;
for(int i=head[x];i;i=nxt[i])
{
int y=to[i];
if(y==f)
continue;
dfs(y,x);
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
dfs(1,0);
for(int i=1;i<=cnt;i++)
printf("%d ",id[i]);
return 0;
}
DFS序的性質
觀察上圖:
和這棵樹的一個DFS序:
1 2 8 8 5 5 2 7 7 4 3 9 9 3 6 6 4 1
我們發現,一個數字兩次出現的位置所夾的區間,正好是以這個數為根的一個子樹。比如:
2 8 8 5 5 2
就表示以2為根的子樹是:2 8 5
我們發現:DFS序的一個性質就是把一棵子樹放在一個區間里。這個優秀的性質把樹狀結構變成了線性結構。方便我們進行統計。
DFS序的部分應用
剛剛提到的DFS序的性質讓DFS序列成為了描述樹的一種方式。准確地來說,DFS序讓我們把樹狀結構變成了一個線性的結構。我們只需要在這個線性結構上進行區間修改、區間查詢,而不需要再一遍遍地遍歷整個子樹來做到修改和查詢。
這種性質的最顯然應用就是在樹鏈剖分中。樹鏈剖分就是把樹拆成一條條輕重鏈來把樹的所有點映射到一棵線段樹上來進行樹上的修改與統計。它的實現原理就是DFS序。
如果有興趣學習樹鏈剖分的讀者,請移步這篇博客:
樹上的很多問題都可以用到DFS序,這里不再一一列舉。希望讀者能掌握:樹轉區間的這個性質。在以后的OI生涯中加以應用。
關於時間戳
時間戳的概念是:按照深度優先遍歷的過程,按每個節點第一次被訪問的順序,依次給予這些節點\(1-N\)的標記,這個標記就是時間戳。
在鄙人的理解中,時間戳是DFS序的反向映射。
就是:DFS序的概念是按照深搜時間順序的節點編號序列,數組下角標存的是時間。
而時間戳的概念是按照深搜時間順序的時間編號序列,數組下角標存的是節點編號。
也就是:
\(id[i]=x\),i是時間,x是節點,id是DFS序。
\(dfn[x]=i\),i是時間,x是節點,dfn是時間戳。
很好理解吧。
一個誤區
網上很多講解說樹鏈剖分是用的DFS序,嚴格地說,這個說法是不太嚴謹的,事實上它是通過維護時間戳來實現樹轉連續區間的。並且,因為樹剖是輕重鏈剖分,並不是嚴格的深度優先遍歷,所以也不能武斷地說它就是DFS序或者時間戳。但是這么久了大家都這么說,況且時間戳也就是DFS序的一個“反函數”,所以大家這么說其實也沒太大問題。這個部分僅表示蒟蒻個人觀點,歡迎大佬們和蒟蒻探討相關問題。
歐拉序的概念和一些性質
還是這張圖:
歐拉序的定義是:從根節點出發到回到根節點為止,按深度優先遍歷的順序所經過的所有點的順序。
比如上面的樹的一個歐拉序就是:
1 2 8 2 5 2 1 7 1 4 3 9 3 4 6 4 1
和DFS序相似的,歐拉序也不唯一。
並且,可以探究出的性質是,每個點在歐拉序中出現的次數等於這個點的度數,因為DFS到的時候加進一次,回去的時候也加進。
所以歐拉序的長度是\(n+n-1=2n-1\)。
歐拉序的應用
歐拉序最主要的應用是求解LCA問題。
也就是將樹上的LCA問題,轉為區間的RMQ問題(最值)。
根據歐拉序的性質,兩個節點第一次出現的位置之間一定有它們的LCA,並且,這個LCA一定是這個區間中深度最小的點。(應該很好理解)
所以當我們預處理歐拉序和深度時,就可以把樹上的LCA問題變成兩個節點在歐拉序中的深度最小值RMQ。即解決了問題。