数据结构与算法——最小生成树(上)

1. 应用场景

涉及到网络中所有节点的最优路径问题,可以使用最小生成树求解。例如:交通网、电力网、通信网等等

应用场景:例如要在n个城市之间铺设光缆,主要目标是要使这 n 个城市的任意两个之间都可以通信,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同,因此另一个目标是要使铺设光缆的总费用最低。这就需要找到带权的最小生成树

2. 基本概念

先介绍生成树以及最小生成树的概念

  • 生成树:是针对连通图来说的。

    • 连通图的生成树必须包含两个条件:
      • 包含连通图中所有的顶点
      • 任意两顶点之间有且仅有一条通路(即不能成环)

在这里插入图片描述

​ a)为一张连通图,b)为对应的生成树

  • 最小生成树:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。

在这里插入图片描述

3. 常用求解算法

有两种常用的求解最小生成树算法,分别是Kruskal算法和Prim算法。

本次只介绍Kruskal算法,下次介绍Prim算法。

3.1 Kruskal算法

此算法可以称为“加边法”,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。

  1. 把图中的所有的边按权值从小到大排序,将图中的n个顶点看成n颗树。
  2. 按权值从小到大选择边,所选的边的两个顶点应当属于不同的树。
  3. 重复2,直到所有顶点都在一颗树上或者已选 n-1条边。

以下图为例,说明算法步骤:

在这里插入图片描述

  • step1:

    先将图中所有边取出,按权重从小到大放到一个列表中

    EdgeWeight
    6 - 71
    2 - 82
    5 - 62
    0 - 14
    2 - 54
    6 - 86
    2 - 37
    7 - 87
    0 - 78
    1 - 28
    3 - 49
    4 - 510
    1 - 711
    3 - 514
  • step2:

    按照权值从小到大依次取边。如果未成环,继续;如果成环,将此边丢弃。

    • cycle1:

      选择代价最小的边:6 - 7

      是否成环:否

      合并顶点6和7,此时已选边的数量为:1

      在这里插入图片描述

    • cycle2:

      选择代价最小的边:2 - 8

      是否成环:否

      合并顶点5和6,此时已选边的数量为:2

      在这里插入图片描述

    • cycle3:

      选择代价最小的边:5 - 6

      是否成环:否

      合并顶点5和6,此时已选边的数量为:3

      在这里插入图片描述

    • cycle4:

      选择代价最小的边:0 - 1

      是否成环:否

      合并顶点0和1,此时已选边的数量为:4

      在这里插入图片描述

    • cycle5:

      选择代价最小的边:2 - 5

      是否成环:否

      合并顶点2和5,此时已选边的数量为:5

      在这里插入图片描述

    • cycle6:

      选择代价最小的边:6 - 8

      是否成环:是

      舍弃边6-8,此时已选边的数量为:5

      在这里插入图片描述

    • cycle7:

      选择代价最小的边:2 - 3

      是否成环:否

      合并顶点2和3,此时已选边的数量为:6

      在这里插入图片描述

    • cycle8:

      选择代价最小的边:7 - 8

      是否成环:是

      舍弃边7-8,此时已选边的数量为:6

      在这里插入图片描述

    • cycle9:

      选择代价最小的边:0 - 7

      是否成环:否

      合并顶点0和7,此时已选边的数量为:7

      在这里插入图片描述

    • cycle10:

      选择代价最小的边:1 - 2

      是否成环:是

      舍弃边1-2,此时已选边的数量为:7

      在这里插入图片描述

    • cycle11:

      选择代价最小的边:3 - 4

      是否成环:否

      合并顶点3和4,此时已选边的数量:8

      在这里插入图片描述

  • step3:

    此时,所有顶点已在树上(已选择n-1条边),算法结束。

4. 算法实现

4.1 并查集找环

使用并查集判断是否存在环。关于并查集,可以看b站视频:link,讲的很清楚

import copy


class Node(object):
    def __init__(self, data, weight=1, nnext=None):
        self.data = data
        self.weight = weight
        self.next = nnext


class Edge(object):
    def __init__(self, ver1, ver2, w=1):
        self.vertex1 = ver1
        self.vertex2 = ver2
        self.weight = w


