DFS序和歐拉序的降維打擊

1. DFS 序和時間戳

1.1 DFS 序

定義:樹的每一個節點在深度優先遍歷中進、出棧的時間序列。

如下樹的 dfs 序就是[1,2,8,8,5,5,2,4,3,9,9,3,6,6,4,7,7,1]

5.png

下圖為生成DFS的過程。對于一棵樹進行DFS序,除了進入當前節點時對此節點進行記錄,同時在回溯到當前節點時對其也記錄一下,所以DFS序中一個節點的信息會出現兩次。

Tips: 因為在樹上深度搜索時可以選擇從任一節點開始,所以DFS序不是唯一的。

6.png

DFS序的特點:

  • 可以把樹數據結構轉換為線性數據結構,從而可以把基于線性數據的算法間接用于處理樹上的問題。堪稱降維打擊。

  • 相同編號之間的節點編號為以此編號為根節點的子樹上的所有節點編號。

    [2,8,8,5,5,2]表示編號 2為根節點的子樹中所有節點為8,5

    [4,3,9,9,3,6,6,4]表示編號 4為根節點的子樹中所有節點為 3,9,6

  • 如果一個節點的編號連續相同,則此節點為葉節點。

  • 樹的DFS序的長度是2NN表示節點的數量)。

2.png

DFS序的代碼:

#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);}id[++cnt]=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;
}

測試數據:

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

1.2 時間戳

按照深度優先遍歷的過程,按每個節點第一次被訪問的順序,依次給予這些節點1?N的標記,這個標記就是時間戳。如果一個點的起始時間和終結時間被另一個點包括,這個點肯定是另一個點的子節點(簡稱括號化定理)。每棵子樹 x 在 DFS 序列中一定是連續的一段,結點 x 一定在這段的開頭。

7.png

dfs與時間戳的關系,對應列表中索引號和值的關系。

8.png

dfs代碼中添加進入節點時的順序和離開節點時的順序。

//……
//in 開始時間 out 結束時間
int in[maxn],out[maxn];
//……
void dfs(int x,int f) {//節點的 dfs 序id[++cnt]=x;//開始時間in[x]=cnt;for(int i=head[x]; i; i=nxt[i]) {int y=to[i];if(y==f)continue;dfs(y,x);}id[++cnt]=x;//結束時間out[x]=cnt;
}
//……

3. DFS 序的應用

3.1 割點

什么是割點?

如果去掉一個節點以及與它連接的邊,該點原來所在的圖被分成兩部分,則稱該點為割點。如下圖所示,刪除 2號節點,剩下的節點之間就不能兩兩相互到達了。例如 4號不能到5號,6號也不能到達1號等等。一個連通分量變成兩個連通分量!

9.png

怎么判斷圖是否存在割點以及如何找出圖的割點?

Tarjan 算法是圖論中非常實用且常用的算法之一,能解決強連通分量、雙連通分量、割點和割邊(橋)、求最近公共祖先(LCA)等問題。

Tarjan算法求解割點的核心理念:

  • 在深度優先遍歷算法訪問到k點時,此時圖會被k點分割成已經被訪問過的點和沒有被訪問過的點。
  • 如果k點是割點,則沒有被訪問過的點中至少會有一個點在不經過k點的情況下,是無論如何再也回不到已訪問過的點了。則可證明k點是割點。

下圖是深度優先遍歷訪問到2號頂點的時候。沒有被訪問到的頂點有4、5、6號頂點。

Tips: 節點邊上的數字表示時間戳。

10.png

其中56號頂點都不可能在不經過2號頂點的情況下,再次回到已被訪問過的頂點(13號頂點),因此2號頂點是割點。

問題變成如何在深度搜索到 k點時判斷,沒有被訪問過的點是否能通過此k或者不能通過此k點回到曾經訪問過的點。

算法中引入了回溯值概念。

回溯值表示從當前節點能回訪到時間戳最小的祖先,回溯值一般使用名為 low的數組存儲,low[i]表示節點 i的回溯值。

