网络算法——基于堆的Prim算法和基于并查集的Kruskal算法

1 实现概述

实验实现了Prim(基于堆)和Kruskal(基于UNION-FIND)算法。堆和并查集结构由自己定义。图的定义使用库networkx,能方便的为节点和边赋予信息。并且结合networkx和matplotlib将图可视化。在测试上使用随机数通过节点,边和权重的随机生成进行验证,计算算法运行时间进行比对。

在堆的结构上,内部使用三个数组分别记录节点,对应节点的最小权重和邻接点。每次在上下调整的时候同时调整三者。在更新值的时候通过节点的列表找到数组中的位置,然后视情况上下调整而非重新建堆。

在并查集的结构上,采用字典的数据结构key为head,value为属于head的所有节点。查询时候只需在value中查找,合并将value合并并删除对应元素较少的key即可。

2 效果演示

输入图:

img

执行结果:

img

image-20230624155332644

3 堆和并查集类定义

类名:Heap

类属性:

  • self.heap = list() 记录堆中的值 对应边的权重

  • self.node = list() 记录堆中权重对应的起始节点

  • self.neighbor = list() 记录堆中节点的邻接节点

    类方法:

类方法作用
def _init_(self):初始化类
def _str_(self):返回类的信息
def Up(self, i: int):将指定节点向上调整
def Down(self, i: int):将指定节点向下调整
def Insert(self, weight: int, node: str, neighbor: str):拆入新节点
def GetMin(self):获取堆的最小值
def Change(self, weight: int, node: str, neighbor: str):更改指定节点的值

类名:Union_Find

类属性:

  • self.data = dict() 采用字典的方式,key为head value为属于head的所有点

    类方法:

类方法作用
def _init_(self):初始化类
def _str_(self):返回类的信息
def Find(self, node: str):放回节点对应的head
def Union(self, head1: str, head2: str):将两个head合并

4 其他函数定义

函数作用
def Prim(g: nx.Graph, begin: str) -> nx.Graph:使用prim算法将给定图转化成最小生成树
def Kruskal(g: nx.Graph) -> nx.Graph:使用kruskal算法将给定图转化成最小生成树
def show(g: nx.Graph, name: str):调用networkx和matplotlib库打印输入图
def Test(test_times: int, max_nodes: int, max_weights):运行时间测试,参数分别制定测试次数,最大节点数目和最大权重值。

5 测试集

在这部分中,通过指定测试集数量,最大节点数和最大权重数目,我们使用random模块随机生成节点,边和权重,通过networkx库的内置函数is_connected检验随机生存的图是否联通,若是则放入生产树算法,最后分别统计运行时间,我们以30000次测试,50为最大权重。分别测试10、20、50、100、200节点数目下的运行时间。如下图所示输出:

img

统计如下表所示:

时间/节点数102040100200
Prim时间1.3174.92222.48578.927332.968
Kru时间1.1144.01224.599124.029668.504
Prim/Kru1.1822262121.2268195410.9140615470.6363592390.498079293

可见在节点数较少的时候,kru算法有较好的运行效果,但是当节点增加到100以上的时候,kru算法的性能急剧下降,在200节点的时候性能只有prim算法的1/2左右。

接着我们探讨在节点数一定,运行效率相似的情况下,稀疏图或者是稠密图对于算法的影响,故而,我们选取30000次和30~40节点作为条件,定义稀疏图的边数在 [ n − 1 , n 2 / 4 ] [n-1,n^2/4] [n1,n2/4]之间,稠密图的边数在 [ n 2 / 4 , n 2 / 2 ] [n^2/4,n^2/2] [n2/4,n2/2]之间,比较结果。

时间/类型40节点稀疏图40节点稠密图上升比例
Prim时间37.59769.8020.856584302
Kru时间37.5787.0271.316396061

可见对于稠密图而言,kru算法运行时间相对与prim算法提高幅度更大。

综上所属,kru算法适合较少节点数和稀疏图,在稠密以及较多节点数的时候表现急剧下降,但是kru算法较为简洁,并且有更低的空间复杂度。

6 遇到的疑惑

首先是选择怎样的存储结构的问题,之前的proj1逻辑上使用了邻接表,实际上借助字典的数据结构实现。但如果为图增加权重信息,又会重新设计,并且也不便于以后项目的进行。

所以选择了现成的网络图分析的库networkx,省去了考虑图的构建,能更容易的关注于算法本身。

两种算法的思路都容易掌握,问题是如何构建堆和并查集,其中重点又是堆的构建。我们没有使用库heapq,因为考虑到更新的时候需要删除整个堆重新构建。问题是如何在记录权重的同时记录所属节点和对应临界点。

我们考虑了两种方法,[(node,weight,next),…]的结构和三个列表的结构。我们虽然使用了第二种,但是现在看来应当是第一种的逻辑更为合理并且操作也更加方便。

仍然是堆的建立,应当将位置0空出,从1开始记录数据更为方便。可见在堆上仍有很大的改进空间。

7 源代码

