解決連通性問題的四種演算法

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
最近做 B 站彈幕分析 的項目,學習 Jieba 中文分詞的動態規划算法,發現自己的演算法知識待系統的學習,遂讀 Sedgewick 的《演算法 C 實現第三版》,這一系列演算法的代碼放在 Github,文章會同步到 SF,隨意轉載。

連通性問題

問題概述

先來看一張圖:

在這個彼此串連和斷開的點網路中,我們可以找到一條 p 點到 q 點的路徑。在電腦網路中判斷兩台主機是否連通、在社交網路中判斷兩個使用者是否存在間接社交關係等,都可以抽象成連通性問題。

問題抽象

可將網路中的點(主機、人)抽象為對象,p-q 表示 p串連到q,連通關係可傳遞: p-q & q-r => p-r;為簡述問題,將兩個對象標記為一個整數對,則給定整數對序列就能描述出點網路。

如結點數 N = 5 的網路(使用 0 ~ N-1表示對象),可用整數對序列 0-1 1-3 2-4 來描述連通關係, 其中 0 和 3 也是連通的,存在兩個連通分量:{0, 1, 3} 和 {2, 4}

問題:給定描述連通關係的整數對序列,任給其中兩個整數 p 和 q,判斷其是否能連通?

問題樣本

輸入     不連通    連通 3-4     3-44-9     4-98-0     8-02-3     2-35-6     5-62-9             2-3-4-9    5-9     5-97-3     7-34-8     4-85-6             5-60-2             0-8-4-3-26-1     6-1

對應的連通圖如下,黑線表示首次串連兩個結點,綠線表示兩結點已存在連通關係:

演算法一:快速尋找演算法

使用數組 id[i] 儲存結點的值, i 為結點序號,即初始狀態序號和數組值相同 :

當輸入前兩個連通關係後, id[i] 變化如下:

可以看出, id[i] 的值是完成連通後,i 串連到的終點結點。若 p 和 q 連通,則 id[p]id[q] 值應相等。

如完成 4-9 後, id[3]id[4] 的值均為終點結點 9。此時判斷 3 和 9 是否連通,直接判斷 id[3]id[9] 的值是否相等,相等則連通,不等則不存在連通關係。顯然 id[3] == id[9] == 9,即存在連通關係。