如下演示如何初始化以及更新節點的 low值。

  • 定義3 個數組。vis[i]記錄節點是否訪問過、dfn[i]記錄節點的時間戳、low[i]記錄節點的回溯值。如下圖所示,從 1號節點開始深搜,搜索到4號節點時,3個數組中的值的變化如下。也就是說,初始,節點的 low值和dfn值相同。或者說此時,回溯值還不能確定。

    Tips:注意一個細節,由1->3,認為 13的父節點。

11.png

  • 搜索到4號時,與4號相連的邊有4->14->1是沒有訪問過的邊,且1號節點已經標記過訪問過,也就是說通過4號點又回到了1號點。所以說4->1是一條回邊,或者說 1-……-4之間存在一個環。則4號點的 low[4]=min( low[4],dfn[1] )=1

12.png

  • 因為 24的父節點,顯然也是能通過4號點回到1號點,所以也需要更新其low值,更新表達式為 low[2]=min(low[2],low[4])。同理3號點是2號點的父節點,也能通過 3->2->4->1回到1號點。所以3號點的low也需要更新。low[3]=min(low[2],low[3])

13.png

  • 繼續更新5、6號節點的low值。

14.png

根據這些信息,如何判斷割點。

  • 如果當前點為根節點時,若子樹數量大于一,則說明該點為割點(子樹數量不等于與該點連接的邊數)。
  • 如果當前點不為根節點,若存在一個兒子節點的low值大于或等于該點的dfn值時(low[子節點] >= dfn[父節點]),該點為割點(即子節點,無法通過回邊,到達某一部分節點(這些節點的dfn值小于父親節點))。這個道理是很好理解的,說明子節點想重回樹的根節點是無法繞開父節點。

3.2 割邊

定義:即在一個無向連通圖中,如果刪除某條邊后,圖不再連通。如下圖刪除2-55-6后,圖不再具有連通性。
15.png

刪除2-55-6邊后。

16.png

那么如何求割邊呢?

只需要將求割點的算法修改一個符號就可以。只需將low[v]>=num[u]改為low[v]>num[u],取消一個等于號即可。因為low[v>=num[u]代表的是點v 是不可能在不經過父親結點u而回到祖先(包括父親)的,所以頂點u是割點。

如果low[y]和num[x]相等則表示還可以回到父親,而low[v]>num[u]則表示連父親都回不到了。倘若頂點v不能回到祖先,也沒有另外一條路能回到父親,那么 w-v這條邊就是割邊,

3.3 Tarjan 算法

#include <iostream>
#include <string.h>
#include <string>
#include <algorithm>
#include <math.h>
#include <vector>
using namespace std;
const int maxn = 123456;
int n, m, dfn[maxn], low[maxn], vis[maxn], ans, tim;bool cut[maxn];
vector<int> edge[maxn];void cut_bri(int cur, int pop) {vis[cur] = 1;// 1表示正在訪問中dfn[cur] = low[cur] = ++tim;int children = 0; //子樹數量for (int i : edge[cur]) { //對于每一條邊if (i == pop || vis[cur] == 2)continue;if (vis[i] == 1) //遇到回邊low[cur] = min(low[cur], dfn[i]); //回邊處的更新 (有環)if (vis[i] == 0) {cut_bri(i, cur);children++;  //記錄子樹數目low[cur] = min(low[cur], low[i]); //父子節點處的回溯更新if ((pop == -1 && children > 1) || (pop != -1 && low[i] >= dfn[cur])) { //判斷割點if (!cut[cur])ans++;   //記錄割點個數cut[cur] = true; //處理割點}if(low[i]>dfn[cur]) { //判斷割邊edge[cur][i]=edge[i][cur]=true;  //low[i]>dfn[cur]即說明(i,cur)是橋(割邊);}}}vis[cur] = 2; //標記已訪問
}
int main() {scanf("%d%d", &n, &m);for (int i = 1; i <= m; i++) {int x, y;scanf("%d%d", &x, &y);edge[x].push_back(y);edge[y].push_back(x);}for (int i = 1; i <= n; i++) {if (vis[i] == 0)cut_bri(i, -1); //防止原來的圖并不是一個連通塊//對于每個連通塊調用一次cut_bri}printf("%d\n", ans);for (int i = 1; i <= n; i++) //輸出割點if (cut[i])printf("%d ", i);return 0;
}

4.歐拉序

定義:進入節點時記錄,每次遍歷完一個子節點時,返回到此節點記錄,得到的 2 ? N ? 1 長的序列;

歐拉序和DFS序的區別,前者在每一個子節點訪問后都要記錄自己,后者只需要訪問完所有子節點后再記錄一次。如下圖的歐拉序就是:
1 2 8 2 5 2 1 7 1 4 3 9 3 4 6 4 1。每個點在歐拉序中出現的次數等于這個點的度數,因為DFS到的時候加進一次,回去的時候也加進。

17.png

1.png

性質:

  • 節點 x 第一次出現與最后一次出現的位置之間的節點均為 x 的子節點;

  • 任意兩個節點的 LCA 是歐拉序中兩節點第一次出現位置中深度最小的節點。兩個節點第一次出現的位置之間一定有它們的LCA,并且,這個LCA一定是這個區間中深度最小的點。

根據歐拉序的性質,可以用來求解CLA。如上圖,求解 LCA(9,6)

  • 在歐拉序中找到96第一次出現的位置。

18.png

  • 直觀比較,知道4號節點是其LCA,特征是96之間深度最小的節點。

歐拉序求LCA,先求圖的歐拉序、時間戳(可以記錄進入和離開節點的時間)以及節點深度。有了這些信息,理論上足以求出任意兩點的LCA。變成了典型的RMQ問題。

19.png

為了提升多次查詢性能,可以使用ST表根據節點的深度緩存節點的信息。j=0時如下圖所示。

20.png

j=1表示區間長度為 2,值為區間長度為 1的兩個子區間的深度值小的節點。

21.png

歐拉序求LCA

#include <iostream>
#include <string.h>
#include <string>
#include <algorithm>
#include <math.h>
#include <vector>
using namespace std;
const int maxn = 10000;
int n, m, dfn[maxn], dep[maxn], tim;
int ol[maxn];
int st[maxn][maxn],lg2[maxn];
vector<int> edge[maxn];
void dfs(int cur, int fa) {ol[++tim]=cur;dfn[cur]=tim;dep[cur]=dep[fa]+1;for (int v : edge[cur]) { //對于每一條邊if(v==fa)continue;dfs(v,cur);ol[++tim]=cur;}
}void stPreprocess() {lg2[0] = -1;  // 預處理 lg 代替庫函數 log2 來優化常數for (int i = 1; i <= (n << 1); ++i) {lg2[i] = lg2[i >> 1] + 1;}for (int i = 1; i <= (n << 1) - 1; ++i) {st[i][0] = ol[i];}for (int j = 1; j <= lg2[(n << 1) - 1]; ++j) {for (int i = 1; i + (1 << j) - 1 <= ((n << 1) - 1); ++i) {st[i][j] = dep[ st[i] [ j - 1 ] ] < dep[ st[ i + (1 << j - 1)][j - 1 ]    ]  ? st[i][j - 1 ] : st[ i + (1 << j - 1)][j - 1 ];}cout<<endl;}
}int getlca(int u, int v) {if(dfn[u]>dfn[v])swap(u,v);u=dfn[u],v=dfn[v];int d=lg2[v-u+1];int f1=st[ u ][d  ];int f2=st[v-(1<<d)+1 ][ d ];return dep[f1]<dep[f2]?f1:f2;
}int main() {scanf("%d%d", &n, &m);for (int i = 1; i <= m; i++) {int x, y;scanf("%d%d", &x, &y);edge[x].push_back(y);edge[y].push_back(x);}dfs(1, 0);for (int i = 1; i <= 2*n-1; i++) //輸出割點printf("%d-%d  ", ol[i],dfn[ ol[i] ]);stPreprocess();int u,v;cin>>u>>v;int res=getlca(u,v);cout<< res;return 0;
}

5. 總結

DFS序和歐拉序并不難理解,正如四兩撥千斤,卻能解決很多復雜的問題。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/166546.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/166546.shtml
英文地址,請注明出處:http://en.pswp.cn/news/166546.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

多線程Thread(初階二:Thread類及常??法)

目錄 一、Thread 的常?構造?法 繼承Thread代碼&#xff1a; 實現Runnable接口代碼: 二、Thread 的?個常?屬性 1、id&#xff1a; 2、獲取線程的名字。 3、進程的狀態&#xff1a; 4、在java中設置的優先級&#xff0c; 5、是否后臺線程&#xff0c; 6、是否存活&a…

ubuntu22.04 arrch64版在線安裝node

腳本 #安裝node#下載node、npm國內鏡像&#xff08;推薦&#xff09;# 判斷是否安裝了nodeif type -p node; thenecho "node has been installed."elsemkdir -p /home/zenglg cd /home/zenglgwget https://registry.npmmirror.com/-/binary/node/v10.14.1/node-v10.…

Linux系統編程 day04 文件和目錄操作

Linux系統編程 day04 文件和目錄操作 1. 文件IO1.1 open 函數1.2 close函數1.3 read函數1.4 write函數1.5 lseek函數1.6 errno變量1.7 文件示例1 讀寫文件1.8 文件示例2 文件大小的計算1.9 文件示例3 擴展文件大小1.10 文件示例4 perror函數的使用1.11 阻塞與非阻塞的測試 2. 文…

關于「光學神經網絡」的一切:理論、應用與發展

/目錄/ 一、線性運算的光學實現 1.1. 光學矩陣乘法器 1.2. 光的衍射實現線性運行 1.3. 基于Rayleigh-Sommerfeld方程的實現方法 1.4. 基于傅立葉變換的實現 1.5. 通過光干涉實現線性操作 1.6. 光的散射實現線性運行 1.7. 波分復用&#xff08;WDM&#xff09;實現線性運…

Educoder中MATLAB數值計算與符號計算

第1關&#xff1a;數據處理 a[20 5 7 19 23 14 25 67 23 12]; %%%%%%%%% Begin %%%%%%%% smaxmax(a); sminmin(a); smeanmean(a); smedianmedian(a); ssumsum(a); %%%%%%%%% End %%%%%%%%% m[smax;smin;smean;smedian;ssum]; disp(m); 第2關&#xff1a;多項式計算與數值微積…

脈沖幅度調制信號的功率譜計算

本篇文章是博主在通信等領域學習時&#xff0c;用于個人學習、研究或者欣賞使用&#xff0c;并基于博主對人工智能等領域的一些理解而記錄的學習摘錄和筆記&#xff0c;若有不當和侵權之處&#xff0c;指出后將會立即改正&#xff0c;還望諒解。文章分類在通信領域筆記&#xf…

風口下的危與機:如何抓住生成式AI黃金發展期?

回顧AI的發展歷程&#xff0c;我們見證過幾次重大突破&#xff0c;比如2012年ImageNet大賽的圖像識別&#xff0c;2016年AlphaGo與李世石的圍棋對決&#xff0c;這些進展都為AI的普及應用鋪設了道路。而ChatGPT的出現&#xff0c;真正讓AI作為一個通用的產品&#xff0c;走入大…

Linux | 創建 | 刪除 | 查看 | 基本命名詳解

Linux | 創建 | 刪除 | 查看 | 基本命名詳解 文章目錄 Linux | 創建 | 刪除 | 查看 | 基本命名詳解前言一、安裝Linux1.1 方法一&#xff1a;云服務器方式1.2 方法二&#xff1a;虛擬機方式 二、ls2.2 ll 三、which3.1 ls -ld 四、pwd五、cd5.1 cd .\.5.2 ls -al5.3 重新認識命…

程序員兼職需要收藏的防坑技巧

不管你是剛剛上車的新職員&#xff0c;還是職場經營多年的老手&#xff0c;在零散時間&#xff0c;通過兼職搞一點零花錢&#xff0c;充實一下自己的生活&#xff0c;這是在正常不過的事情&#xff0c;但是很多同學害怕兼職有風險&#xff0c;被騙或者說找不到門路&#xff0c;…

優思學院|質量工程師在汽車行業待遇好嗎?

優思學院認為質量工程師在汽車行業的待遇有可能相對較好的。隨著中國汽車品牌在國內市場的崛起&#xff0c;特別是在電動汽車領域的增長&#xff0c;質量工程師在保障產品質量和安全性方面變得非常重要。由于中國汽車制造商對產品質量的高度重視&#xff0c;質量工程師在制定和…

AC自動機(簡單模板)

AC自動機&#xff0c;就相當于是在字典樹上用kmp。next數組回退的位置為最大匹配字符串在字典樹上的節點位置。 在獲取字典樹上的next數組的時候用的是BFS每次相當與處理的一層。 下圖中紅線為&#xff0c;可以回退的位置&#xff0c;沒有紅線的節點回退的位置都是虛擬原點。…

基于C#實現線段樹

一、線段樹 線段樹又稱"區間樹”&#xff0c;在每個節點上保存一個區間&#xff0c;當然區間的劃分采用折半的思想&#xff0c;葉子節點只保存一個值&#xff0c;也叫單元節點&#xff0c;所以最終的構造就是一個平衡的二叉樹&#xff0c;擁有 CURD 的 O(lgN)的時間。 從…

關于同一接口有多個不同實現的設計方案

關于同一接口有多個不同實現的設計方案 前言 最近公司做了一個銀行相關的項目&#xff0c;告訴我公司對接了多個銀行的支付&#xff0c;每個銀行都有對應的接口要去對接&#xff0c;比如&#xff1a;交易申請&#xff0c;交易取消&#xff0c;支付&#xff0c;回單&#xff0…

rabbitMQ發布確認-交換機不存在或者無法抵達隊列的緩存處理

rabbitMQ在發送消息時&#xff0c;會出現交換機不存在&#xff08;交換機名字寫錯等消息&#xff09;&#xff0c;這種情況如何會退給生產者重新處理&#xff1f;【交換機層】 生產者發送消息時&#xff0c;消息未送達到指定的隊列&#xff0c;如何消息回退&#xff1f; 核心&…

麒麟KYSEC使用方法05-命令設置密碼強度

原文鏈接&#xff1a;麒麟KYSEC使用方法05-命令設置密碼強度 hello&#xff0c;大家好啊&#xff0c;今天給大家帶來麒麟KYLINOS的kysec使用方法系列文章第五篇內容----使用命令設置密碼強度&#xff0c;密碼強度策略有兩個文件需要修改&#xff0c;pwquality.conf/login.defs&…

命令執行總結

之前做了一大堆的題目 都沒有進行總結 現在來總結一下命令執行 我遇到的內容 這里我打算按照過濾進行總結 依據我做過的題目 過濾system 下面是一些常見的命令執行內容 system() passthru() exec() shell_exec() popen() proc_open() pcntl_exec() 反引號 同shell_exec() …

大語言模型概述(三):基于亞馬遜云科技的研究分析與實踐

上期介紹了基于亞馬遜云科技的大語言模型相關研究方向&#xff0c;以及大語言模型的訓練和構建優化。本期將介紹大語言模型訓練在亞馬遜云科技上的最佳實踐。 大語言模型訓練在亞馬遜云科技上的最佳實踐 本章節內容&#xff0c;將重點關注大語言模型在亞馬遜云科技上的最佳訓…

解決Chrome瀏覽器無法啟動,因為應用程序的并行配置不正確

目錄 現象 方法1 方法2 附帶&#xff1a;書簽路徑 一次比較奇怪的問題&#xff0c;花了一些時間&#xff0c;記錄下來。 現象 進到本機默認安裝路徑&#xff1a; C:\Users\你的用戶名\AppData\Local\Google\Chrome\Application 下面會有個版本號的目錄&#xff0c;如我的…

跨地區企業組網方案對比與推薦

跨地區的企業&#xff0c;需要在不同的辦公室之間實現內部通信來進行業務協作。然而&#xff0c;在不同的地方建立局域網并將它們連接起來是一個棘手的問題。傳統的企業組網方案可能會面臨各種挑戰&#xff0c;包括網絡延遲、數據安全性、維護困難等等。 常見的組網方案有&…

快手ConnectionError

因為運行的程序被中斷導致 top然后查看站用處內存高的accelerate kill進程號 9回車