class AdjTable(object):
    def __init__(self):
        self.node_list = []
        self.edge_list = []
        self.parent = []

    def add_vertex(self, node_list):
        self.node_list.extend(node_list)
        self.parent = [-1] * len(self.node_list)

    # 获取顶点,但不获取连接,用于复制一份只有顶点的图
    def get_vertex(self):
        temp = copy.deepcopy(self.node_list)
        for node in temp:
            node.next = None
        return temp

    # 添加边
    def add_edge(self, orgin, end, weight=1):
        self.edge_list.append(Edge(orgin, end, weight))
        node1 = Node(end.data, weight, orgin.next)
        node2 = Node(orgin.data, weight, end.next)
        orgin.next = node1
        end.next = node2

    # 检测到成环,删除刚添加的边
    def del_edge(self, orgin, end):
        self.node_list[self.get_index(orgin.data)].next = self.node_list[self.get_index(orgin.data)].next.next
        self.node_list[self.get_index(end.data)].next = self.node_list[self.get_index(end.data)].next.next
        self.edge_list.pop()

    # 根据数据获取索引 {"v0": 0, ...}
    def get_index(self, data):
        my_dict = {node.data: idx for idx, node in enumerate(self.node_list)}
        return my_dict[data]

    # 返回根节点的索引
    def find_root(self, vertex_data):
        index = self.get_index(vertex_data)
        while self.parent[index] != -1:
            index = self.parent[index]
        return index

    # 合并边上两点,并判断是否有环
    def merge(self, edge):
        node1 = edge.vertex1
        node2 = edge.vertex2
        node1_root = self.find_root(node1.data)
        node2_root = self.find_root(node2.data)

        if node1_root == node2_root:
            return True
        else:
            # 将node2的父节点设为node1
            self.parent[node2_root] = node1_root
            return False


def take_weight(elem):
    return elem.weight


def kruskal(table):
    # 按照权重对边进行排序
    edges = table.edge_list
    edges.sort(key=take_weight, reverse=False)

    # 创建一张新的图
    graph = AdjTable()
    graph.add_vertex(table.get_vertex())

    # 依次取出权重最小的边
    for edge in edges:
        # 将权重最小的边添加到图中
        graph.add_edge(graph.node_list[graph.get_index(edge.vertex1.data)],
                       graph.node_list[graph.get_index(edge.vertex2.data)],
                       edge.weight)

        if graph.merge(edge):
            graph.del_edge(edge.vertex1, edge.vertex2)

        # 生成树中边数 = 节点数 - 1,停止
        if len(graph.edge_list) == len(graph.node_list) - 1:
            break

    return graph


if __name__ == '__main__':
    v0, v1, v2, v3, v4, v5, v6, v7, v8 = Node("v0"), Node("v1", ), Node("v2"), Node("v3"), \
                                         Node("v4"), Node("v5"), Node("v6"), Node("v7"), Node("v8")

    table = AdjTable()
    table.add_vertex([v0, v1, v2, v3, v4, v5, v6, v7, v8])
    table.add_edge(v0, v1, 4)
    table.add_edge(v0, v7, 8)
    table.add_edge(v1, v2, 8)
    table.add_edge(v1, v7, 11)
    table.add_edge(v2, v3, 7)
    table.add_edge(v2, v5, 4)
    table.add_edge(v2, v8, 2)
    table.add_edge(v3, v4, 9)
    table.add_edge(v3, v5, 14)
    table.add_edge(v4, v5, 10)
    table.add_edge(v5, v6, 2)
    table.add_edge(v6, v7, 1)
    table.add_edge(v6, v8, 6)
    table.add_edge(v7, v8, 7)

    graph = kruskal(table)

    # 查看最小生成树连接情况
    print("最小生成树")
    for i in range(len(graph.node_list)):
        node = graph.node_list[i]
        print("v{} links:".format(i), end="")

        while node.next:
            print(node.next.data, end=" ")
            node = node.next
        print()

4.2 DFS找环

使用DFS判断是否成环,思路是从一点出发,如果到另外一点有两条路径,即可判断有环存在

import copy


class Stack(object):
    def __init__(self):
        self.list = []

    def is_empty(self):
        return len(self.list) == 0

    def push(self, data):
        self.list.insert(0, data)

    def pop(self):
        del self.list[0]

    def get_top(self):
        return self.list[0]

    def size(self):
        return len(self.list)

    def show_stack(self):
        return [node.data for node in self.list]


class Node(object):
    def __init__(self, data, weight=1, nnext=None):
        self.data = data
        self.weight = weight
        self.next = nnext

    def get_neighbor(self):
        node = self.next
        neighborhood = []
        while node:
            # 不能用 node,用node.data,否则会出现单链之间互相链接
            neighborhood.append(node.data)
            node = node.next
        return neighborhood


class Edge(object):
    def __init__(self, ver1, ver2, w=1):
        self.vertex1 = ver1
        self.vertex2 = ver2
        self.weight = w


