圖論:有向無環圖的排序——拓撲排序


圖論:有向無環圖的排序——拓撲排序

一、什么是拓撲排序

在圖論中,拓撲排序(Topological Sorting)是一個有向無環圖(DAG, Directed Acyclic Graph)的所有頂點的線性序列。且該序列必須滿足下面兩個條件:

  1. 每個頂點出現且只出現一次。
  2. 若存在一條從頂點 A 到頂點 B 的路徑,那么在序列中頂點 A 出現在頂點 B 的前面。

有向無環圖(DAG)才有拓撲排序,非 DAG 圖沒有拓撲排序一說。

例如,下面這個圖:

img

它是一個 DAG 圖,那么如何寫出它的拓撲排序呢?這里說一種比較常用的方法:

  1. 從 DAG 圖中選擇一個 沒有前驅(即入度為 0)的頂點並輸出。
  2. 從圖中刪除該頂點和所有以它為起點的有向邊。
  3. 重復 1 和 2 直到當前的 DAG 圖為空或當前圖中不存在無前驅的頂點為止。后一種情況說明有向圖中必然存在環。

img

於是,得到拓撲排序后的結果是 {1, 2, 4, 3, 5}。

通常,一個有向無環圖可以有一個或多個拓撲排序序列。

二、拓撲排序的應用

拓撲排序通常用來 “排序” 具有依賴關系的任務。

比如,如果用一個 DAG 圖來表示一個工程,其中每個頂點表示工程中的一個任務,用有向邊<A,B><A,B>表示在做任務 B 之前必須先完成任務 A。故在這個工程中,任意兩個任務要么具有確定的先后關系,要么是沒有關系,絕對不存在互相矛盾的關系(即環路)。

三、拓撲排序的實現

根據上面講的方法,我們關鍵是要維護一個入度為 0 的頂點的集合

圖的存儲方式有兩種:鄰接矩陣和鄰接表。這里我們采用鄰接表來存儲圖,C++ 代碼如下:

#include<iostream>
#include <list>
#include <queue>
using namespace std;

/************************類聲明************************/
class Graph
{
    int V;             // 頂點個數
    list<int> *adj;    // 鄰接表
    queue<int> q;      // 維護一個入度為0的頂點的集合
    int* indegree;     // 記錄每個頂點的入度
public:
    Graph(int V);                   // 構造函數
    ~Graph();                       // 析構函數
    void addEdge(int v, int w);     // 添加邊
    bool topological_sort();        // 拓撲排序
};

/************************類定義************************/
Graph::Graph(int V)
{
    this->V = V;
    adj = new list<int>[V];

    indegree = new int[V];  // 入度全部初始化為0
    for(int i=0; i<V; ++i)
        indegree[i] = 0;
}

Graph::~Graph()
{
    delete [] adj;
    delete [] indegree;
}

void Graph::addEdge(int v, int w)
{
    adj[v].push_back(w); 
    ++indegree[w];
}

bool Graph::topological_sort()
{
    for(int i=0; i<V; ++i)
        if(indegree[i] == 0)
            q.push(i);         // 將所有入度為0的頂點入隊

    int count = 0;             // 計數,記錄當前已經輸出的頂點數 
    while(!q.empty())
    {
        int v = q.front();      // 從隊列中取出一個頂點
        q.pop();

        cout << v << " ";      // 輸出該頂點
        ++count;
        // 將所有v指向的頂點的入度減1,並將入度減為0的頂點入棧
        list<int>::iterator beg = adj[v].begin();
        for( ; beg!=adj[v].end(); ++beg)
            if(!(--indegree[*beg]))
                q.push(*beg);   // 若入度為0,則入棧
    }

    if(count < V)
        return false;           // 沒有輸出全部頂點,有向圖中有回路
    else
        return true;            // 拓撲排序成功
}

測試如下 DAG 圖:

img

