【算法日積月累】14-圖論基礎(chǔ)

圖論基礎(chǔ)(Graph Theory)

圖是由結(jié)點(Vertex)和邊(Edge)組成的抽象的數(shù)據(jù)結(jié)構(gòu)。結(jié)點和結(jié)點之間靠邊連接起來。

圖的分類 1

1、無向圖(Undirected Graph),是我們主要研究的部分,我們可以把無向圖看成一種特殊的有向圖;

2、有向圖(Directed Graph),有向圖具有不對稱的特性。

無向圖和有向圖

圖的分類 2

1、無權(quán)圖(Unweighted Graph)

2、有權(quán)圖(Weighted Graph)

無權(quán)圖和有權(quán)圖

理解圖的連通性。

圖的連通性

上面這張圖,它的連通分量為 3 。

簡單圖(Simple Graph):無自環(huán)邊(self-loop),并且無平行邊的圖(parallel-edges)。

自環(huán)邊和平行邊

我們的學(xué)習(xí)將從簡單圖開始。

圖的表示

本章僅介紹無權(quán)圖。圖這種數(shù)據(jù)結(jié)構(gòu)可以使用“鄰接矩陣”和“鄰接表”來表示。

鄰接矩陣

無向圖的鄰接矩陣表示是一個對稱矩陣。

鄰接矩陣

鄰接表

鄰接表

稀疏圖與稠密圖

鄰接表適合表示稀疏圖(Sparse Graph),邊較少。

鄰接矩陣適合表示稠密圖(Dense Graph),邊較多。

鄰接表和鄰接矩陣適合表示

我們的圖是實現(xiàn)了如下接口的 Java 類。

Java 代碼:

// 圖的接口,統(tǒng)一表示稀疏圖與稠密圖
public interface Graph {

    // 返回頂點的個數(shù)
    int V();

    // 返回邊的條數(shù)
    int E();

    // 在頂點 v 和 w 之間添加一條表
    void addEdge(int v, int w);

    // 頂點 v 與 頂點 w 是否有邊連接
    boolean hasEdge(int v, int w);

    // 顯示這個圖中的元素
    void show();

    // 得到頂點 v 的所有鄰居頂點
    Iterable<Integer> adj(int v);
}

編寫相鄰結(jié)點迭代器

為了方便遍歷一個點的所有鄰居結(jié)點,我們需要實現(xiàn)一個迭代器。

稀疏圖(鄰接表)的相鄰點迭代器

由稀疏圖(鄰接表)的表示決定了返回一個結(jié)點的所有相鄰結(jié)點是十分容易的。

Java 代碼:

public Iterable<Integer> adj(int v) {
    assert v >= 0 && v < n;
    return g[v];
}

稠密圖(鄰接矩陣)的相鄰點迭代器

由稠密圖(鄰接矩陣)得到一個結(jié)點的所有相鄰結(jié)點只須遍歷鄰接矩陣中的一行,把值為 True 的所有頂點拿到即可。

Java 代碼:

public Iterable<Integer> adj(int v){
    assert v >= 0 && v < n;
    Vector<Integer> adjV = new Vector<>();
    for (int i = 0; i < n; i++) {
        if(g[v][i]){
            adjV.add(i);
        }
    }
    return adjV;
}

我們將圖的數(shù)據(jù)以一定格式保存在一個文件中,因此我們要編寫一個工具類讀這個圖文件。

從一個文件讀出一個圖

我們圖的信息存在一個文件中,然后通過讀取這個文件的內(nèi)容得到圖的頂點和邊的信息,為此我們編寫了如下代碼。

編寫一個讀圖的工具類

例1:文件 testG1.txt 的內(nèi)容如下,其中第 1 行的第 1 個數(shù)表示這張圖有多少個頂點,第 2 個數(shù)表示這張圖有多少條邊。
第 2 行直至最后一行表示的是所有的邊,以兩個頂點來表示一條邊。

13,13
0,5
4,3
0,1
9,12
6,4
5,4
0,2
11,12
9,10
0,6
7,8
9,11
5,3

如果它表示一個無向圖,那么畫出來就是:

例 1

