1. 程式人生 > >[從今天開始修煉資料結構]無環圖的應用 —— 拓撲排序和關鍵路徑演算法

[從今天開始修煉資料結構]無環圖的應用 —— 拓撲排序和關鍵路徑演算法

上一篇文章我們學習了最短路徑的兩個演算法。它們是有環圖的應用。下面我們來談談無環圖的應用。

  一、拓撲排序

    博主大學學的是土木工程,在老本行,施工時很關鍵的節約人力時間成本的一項就是流水施工,鋼筋沒綁完,澆築水泥的那幫兄弟就得在那等著,所以安排好流水施工,讓工作週期能很好地銜接就很關鍵。這樣的工程活動,抽象成圖的話是無環的有向圖。

    在表示工程的有向圖中,用頂點表示活動,弧表示活動之間的優先關係,這樣的有向圖為頂點表示活動的網,成為AOV網(Active On Vertex Network)

   ※ 若在一個具有n個頂點的有向圖G =  (V,E)中,V中的頂點序列v1,v2, …… , vn滿足若從頂點vi到vj有一條路徑,則在頂點序列中,頂點vi必然在vj之前。則我們稱這樣的頂點序列為一個拓撲序列。

    所謂拓撲排序,其實就是對一個有向圖構造拓撲序列的過程。構造時會有兩個結果:1,此網的全部頂點都被輸出,則說明它是不存在環的AOV網;2,如果輸出頂點數少了,哪怕是少了一個,也說明這個網存在迴路,不是AOV網。

  拓撲排序演算法:

   對AOV網進行拓撲排序的基本思路是:從AOV網中選擇一個入度為0的頂點輸出,然後刪去此頂點和以此頂點為尾的弧,繼續重複此步驟,直到輸出全部頂點或者AOV網中不存在入度為0的頂點為止。

    拓撲排序演算法的實現顯然用鄰接表比較方便。我們還需要另外一個輔助的棧來儲存入度為0的頂點,免得每次找入度為0的頂點都要遍歷整個圖。

    給出示例圖如下:

    

 

 

   程式碼實現:

//邊結點的定義
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)

二、關鍵路徑

  關鍵路徑是為了解決工程完成需要的最短時間問題。

  在一個表示工程的帶權有向圖中,用頂點表示事件,用有向圖表示活動,用邊上的權值表示活動的持續時間,這種有向圖的邊表示活動的網,我們稱之為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());
                }
            }
        }
     }

這個例子只是求得了唯一一條關鍵路徑,並不代表再別的例子中不存在多條關鍵路徑。

 

到這裡圖就基本講的差不多了,下面放框架