int main()
{
    Graph g(6);   // 創建圖
    g.addEdge(5, 2);
    g.addEdge(5, 0);
    g.addEdge(4, 0);
    g.addEdge(4, 1);
    g.addEdge(2, 3);
    g.addEdge(3, 1);

    g.topological_sort();
    return 0;
}

輸出結果是 4, 5, 2, 0, 3, 1。這是該圖的拓撲排序序列之一。

每次在入度為 0 的集合中取頂點,並沒有特殊的取出規則,隨機取出也行,這里使用的queue。取頂點的順序不同會得到不同的拓撲排序序列,當然前提是該圖存在多個拓撲排序序列。

由於輸出每個頂點的同時還要刪除以它為起點的邊,故上述拓撲排序的時間復雜度為O(V+E)O(V+E)。

另外,拓撲排序還可以采用 深度優先搜索(DFS)的思想來實現,詳見《topological sorting via DFS》。


拓撲排序的Java實現:

package com.jiading.topo;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;

/**
 * 拓撲排序,當前方案並沒有在節點類中加入過多的內容
 * 但是在圖類中加入了邊的集合adjaNode
 */
public class TopoSort {
    /**
     * 拓撲排序節點類
     */
    private static class Node {
        public Object val;
        public int pathIn = 0; // 入度
        //因為拓撲排序用不到出度,所以這里就沒算出度

        public Node(Object val) {
            this.val = val;
        }
    }

    /**
     * 拓撲圖類
     */
    private static class Graph {
        // 圖中節點的集合
        public Set<Node> vertexSet = new HashSet<Node>();
        // 相鄰的節點,紀錄邊
        //key是一個節點,value是一個set用來保存與該節點相連的節點
        public Map<Node, Set<Node>> adjaNode = new HashMap<Node, Set<Node>>();

        // 將節點加入圖中
        public boolean addNode(Node start, Node end) {
            if (!vertexSet.contains(start)) {
                vertexSet.add(start);
            }
            if (!vertexSet.contains(end)) {
                vertexSet.add(end);
            }
            if (adjaNode.containsKey(start)
                    && adjaNode.get(start).contains(end)) {
                return false;
            }
            if (adjaNode.containsKey(start)) {
                adjaNode.get(start).add(end);
            } else {
                Set<Node> temp = new HashSet<Node>();
                temp.add(end);
                adjaNode.put(start, temp);
            }
            end.pathIn++;
            return true;
        }
    }

    //Kahn算法
    private static class KahnTopo {
        private List<Node> result; // 用來存儲結果集
        private Queue<Node> setOfZeroIndegree; // 用來存儲入度為0的頂點
        private Graph graph;

        //構造函數,初始化
        public KahnTopo(Graph di) {
            this.graph = di;
            this.result = new ArrayList<Node>();
            this.setOfZeroIndegree = new LinkedList<Node>();
            // 對入度為0的集合進行初始化
            /**
             * 注意如果開始時有多個點的入度為0,拓撲排序並不保證其順序
             */
            for (Node iterator : this.graph.vertexSet) {
                if (iterator.pathIn == 0) {
                    this.setOfZeroIndegree.add(iterator);
                }
            }
        }

        //拓撲排序處理過程
        private void process() {
            while (!setOfZeroIndegree.isEmpty()) {
                Node v = setOfZeroIndegree.poll();

                // 將當前頂點添加到結果集中
                result.add(v);

                if (this.graph.adjaNode.keySet().isEmpty()) {
                    return;
                }

                // 遍歷由v引出的所有邊
                for (Node w : this.graph.adjaNode.get(v)) {
                    // 將該邊從圖中移除,通過減少邊的數量來表示
                    w.pathIn--;
                    if (0 == w.pathIn) // 如果入度為0,那么加入入度為0的集合
                    {
                        setOfZeroIndegree.add(w);
                    }
                }
                //從點和邊的集合中刪去這個點
                this.graph.vertexSet.remove(v);
                this.graph.adjaNode.remove(v);
            }

            // 如果此時圖中還存在邊,那么說明圖中含有環路
            if (!this.graph.vertexSet.isEmpty()) {
                throw new IllegalArgumentException("Has Cycle !");
            }
        }

