這篇文章開始討論有關「樹」的一些簡單的概念和算法。算法
樹是一種基本的數據結構,之因此叫樹是由於來自於仿生——樹枝分叉的結構或者樹根分叉的結構,它很是好的表示出了各個節點之間的邏輯關係,它也是圖論當中一個很重要的結構。從它的名字的角度,咱們發現不少科學思惟的生髮都是源於對天然的敏銳的觀察的,這給科研人員提供了一個很是好的方法。數組
咱們觀察天然界的樹結構,很容易發現沒有哪棵樹的兩個枝葉長到了一塊兒,而抽象化的樹結構也是這樣,從根節點出發,一條路徑越走越深,是不會有迴路的,基於這個性質,咱們能夠外推樹的不少別的特性。數據結構
1.一棵樹中任意兩個節點有且僅有一條路徑連通。(很顯然,若是不是這樣,便會產生迴路)函數
2.一棵樹若是有n個節點,那麼它剛好有n-1條邊。spa
3.在一棵樹中加入一條邊將會構成一個迴路。code
基於樹的抽象模型,咱們在生活中各個方面其實都用到了這種結構,好比生物中的遺傳系譜圖、公司組織構圖,書的目錄,世界盃足球隊的對陣等等,經過這種基本的數據結構,咱們可以將生活不少雜亂的數據變得有條理、有邏輯而造成體系,這即是抽象化的事物給咱們的現實生活帶來的遍歷,也是咱們科學知識的初衷。blog
二叉樹:排序
樹結構中一個最基本也是最好用的結構就是二叉樹,性質如其名字,每一個節點至多有兩個子節點的樹結構叫作二叉樹。遞歸
滿二叉樹:對於高度爲h的二叉樹,除了第h層之外,1~h-1層的節點的子節點數都達到最大(2個),那麼這樣的結構成爲二叉樹。get
徹底二叉樹:對於一個二叉樹,除去葉節點的剩餘節點的子節點數都達到了最大(2個),那麼這樣的二叉樹稱爲徹底二叉樹。
徹底二叉樹有一個奇妙的性質,咱們從根節點開始按照「s」型給各個節點標號,會發現,對於第k個節點,它的左兒子的標號是2k , 右兒子的標號是2k + 1,反過來,它的根節點是(int)k/2。基於這條很好的性質,咱們可以將一個徹底二叉樹用一個一位數組存儲記錄。
基於徹底二叉樹的堆排序:
首先咱們來給出最小堆的定義:對於一個徹底二叉樹,這裏咱們在每一個節點中放入權值,並用一個一維數組heap[]來記錄,對於任意一個有子節點的節點i,都有heap[2i] > heap[i],heap[2i + 1] > heap[i]。這個定義通俗點來理解,就是說對於任意一個節點的權值都比它的兩個子節點的權值小。最大堆也有着相似的定義。
基於最小堆的定義,咱們很容易看到,對於一個長度爲n的最小堆,heap[1](也就是堆頂的元素),必定是最小的元素,那麼基於此,咱們來想想如何利用這樣一個最小堆來完成對數的排序。
step1:取出堆頂元素。
step2:將堆底最後一個元素拿到堆頂,此時顯然破壞了最小堆的性質,咱們須要從新構造出一個有n-1個節點的最小堆。
step3:重複step1,循環直到堆變成了空集。
很顯然,按照這樣的輸出順序便將n個數從小到大進行了排序,這邊是所謂的基於徹底二叉樹的堆排序過程。
那麼咱們如今要解決的一個重要問題即是,如何構造一個這樣很是有利於排序的最小堆呢?
從定義出發,咱們嘗試將這個大問題給子問題化,顯然若是每一個有兒子的節點都知足最小堆的性質,那麼這個徹底二叉樹即是一個最小堆,所以咱們只須要遍歷全部有兒子的節點,使其知足最小堆的性質便可。即找到當前節點及其兩個兒子的最小權值,而後利用交換,使得當前的根節點記錄這個最小權值便可。
簡單的參考代碼及註釋以下。
#include<cstdio> int h[101]; //記錄最小堆的二叉樹 int n; void swap(int x , int y) //交換函數 { int t; t = h[x]; h[x] = h[y]; h[y] = t; } void siftdown(int i) //調整第i個節點,與其兩個子節點,使其知足最小堆 { int t , flag = 0; while(2*i <= n && flag == 0) { if(h[i] > h[2*i]) t = 2*i; //將根節點i和左兒子比較 else t = i; if(2*i + 1 <= n) //將根節點i和右兒子比較 { if(h[t] > h[2*i + 1]) t = 2*i + 1; } if(t != i) { swap(t , i); i = t; } else flag = 1; } } void creat() //建立最小堆 , 遍歷有兒子節點從第n/2往前面,第n/2是最後一個有兒子的節點 { int i; for(i = n/2;i >= 1;--i) siftdown(i); } int deletemin() { int t; //刪除堆頂元素,並將堆底元素放到堆頂,從新構建最小堆 , 完成n個整數由小到大的排序 t = h[1]; h[1] = h[n]; n--; siftdown(1); return t; } int main() { int i , num ; scanf("%d",&num); for(i = 1;i <= num;i++) scanf("%d",&h[i]); n = num; creat(); for(i = 1;i <= num;i++) printf("%d ",deletemin()); return 0; }
並查集:
考慮這樣一個謎題,如今警方已知n個黑社會,m條線索,每條線索表示A是B的boss,那麼請問你這n個嫌疑人中,有多少個幫派,各個幫派的大boss又是誰?
咱們抽象化得來看謎題中給出的各個量之間的關係,n我的視爲n個點,而每條線索其實就表徵了兩個點之間的關係,想象一下,所謂幾個幫派是否是就是整個圖中造成的不相交集合(這即是所謂並查集的內涵)?而在每一個小集合當中,咱們基於相關的線索,是否也可以構建出一個」有方向「的樹結構,好比說,A是B的boss,那麼就將A視爲B的祖先,那麼這棵」幫派樹「構造下來,根節點即是這個幫派的大boss。
那麼好了,如今總體思路有了,如今咱們面臨這樣一個問題,給出一條線索以後,咱們如何判斷相關的兩我的是否屬於某個幫派呢?這邊是並查集的核心所在了。其實很是相似咱們用一維數組記錄二叉樹,這裏也是利用數組記錄樹結構,咱們設置f[i]記錄vi的祖先,假設當前給出一條線索說,A是B的boss,咱們經過f[]數組來訪問A、B所在幫派的大boss,若是相同,說明他們原本就在一個幫派裏面了,若是不相同呢?那麼須要把B加入到A的幫派當中,爲何是B加入到A當中呢?由於A是B的boss嘛,幫派顯然也是要從高層往下構建嘛。這裏須要注意的是,這個過程當中咱們只關心A、B是否在一個幫派當中,所以咱們須要訪問的是A、B兩人所在樹結構的根節點,也就是說,咱們在構建並查集的時候,對於f[i],咱們須要一直記錄vi所在樹結構的根節點。
那麼如今問題又來了,如何訪問vi所在樹結構的根節點呢?這裏也是並查集算法比較巧妙的一個地方,咱們初始化f[i] = i來表示每一個人都是本身的boss,而後當給出線索代表vj是vi的boss以後,咱們記錄,f[i] = j。所以對於訪問vi的根節點這件事情,就很好處理了,咱們訪問vi的父節點vj,判斷f[j]是否等於j,不然訪問f[j]的父節點vk……,很顯然嘛,這造成了一個遞歸,直到找到了根節點後返回。
其實上面基於的模型考慮到了邊的方向性,其實在不少實際問題的處理中,即便沒有邊的方向性,並查集也是可以處理的,這裏這樣描述只是爲了更清晰的引入並查集這個算法的過程。例如一開始的謎題,線索僅僅給出A和B是同夥,請你求解這n個黑社會中有幾個幫派,就是一種忽略邊的方向性的模型。
通過上文的分析,咱們可以簡單的代碼實現,參考以下。
#include<cstdio> int f[1000] = {0} , n , k , m , sum = 0; void init() { int i; for(i = 1;i <= n;i++) f[i] = i; } int getf(int v) { if(f[v] == v) return v; else { f[v] = getf(f[v]); return f[v]; } } void Merge(int v , int u) { int t1 , t2; t1 = getf(v); t2 = getf(u); if(t1 != t2) { f[t2] = t1; } } int main() { int i , x , y; scanf("%d %d",&n,&m); init(); for(i = 1;i <= m;i++) { scanf("%d %d",&x,&y); Merge(x , y); } for(i = 1;i <= n;i++) { if(f[i] == i) sum++; } printf("%d\n",sum); }
圖的最小生成樹:Kruskal算法
來考慮這樣一個謎題:給出n個城鎮和n個城鎮之間修建m條道路的費用(每條道路的起點終點都是某個城鎮),那麼如今爲了使n個城鎮中任意兩個城鎮都有路走,咱們修建公路的最小費用是多少?
首先考慮這樣一個問題,既然題目的要求僅僅是任意了兩個城市有路可走,那麼應該想到,咱們修出來的路是不須要構成環的,這很好理解,由於在印個環結構中,去掉任意一條邊來破壞這個環結構,構成環的那些點依然是彼此連通的。誒,沒有環結構,想想,是否是就是咱們提到樹結構的特色呢?而咱們要找的就是原圖(n個點、m條邊)的一個子圖,也就叫作生成樹,結合權值之和最小,這即是所謂的「最小生成樹」。
Kruskal算法給出了這樣一個貪心算法:
step1:將m條邊的權值由小到大進行排序。
step2:從最小的邊開始想只有點的樹結構中添加邊,並刪除該邊,前提是添加該邊不會使樹結構出現環。
step3:很明顯,n個節點造成的樹結構的邊數是n-1,所以該步驟是重複step2,直到當前樹結構中有n-1條邊。
Kruskal算法的正確性是不言自明的,就像上帝給的你一道乍現的靈光,讓你感受一切的文字解釋都顯得累贅而醜陋。所以這裏須要用到那個經典的詞語,顯然。
可是如今咱們面臨一個重要的問題,整個過程一個核心步驟是判斷是否有環出現(由於整個算法過程一直在迴避圈,由於這個算法也成爲「避圏法」),如何實現呢?結合咱們剛剛介紹過的並查集,能夠看大,當前咱們想要添加ei,鏈接着vi和vj,若是咱們利用並查集的方法查一下vi和vj的根節點,若是相同,顯然代表添加了ei會造成環;若是不相同,就能夠放心大膽的將當前權值最小的邊添加到正在構造的樹結構裏啦。
下面有簡單的參考代碼。
#include<cstdio> #include<algorithm> using namespace std; struct edge { int u; int v; int w; }; bool cmp(edge a , edge b) { return a.w < b.w; } struct edge e[10]; int n , m; int f[7]={0},sum = 0 , cnt = 0; int getf(int v) { if(f[v] == v) return v; else { f[v] = getf(f[v]); return f[v]; } } int Merge(int v , int u) { int t1 , t2; t1 = getf(v); t2 = getf(u); if(t1 != t2) { f[t2] = t1; return 1; } return 0; } int main() { int i; scanf("%d %d",&n,&m); for(i = 1;i <= m;i++) scanf("%d %d %d",&e[i].u,&e[i].v,&e[i].w); sort(e + 1,e + m + 1,cmp);//給m個邊的權值排序 for(i = 1;i <= n;i++)//並查集父節點初始化 f[i] = i; for(i = 1;i <= m;i++)//圖的最小生成樹:Kruskal算法 { if(Merge(e[i].u , e[i].v))//若是連通,則刪除此邊,不然選擇該邊 { cnt++; sum = sum + e[i].w; } if(cnt == n - 1) break; } printf("%d",sum); }