用鄰接表表示是這樣的:

例 1 鄰接表表示

用鄰接矩陣表示是這樣的:

例 1 鄰接矩陣表示

讀圖的工具類(Java)

深度優(yōu)先遍歷與圖的連通分量

和二分搜索樹的遍歷一樣,圖的遍歷也可以分為深度優(yōu)先遍歷和廣度優(yōu)先遍歷。

圖的深度優(yōu)先遍歷

  • 從圖中任意一個結(jié)點開始,遍歷它的相鄰結(jié)點,只要這個相鄰的結(jié)點還有相鄰的結(jié)點就繼續(xù)遍歷,遍歷完成以后才返回。深度優(yōu)先遍歷可以使用遞歸來完成。

  • 遞歸到底的情形在編碼的時候不容易發(fā)現(xiàn),其實就是在所有的結(jié)點都被遍歷完成的時候,就是遞歸到底、方法應(yīng)該返回的時候。

  • 實現(xiàn)深度優(yōu)先遍歷有兩個關(guān)鍵點:

  1. 從一個頂點開始不停地向下試,直到試不下去了為止;

  2. 圖和樹不一樣的,樹到了葉子結(jié)點就走不下去了,在圖中要記錄每一個結(jié)點是否被遍歷過。

圖的深度優(yōu)先遍歷的應(yīng)用

應(yīng)用1:計算圖的連通分量;

Java 代碼:經(jīng)過一次深度優(yōu)先遍歷得到連通分量

public class Component {

    private Graph graph;

    private boolean[] visited;

    // 連通分量的個數(shù),同時可以在遍歷的過程中,為各個連通分量所在的集合進行標記
    private int ccount;

    private void dfs(int i) {
        // 記錄是否被訪問過,這個非常關(guān)鍵,深度遍歷一開始就要記錄這些結(jié)點被訪問過
        visited[i] = true;
        Iterable<Integer> adj = graph.adj(i);
        for(Integer v:adj){
            if(!visited[v]){
                dfs(v);
            }
        }
    }

    // 構(gòu)造方法,通過傳入一個圖,經(jīng)過了深度優(yōu)先遍歷,計算無權(quán)圖的連通分量
    public Component(Graph graph) {
        this.graph = graph;
        int vCount = graph.V(); // 圖中頂點的個數(shù)
        // 可以不用顯式賦值,因為默認就是 false
        visited = new boolean[vCount];
        for (int i = 0; i < vCount; i++) {
            if (!visited[i]){// 如果沒有遍歷過,就進行一次深度優(yōu)先遍歷
                // depth first search 深度優(yōu)先遍歷
                // 注意:深度優(yōu)先遍歷,不是遍歷這個結(jié)點的所有鄰居
                // 而是把所有與該點向量的所有結(jié)點都遍歷一遍
                dfs(i);
                ccount++; // 經(jīng)過了一次深度優(yōu)先遍歷以后,連通分量 +1
            }
        }
    }

    // 返回這個圖的連通分量的個數(shù)
    public int getCcount() {
        return ccount;
    }
}
  • 應(yīng)用2:通過計算圖的連通分量,還可以很快地判斷出兩個結(jié)點是否相連。

在計算連通分量的同時,給每一個頂點編號,編號的規(guī)則是:屬于一個連通分量的應(yīng)該編號一樣。在上面的代碼中,在每一次記錄頂點 i 被訪問過以后 visited[i] = true;,馬上給這個頂點編號,編號就用到此時得到的連通分量的最大值。通過給每個頂點標記,我們就能很輕松地實現(xiàn)查詢頂點是否相連,只需要查詢它們的連通分量的組別就行了。

Java 代碼:

public class Component {

    private Graph graph;
    private boolean[] visited;
    private int[] id;
    private int ccount;

    private void dfs(int i) {
        visited[i] = true;
        // 在計算連通分量的同時,給每一個頂點編號,編號的規(guī)則是:屬于一個連通分量的應(yīng)該編號一樣。
        id[i] = ccount;
        Iterable<Integer> adj = graph.adj(i);
        for (Integer v : adj) {
            if (!visited[v]) {
                dfs(v);
            }
        }
    }