        //結果集
        public Iterable<Node> getResult() {
            return result;
        }
    }

    //測試
    public static void main(String[] args) {
        Node A = new Node("A");
        Node B = new Node("B");
        Node C = new Node("C");
        Node D = new Node("D");
        Node E = new Node("E");
        Node F = new Node("F");

        Graph graph = new Graph();
        graph.addNode(A, B);
        graph.addNode(B, C);
        graph.addNode(B, D);
        graph.addNode(D, C);
        graph.addNode(E, C);
        graph.addNode(C, F);

        KahnTopo topo = new KahnTopo(graph);
        topo.process();
        /**
         * 拓撲排序的順序是由入度為0的點到出度為0的點,當然出度為0的終點是不斷減去點之后剩下的,所以這里不記錄出度也能找到
         */
        for (Node temp : topo.getResult()) {
            System.out.print(temp.val.toString() + "-->");
        }
    }
}

用拓撲排序求最長路徑(關鍵路徑)和最短路徑

給定一個帶權有向無環圖及源點 S, 在圖中找出從 S 出發到圖中其它所有頂點的最長距離。

對於一般的圖,求最長路徑並不向最短路徑那樣容易,因為最長路徑並沒有最優子結構的屬性。實際上求最長路徑屬於 NP-Hard 問題。然而,對於有向無

環圖,最長路徑問題有線性時間的解。思路與通過使用拓撲排序在線性時間求最短路徑 [1] 一樣。

首先初始化到所有頂點的距離為負無窮大,到源點的距離為 0,然后找出拓撲序。圖的拓撲排序代表一個圖的線性順序。(圖 b 是圖 a 的一個線性表示)。

當找到拓撲序后,逐個處理拓撲序中的所有頂點。對於每個被處理的頂點,通過使用當前頂點來更新到它的鄰接點的距離。

img

圖 (b) 中,到點 s 的距離初始化為 0, 到其它點的距離初始化為負無窮大,而圖 (b) 中的邊表示圖 (a) 中邊的權值。

圖 (c) 中,求得從 s 到 r 的距離為負無窮。

圖 (d) 中,求得 s 到 t 的最長距離為 2, 到 x 的最長距離為 6。

圖 (e) 至圖 (h) 依次求得可達點間的最長距離。

下面是尋找最長路徑的算法

  1. 初始化 dist[] = {NINF, NINF, ….} ,dist[s] = 0 。s 是源點,NINF 表示負無窮。dist 表示源點到其它點的最長距離。
  2. 建立所有頂點的拓撲序列。
  3. 對拓撲序列中的每個頂點 u 執行下面算法。

​ 對 u 的每個鄰接點 v

​ if (dist[v] < dist[u] + weight(u, v)) ………………………dist[v] = dist[u] + weight(u, v)

下面是 C++ 的實現。

// A C++ program to find single source longest distances in a DAG
#include <iostream>
#include <list>
#include <stack>
#include <limits.h>
#define NINF INT_MIN
using namespace std;
 
//圖通過鄰接表來描述。鄰接表中的每個頂點包含所連接的頂點的數據,以及邊的權值。
class AdjListNode
{
    int v;
    int weight;
public:
    AdjListNode(int _v, int _w)  { v = _v;  weight = _w;}
    int getV()       {  return v;  }
    int getWeight()  {  return weight; }
};
 
// Class to represent a graph using adjacency list representation
class Graph
{
    int V;    // No. of vertices’
 
    // Pointer to an array containing adjacency lists
    list<AdjListNode> *adj;
 
    // A function used by longestPath
    void topologicalSortUtil(int v, bool visited[], stack<int> &Stack);
public:
    Graph(int V);   // Constructor
 
    // function to add an edge to graph
    void addEdge(int u, int v, int weight);
 
    // Finds longest distances from given source vertex
    void longestPath(int s);
};
 