演算法實現
/** file: 1.1-quick_find.go */package mainimport ...const N = 10var id [N]intfunc main() {    reader := bufio.NewReader(os.Stdin)    // 初始化 id 數組,元素值與結點序號相等    for i := 0; i < N; i++ {        id[i] = i    }    // 讀取命令列輸入    for {        data, _, _ := reader.ReadLine()        str := string(data)        if str == "\n" {            continue        }        if str == "#" {            break        }        values := strings.Split(str, " ")        p, _ := strconv.Atoi(values[0])        q, _ := strconv.Atoi(values[1])        if Connected(p, q) {            fmt.Printf("Already Connected nodes: %d-%d\n", p, q)            continue        }        Union(p, q)    }}// 判斷整數 p 和 q 的結點是否連通func Connected(p, q int) bool {    return id[p] == id[q]}// 連通 p-q 結點func Union(p, q int) {    pid := id[p]    qid := id[q]    // 遍曆 id 數組,將所有值為 id[p] 的結點全部替換為 id[q]    for i := 0; i < N; i++ {        if id[i] == pid {            id[i] = qid        }    }    fmt.Printf("Unconnected nodes: %d-%d\n", p, q)}
運行效果:能判斷 2-9 已存在連通關係

複雜度

快速尋找演算法在判斷 p 和 q 是否連通時,只需判斷 id[p]id[q] 是否相等。但 p 和 q 不連通時會進行合并,每次合并都需要遍曆整個數組。特性:尋找快、合并慢

演算法二:快速合并演算法

概述

快速尋找演算法每次合并都會全遍曆數組導致低效。我們想能不能不要每次都遍曆 id[] ,最佳化為每次只遍曆數組的部分值,複雜度都會降低。

這時應想到樹結構,在連通關係的傳遞性中,p->r & q->r => p->q,可將 r 視為根,p 和 q 視為子結點,因為 p 和 q 有相同的根 r,所以 p 和 q 是連通的。這裡的樹是連通關係的抽象。

資料結構

使用數組作為樹的實現:

  • 結點數組 id[N]id[i] 存放 i 的父結點
  • i 的根結點是 id[id[...id[i]...]],不斷向上找父結點的父結點...直到根結點(父結點是自身)
使用樹的優勢

將整數對序列的表示從數組改為樹,每個結點儲存它的父結點位置,這種樹有 2 點好處:

  1. 判斷 p 和 q 是否連通:是否有相同的根結點
  2. 合并 p 到 q:將 p 的根結點改為 q 的根結點(無需全遍曆,快速合并)
例子:

對於上邊的整數對序列,尋找、合并過程如下,橙色是合并動作、灰色是已連通狀態、綠色是儲存樹的數組。

注意紅色的 2-3,不是直接把 2 作為 3 的子結點,而是找到 3 的根結點 9,合并 2-33-4-9 ,產生 2-9

演算法實現:

/** file: 1.2-quick_union.go */// p 和 q 有相同的根結點,則是連通的func Connected(p, q int) bool {    return getRoot(p) == getRoot(q)}// 連通 p-q 結點func Union(p, q int) {    pRoot := getRoot(p)    qRoot := getRoot(q)    id[pRoot] = qRoot        // q 樹的根此時有了父結點(p 樹的根),完成合併    fmt.Printf("Unconnected nodes: %d-%d\n", p, q)}// 擷取結點 i 的根結點func getRoot(i int) int {    // 沒到根結點就繼續向上尋找    for i != id[i] {        i = id[i]    }    return i}

演算法三:帶權快速合并演算法

概述

快速合并演算法有一個缺陷:資料量很大時,任意合并子樹,會導致樹越來越高,在尋找根結點時要遍曆數組大部分的值,依舊會很慢。中判斷 p、q 是否連通,就需要尋找 13 個結點:

如果樹合并後的依舊比較矮,各子樹之間平衡,則尋找根結點會少遍曆很多結點,中再判斷 p、q 是否連通,只需尋找 7 個結點:

平衡樹的構建

構建平衡的樹需要在合并時,將小樹合并到大樹上,保證合并後的樹增高緩慢或者就不增高,從而使大部分的合并需要遍曆的結點大大減少。區分小樹、大樹使用的是樹的權值:子樹含有結點的個數。

資料結構

樹結點的儲存依舊使用 id[i] ,但需要一個額外的數組 size[i],記錄結點 i 的子結點數。

演算法實現
/**file: 1.3-weighted_version.go在快速合并演算法的基礎上,只需要在合併作業中,將小樹合并到大樹上即可*/var id [N]intvar size [N]intfunc main() {     // 初始化 id 數組,元素值與結點序號相等    for i := 0; i < N; i++ {        id[i] = i        size[i] = i    }       ...}  ...// 連通 p-q 結點func Union(p, q int) {    pRoot := getRoot(p)    qRoot := getRoot(q)    // p 樹是大樹    if size[pRoot] < size[qRoot] {        id[pRoot] = qRoot        size[qRoot] += size[pRoot]    } else {        id[qRoot] = id[pRoot]        size[pRoot] += size[qRoot]    }    id[pRoot] = qRoot // q 樹的根此時有了父結點(p 樹的根),完成合併    fmt.Printf("Unconnected nodes: %d-%d\n", p, q)}

演算法四:路徑壓縮的加權快速合并演算法

概述

加權快速合并演算法在大部分整數對都是直接連接的情況下,產生的樹依舊會比較高,比如序列:

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

產生的樹如下:

此時判斷 9-2 的連通關係,需要分別找到 9 和 2 的根結點。在尋找 9 的根結點時經過 6、3、1樹,因為6、3、1樹的子節點和 9 一樣,根結點都是 0,所以直接把6、3、1樹變成 0 的子樹。如下:

最佳化

每次計算某個節點的根結點時,將沿路檢查的結點也指向根結點。儘可能的展平樹,在檢查連通狀態時將大大減少遍曆的結點數目。

演算法實現
/**file: 1.4-path_compression_by_halving.go改動的代碼很少,但很精妙*/// 擷取結點 i 的根結點func getRoot(i int) int {    // 沒到根結點就繼續向上尋找    for i != id[i] {        id[i] = id[id[i]]        // 將結點、結點的父結點不斷往上挪動,直到都串連上了根結點        i = id[i]    }    return i}
複雜度

N 是結點集合的大小,T 是樹的高度。

演算法 初始化的複雜度 合并複雜度 尋找複雜度
快速尋找 N N(全遍曆) 1(數組取值對比)
快速合并 N T(遍曆樹) T(遍曆樹)
帶權快速合并 N lg N lg N
路徑壓縮的帶權快速合并 N 接近1(樹的高度幾乎為2) 接近1

總結

上邊介紹了 4 種解決連通性問題的演算法,從低效完成準系統的快速尋找,到不斷最佳化降低複雜度接近1 的路徑壓縮帶權快速合并。可以學到演算法解決程式問題的大致步驟:先完成準系統,再針對低效操作來最佳化降低複雜度。

原文:https://wuyin.io/2018/01/27/c...

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.