class AdjTable(object):
    def __init__(self):
        self.node_list = []
        self.edge_list = []

    def add_vertex(self, node_list):
        self.node_list.extend(node_list)

    # 获取顶点,但不获取连接,用于复制一份只有顶点的图
    def get_vertex(self):
        temp = copy.deepcopy(self.node_list)
        for node in temp:
            node.next = None
        return temp

    # 添加边
    def add_edge(self, orgin, end, weight=1):
        self.edge_list.append(Edge(orgin, end, weight))
        node1 = Node(end.data, weight, orgin.next)
        node2 = Node(orgin.data, weight, end.next)
        orgin.next = node1
        end.next = node2

    # 成环之后删除刚添加的边
    def del_edge(self, orgin, end):
        self.node_list[self.get_index(orgin.data)].next = self.node_list[self.get_index(orgin.data)].next.next
        self.node_list[self.get_index(end.data)].next = self.node_list[self.get_index(end.data)].next.next
        self.edge_list.pop()

    # 根据数据获取索引 {"v0": 0, ...}
    def get_index(self, data):
        my_dict = {node.data: idx for idx, node in enumerate(self.node_list)}
        return my_dict[data]

    # 使用dfs检测是否成环
    def is_ring(self, start_node):
        record = []
        s = Stack()
        visited = [0] * len(self.node_list)
        ring_flag = [0] * len(self.node_list)
        s.push(start_node)
        visited[self.get_index(start_node.data)] = 1

        while not s.is_empty():
            current_node = s.get_top()
            neighbor = current_node.get_neighbor()
            unvisited_neighbor = []

            for data in neighbor:
                if visited[self.get_index(data)] == 0:
                    # 记录到某点的路径
                    record.append(current_node.data + "-" + data)
                    unvisited_neighbor.append(data)

            if len(unvisited_neighbor):
                s.push(self.node_list[self.get_index(unvisited_neighbor[0])])
                visited[self.get_index(unvisited_neighbor[0])] = 1
            else:
                s.pop()

        # 如果从一点出发到另外一点,有两条路径,则认为成环
        record = list(set(record))
        for ele in record:
            ring_flag[int(ele[-1])] += 1

        # 判断是否成环
        if max(ring_flag) == 2:
            return True
        else:
            return False


def take_weight(elem):
    return elem.weight


def kruskal(table):
    # 按照权重对边进行排序
    edges = table.edge_list
    edges.sort(key=take_weight, reverse=False)

    # 创建一张新的图
    graph = AdjTable()
    graph.add_vertex(table.get_vertex())

    # 依次取出权重最小的边
    for edge in edges:
        # 将权重最小的边添加到图中
        graph.add_edge(graph.node_list[graph.get_index(edge.vertex1.data)],
                       graph.node_list[graph.get_index(edge.vertex2.data)],
                       edge.weight)

        # 判断是否成环,成环舍弃此边
        for node in graph.node_list:
            if graph.is_ring(node):
                # 成环删除此边
                graph.del_edge(edge.vertex1, edge.vertex2)
                break

        # 生成树中边数 = 节点数 - 1,停止
        if len(graph.edge_list) == len(graph.node_list) - 1:
            break

    return graph


if __name__ == '__main__':
    v0, v1, v2, v3, v4, v5, v6, v7, v8 = Node("v0"), Node("v1", ), Node("v2"), Node("v3"), \
                                         Node("v4"), Node("v5"), Node("v6"), Node("v7"), Node("v8")

    table = AdjTable()
    table.add_vertex([v0, v1, v2, v3, v4, v5, v6, v7, v8])
    table.add_edge(v0, v1, 4)
    table.add_edge(v0, v7, 8)
    table.add_edge(v1, v2, 8)
    table.add_edge(v1, v7, 11)
    table.add_edge(v2, v3, 7)
    table.add_edge(v2, v5, 4)
    table.add_edge(v2, v8, 2)
    table.add_edge(v3, v4, 9)
    table.add_edge(v3, v5, 14)
    table.add_edge(v4, v5, 10)
    table.add_edge(v5, v6, 2)
    table.add_edge(v6, v7, 1)
    table.add_edge(v6, v8, 6)
    table.add_edge(v7, v8, 7)

    graph = kruskal(table)

    # 查看最小生成树连接情况
    print("最小生成树")
    for i in range(len(graph.node_list)):
        node = graph.node_list[i]
        print("v{} links:".format(i), end="")

        while node.next:
            print(node.next.data, end=" ")
            node = node.next
        print()

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值