接上文,研究了一下算法之后,發現大話數據結構的代碼風格更適合與前文中鄰接矩陣的定義相關聯,所以硬着頭皮把大話中的最小生成樹用自己的話整理了一下,希望大家能夠看懂。
一、最小生成樹
1,問題
最小生成樹要解決的是帶權圖 即 網 結構的問題,就是n個頂點,用n-1條邊把一個連通圖連接起來,並且使得權值的和最小。可以廣泛應用在修路建橋、管線運輸、快遞等各中網絡方面。我們把構造連通圖的最小代價生成樹成為最小生成樹。
最小生成樹有兩個算法 普里姆算法和克魯斯卡爾算法
2,普里姆算法
(1)普里姆算法的思路是,從一個入口進入,找到距離入口最近的下一個結點;現在你有了兩個結點,找到分別與這兩個結點連通的點中,權值最小的;現在你有了三個結點,分別找到與這三個結點聯通的點中,權值最小的;……
從思路可以看出,這就是一個切分問題。將一副加權圖中的點進行區分,它的橫切邊中權值最小的必然屬於子圖的最小生成樹。(橫切邊,指連接兩個部分的頂點的邊)也就是將已經找到的點,和沒找到的點進行區分。
解決思路就是貪心算法,使用切分定理找到最小生成樹的一條邊,並且不斷重復直到找到最小生成樹的所有邊。
(2)實現思路
下面我們就來一步一步地實現普里姆算法。為了方便討論,我們先規定一些事情。a,只考慮連通圖,不考慮有不連通的子圖的情況。b,只考慮每條邊的權值都不同的情況,若有權值相同,會導致生成樹不唯一
c,Vi的角標對應了其在vertex[]中存儲的角標。 d,這里我們從V0為入口進入。e,這里我們使用的是上一篇文章實現的鄰接矩陣存儲的圖結構。
首先,我們拿到一張網。
我們從V0進入,找到與V0相連的邊,鄰接點和權值。我們需要容器來存儲,因為是鄰接矩陣存儲的,所以我們這里用一維數組來存儲這些元素,我們仿照書中的代碼風格,定義一個adjvex[numVertex]和一個lowcost[numVertex]。這兩個數組的含義和具體用法我們后面再說,現在你只需要知道它們是用來橫切邊,鄰接點和權值的。adjvex[]的值被初始為0,因為我們從V0開始進入。lowcost[]的值被初始化為INFINITY,因為我們要通過這個數組找到最小權值,所以要初始化為一個不可能的大值,以方便后續比較,但這里的INFINITE 不需要手動設置,因為edges中已經將沒有邊的位置設置為了I,所以只需要在拷貝權值時同事將I也拷貝過來。
現在我們的切分只區分了V0和其他點,橫切邊權值有10,11,分別對應鄰接點V1,V5,我們將V1,V5的對應權值按照其在vertex[]中存儲的角標1,5來存入lowcost,將與V1,V5對應的鄰接點V0的角標0存入adjvex[]中(其實所有頂點的鄰接點對應角標都被初始化為0)。所以我們現在得到
adjvex[] = { 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 };
lowcost[] = { 0 ,10 , I , I , I , 11 , I , I , I };
這里大家要理解這兩個數組的含義,我再仔細解釋一下。lowcost中存有非I元素的位置的下標,表示的是,橫切邊所連接的,沒有被連入生成樹的一端的頂點在vertex[]中保存的下標,在這里也等於Vi的下標,也可以理解為“剛剛被發現的結點的下標”。
然后adjvex[]中保存了非0元素的下標,與lowcost的中的下標是一樣的意義,只不過這里因為被划分的點是0所以數組中沒有非0元素。
lowcost中存有的非I元素,表示的是,這個位置對應的頂點與現有最小生成樹的橫切邊中權值最小的一條邊的權值。
adjvex中存有的非0元素,是一個頂點在vertex[]中的下標,表示的是該角標index對應的vertex[index]與adjvex[index]這兩個頂點之間的權值最小,該權值是lowcost[index]。
這樣大家應該能夠對大話數據結構中這一部分有完整的理解了。
我們現在從lowcost[]中找到最小的權值,然后將該權值對應的頂點加入最小生成樹。加入的方式是將該權值置為0,並且將該新結點的鄰接點在adjvex中對應的角標置為該新結點的index。
對應現在的情況,就是把V1加入生成樹,把兩個數組調整如下:
adjvex[] = { 0 , 0 , 1 , 0 , 0 , 0 , 1 , 0 , 1 };
lowcost[] = { I , 0 , 18, I , I ,11 , 16, I , 12};
接下來我們有四條待選邊,lowcost中找到最小的非零權值是11,對應index為5,去vertex[5]找到V5,所以此時V5加入生成樹,將lowcost[5]置為0,V5的鄰接點加入adjvex和lowcost,如下
adjvex[] = { 0 , 0 , 1 , 0 , 5 , 0 , 1 , 0 , 1 };
lowcost[] = { I , 0 , 18, I , 26, 0 , 16, I , 12};
反復重復上面的動作v-1次,此時就加入了v-1條邊,得到了最小生成樹。
代碼實現如下:
public void MiniSpanTree_Prim(){ int[] adjvex = new int[numVertex]; int[] lowcost = new int[numVertex]; adjvex[0] = 0; lowcost[0] = 0; /* for (int i = 1; i < numVertex; i++){ lowcost[i] = INFINITY; } */ for (int i = 1; i < numVertex; i++) { lowcost[i] = edges[0][i]; adjvex[0] = 0; } for (int i = 1; i < numVertex; i++){ int min = INFINITY; int k = 0; for (int j = 1; j < numVertex; j++){ if (lowcost[j] != 0 && lowcost[j] < min){ min = lowcost[j]; k = j; } } System.out.printf("(%d,%d,w = %d)加入生成樹",adjvex[k],k,min); System.out.println(); lowcost[k] = 0; for (int j = 1; j < numVertex; j++){ if (lowcost[j] != 0 && lowcost[j] > edges[k][j]){ lowcost[j] = edges[k][j]; adjvex[j] = k; } } } }
2,克魯斯卡爾(Kruskal)算法
如果說普里姆算法是面向頂點進行運算,那么克魯斯卡爾算法就是面向邊來進行運算的。
(1)思路:克魯斯卡爾算法的思路是,在離散的頂點集中,不斷尋找權值最小的邊,如果加入該邊不會在點集中生成環,則加入這條邊,直到獲得最小生成樹。
所以我們的問題就是兩個,第一個:將邊按照權值排序 第二個:判斷加入一條邊之后會不會生成環
將邊按照權值排序很容易,這里我們用邊集數組
那么如何判斷環呢? 克魯斯卡爾判斷環的依據是,在一棵樹結構中,如果對其中兩個結點添加一條原本不存在的邊,那么一定會產生一個環。而一棵樹中,根節點是唯一確定的。我們將添加邊抽象為建立一棵樹,並用數組parent[numVertex]存儲這棵樹的結構,下標index與結點在vertex[]中的位置vertex[index]相同,parent[index]是這個結點在這棵樹中的父親結點。每次添加一條邊,就是在擴大森林中某一棵樹,當森林全部連成一棵樹,則得到了最小生成樹。
(2)具體步驟:我們這里使用與普里姆相同的例子
其邊集數組排序后為
我們遍歷邊集數組,首先拿到edges[0],分別判斷4和7是否擁有相同的最大頂點,方法是進入parent[]中查詢它們所在的樹的根結點是否相同。因為是第一次查詢,所以結果都是0,即它們不是同一棵樹,可以連線。連線時,將parent[4]置7或者將parent[7]置4都可以,這里我們選擇前者。
下面拿到edges[1],查詢parent[2],parent[8]得均為0,則不是同一棵樹,可以連線。我們將parent[2]置8
下面是edges[2],查詢0,1,可以連線。
接下來edges[3],查詢0,5,此時V0的父是V1,V1對應的parent[1]中存儲的0表示V1是這棵樹的父,parent[5]=0,即V0和V5不是同一棵樹,可以連線。將parent[1]置為5
接下來edges[4], 查詢1,8,不在同一棵樹,此時1所在樹的根是5,將1和8連線,此時樹合並應該將根節點5的parent[5]置為8.現在上圖的兩個棵樹合並了
接下來是edges[5],查詢3,7,不在同一子樹,連線。
接下來是edges[6],查詢1,6,不在同一子樹,連線。
接下來是edges[7]查詢5,6,發現它們的根節點都是8,在同一棵子樹,所以不連線。
下面我就不再重復了,總之這樣循環檢測,可以得到最終的最小生成子樹。(注!最小生成子樹和我們上面用來判斷是否連通的樹是不同的!parent數組也並不是唯一的!因為在構造判斷樹的時候,不管把誰做父,誰做子,都可以構建樹,並不影響判斷環的結果)
(3)代碼實現
/* 定義邊結構的內部類。 */ private class Edge implements Comparable<Edge> { private int begin; private int end; private int weight; private Edge(int begin, int end, int weight){ this.begin = begin; this.end = end; this.weight = weight; } @Override public int compareTo(Edge e) { return this.weight - e.weight; } public int getBegin() { return begin; } public void setBegin(int begin) { this.begin = begin; } public int getEnd() { return end; } public void setEnd(int end) { this.end = end; } public int getWeight() { return weight; } public void setWeight(int weight) { this.weight = weight; } }
/** * 得到排序好的邊集數組,用ArrayList存儲 * @return */ public ArrayList<Edge> getOrderedEdges() { ArrayList<Edge> edgeList = new ArrayList<>(); for (int row = 0; row < numVertex; row++){ for (int col = row; col < numVertex; col++){ if(edges[row][col] != 0 && edges[row][col] != INFINITY){ edgeList.add(new Edge(row, col, edges[row][col])); } } } Collections.sort(edgeList); return edgeList; } /** * 克魯斯卡爾算法 */ public void MiniSpanTree_Kruskal(){ ArrayList<Edge> edgeList = getOrderedEdges(); int[] parent = new int[numVertex]; for (int i = 0; i < numVertex; i++){ parent[i] = 0; } for (int i = 0; i < edgeList.size(); i++){ int m = findRoot(edgeList.get(i).getBegin(), parent); int n = findRoot(edgeList.get(i).getEnd(), parent); if (m != n){ link(edgeList.get(i), parent, m, n); } } } /* 連接兩點,並且設置parent數組 */ private void link(Edge edge, int[] parent, int m, int n) { System.out.printf("(%d,%d),weight = %d 加入最小生成樹", edge.getBegin(), edge.getEnd(), edge.getWeight()); System.out.println(); parent[m] = n; } /* 找到本子樹的根節點 */ private int findRoot(int root, int[] parent) { while (parent[root] > 0){ root = parent[root]; } return root; }
總結:克魯斯卡爾的FindRoot函數的時間復雜度由邊數e決定,時間復雜度為O(loge),而外面有一個for循環e次,所以克魯斯卡爾的時間復雜度是O(eloge)
對立兩個算法,克魯斯卡爾主要針對邊展開,邊少時時間效率很高,對於稀疏圖有很大優勢,;而普里姆算法對於稠密圖會更好一些。