图算法总结

图算法

图算法表示用于对图的算法,常见的广度优先搜索以及深度优先搜索都属于图算法。
以下为我对遇到过的图算法的学习总结。

图的表示

为了对图进行计算,首先要清楚如何表示图。

图有两种标准的方法表示,分别为:

  • 邻接表
  • 邻接矩阵

有以下有向图:

有向图示例

邻接表

上述有向图的邻接表为:

邻接表示例

《算法导论》中对邻接表的说明:

对于图 G = (V, E) 来说,其邻接表表示由一个包含 |V| 条链表的数组 Adj 所构成,每个结点由一条链表。对于每个结点 u ∈ V ,邻接表 Adj[u] 包含所有与结点 u 之间的有边相邻的结点 v ,即 Adj[u] 所包含图中所有与 u 邻接的结点(也可以说,该链表里包含指向这些结点的指针)。

邻接矩阵

上述有向图的邻接矩阵为:

邻接矩阵示例

《算法导论》中对邻接矩阵的说明:

邻接表的一个潜在缺陷是无法快速判断一条边 (u, v) 是否是图中的一条边,唯一的办法是在邻接表 Adj[u] 里面搜索结点 v 。邻接矩阵克服了这个缺陷,但付出的代价是更大的存储空间消耗。
对于邻接矩阵表示来说,通常会将图 G 中的结点编为 1, 2, ……, |V| 这种编号可以是任意的。在进行此种编号之后,图 G 的邻接矩阵表示由一个 |V| × |V| 的矩阵 A 予以表示,该矩阵满足下述条件:

邻接矩阵公式

除此之外,其实还有一种图的表示方法,那就是对图的抽象表示,即我们常见的二叉树对象,它也是一种对图的表示,只不过是特定于一种类型的图。

拓扑排序

以下为维基上对拓扑排序的定义:

在计算机科学中,拓扑排序是有向图的线性排序,对于图中的每条有向边 (u, v) ,在拓扑排序中,顶点 u 在顶点 v 之前。

可以利用广度优先搜索或是深度优先搜索完成图的拓扑排序。

深度优先搜索完成拓扑排序

深度优先搜索完成拓扑排序很简单,通过深度优先搜索遍历图,当结点所有的出发边都发现后,将结点存储到一个列表中,完成深度优先搜索时,将列表进行倒序,得到的就是图的拓扑排序结果。

不过对图进行拓扑排序,首先要确定进行拓扑排序的图是有向无环图。
而要确认图是否有环,首先需要了解关于深度优先搜索的“着色”。《算法导论》中,在深度优先搜索过程中对各个结点进行“着色”。每个结点初始都为白色,在结点初次被发现后,变为灰色,当扫描完成后(即结点的所有出发边都被发现),结点变为黑色
再根据《算法导论》中的结论:假如图是有向无环图,那么在进行深度优先搜索时,遇到的结点不可能是灰色的。
简单的分析一下上述的结论,当遇到的结点为白色,即结点初次被发现。而遇到的结点为黑色,即表示该结点在完成了一次扫描,继续沿着这个结点进行遍历,即会重复上一次扫描的行为。而如果遇到灰色结点,表示在一次遍历过程中重复发现了一个结点,而只有图中存在环时,才有可能重复发现结点。

有以下例题:Course ScheduleCourse Schedule II

两道题的问题本质是一致,Course Schedule 要解决的是给出的图是否能进行拓扑排序,即是否有环,而 Course Schedule II 除了判断能否进行拓扑排序外,还需要给出拓扑排序的结果。
在解决问题的时候,使用 0 表示白色,-1 表示灰色,1 表示黑色。在深度优先搜索的过程中,如果遇到 -1 的结点即表示图有环,不可以进行拓扑排序。
最后需要注意的是,题目需要我们对图进行邻接表的表示。以下分别为解题源码:

class Solution:
    def canFinish(self, numCourses, prerequisites):
        """
        :type numCourses: int
        :type prerequisites: List[List[int]]
        :rtype: bool
        """

        def dfs(adj, visited, i):
            if visited[i] == -1:
                return False
            if visited[i] == 1:
                return True
            visited[i] = -1
            for node in adj[i]:
                if not dfs(adj, visited, node):
                    return False
            visited[i] = 1
            return True

        adj = [[] for i in range(numCourses)]
        visited = [0 for _ in range(numCourses)]
        for prerequisite in prerequisites:
            adj[prerequisite[1]].append(prerequisite[0])

        for i in range(numCourses):
            if not dfs(adj, visited, i):
                return False
        return True

class Solution:
    def findOrder(self, numCourses, prerequisites):
        """
        :type numCourses: int
        :type prerequisites: List[List[int]]
        :rtype: List[int]
        """

        def dfs(adj, visited, topology_list, i):
            if visited[i] == -1:
                return False
            if visited[i] == 1:
                return True
            visited[i] = -1
            for node in adj[i]:
                if not dfs(adj, visited, topology_list, node):
                    return False
            visited[i] = 1
            topology_list.append(i)
            return True

        adj = [[] for i in range(numCourses)]
        visited = [0 for _ in range(numCourses)]
        for prerequisite in prerequisites:
            adj[prerequisite[1]].append(prerequisite[0])

        topology_list = []
        for i in range(numCourses):
            if not dfs(adj, visited, topology_list, i):
                return []
        return topology_list[::-1]

广度优先搜索完成拓扑排序

不论使用何种方法去完成拓扑排序,判断图是否有环是重要的一环,因为有环图无法完成拓扑排序。
而《数据结构与算法》拓扑排序一节中的做法是先找出一任意一个没有入边的顶点。然后显示出该顶点,并将它及其边一起从图中删除。然后,对图的其余部分同样应用这样的方法处理。如果一个图含有环的话,将会遇到无法删除的结点,即结点总是有入边。

顶点 v 的入度为边 (u, v) 的条数,通过计算、修改顶点的入度来完成拓扑排序。

以下为参考自《数据结构与算法》的拓扑排序的伪代码:

def topsort(graph):
    queue = []
    # 记录结点出队的顺序,也是结点在拓扑排序中的序号
    int counter = 0
    for v in graph.V:
        # 将入度为 0 的结点入队
        if v.indegree == 0:
            queue.append(v)
    while queue:
        v = queue.pop(0)
        counter += 1
        # 记录结点在拓扑排序中的序号
        v.topNum = counter

        for w in graph.Adj[v]:
            # 将与 v 相连的结点 w 的入度都减去 1 ,以此表示删除结点 v
            w.indegree -= 1
            if w.indegree == 0:
                queue.append(w)

    # 当队列为空时,表示拓扑排序处理完成。如果此时完成拓扑排序的结点不等于图的结点个数,即表示有环,存在环时,将有结点因入度始终不为 0 而得不到处理。
    if counter != NUM_VERTICES:
        raise CycleFoundException()

最后,同样以题目 Course ScheduleCourse Schedule II 作为例题。
解题思路和上面利用深度优先搜索解决的方法一致,对图进行拓扑排序,判断是否图是否有环。不过这次改为利用广度优先搜索的思路进行拓扑排序。

class Solution:
    def canFinish(self, numCourses, prerequisites):
        """
        :type numCourses: int
        :type prerequisites: List[List[int]]
        :rtype: bool
        """

        adj = [[] for i in range(numCourses)]
        indegree = [0 for _ in range(numCourses)]
        for prerequisite in prerequisites:
            adj[prerequisite[1]].append(prerequisite[0])
            indegree[prerequisite[0]] += 1

        queue = []
        for i in range(numCourses):
            if indegree[i] == 0:
                queue.append(i)

        counter = 0

        while queue:
            vertex = queue.pop(0)
            counter += 1
            for u in adj[vertex]:
                indegree[u] -= 1
                if indegree[u] == 0:
                    queue.append(u)

        return counter == numCourses