Graph::Graph(int V) // Constructor
{
    this->V = V;
    adj = new list<AdjListNode>[V];
}
 
void Graph::addEdge(int u, int v, int weight)
{
    AdjListNode node(v, weight);
    adj[u].push_back(node); // Add v to u’s list
}
 
// 通過遞歸求出拓撲序列. 詳細描述,可參考下面的鏈接。
// http://www.geeksforgeeks.org/topological-sorting/
void Graph::topologicalSortUtil(int v, bool visited[], stack<int> &Stack)
{
    // 標記當前頂點為已訪問
    visited[v] = true;
 
    // 對所有鄰接點執行遞歸調用
    list<AdjListNode>::iterator i;
    for (i = adj[v].begin(); i != adj[v].end(); ++i)
    {
        AdjListNode node = *i;
        if (!visited[node.getV()])
            topologicalSortUtil(node.getV(), visited, Stack);
    }
 
    // 當某個點沒有鄰接點時,遞歸結束,將該點存入棧中。
    Stack.push(v);
}
// 根據傳入的頂點,求出到到其它點的最長路徑. longestPath使用了
// topologicalSortUtil() 方法獲得頂點的拓撲序。
void Graph::longestPath(int s)
{
    stack<int> Stack;
    int dist[V];
 
    // 標記所有的頂點為未訪問
    bool *visited = new bool[V];
    for (int i = 0; i < V; i++)
        visited[i] = false;
 
    // 對每個頂點調用topologicalSortUtil,最終求出圖的拓撲序列存入到Stack中。
    for (int i = 0; i < V; i++)
        if (visited[i] == false)
            topologicalSortUtil(i, visited, Stack);
 
    //初始化到所有頂點的距離為負無窮
    //到源點的距離為0
    for (int i = 0; i < V; i++)
        dist[i] = NINF;
    dist[s] = 0;
 
    // 處理拓撲序列中的點
    while (Stack.empty() == false)
    {
        //取出拓撲序列中的第一個點
        int u = Stack.top();
        Stack.pop();
 
        // 更新到所有鄰接點的距離
        list<AdjListNode>::iterator i;
        //能得到從原點到各個點的最長距離
        if (dist[u] != NINF)
        {
            /**
            把下面的語句換成
            if (dist[i->getV()] > dist[u] + i->getWeight())
                dist[i->getV()] = dist[u] + i->getWeight();
            求出來的就是最短距離
            **/
          for (i = adj[u].begin(); i != adj[u].end(); ++i)
             if (dist[i->getV()] < dist[u] + i->getWeight())
                dist[i->getV()] = dist[u] + i->getWeight();
        }
    }
 
    // 打印最長路徑
    for (int i = 0; i < V; i++)
        (dist[i] == NINF)? cout << "INF ": cout << dist[i] << " ";
}
// Driver program to test above functions
int main()
{
    // Create a graph given in the above diagram.  Here vertex numbers are
    // 0, 1, 2, 3, 4, 5 with following mappings:
    // 0=r, 1=s, 2=t, 3=x, 4=y, 5=z
    Graph g(6);
    g.addEdge(0, 1, 5);
    g.addEdge(0, 2, 3);
    g.addEdge(1, 3, 6);
    g.addEdge(1, 2, 2);
    g.addEdge(2, 4, 4);
    g.addEdge(2, 5, 2);
    g.addEdge(2, 3, 7);
    g.addEdge(3, 5, 1);
    g.addEdge(3, 4, -1);
    g.addEdge(4, 5, -2);
 
    int s = 1;
    cout << "Following are longest distances from source vertex " << s <<" \n";
    g.longestPath(s);
 
    return 0;
}

輸出結果:

從源點1到其它頂點的最長距離
INF 0 2 9 8 10

時間復雜度:拓撲排序的時間復雜度是 O(V+E). 求出拓撲順序后,對於每個頂點,通過循環找出所有鄰接點,時間復雜度為 O(E)。所以內部循環運行 O(V+E) 次。

因此算法總的時間復雜度為 O(V+E)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM