最小生成樹(shù)算法

什么是生成樹(shù)?

連通無(wú)向圖中的所有頂點(diǎn)且任意兩個(gè)頂點(diǎn)間只有一條通路的子圖。生成樹(shù)中邊的數(shù)量 = 頂點(diǎn)數(shù) - 1。如下圖均為生成樹(shù)。

生成樹(shù)

什么是最小生成樹(shù)?

最小生成樹(shù)是指所有生成樹(shù)中邊的權(quán)重和最小的生成樹(shù)。

最小生成樹(shù)算法

最小生成樹(shù)算法主要有兩個(gè):kruskal(克魯斯卡爾)算法和prim(普里姆)算法。下面就分別介紹下這兩類(lèi)算法。

1. kruskal 算法

其主要步驟如下:

  • 首先獲取所有的邊,對(duì)這些邊進(jìn)行從小到大的排序
  • 然后不斷往圖里添加邊,并且避免造成環(huán)
  • 直到所有點(diǎn)都能互相訪問(wèn)后停止算法

偽代碼如下:

function kruskal(graph) {
    // 對(duì)圖里的邊進(jìn)行排序
    const sortedEdges = graph.edges.sort()

    // 去掉圖里的邊
    graph.removeAllEdges()

    for (let i = 0; i < sortedEdges.length; i++) {
        // 如果添加的邊未造成環(huán),那么就進(jìn)行添加
        if (!graph.hasCycle()) {
            graph.addEdge(sortedEdges[i])
        }
    }

    return graph
}

算法要點(diǎn)在于怎么判斷是否存在環(huán)。

在此之前,先說(shuō)明三個(gè)定義:

  • 無(wú)向圖 G 的一個(gè)極大連通子圖稱(chēng)為 G 的一個(gè)連通分量(或連通分支)。連通圖只有一個(gè)連通分量,即其自身;非連通的無(wú)向圖有多個(gè)連通分量。連通分量與連通分量之間沒(méi)有任何邊相連。
  • 在無(wú)向圖中,如果從頂點(diǎn)v_i到頂點(diǎn)v_j有路徑,則稱(chēng)v_iv_j連通。如果圖中任意兩個(gè)頂點(diǎn)之間都連通,則稱(chēng)該圖為連通圖,否則,將其中較大的連通子圖稱(chēng)為連通分量。
  • 在有向圖中,如果對(duì)于每一對(duì)頂點(diǎn)v_iv_j,從v_iv_j和從v_jv_i都有路徑,則稱(chēng)該圖為強(qiáng)連通圖;否則,將其中的極大連通子圖稱(chēng)為強(qiáng)連通分量。

kruskal 就可以這樣理解:初始時(shí),把圖中的n個(gè)頂點(diǎn)看成是獨(dú)立的n個(gè)連通分量,從樹(shù)的角度看,也是n個(gè)根節(jié)點(diǎn)。選邊的標(biāo)準(zhǔn)是這樣的:若邊上的兩個(gè)頂點(diǎn)從屬于兩個(gè)不同的連通分量,則此邊可取,否則考察下一條權(quán)值最小的邊。
那么如何判斷兩個(gè)頂點(diǎn)是否屬于同一個(gè)連通分量呢?這個(gè)可以參照并查集的做法解決。它的思路是:如果兩個(gè)頂點(diǎn)的根節(jié)點(diǎn)是一樣的,則屬于同一個(gè)連通分量。這也同樣暗示著:在加入新邊時(shí),要更新父節(jié)點(diǎn)。
先定義邊的數(shù)據(jù)結(jié)構(gòu):

class Edge(object):

    def __init__(self, start, end, weight):
        """
        定義邊,頂點(diǎn)采用 0至頂點(diǎn)個(gè)數(shù)-1編號(hào)
        :param start: 頂點(diǎn)start
        :param end: 頂點(diǎn)end
        :param weight: 頂點(diǎn)start 和頂點(diǎn)end 之間邊的權(quán)重
        """
        self.start = start
        self.end = end
        self.weight = weight

下面是 Kruskal 算法實(shí)現(xiàn):

class Kruskal(object):

    def __init__(self, edges, node_count=0):
        """
        Kruskal 算法
        :param edges: list:[Edge]
        """
        self.edges = edges
        self.root = list(range(node_count))
        self.path_length = 0

    def is_same_root(self, i, j):
        """
        判斷 i 和 j 的根節(jié)點(diǎn)是否相同
        :param i:
        :param j:
        :return: bool
        """
        while self.root[i] != i:
            i = self.root[i]
        while self.root[j] != j:
            j = self.root[j]
        return i == j

    def update_root(self, i, j):
        """
        將 j 的根節(jié)點(diǎn)更新為 i 的根節(jié)點(diǎn)
        """
        if i > j:
            i, j = j, i
        while self.root[i] != i:
            i = self.root[i]
        self.root[j] = i

    def kruskal(self):
        # 先排序
        edges = sorted(self.edges, key=lambda edge: edge.weight)
        for edge in edges:
            i = edge.start
            j = edge.end
            if not self.is_same_root(i, j):
                print("%d 和 %d 是相連的" % (i, j))
                self.update_root(i, j)
                self.path_length += edge.weight
        return self.path_length

其中判斷是否是相同根節(jié)點(diǎn)也可以利用查并集的方式:

    def find(self, i):
        if self.root[i] != i:
            self.root[i] = self.find(self.root[i])
        return self.root[i]

最后 Kruskal 算法可寫(xiě)為

    def kruskal(self):
        edges = sorted(self.edges, key=lambda edge: edge.weight)
        for edge in edges:
            i = edge.start
            j = edge.end
            w = edge.weight
            root_i, root_j = self.find(i), self.find(j)
            if root_i != root_j:
                print("%d 和 %d 是相連的" % (i, j))
                self.root[root_j] = root_i
                self.path_length += w
        return self.path_length

2. prim 算法

其主要步驟如下:

  • 從一個(gè)節(jié)點(diǎn)(隨機(jī)選一個(gè))開(kāi)始,去找與這個(gè)節(jié)點(diǎn)相鄰最短的邊
  • 將找到的邊添加到這個(gè)節(jié)點(diǎn)上,形成一個(gè)組件
  • 再?gòu)倪@個(gè)組件開(kāi)始去找相鄰權(quán)重最小的邊,再添加到這個(gè)組件上,不斷重復(fù)直到所有節(jié)點(diǎn)都能被訪問(wèn)

偽代碼如下

function prim(graph) {
    const visited = []
    // 從一個(gè)節(jié)點(diǎn)開(kāi)始
    const sourceComponent = graph.randomVertex()

    // 如果還有節(jié)點(diǎn)不能被訪問(wèn)就繼續(xù)找
    while (visited.length < graph.vertices.length) {
        // 將相鄰權(quán)重最小的邊作為組件的一部分,且不構(gòu)成環(huán)
        const smallestEdge = sourceComponent.findSmallestEdge()
        sourceComponent.addEdge(smallestEdge)

        visited.push(smallestEdge.toVertex)
    }
}

下面是 prim 算法實(shí)現(xiàn):

class Prim(object):

    def __init__(self, graph):
        """
        :param graph: 鄰接表形式的圖,具體值表示邊的權(quán)重
        """
        self.graph = graph

    def prim(self, start=0):
        length = len(self.graph)
        distances = [float('inf')] * length
        visited = [False] * length

        visited[start] = True
        index = start
        path_length = 0
        # 與 start 相連的點(diǎn) 進(jìn)行初始化
        for i in range(length):
            distances[i] = self.graph[start][i]
        for i in range(length):
            if i == start:
                continue
            minor = float('inf')
            for j in range(length):
                # 未訪問(wèn)的節(jié)點(diǎn)中 距離最近的節(jié)點(diǎn)
                if not visited[j] and distances[j] < minor:
                    minor = distances[j]
                    index = j
            visited[index] = True
            # print(i, index, minor)
            path_length += minor
            # 更新最近的距離
            for j in range(length):
                if not visited[j] and distances[j] > self.graph[index][j]:
                    distances[j] = self.graph[index][j]
        return path_length