class Solution:
    def findOrder(self, numCourses, prerequisites):
        """
        :type numCourses: int
        :type prerequisites: List[List[int]]
        :rtype: List[int]
        """

        adj = [[] for i in range(numCourses)]
        indegree = [0 for _ in range(numCourses)]
        for prerequisite in prerequisites:
            adj[prerequisite[1]].append(prerequisite[0])
            indegree[prerequisite[0]] += 1

        queue = []
        for i in range(numCourses):
            if indegree[i] == 0:
                queue.append(i)

        counter = 0
        res = []

        while queue:
            vertex = queue.pop(0)
            counter += 1
            res.append(vertex)
            for u in adj[vertex]:
                indegree[u] -= 1
                if indegree[u] == 0:
                    queue.append(u)

        return res if counter == numCourses else []

单源最短路径

对于求图的单源最短路径,根据图的特性不同,有不同的算法解决。如果图是无权的,使用广度优先搜索即可,详情查看无权图的单源最短路径计算。而如果图是带权的,就需要使用其他算法了,而再根据图的带权情况,又有不同的算法。

在论述前,先要理解松弛 relax 操作。

对一条边的 (u, v) 的松弛过程为:首先测试一下是否可以对从 s 到 v 的最短路径进行改善。测试的方法是,将从结点 s 到结点 u 之间的最短路径距离加上结点 u 与 v 之间的边权重,并与当前的 s 到 v 的最短路径估计进行比较,如果前者更小,则对 v.d 和 v.p 进行更新。松弛步骤可能降低最短路径的估计值 v.d 并更新 v 的前驱属性 v.p 。

def relax(u, v, w):
    if v.d > u.d + w(u, v):
        v.d = u.d + w(u ,v)
        v.p = u

Bellman-Ford

Bellman-Ford 算法通过对边进行松弛操作来渐进地降低从源结点 s 到每个结点 v 的最短路径的估计值 v.d ,直到该估计值与实际的最短路径权重 δ(s, v) 相同为止。
此外 Bellman-Ford 算法可以处理带负权值边的图。

Bellman-Ford 算法的伪代码如下:

procedure BellmanFord(list vertices, list edges, vertex source)

    // 初始化图
    for each vertex v in vertices:
        if v is source then distance[v] := 0
        else distance[v] := infinity
        predecessor[v] := null

    // 对每一个条边重复操作
    for i from 1 to size(vertices)-1:
        for each edge (u, v) with weight w in edges:
            if distance[u] + w < distance[v]:
                distance[v] := distance[u] + w
                predecessor[v] := u

    for each edge (u, v) with weight w in edges:
        if distance[u] + w < distance[v]:
            error "图包含负权重的圈"

Bellman-Ford 算法对所有边都进行松弛操作,就是遍历所有的边,计算出最短距离。在重复的计算中,已计算得到正确的距离的边的数量不断增加,直到所有边都计算得到了正确的路径。可以看出 Bellman-Ford 算法的效率是不高的,但是有很好的“兼容性”,适用于更多种类的输入。

SPFA

SPFA 算法,全称为最短路径快速算法 ,是对 Bellman-Ford 算法的优化。

SPFA 算法的基本思路与贝尔曼-福特算法相同,即每个节点都被用作用于松弛其相邻节点的备选节点。相较于贝尔曼-福特算法,SPFA 算法的提升在于它并不盲目尝试所有节点,而是维护一个备选节点队列,并且仅有节点被松弛后才会放入队列中。整个流程不断重复直至没有节点可以被松弛。

以下为 SPFA 算法的伪代码:

procedure Shortest-Path-Faster-Algorithm(G, s)
    for each vertex v ≠ s in V(G)
        d(v) := ∞
    d(s) := 0
    offer s into Q
    while Q is not empty
        u := poll Q
        for each edge (u, v) in E(G)
            if d(u) + w(u, v) < d(v) then
                d(v) := d(u) + w(u, v)
                if v is not in Q then
                    offer v into Q

其实 SPFA 还是广度优先搜索思想。SPFA 筛选入队的结点,其实是对生成的求解树进行了剪枝。

Dijkstra

维基上对 Dijkstra 算法的描述:

这个算法是通过为每个顶点 v 保留当前为止所找到的从 s 到 v 的最短路径来工作的。初始时,原点 s 的路径权重被赋为 0 (d[s] = 0)。若对于顶点 m 存在能直接到达的边(s,m),则把 d[m]设为 w(s, m),同时把所有其他(s 不能直接到达的)顶点的路径长度设为无穷大,即表示我们不知道任何通向这些顶点的路径(对于所有顶点的集合 V 中的任意顶点 v, 若 v 不为 s 和上述 m 之一, d[v] = ∞)。当算法结束时,d[v] 中存储的便是从 s 到 v 的最短路径,或者如果路径不存在的话是无穷大。

边的拓展是 Dijkstra 算法的基础操作:如果存在一条从 u 到 v 的边,那么从 s 到 v 的最短路径可以通过将边(u, v)添加到从 s 到 u 的路径尾部来拓展一条从 s 到 v 的路径。这条路径的长度是 d[u] + w(u, v)。如果这个值比当前已知的 d[v] 的值要小,我们可以用新值来替代当前 d[v] 中的值。拓展边的操作一直运行到所有的 d[v] 都代表从 s 到 v 的最短路径的长度值。此算法的组织令 d[u] 达到其最终值时,每条边(u, v)都只被拓展一次。

算法维护两个顶点集合 S 和 Q。集合 S 保留所有已知最小 d[v] 值的顶点 v ,而集合 Q 则保留其他所有顶点。集合 S 初始状态为空,而后每一步都有一个顶点从 Q 移动到 S。这个被选择的顶点是 Q 中拥有最小的 d[u] 值的顶点。当一个顶点 u 从 Q 中转移到了 S 中,算法对 u 的每条外接边 (u, v) 进行拓展。

伪代码如下:

  function Dijkstra(G, w, s)
     for each vertex v in V[G]
           d[v] := infinity
           previous[v] := undefined
     d[s] := 0
     S := empty set
     Q := set of all vertices
     while Q is not an empty set
           u := Extract_Min(Q)
           S.append(u)
           for each edge outgoing from u as (u,v)
                  if d[v] > d[u] + w(u,v)
                        d[v] := d[u] + w(u,v)
                        previous[v] := u

要理解 Dijkstra 算法,我觉得首先要清楚这个算法的本质,即该算法是什么样的一个算法,这个问题参考与 戴克斯特拉算法(Dijkstra)的本质是贪心,还是动态规划?

我个人的对于 Dijkstra 的理解是一种特殊的搜索算法,是广度优先搜索思想的扩展。
广度优先搜索利用队列先进先出的特性,做到总是优先处理与被发现结点相连的结点,再进一步来说,总是优先处理与距离源结点更近的结点,以源结点为中心,由近到远遍历处理结点。
而 Dijkstra 算法也是这样的一种思想与做法,由于图是带权的,所以利用优先队列的特性去控制结点处理的先后顺序,本质上还是优先处理与距离源结点更近的结点,以源结点为中心,由近到远遍历处理结点。

二分图判断

顶点集 V 可分割为两个互不相交的子集,并且图中每条边依附的两个顶点都分属于这两个互不相交的子集,两个子集内的顶点不相邻。

而要判断图是否为二分图,可以使用染色法进行判断。

使用两种颜色分别对图的各个结点进行染色,被染为相同颜色的结点为同一个集合。使用搜索算法遍历结点,在遍历的同时进行染色,结点 v 染黄色,而后续通过搜索算法发现的与结点 v 相连的结点 u 染绿色,如果 u 已经被发现染色,就需要判断顶点 v 与 顶点 u 的颜色是否为一样,如果颜色一样,表示存在边只属于一个结点集合,不为二分图。

题目 Is Graph Bipartite? 为一道判断图是否为二分图的题目,以下为解题源码:

class Solution:
    def isBipartite(self, graph: 'List[List[int]]') -> 'bool':
        color = [0 for _ in range(len(graph))]

        for root in range(len(graph)):
            if color[root] != 0:
                continue
            queue = []
            queue.append(root)
            color[root] = 1
            while queue:
                v = queue.pop(0)
                for u in graph[v]:
                    if color[u] == 0:
                        color[u] = -color[v]
                        queue.append(u)
                    else:
                        if color[v] == color[u]:
                            return False
        return True

转载于:https://my.oschina.net/bingzhong/blog/3011782

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值