上一篇文章咱們學習了最短路徑的兩個算法。它們是有環圖的應用。下面咱們來談談無環圖的應用。java
1、拓撲排序node
博主大學學的是土木工程,在老本行,施工時很關鍵的節約人力時間成本的一項就是流水施工,鋼筋沒綁完,澆築水泥的那幫兄弟就得在那等着,因此安排好流水施工,讓工做週期能很好地銜接就很關鍵。這樣的工程活動,抽象成圖的話是無環的有向圖。算法
在表示工程的有向圖中,用頂點表示活動,弧表示活動之間的優先關係,這樣的有向圖爲頂點表示活動的網,成爲AOV網(Active On Vertex Network)數組
※ 若在一個具備n個頂點的有向圖G = (V,E)中,V中的頂點序列v1,v2, …… , vn知足若從頂點vi到vj有一條路徑,則在頂點序列中,頂點vi必然在vj以前。則咱們稱這樣的頂點序列爲一個拓撲序列。框架
所謂拓撲排序,其實就是對一個有向圖構造拓撲序列的過程。構造時會有兩個結果:1,此網的所有頂點都被輸出,則說明它是不存在環的AOV網;2,若是輸出頂點數少了,哪怕是少了一個,也說明這個網存在迴路,不是AOV網。學習
拓撲排序算法:this
對AOV網進行拓撲排序的基本思路是:從AOV網中選擇一個入度爲0的頂點輸出,而後刪去此頂點和以此頂點爲尾的弧,繼續重複此步驟,直到輸出所有頂點或者AOV網中不存在入度爲0的頂點爲止。spa
拓撲排序算法的實現顯然用鄰接表比較方便。咱們還須要另一個輔助的棧來存儲入度爲0的頂點,省得每次找入度爲0的頂點都要遍歷整個圖。code
給出示例圖以下:blog
代碼實現:
//邊結點的定義 package Graph.TopologicalSort; public class Edge { private int begin; private int end; private Edge next; public Edge getNext() { return next; } public void setNext(Edge next) { this.next = next; } public Edge(int begin, int end){ this.begin = begin; this.end = end; this.next = null; } 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; } }
//頂點結點的定義 package Graph.TopologicalSort; public class Vertex { private int data; private int in; private int out; private Edge edge; public Vertex(int data){ this.data = data; this.in = 0; this.out = 0; edge = null; } public Edge getEdge() { return edge; } public void setEdge(Edge next) { this.edge = next; } public int getData() { return data; } public void setData(int data) { this.data = data; } public int getIn() { return in; } public void setIn(int in) { this.in = in; } public int getOut() { return out; } public void setOut(int out) { this.out = out; } }
package Graph.TopologicalSort; import java.util.Stack; public class DigraphAdjust { private int numVertex; private int maxNumVertex; private Vertex[] vertexs; public DigraphAdjust(int maxNumVertex){ this.maxNumVertex = maxNumVertex; vertexs = new Vertex[maxNumVertex]; numVertex = 0; } public void addVertex(int data){ Vertex newVertex = new Vertex(data); vertexs[numVertex++] = newVertex; } public void addEdge(int begin, int end){ Edge newEdge = new Edge(begin, end); Vertex beginV = vertexs[begin]; beginV.setOut(vertexs[begin].getOut() + 1); vertexs[end].setIn(vertexs[end].getIn() + 1); if (beginV.getEdge() == null) { beginV.setEdge(newEdge); }else { Edge e = beginV.getEdge(); beginV.setEdge(newEdge); newEdge.setNext(e); } } public void deleteVertex(int index){ Edge e = vertexs[index].getEdge(); int k; for (; e != null; e = e.getNext()){ vertexs[e.getEnd()].setIn(vertexs[e.getEnd()].getIn() - 1); k = vertexs[e.getEnd()].getIn(); if (k == 0){ zeroIn.push(e.getEnd()); } } //這裏並不是真的刪除頂點,而是隻讓後續結點的入度減一便可 /* for (int i = index; i < numVertex - 1; i++) { vertexs[i] = vertexs[i + 1]; } numVertex--; */ } private Stack<Integer> zeroIn = new Stack<>(); //拓撲算法 public boolean TopologicalSort(){ for (int i = 0; i < numVertex; i++){ if (vertexs[i].getIn() == 0){ zeroIn.push(i); } } int count = 0; while (!zeroIn.isEmpty()){ int node = zeroIn.pop(); System.out.println(vertexs[node].getData() + " " + ++count); deleteVertex(node); } if (count < numVertex){ return false; }else { return true; } } }
總結:對一個具備n個頂點e條弧的AOV網來講,掃描頂點表將入度爲0的頂點入棧的時間複雜度是O(n),以後的while循環中,每一個頂點進一次棧,出一次棧,入度減1的操做共執行了e次,因此整個算法的時間複雜度爲O(n+e)
2、關鍵路徑
關鍵路徑是爲了解決工程完成須要的最短期問題。
在一個表示工程的帶權有向圖中,用頂點表示事件,用有向圖表示活動,用邊上的權值表示活動的持續時間,這種有向圖的邊表示活動的網,咱們稱之爲AOE網。
以下:
路徑上各個活動所持續的時間之和成爲路徑長度,從原點到終點具備最大長度的路徑叫作關鍵路徑,在關鍵路徑上的活動叫作關鍵活動。
爲此,須要定義以下幾個參數:
1.事件的最先發生時間etv(earliest time of vertex):即頂點vkvk的最先發生時間
2.事件的最晚發生時間ltv(latest time of vertex):即頂點vkvk的最晚發生時間,也就是每一個頂點對應的事件最晚須要開始時間,超出此時間將會延誤整個工期
3.活動的最先開工時間ete(earliest time of edge):即弧akak的最先發生時間
4.活動的最晚開工時間lte(latest time of edge):即弧akak的最晚發生時間,也就是不推遲工期的最晚開工時間
如何找關鍵路徑:
若是一個活動,它的最先開始時間和最晚開始時間是同樣的,也就是說,它不能被拖延,那麼它就是關鍵活動了。關鍵活動的長度決定了工程總耗時。那麼咱們找到全部活動的最先開始時間和最晚開始時間,比較哪些活動的兩者是相等的,這些活動就是關鍵活動。
關鍵路徑算法:
咱們先求事件的最先發生時間etv,利用咱們上面講過的從頭到尾找拓撲序列的過程,而且在這個過程當中存下每一個頂點前驅的發生時間加上兩者之間邊的權值,就是該頂點的最先發生時間。
代碼以下
import java.util.Stack; public class Graph { private int numVertex; private int maxNumVertex; private VertexC[] vertexs; public Graph(int maxNumVertex){ this.maxNumVertex = maxNumVertex; vertexs = new VertexC[maxNumVertex]; numVertex = 0; } public void addVertex(int data){ VertexC newVertex = new VertexC(data); vertexs[numVertex++] = newVertex; } public void addEdge(int begin, int end, int weight){ EdgeC newEdge = new EdgeC(begin, end, weight); VertexC beginV = vertexs[begin]; beginV.setOut(vertexs[begin].getOut() + 1); vertexs[end].setIn(vertexs[end].getIn() + 1); if (beginV.getEdge() == null) { beginV.setEdge(newEdge); }else { EdgeC e = beginV.getEdge(); beginV.setEdge(newEdge); newEdge.setNext(e); } } public void deleteVertex(int index){ EdgeC e = vertexs[index].getEdge(); int k; for (; e != null; e = e.getNext()){ vertexs[e.getEnd()].setIn(vertexs[e.getEnd()].getIn() - 1); k = vertexs[e.getEnd()].getIn(); if (k == 0){ zeroIn.push(e.getEnd()); } //關鍵部分:求各頂點事件的最先發生時間。 //即剛剛被刪除的頂點的最先發生時間加上這兩點之間權值 與 要求的頂點以前的最先發生時間 之間取較大值 if(etv[topoStack.peek()] + e.getWeight() > etv[e.getEnd()]){ etv[e.getEnd()] = etv[topoStack.peek()] + e.getWeight(); } } //這裏並不是真的刪除頂點,而是隻讓後續結點的入度減一便可 /* for (int i = index; i < numVertex - 1; i++) { vertexs[i] = vertexs[i + 1]; } numVertex--; */ } private Stack<Integer> zeroIn = new Stack<>(); private Stack<Integer> topoStack = new Stack<>(); private int[] etv; //事件的最先發生時間 private int[] ltv; //事件的最晚發生時間 //拓撲排序算法 public boolean TopologicalSort(){ for (int i = 0; i < numVertex; i++){ etv = new int[numVertex]; if (vertexs[i].getIn() == 0){ zeroIn.push(i); } etv[i] = 0; } int count = 0; while (!zeroIn.isEmpty()){ int node = zeroIn.pop(); //System.out.println(vertexs[node].getData() + " " + ++count); topoStack.push(node); //將彈出的頂點序號壓入拓撲排序的棧 deleteVertex(node); } if (count < numVertex){ return false; }else { return true; } }
而後將ltv數組初始化爲etv[]最後一個元素的值,每一個頂點的最晚發生時間是其每一個後繼節點的最晚發生時間減去兩者之間活動的持續時間,這樣咱們求得了ltv數組。
以後再根據etv數組和ltv數組求得ete數組。ete數組是活動的最先開工時間,它等於它的前驅事件的最先發生時間,也就是說ete數組和etv數組是相等的。
lte數組是活動的最晚開工時間,也就等於它的後繼事件的最晚發生時間減去活動的持續時間,也就等於對應的ltv數組減去weight。這樣咱們把兩個數組都求出來了。
後面只須要比較每一個頂點的ete和lte是否相等,就知道這個活動是否是關鍵活動。代碼以下
public void CriticalPath(){ int[] ete = new int[numVertex]; //保存邊上活動的最先開始時間,其index表示該邊begin的index int[] lte = new int[numVertex]; //保存邊上活動的最晚開始時間 EdgeC e; //下面用來保存頂點的臨時變量 TopologicalSort(); //先經過拓撲排序求出etv //初始化ltv ltv = new int[numVertex]; for (int i = 0; i < numVertex; i++){ ltv[i] = etv[numVertex - 1]; } while (!topoStack.isEmpty()) { int node = topoStack.pop(); int adj; //求得每個頂點事件的最晚發生時間,相似反向拓撲排序 for (e = vertexs[node].getEdge(); e != null; e = e.getNext()) { adj = e.getEnd(); if (ltv[adj] - e.getWeight() < ltv[node]) { ltv[node] = ltv[adj] - e.getWeight(); } } for (int i : ltv) { System.out.println(i); } } //求關鍵路徑 . + for (int index = 0; index < numVertex; index++){ for (e = vertexs[index].getEdge(); e != null; e = e.getNext()){ ete[index] = etv[index]; lte[index] = ltv[e.getEnd()] - e.getWeight(); if (ete[index] == lte[index]){ System.out.printf("(%d,%d) : %d \n", vertexs[index].getData(), vertexs[e.getEnd()].getData(), e.getWeight()); } } } }
這個例子只是求得了惟一一條關鍵路徑,並不表明再別的例子中不存在多條關鍵路徑。
到這裏圖就基本講的差很少了,下面放框架