import networkx as nx
import matplotlib.pyplot as plt
import matplotlib as mpl
import time
import random

"""
缺少包请cmd
pip install networkx
pip install matplotlib
如果遇到图片打开后就停止运行的 很抱歉是编辑器的问题 pycharm专业版可以 社区版不行
很抱歉实在调不过来了 把窗口关掉就行了
友情提示 虽然算法跑的快 但是networkx库跑的慢 增加边需要很长时间
测试量和节点不要太大 具体结果看报告就好
"""


class Heap():
    # 函数中的索引按照123开始
    def __init__(self):
        self.heap = list()
        self.node = list()
        self.neighbor = list()

    def __str__(self):
        ans = '堆中元素:' + self.heap.__str__() + '\n' + '对应节点:' + self.node.__str__() + '\n' + '最小邻居节点:' + self.neighbor.__str__()
        return ans

    def Up(self, i: int):
        while True:
            if i < 2:
                return
            if self.heap[i // 2 - 1] > self.heap[i - 1]:
                self.heap[i // 2 - 1], self.heap[i - 1] = self.heap[i - 1], self.heap[i // 2 - 1]
                self.node[i // 2 - 1], self.node[i - 1] = self.node[i - 1], self.node[i // 2 - 1]
                self.neighbor[i // 2 - 1], self.neighbor[i - 1] = self.neighbor[i - 1], self.neighbor[i // 2 - 1]
                i = i // 2
            else:
                return

    def Down(self, i: int):
        n = len(self.heap)
        while True:
            if i > n // 2:
                return

            temp = 2 * i + 1 if 2 * i + 1 - 1 <= n - 1 and self.heap[2 * i - 1] > self.heap[(2 * i + 1) - 1] else 2 * i
            if self.heap[temp - 1] < self.heap[i - 1]:
                self.heap[temp - 1], self.heap[i - 1] = self.heap[i - 1], self.heap[temp - 1]
                self.node[temp - 1], self.node[i - 1] = self.node[i - 1], self.node[temp - 1]
                self.neighbor[temp - 1], self.neighbor[i - 1] = self.neighbor[i - 1], self.neighbor[temp - 1]
                i = temp
            else:
                return

    def Insert(self, weight: int, node: str, neighbor: str):
        self.heap.append(weight)
        self.node.append(node)
        self.neighbor.append(neighbor)
        # 向上调整
        self.Up(len(self.heap))

    def GetMin(self):
        ans = {'weight': self.heap[0], 'node': self.node[0], 'neighbor': self.neighbor[0]}
        self.heap[0] = self.heap[-1]
        self.node[0] = self.node[-1]
        self.neighbor[0] = self.neighbor[-1]
        self.heap.pop()
        self.node.pop()
        self.neighbor.pop()
        self.Down(1)
        return ans

    def Change(self, weight: int, node: str, neighbor: str):
        i = self.node.index(node)
        self.heap[i] = weight
        self.neighbor[i] = neighbor
        if (i + 1) // 2 - 1 >= 0 and self.heap[i] < self.heap[(i + 1) // 2 - 1]:
            # 变更后的值 小于父节点的值
            # 向上调整
            self.Up(i + 1)
        else:
            self.Down(i + 1)


class Union_Find():
    def __init__(self, node: list):
        # 采用字典的方式 key为head value为集合
        self.data = dict()
        for i in node:
            self.data.update({i: [i]})

    def __str__(self):
        return self.data.__str__()

    def Find(self, node: str):
        # 返回节点的父节点
        for key, value in self.data.items():
            # print(key, value)
            if node in value:
                return key
        else:
            return None

    def Union(self, head1: str, head2: str):
        if len(self.data[head1]) < len(self.data[head2]):
            left = head2
            temp = head1
        else:
            left = head1
            temp = head2
        # 将较少的集合扩展到较多的集合
        self.data[left].extend(self.data[temp])
        # 删除短集合
        del self.data[temp]


def show(Ori: nx.Graph, Prim: nx.Graph, Kruskal: nx.Graph):
    lables = ['Ori', 'Prim', 'Kruskal']
    for i in range(3):
        plt.subplots(1, 1)
        # plt.figure(num=lables[i])
        g = eval(lables[i])
        Edges = []
        colors = [g.edges[u, i]['weight'] for u, i in g.edges]
        weight_sum = sum(colors)
        nx.draw_networkx(g, pos=nx.circular_layout(g), edge_color=colors,
                         width=5, edge_cmap=plt.cm.Blues, with_labels=True, edge_vmin=0, alpha=0.9, node_size=400)
        pc = mpl.collections.PatchCollection(Edges, cmap=plt.cm.Blues)
        pc.set_array(colors)
        plt.colorbar(pc, ax=plt.gca())
        title = f'{lables[i]}——Sum(weight)={weight_sum}'
        plt.title(title)
    plt.show()
    plt.close('all')


def Prim(g: nx.Graph, begin: str) -> nx.Graph:
    h = Heap()
    tree = nx.Graph()
    h.Insert(0, begin, None)
    for item in set(g.nodes()) - {begin}:
        h.Insert(float('inf'), item, None)

    while len(h.heap) != 0:
        temp = h.GetMin()
        node = temp['node']
        neighbor = temp['neighbor']
        weight = temp['weight']
        if neighbor is not None:
            tree.add_edge(node, neighbor, weight=weight)
        # 新邻居中的最小值
        for now_neighbor in g.neighbors(node):
            now_weight = g.get_edge_data(now_neighbor, node)['weight']
            # 有些节点已经在堆中去除了 为了不写复杂的判断语句 直接使用try-except
            try:
                i = h.node.index(now_neighbor)
                if h.heap[i] > now_weight:
                    h.Change(now_weight, now_neighbor, node)
            except:
                pass

    return tree


def Kruskal(g: nx.Graph) -> nx.Graph:
    edge_lst = []
    for u, i in g.edges:
        edge_lst.append((u, i, g.edges[u, i]['weight']))
    # 将边集合按照升序排列
    edge_lst = sorted(edge_lst, key=lambda edge: edge[2])

    # 初始化union
    un = Union_Find(list(g.nodes))
    # 初始化生成树
    tree = nx.Graph()
    for item in edge_lst:
        begin = item[0]
        end = item[1]
        weight = item[2]
        head1 = un.Find(begin)
        head2 = un.Find(end)
        if un.Find(begin) != un.Find(end):
            tree.add_edge(begin, end, weight=weight)
            un.Union(head1, head2)
    return tree


def Test(test_times: int, max_nodes: int, max_weights):
    print(f'测试集个数:{test_times}\t最大节点个数:{max_nodes}\t最大权重值:{max_weights}\t')
    real_test = 0
    real_nodes = 0
    real_vexs = 0
    time_prim = 0
    time_kru = 0
    finish = 1
    for _ in range(test_times):
        print(f'\r' + '=' * int(50 * finish / test_times) + f'=>{finish}/{test_times}', end='')
        finish += 1
        # 定义一个无向图
        g = nx.Graph()
        # 随机生成节点个数
        now_nodes = random.randint(30, max_nodes)
        # 随机生成边的个数 最少为n-1 最多为n(n-1)/2 尽可能保持联通 可以有重边
        # now_nodes * (now_nodes - 1) / 2
        now_vexs = random.randint(now_nodes, int(now_nodes * (now_nodes - 1) / 2))
        # 加入边
        for _ in range(now_vexs):
            # 随机生成起始节点和边及其权重
            begin = str(random.randint(0, now_nodes))
            end = str(random.randint(0, now_nodes))
            weight = random.randint(0, max_weights)
            g.add_edge(begin, end, weight=weight)

        if nx.is_connected(g):
            real_test += 1
            real_nodes += now_nodes
            real_vexs += now_vexs
            t1 = time.time()
            Prim(g, list(g.nodes)[0])
            t2 = time.time()
            Kruskal(g)
            t3 = time.time()
            time_prim += t2 - t1
            time_kru += t3 - t2

    print(f'\n有效测试数量{real_test}\n总节点数{real_nodes}\n总边数{real_vexs}\nPrim用时{time_prim}\tKru用时{time_kru}')


if __name__ == '__main__':
    while True:
        print('如果遇到图片打开后就停止运行的 很抱歉是编辑器的问题 实在调不过来了 把窗口关掉就行了')
        print('1\t示例执行')
        print('2\t自定义图执行')
        print('3\t测试函数')
        print('0\t退出')
        choose = int(input('请输入要执行的选项:'))
        if choose == 1:
            g = nx.Graph()
            g.add_weighted_edges_from(
                [('A', 'B', 6), ('A', 'C', 1), ('A', 'D', 5), ('B', 'E', 3), ('B', 'C', 5), ('C', 'E', 6),
                 ('C', 'F', 4),
                 ('C', 'D', 5), ('D', 'F', 2), ('E', 'F', 6)])
            tree1 = Prim(g, 'A')
            tree2 = Kruskal(g)

            show(g, tree1, tree2)

        elif choose == 2:
            print('请按照以下格式输入边')
            print('A B 9;A C 3')
            print('回车结束输入')
            g = nx.Graph()
            try:
                temp = input()
                temp = temp.split(';')
                for item in temp:
                    a = item.split(' ')
                    g.add_edge(a[0], a[1], weight=int(a[2]))
            except:
                pass
            print('输入结束')
            if nx.is_connected(g):
                tree1 = Prim(g, 'A')
                tree2 = Kruskal(g)
                show(g, tree1, tree2)
            else:
                print('请您输入连通图……')
        elif choose == 3:
            print('友情提示 虽然算法跑的快 但是networkx库跑的慢 增加边需要很长时间\n测试量和节点不要太大\n测试结果放在报告了')
            test_times = int(input('测试集个数'))
            max_nodes = int(input('最大节点个数 n>2'))
            max_weights = int(input('最大权重值'))
            Test(test_times, max_nodes, max_weights)
        elif choose == 0:
            break
        else:
            print('您输错了……')

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值