    public Component(Graph graph) {
        this.graph = graph;
        int vCount = graph.V();
        visited = new boolean[vCount];
        id = new int[vCount];

        for (int i = 0; i < vCount; i++) {
            visited[i] = false;
            id[i] = -1;
        }

        for (int i = 0; i < vCount; i++) {
            if (!visited[i]){
                dfs(i);
                ccount++;
            }
        }
    }

    public int getCcount() {
        return ccount;
    }

    public boolean isConnected(int v, int w) {
        assert v >= 0 && v < graph.V();
        assert w >= 0 && w < graph.V();
        return id[v] == id[w];
    }
}

尋路算法

在進行深度優(yōu)先遍歷的過程中形成了一條路徑,我們這一節(jié)的目標是得到這條路徑,但我們要知道,使用深度優(yōu)先遍歷并不能保證這條路徑是最短的。

尋路算法是一個常見的算法,實現(xiàn)起來也很簡單:在每一次遍歷的時候,我們都記錄一下,這一次遍歷從何而來。

使用深度優(yōu)先遍歷獲得兩點之間的一條路徑

Java 代碼:

import java.util.Stack;
import java.util.Vector;

// 尋路算法,只能得到無權(quán)圖的一條路徑,該路徑并不是最短路徑
// 該算法對有向圖依然有效
public class Path {
    private Graph graph;
    private boolean[] visited;
    private int[] from;

    private void dfs(Integer v) {
        visited[v] = true;
        Iterable<Integer> adj = graph.adj(v);
        for (Integer i : adj) {
            if (!visited[i]) {
                from[i] = v;
                dfs(i);
            }
        }
    }

    public Path(Graph graph, int s) {
        assert s >= 0 && s < graph.V();
        this.graph = graph;
        int vCount = graph.V();
        visited = new boolean[vCount];
        from = new int[vCount];
        for (int i = 0; i < vCount; i++) {
            visited[i] = false;
            from[i] = -1;
        }
        // 從 source 開始,做一次深度優(yōu)先遍歷
        dfs(s);
    }

    // 查詢從 s 點到 w 點是否有路徑
    public boolean hasPath(int w) {
        assert w >= 0 && w < graph.V();
        return visited[w];
    }

    // 得到從 s 到 w 的一條路徑
    public void path(int w, Vector<Integer> vec) {
        assert hasPath(w);
        // 我們要倒著把路徑得到,所以應(yīng)該借助棧來完成該工作
        Stack<Integer> stack = new Stack<>();
        // 這一段邏輯比較繞,但不難
        int p = w;
        while (p != -1) {
            stack.add(p);
            p = from[p];
        }
        while (!stack.isEmpty()) {
            vec.add(stack.pop());
        }
    }

    // 顯示從 s 到 w 的一條路徑
    public void showPath(int w) {
        Vector<Integer> vec = new Vector<>();
        path(w, vec);
        for (int i = 0; i < vec.size(); i++) {
            System.out.print(vec.get(i));
            if (i == vec.size() - 1) {
                System.out.println();
            } else {
                System.out.print(" -> ");
            }
        }
    }
}
  • [ ] 練習(xí) 1 :檢查有向圖中是否有環(huán)。
  • [ ] 練習(xí) 2 :檢查有向圖是否是二分圖。

10.7 圖的廣度優(yōu)先遍歷和無權(quán)圖的最短路徑

  • 和樹的廣度優(yōu)先遍歷是一致,使用隊列作為輔助的數(shù)據(jù)結(jié)構(gòu);
  • visited 數(shù)組應(yīng)該是對加入到隊列中的元素進行處理,因為它加入到隊列了,早晚都會被處理;
  • 先遍歷到的結(jié)點比后遍歷到結(jié)點距離起始結(jié)點更近,所以可以解決最短路徑的問題;
  • 針對無向圖和有向圖都有效。

10.7.1 圖的廣度優(yōu)先遍歷可以回答的問題

圖的廣度優(yōu)先遍歷 API

Java 代碼:

import java.util.*;

public class ShortestPath {

