dfs求連通塊

2021-09-25 08:43:46 字數 3379 閱讀 2689

遞迴是什麼?絕大部分人都會說:自己呼叫自己,剛開始我也是這樣理解遞迴的。確實沒錯,遞迴的確是自己呼叫自己。遞迴簡單的應用:編寫乙個能計算斐波那契數列的函式,也就是這樣:

int fac(int n)
相信絕大部分人都能看懂這段**。遞迴除了可以用自己呼叫自己這樣描述之外,還可以這樣表示遞迴函式:遞推式+邊界處理。很顯然,fac(n) = f(n-1) + f(n-2)就是這個計算斐波那契數列的遞推式,而上面的if語句就是邊界處理。但是,當我接觸到二叉樹這個資料結構時,這樣的遞迴定義顯然還不夠完整,還差一點。我們先介紹乙個資料結構,鍊錶,鍊錶是常見的基礎資料結構,對於鍊錶的實現書和網上都有講,那麼我們可不可以給鍊錶這樣乙個遞迴定義:

鍊錶:要麼結點為空,要麼由結點和子節點構成

這樣定義了鍊錶後,其實二叉樹的遞迴定義就好理解了:

二叉樹:要麼為空,要麼由根結點、左子樹和右子樹組成,而左子樹和右子樹分別是一棵二叉樹。 摘自《演算法競賽入門經典》

看完了二叉樹,就可以想到二叉樹的結點是可以用類似與實現鍊錶的方法來進行實現:

struct node;
和鍊錶相比,二叉樹只不過是多了乙個指標而已。但是呢,多了乙個指標有變得有點麻煩:我們在遍歷鍊錶時,只需要用乙個迴圈就能遍歷完煉表中的結點。

struct node;

void fun(node* root)

}

而對於二叉樹要寫多少個迴圈呢,乙個,兩個?我們發現,二叉樹並不能像鍊錶一樣簡單的遍歷,因為二叉樹每到乙個結點就有兩個方向可以走,並不像乙個for迴圈只規定乙個方向。還有乙個問題:當我們從根節點出發時,如果按照我們普通的方法遍歷,應該是從左到右遍歷,也就是先遍歷左子樹,遍歷完後再遍歷右子樹。

當遍歷左子樹時,我們發現,這個左子樹的跟結點也連線有左子樹和右子樹。

這時遍歷過程如果用迴圈寫就變得異常複雜,最關鍵的是,怎樣從左子樹遍歷完後開始右子樹的遍歷。有的人說,到了樹的末端就停止左子樹的遍歷,然後進行右子樹的遍歷。但是右子樹又要從哪個結點開始遍歷呢,所以我們還要寫乙個回溯的**,而這僅僅用迴圈是很難實現的,下面是回溯圖:

看起來非常複雜,其實我們用遞迴就可以解決這個問題,關鍵是我們對遞迴怎樣進一步地去理解。我們再次看回斐波那契函式的**:

int fac(int n)
其中if(n == 1 || n == 2) return 1;之前被認為是邊界處理,其實這裡還有個操作:回溯,也就是return 1;這個語句。在遞迴到達邊界後,就會把值返回給上乙個狀態。下面的return fac(n-1) + fac(n-2);中的return的作用也是回溯的操作。那麼,這個遞迴函式還有什麼值得研究的嗎?之前我們說的遞推式fac(n-1) + fac(n-2),在這裡我們把它稱為要重複做的事。所以,根據上面的解釋,我們又可以這樣理解遞迴:遞迴是可以幫你完成要重複做的事情,只要你規定好邊界和處理好回溯的問題。那麼遞迴相比於我們普通寫的迴圈(遞推)有什麼優勢呢?首先,相同點我們都知道,就是同樣可以完成要重複做的事,不同在於迴圈一般是完成單方向的重複做的事,如果是多方向的重複做的事可能要寫多重迴圈,甚至多重迴圈都不一定解決的了,**實現相對較難。而遞迴呢,則單方向和多方向要完成重複做的事都可以,而且關注點只是重複做的事情,處理好邊界和回溯問題就行了,減少思考的時間(這個時間因情況而定,如果你每一步遞迴全都要思考一遍,把過程寫出來,自然是會消耗不少時間,減少時間的前提是你把要重複做的事抽象化出來,處理好邊界問題後相信遞迴能計算出來),我先擺上遍歷二叉樹**:

struct node;

void dfs(node* root)

是不是遞迴函式的**很簡潔?我們分析為什麼遍歷二叉樹可以這樣寫:看回二叉樹的遞迴定義:結點,左子樹和右子樹。所以我們在遍歷時重複的操作是遍歷左子樹和右子樹,那麼怎樣遍歷左子樹和右子樹呢?首先肯定是要到左子樹和右子樹的根節點才能繼續遍歷。於是完整要做的重複事情是:到達乙個節點後,遍歷它的左結點和右結點。於是**就變成這樣:

void dfs(node* root)
這時遞迴函式的主要框架已經完成,也就是我們搞定了要重複做的事。接下來就要考慮邊界和回溯的問題。首先考慮邊界吧。當到達樹的底部時:

我們怎樣停止遍歷,也就是判斷的依據是什麼?

我們可以看到上圖,乙個結點是邊界的標誌是它的左右子節點都為空,也就是:

root->left == null && root->right == null
於是原來的**可以這樣寫:

void dfs(node* root)
當然,也可以這樣寫:

void dfs(node* root)
這樣寫看起來更簡潔一些,為什麼可行呢?當我們遍歷到最後乙個結點時,這個**會繼續遍歷左結點,然後到了左結點這個狀態。檢查這個結點,發現為空,所以返回。返回後遍歷右結點,發現右結點也為空,所以返回。然後遍歷完左結點和右結點後返回。這裡有些小夥伴可能會有些疑問?為什麼遍歷完左結點和右結點後會返回呢?這裡沒有返回**啊!其實這裡的返回只是省略不寫,因為是void型別啊,執行完後就會自動返回。所以完整的遍歷二叉樹的**是:

struct node;

void dfs(node* root)

這裡有個小坑:如果你的邊界處理是這樣:if(root->left == null && root->right == null) return;你要執行的操作應該在這個語句前面,否則會導致最後乙個點遍歷不了。所以最好寫成上面完整**的形式。

終於講到dfs,我要die了 dfs:深度優先搜尋,英文全稱:depth-first-search。剛剛遍歷二叉樹時,我們的函式名是不是寫成了dfs?對,沒錯,剛剛遍歷二叉樹的方法就是一種dfs。那麼,dfs如何實現呢?我想大家應該都猜到了:遞迴。所以理解遞迴尤為關鍵。

poj 1111 dfs(求連通塊周長)

題意 先輸入乙個矩陣,再輸入乙個起始位置,然後輸出與這個x在八個方向能夠連起來的所有x組成的圖形的周長。思路 找到這個連通塊顯然深搜即可。找周長也很簡單,只要對每個位置看看其四周是否不為x,如果是,周長加1。程式裡將所有位置初始化為 方便判斷。include include include incl...

dfs之找連通塊

尋找連通塊 題目傳送門 題目的大致意思就是給你一張地圖,找到規定的字元所組成的連通塊的數量。sample input 1 1 3 5 1 8 5 5 0 0 sample output01 22這個題,我們找由 組成的連通塊的數量。這裡的連通塊的定義為以某點為中心,向周圍八個點擴散的都是和中心點連通...

dfs的連通塊問題

01迷宮 連通塊 在搜尋的過程中,從開始的塊開始,它能走過的塊是連在一起的,即它們能夠走的最大距離是是相同的。所以在搜尋的過程中,我們要把走過的先連在一起,用個陣列儲存走過的每一塊,然後在搜尋的最後,對在陣列的每一塊賦值。include include using namespace std int...