其中尋找距離最近的節(jié)點(diǎn)也可以利用堆進(jìn)行優(yōu)化:

    def prim(self, start=0):
        length = len(self.graph)
        distances = [float('inf')] * length
        visited = [False] * length

        path_length = 0
        # 元素為 (距離, 下標(biāo))
        heap = []
        heapq.heappush(heap, (0, start))
        while heap:
            dist, index = heapq.heappop(heap)
            if visited[index]:
                continue
            visited[index] = True
            path_length += dist
            # 將與index相連的節(jié)點(diǎn)加入
            for j in range(length):
                w = self.graph[index][j]
                if not visited[j] and w < distances[j]:
                    distances[j] = w
                    heapq.heappush(heap, (w, j))
        return path_length

3. leetcode 題目

3.1 1584. 連接所有點(diǎn)的最小費(fèi)用
給你一個(gè)points 數(shù)組,表示 2D 平面上的一些點(diǎn),其中 points[i] = [xi, yi] 。 
連接點(diǎn) [xi, yi] 和點(diǎn) [xj, yj] 的費(fèi)用為它們之間的 曼哈頓距離 :|xi - xj| + |yi - yj| ,
其中 |val| 表示 val 的絕對(duì)值。 請(qǐng)你返回將所有點(diǎn)連接的最小總費(fèi)用。
只有任意兩點(diǎn)之間 有且僅有 一條簡(jiǎn)單路徑時(shí),才認(rèn)為所有點(diǎn)都已連接。 

示例 1: 
輸入:points = [[0,0],[2,2],[3,10],[5,2],[7,0]]
輸出:20

示例 2: 
輸入:points = [[3,12],[-2,5],[-4,1]]
輸出:18

示例 3: 
輸入:points = [[0,0],[1,1],[1,0],[-1,1]]
輸出:4

示例 4: 
輸入:points = [[-1000000,-1000000],[1000000,1000000]]
輸出:4000000

示例 5: 
輸入:points = [[0,0]]
輸出:0

提示: 
1 <= points.length <= 1000 
-106 <= xi, yi <= 106 
所有點(diǎn) (xi, yi) 兩兩不同。 
 
Related Topics 并查集 

題目要求連接所有點(diǎn),有且僅有一條路徑,即最終結(jié)果是生成樹(shù)。因?yàn)橐蟮氖亲钚〉目傎M(fèi)用,便是最小生成樹(shù)。我們可以采用前面總結(jié)的最小生成樹(shù)算法來(lái)實(shí)現(xiàn)。
首先定義曼哈頓距離的計(jì)算函數(shù):

def manhattanDistance(points, index1, index2):
    point1 = points[index1]
    point2 = points[index2]
    return abs(point1[0] - point2[0]) + abs(point1[1] - point2[1])

若采用 Kruskal 算法,我們需要將題目給定的一系列點(diǎn)轉(zhuǎn)換成類(lèi) Kruskal 初始化需要的格式,并進(jìn)行初始化,最后求的結(jié)果:

if __name__ == '__main__':
    points = [[0, 0], [2, 2], [3, 10], [5, 2], [7, 0]]
    n = len(points)
    edges = []
    for i in range(n):
        for j in range(i+1, n):
            edges.append(Edge(i, j, manhattanDistance(points, i, j)))

    s = Kruskal(edges, node_count=n)
    print(s.kruskal())

若采用 Prim 算法,則需要將題目給定的一系列點(diǎn)轉(zhuǎn)換成鄰接表的形式,最終求的結(jié)果:

if __name__ == '__main__':
    points = [[0, 0], [2, 2], [3, 10], [5, 2], [7, 0]]
    n = len(points)
    graph = [[0] * n for _ in range(n)]
    for i in range(n):
        for j in range(i+1, n):
            weight = manhattanDistance(points, i, j)
            graph[i][j] = weight
            graph[j][i] = weight

    s = Prim(graph)
    print(s.prim(0))

參考

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

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

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