    private Graph graph;
    private int s; // source
    private boolean[] visited;
    private int[] from; // 記錄了從哪個結(jié)點訪問而來
    private int[] ord;     // 記錄了層序

    public ShortestPath(Graph graph, int s) {
        this.graph = graph;
        assert s >= 0 && s < graph.V();

        visited = new boolean[graph.V()];
        from = new int[graph.V()];
        ord = new int[graph.V()];

        for (int i = 0; i < graph.V(); i++) {
            visited[i] = false;
            from[i] = -1;
            ord[i] = -1;
        }
        this.s = s;

        // 特別要注意:使用鏈表作為隊列的時候,出隊和入隊不能混淆
        // 如果入隊的方向是從右到左,出隊也是從右到左
        // 如果入隊的方向是從左到右,出隊也是從左到右
        LinkedList<Integer> queue = new LinkedList<>();
        queue.push(s);
        visited[s] = true;
        ord[s] = 0;
        while (!queue.isEmpty()) {
            // peek 的意思是:我只是瞅了一眼,并沒有真的把它拿出來
            int v = queue.pollLast();
            for (Integer i : graph.adj(v)) {
                if (!visited[i]) {
                    // 注意:LinkedList 有 push 和 add 兩種方法
                    // 這一點區(qū)別是我在使用測試用例的過程中發(fā)現(xiàn)了錯誤,使用 IDEA 的 debug 調(diào)試代碼的過程中發(fā)現(xiàn)的
                    // push 的源代碼點開,你會看到 addFirst
                    // add 的源代碼點開,你會看到 linkLast
                    queue.push(i);
                    visited[i] = true;
                    from[i] = v;
                    ord[i] = ord[v] + 1;
                }
            }
        }
    }

    // 圖的廣度優(yōu)先遍歷可以回答的問題 1 : 判斷到結(jié)點 w 是否有路徑
    public boolean hasPath(int w) {
        assert w >= 0 && w < graph.V();
        return visited[w];
    }

    // 圖的廣度優(yōu)先遍歷可以回答的問題 2 :打印從 s 到 w 的最短路徑
    public void showPath(int w) {
        assert hasPath(w);
        List<Integer> paths = path(w);
        for (int i = 0; i < paths.size(); i++) {
            if (i == paths.size() - 1) {
                System.out.println(paths.get(i));
            } else {
                System.out.print(paths.get(i) + " -> ");
            }
        }
    }

    // 圖的廣度優(yōu)先遍歷可以回答的問題 3 :計算從 s 到 w 的最短路徑
    public List<Integer> path(int w) {
        Stack<Integer> stack = new Stack<>();
        int cur = w;
        while (cur != -1) {
            stack.push(cur);
            cur = from[cur];
        }
        List<Integer> ret = new ArrayList<>();
        while (!stack.isEmpty()) {
            ret.add(stack.pop());
        }
        return ret;
    }

    // 圖的廣度優(yōu)先遍歷可以回答的問題 4 :查看從 s 點到 w 點的最短路徑長度,若從 s 到 w 不可達,返回 -1
    public int length(int w) {
        assert w >= 0 && w < graph.V();
        return ord[w];
    }
}

關(guān)于無權(quán)圖算法的測試:

// 關(guān)于無權(quán)圖算法的測試
public class Main {

    public static void main(String[] args) {
        // 稠密圖
        // DenseGraph graph = new DenseGraph(13, false);
        // new ReadGraphUtil(graph, "testG1.txt");
        // graph.show();

        // 稀疏圖
        SparseGraph graph = new SparseGraph(13, false);
        new ReadGraphUtil(graph, "testG1.txt");
        // graph.show();

        Component component = new Component(graph);
        System.out.println("圖的連通分量:" + component.getCcount());

        // 通過深度優(yōu)先遍歷,得到一個路徑
        Path path = new Path(graph, 0);
        // 此時的路徑不是最短路徑
        path.showPath(6);

        // 廣度優(yōu)先遍歷
        ShortestPath shortestPath = new ShortestPath(graph, 0);
        shortestPath.showPath(3);
    }
}

本文源代碼

Python:代碼文件夾,Java:代碼文件夾

(本節(jié)完)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容