【算法与数据结构】之图论

博客介绍了图的相关算法,包括图的遍历(深度优先和广度优先)、寻路算法。还阐述了最小生成树的Prim、Kruskal和Vyssotsky算法。在最短路径问题上,介绍了Dijkstra、Bellman - Ford等算法。此外,提到最长路径问题可使用Bellman - Ford算法等。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【总结】

图的表示方法:
邻接矩阵:适合表示稠密图
邻接表:适合表示稀疏图

最短路径问题:

  • 无向图——广度优先遍历即可实现,
  • 有向图
    (1)无负权边时:Dijkstra算法
    (2)有负权边但无负权环时:Bellman-ford算法,Floyed算法
    (3)无环图:拓扑排序

最长路径问题:Bell-Ford算法

 

一. 图的遍历算法

1. 深度优先遍历

本博客均以下图为例,给定无向图,和其邻接表adj = {0:[1,2,5,6],1:[0],2:[0],3:[4,5],4:[3,5,6],5:[0,3,4],6:[0,4]},据此写出遍历算法。

图的深度优先遍历复杂度:
稀疏图(邻接表):O(V+E)
稠密图(邻接矩阵):O(V^2)

深度优先遍历类似与树的前序遍历,是递归的,也可以使用非递归(利用栈)来实现。下面给出递归版本和非递归版本的深度优先遍历算法(参考
深度优先遍历结果:[0, 1, 2, 5, 3, 4, 6]

#### ----------图的深度优先遍历(递归版)--------------
def dfs_recursively(adj, start, visited = None):
    if visited is None:
        visited = [start]
    elif start not in visited:
        visited.append(start)

    for i in adj[start]:
        if i not in visited:
            visited.append(i)
            dfs_recursively(adj, i, visited)
    return visited


#### -----------图的深度优先遍历(迭代版)-------------
def dfs_iteratively(adj, start):
    stack = []
    visited = [start]
    stack.append([start, 0]) # 把start节点的第一个邻接节点push进去

    while stack:
        v, next_child_idx = stack[-1] # 获取当前节点及其邻接节点的序号
        if v not in adj or next_child_idx >= len(adj[v]):
            stack.pop()
            continue
        next_child = adj[v][next_child_idx] # 获取邻接节点
        stack[-1][1] += 1
        if next_child in visited: # 如果邻接节点已经遍历过了,直接continue
            continue

        visited.append(next_child) # 将该邻接节点加入visited中
        stack.append([next_child, 0]) # 将该邻接节点加入栈中,下次循环遍历该节点的第一个邻接节点
    return visited
    
adj = {0:[1,2,5,6],1:[0],2:[0],3:[4,5],4:[3,5,6],5:[0,3,4],6:[0,4]} # adj就是邻接表的意思
print(dfs_recursively(adj, 0))
print(dfs_iteratively(adj, 0))

 

2. 广度优先遍历

使用BFS(广度优先遍历)解决最短路径问题仅适用于无权图,对于有权图的最短路径问题,需要使用专门的算法,比如迪杰特斯拉算法。

类似与树的层序遍历,先遍历到的节点距离起始点的距离更近,可以利用队列来实现。
广度优先遍历最有用的性质是可以遍历一次就生成中心结点到所遍历结点的最短路径,这一点在求无权图的最短路径时非常有用。
图的广度优先遍历结果:[0, 1, 2, 5, 6, 3, 4]

#### -------------图的广度优先遍历-------------
def bfs(adj, start):
    visit = [start]
    queue = [start]
    while queue:
        cur_node = queue.pop(0)
        for next_node in adj[cur_node]:
            if next_node not in visit: # 如果当前节点的邻接节点没访问过
                visit.append(next_node)
                queue.append(next_node)
    return visit

 

二、寻路算法

使用深度优先遍历,完成三个问题,1.两个点之间有没有路径,2. 路径具体是什么,3. 展示路径。

#### ----------图的深度优先遍历(递归版)--------------
class FindPath:
    def __init__(self, adj):
        self.graph, self.V = self.initGraph_adj(adj) # 获取图和图的所有顶点
        self.visited = [False] * len(self.graph)
        self.parent = [-1] * len(self.graph) # 这个节点的父节点(这个节点是由谁遍历得到的)


    def initGraph(self, adj): # 初始化成稠密图
        graph = [[0] * len(adj) for _ in range(len(adj))]
        V = set()
        for nodes, child_nodes in adj.items():
            V.add(nodes)
            for child_node in child_nodes:
                graph[nodes][child_node] = 1
        return graph, V


    def initGraph_adj(self, adj): #初始化成稀疏图
        graph = adj
        V = set(graph.keys())
        return graph, V


    # 图的深度优先遍历(递归版),dfs后,visited数组全部为1,返回值res为深度优先遍历序列
    def dfs_recursively(self, start, res = None):
        self.visited[start] = True
        if not res:
            res = []
        res.append(start)
        for node in self.graph[start]:
            if not self.visited[node]: # 如果start节点的邻接节点没有访问到,则dfs
                self.parent[node] = start # 维护parent数组,如果node没有被访问过,则它的parent就是start
                self.dfs_recursively(node, res)
        return res


    # 判断节点start到w之间有没有路径
    def hasPath(self, start, w):
        assert w < len(self.V)
        self.dfs_recursively(start)
        return self.visited[w] # 如果visited为True,说明dfs从start访问到了w,说明两个节点之间有路


    # 输出由start到w的路径(使用parent数组一步步倒推回去)
    def Path(self, start, w):
        assert w <= len(self.V)
        path = []
        while w != -1:
            path.append(w) # path中存的是路径的倒序,因此使用reversed函数倒序,或者使用stack。
            w = self.parent[w]
        return list(reversed(path))



adj = {0:[1,2,5,6],1:[0],2:[0],3:[4,5],4:[3,5,6],5:[0,3,4],6:[0,4]} # adj就是邻接表的意思
S = FindPath(adj)
print(S.V)
res = S.hasPath(0, 6)
print(S.visited)
print(S.parent)
print(S.Path(0,6))

三、最小生成树

最小生成树问题:使用n-1条边,将图的所有n个节点连接起来,并且使得这n-1条边的总和最小。最经典的是Prim和Kruskal算法。
针对带权无向图,针对连通图。

切分定理

1. Prim算法(借助最小索引堆)

不断寻找横切边中最短的那条边,并将边的节点加入进来。

2. Kruskal算法(借助并查集)

先对所有边排序(复杂度O(ElogE),E为边数),按顺序遍历,只要加入最短的边后不构成环,则这条边就是最小生成树中的边。
如何判断加入了一条边之后,是否能够构成环?——并查集:

  • 在将一条边加入最小生成树时,需要对这条边的两个节点进行一次union操作,使这两个节点的根变成相同的,
  • 在判断加入了一条边后能够构成环,isConnect操作,

使用并查集快速判断环

复杂度比较:(V为节点数,E为边数)

Lazy Prim——O(ElogE)
Prim——O(ElogV)
Kruskal——O(ElogE)
Kruskal算法比Prim算法效率略差,但是思路简单,易于实现。

如果横切变中有相等的边:
则根据算法的具体实现,每次选择一个边,此时,图存在多个最小生成树,但是按照上面的两种方法,最终只能找到一个最小生成树。(如果横切边中不含相等的边,则得到的最小生成树是唯一的)

对于一个图,有多少个最小生成树

3. Vyssotsky算法

思想:将边逐渐地添加到生成树中,一旦形成环,删除环中权值最大的边。
但是由于目前没有支持该算法的数据结构,因此不常用。

 

四、最短路径问题

https://blog.youkuaiyun.com/wzy_2017/article/details/78910697
最短路径问题:从某个源点开始,到其余各顶点的最短路径。
从某一个节点开始,进行广度优先遍历,其实就是求了一个最短路径。
最短路径问题和最小生成树问题的区别:

最小生成树是说,所有的边的权值总和是最小的,
最短路径问题是,所有的点,到起始顶点的距离是最小的(起始顶点是固定的),其实形成了一个以该节点为根的树,这其实是一个特殊的最小生成树,加了一个限制条件——限制了顶点,这棵树称为最短路径树,求解最短路径树的问题称为——单源最短路径问题(单源的意思是:单一起始点)。

这里求解带权图的最短路径问题。核心操作是:松弛操作。

1. Dijkstra单源最短路径算法(无负权边,O(Elog(V)))

Dijkstra算法的前提(也是局限):图中不能有负权边!

复杂度O(Elog(V)),借助最小索引堆,每次插入和更新复杂度为O(logV),遍历所有节点的复杂度为O(n)。

【Dijkstra思想】:
每次找到离源点(如1号结点)最近的一个顶点,然后以该顶点为中心进行松弛操作,最终得到源点到其余所有点的最短路径。

【前期准备】:

  1. 使用distTo数组,记录每一个节点到源点的距离,初始值除源点设为0外,其余都设为无穷大
  2. 使用marked数组,记录该节点是否已经是最小路径了,初始值除源点外,其余都设为False
  3. 使用from数组,记录最短路径中,每个节点的前驱结点,初始化全部为False
  4. 使用最小索引堆ipq,记录所有节点到源点的路径值,那么每次获取最小值时只需要O(1)复杂度。

【基本步骤】:

  1. 将源点的所有邻边加入distTo数组中,并且将这些边同时加入最小索引堆;
  2. 如果ipq不为空,则取出离源点最近的节点v,将marked[v]标记为True(Dijkstra算法的前提,没有负权边,当前源点s到v的路径是最小的,则认为这个距离就是源点到v的最短路径,)并且以v为中心进行松弛操作;
  3. 重复第2步操作,直至ipq为空。

【松弛操作的含义】:遍历v的每一个邻边w,判断从源点到v的最短路径,加上从v到w的路径之和,是否小于直接从源点s到w的路径,如果是,则更新distTo[w]为更短的路径,更新w的前驱结点为v,更新ipq中从源点s到w的最短路径;

代码实现(写了一个比较完整的版本,想看代码简化版的,看这里):

class Dijkstra:
    def __init__(self, adj, s): # 源点
        self.ipq = IndexMinHeap()  # 最小索引堆,只需要开辟节点个数那么多的空间就足够了
        self.graph = adj
        self.s = s
        self.distTo = [float('inf')] * len(self.graph)  # 源点s到每一个顶点的最短距离
        self.marked = [False] * len(self.graph) # 已经找到了最小路径的顶点进行标记
        self.from_ = [False] * len(self.graph) # 用来记录最短路径中,每个顶点的前驱结点

    def Dijkstra(self):
        self.distTo[self.s] = 0
        self.marked[self.s] = True
        for node in self.graph[self.s]: # 将源点的所有邻边加入distTo数组和最小索引堆ipq
            self.distTo[node] = self.graph[self.s][node]
            self.ipq.insert(self.s, node, self.distTo[node])
        while not self.ipq.isEmpty():
            s, v, distance = self.ipq.pop() # 提取当前离源点s路径最短的节点,进行松弛操作
            self.marked[v] = True
            for w in self.graph[v]: # 遍历v的所有临边
                if not self.marked[w]:
                    if not self.from_ or self.distTo[v] + self.graph[v][w] < self.distTo[w]: # 如果w节点还没有访问过,或者从v过去更短,则更新路径
                        self.distTo[w] = self.distTo[v] + self.graph[v][w]
                        self.from_[w] = v
                        if not self.ipq.change(s, s, w, self.distTo[w]): # 更新最小索引堆,源点到w点的最短路径
                            self.ipq.insert(s, w, self.distTo[w])
        return self.distTo

    def shortestPathTo(self, w): # 顶点s到某一个点w的权重是多少
        return self.distTo[w]

    def hasPathTo(self, w): # 判断源点能够到达该点(判断是否连通)
        return self.marked[w]

    def showShortestPath(self): # 输出到各节点的最短路径的具体过程
        for i in range(len(self.graph)):
            res = [i]
            e = self.from_[i]
            while e != self.s:
                res.insert(0, e)
                e = self.from_[e]
            res.insert(0, self.s)
            print('from s to {}: {}'.format(i, res))

class IndexMinHeap: #### 维护一个最小索引堆
    def __init__(self):
        self.data = []
        self.count = 0
        self.indexes = []

    def isEmpty(self):
        return self.count == 0

    def insert(self, start_node, end_node, weight):
        self.data.append([start_node, end_node, weight])
        self.count += 1
        self.indexes.append(self.count - 1)
        self.ShiftUp(self.count - 1)


    def ShiftUp(self, k):
        while (k - 1) // 2 >= 0:  # 如果存在父节点
            if self.data[self.indexes[k]][-1] < self.data[self.indexes[(k - 1) //2]][-1]:
                self.indexes[k], self.indexes[(k - 1)//2] = self.indexes[(k - 1)//2], self.indexes[k]
            k = (k -1) //2

    def pop(self):
        assert self.count > 0
        tmp_index = self.indexes[0]  #记录弹出的索引号,大于这个索引的序号,依次减一
        self.indexes[0], self.indexes[-1] = self.indexes[-1], self.indexes[0]
        res = self.data[self.indexes.pop()]
        self.data.remove(res)
        self.count -= 1

        for i in range(len(self.indexes)):
            if self.indexes[i] > tmp_index:
                self.indexes[i] -= 1
        self.ShiftDown(0) # 弹出元素后,一定要维护最小堆
        return res

    def ShiftDown(self, index):
        while 2 * index + 1 < self.count: # 如果包含左孩子
            left = 2 * index + 1
            if left + 1 < self.count and self.data[self.indexes[left]][-1] > self.data[self.indexes[left + 1]][-1]:
                left = left + 1
            if self.data[self.indexes[index]][-1] < self.data[self.indexes[left]][-1]:
                break
            self.indexes[index], self.indexes[left] = self.indexes[left], self.indexes[index]
            index = left

    # 将[old_start, end_node,weight]的一条边替换成[new_start, end_node, item],其中item < weight
    def change(self, old_start, new_start, end_node, item):
        ### 找到self.indexes[j] = i, j表示data[i]在堆中的位置
        # 对j分别进行ShiftUp & ShiftDown
        flag = True
        for j in range(self.count):
            if self.data[j][0] == old_start and self.data[j][1] == end_node:
                flag = True
                self.data[j][0] = new_start
                self.data[j][-1] = item
                self.ShiftUp(j)
                self.ShiftDown(j)
        return True if flag else False


adj = {0:{1 : 5, 2 : 2, 3 : 6},1:{4 : 1}, 2:{1 : 1, 3: 3, 4: 5}, 3:{4: 2}, 4:{}}
S = Dijkstra(adj, 0)
print(S.Dijkstra())
S.showShortestPath()

2. Bellman-Ford单源最短路径算法(无负权环,O(EV))

前提:图中可以有负权边,但不能有负权环(因为拥有负权环的图,没有最短路径),Bellman-Ford算法可以判断图中是否有负权环。

Ford算法比Dijkstra算法处理的范围更广,代价是复杂度更高,Ford的复杂度是O(EV),Dijkstra的复杂度是O(ElogV)(E为边数,V为顶点数)

【Ford算法的思想】:
从源点出发,对所有的点进行V-1次松弛操作(Dijkstra每次只拿出最短的边来进行松弛操作),每经过一次松弛操作,找到经过这个点的另外一条路径,多一条边,权值更小。当经过了V-1次松弛操作后,理论上就找到了从源点到其他所有点的最短路径,如果还可以继续松弛,说明图中有负权环。

如果一个图没有负权环,从一点到另外一点的最短路径,最多经过所有的V的顶点,有V-1条边;

松弛操作的理解:能不能从一个点出发,经由另外一个点,再回到找另外一个点,

代码实现:

class Bellman_Ford:
    def __init__(self, adj, s): # s表示源点
        self.graph = adj
        self.s = s
        self.distTo = [float('inf')] * len(self.graph)
        self.from_ = [False] * len(self.graph)
        self.hasNegativeCycle = False # 检测是否有负权环

    def BellmanFord(self):
        self.distTo[self.s] = 0
        self.from_[self.s] = self.s
        # 三重循环,对所有节点进行V-1轮松弛操作
        for p in range(1, len(self.graph)): # 最外层的V-1轮
            for v in range(len(self.graph)):  # 所有节点
                for w in self.graph[v]:
                    if self.distTo[v] + self.graph[v][w] < self.distTo[w]:
                        self.distTo[w] = self.distTo[v] + self.graph[v][w]
                        self.from_[w] = v
        self.hasNegativeCycle = self.detectNegativeCycle()
        if self.hasNegativeCycle: # 如果有负权环,则不存在最短路径
            return "The graph contains negative cycle!"
        return self.distTo

    # 检测负权环
    def detectNegativeCycle(self): # 再进行一轮松弛操作,如果出现了更短的路径,说明存在负权环
        for v in range(len(self.graph)):  # 所有节点
            for w in self.graph[v]:
                if self.distTo[v] + self.graph[v][w] < self.distTo[w]: # 如果出现了更短路径,返回True
                    return True
        return False

    def showShortestPath(self): # 输出到各节点的最短路径的具体过程
        for i in range(len(self.graph)):
            res = [i]
            e = self.from_[i]
            while e != self.s:
                res.insert(0, e)
                e = self.from_[e]
            res.insert(0, self.s)
            print('from s to {}: {}'.format(i, res))

adj = {0:{1 : 5, 2 : 2, 3 : 6},1:{2 : -4, 4 : 2}, 2:{3: 3, 4: 5}, 3:{}, 4:{3: -3}}
S = Bellman_Ford(adj, 0)
print(S.BellmanFord())
S.showShortestPath()

Bellman-Ford算法的优化方法——利用队列数据结构,queue-based bellman-ford算法,优化的复杂度依旧是O(EV)

有向无环图:限制条件更多,不能处理有环图,不能处理无向图。

3. 所有对最短路径算法

单源最短路径算法的局限:用户需要提前指定这个“源”究竟是谁,而所有对最短路径算法可以解决:任何两个点之间的最短路径。

4. Floyed算法——处理无负权环的图,O(V^3)

动态规划思想。

五、最长路径算法

最长路径问题不能有正权环;
无权图的最长路径问题是指数级难度的;
对于有权图,不能使用Dijkstra求最长路径问题;
可以使用Bellman-Ford算法。
有向无环图DAG的拓扑排序算法,以及用索引算法处理所有对的最短路径算法,也可以通过改造,来解决最长路径问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值