算法通关村-----图的基本算法

图的实现方式

邻接矩阵

定义

邻接矩阵是一个二维数组,其中的元素表示图中节点之间的关系。通常,如果节点 i 与节点 j 之间有边(无向图)或者从节点 i 到节点 j 有边(有向图),则矩阵中的元素值为 1 或者表示边的权重值。如果没有边相连,则元素值为 0 或者一个特定的标记(通常表示无穷大)。

优点

适用于稠密图,即节点之间有很多边的情况,因为它不会浪费太多空间。支持常数时间内的边的查找操作。

缺点

对于稀疏图,它会浪费大量的空间,因为大部分元素都是 0。插入和删除边的操作相对较慢,需要修改矩阵的值。

邻接表

定义

邻接表是一种数据结构,用于表示图的每个节点及其相邻节点的列表。通常使用数组(或哈希表)和链表(或其他数据结构)的组合来实现。每个节点对应一个数组元素,该元素存储指向该节点相邻节点的链表或数组。

优点

适用于稀疏图,因为它不会浪费大量空间。插入和删除边的操作相对较快,因为只需要修改链表或数组。

缺点

查找两个节点之间是否存在边需要遍历相邻节点列表,时间复杂度取决于节点的度数。

图的遍历

深度优先搜索(DFS)

定义

DFS从一个起始节点开始,沿着一条边尽可能深地探索图,直到无法继续为止,然后回溯到上一个节点,继续深入其他分支。DFS通常使用递归或栈数据结构来实现。

步骤

从起始节点开始,将其标记为已访问。

对于当前节点,遍历其未被访问的邻居节点。

选择一个邻居节点,将其标记为已访问,并递归或使用栈继续探索该邻居节点。

重复步骤3,直到没有未被访问的邻居节点,然后回溯到上一个节点,继续探索其他邻居。

特点

深度优先搜索会一直沿着一条分支深入,直到到达最深处,然后返回并探索其他分支。使用递归时,可能导致堆栈溢出,因此对于大型图,可能需要使用非递归实现,使用显式的栈数据结构。

广度优先搜索(BFS)

定义

BFS从一个起始节点开始,首先访问其所有未被访问的邻居节点,然后按照距离从起始节点逐层扩展,直到遍历完整个图。BFS通常使用队列数据结构来实现。

步骤

从起始节点开始,将其标记为已访问,并将其加入队列。

从队列中取出一个节点,访问其所有未被访问的邻居节点,并将它们标记为已访问,并加入队列。

重复步骤2,直到队列为空。

特点

广度优先搜索首先访问离起始节点最近的节点,然后逐层扩展,确保了最短路径的搜索。使用队列来实现,确保了节点按照距离逐层访问。

最小生成树

概念

最小生成树(Minimum Spanning Tree,简称 MST)是在一个连通图中找到一棵包含图中所有节点的树,并且该树的所有边的权重之和最小的树。通俗地说,最小生成树是一棵树,它连接了图中的所有节点,并且总权重最小。Prim算法和Kruskal算法是两种常见的用于求解最小生成树问题的算法。

Prim算法

思想

Prim算法的关键思想是始终选择当前最小权重的边,以确保最小生成树的总权重最小。这种贪心策略保证了Prim算法的正确性。

步骤

选择一个起始节点:首先,从图中选择一个任意的起始节点作为最小生成树的根节点。

初始化:创建一个空的最小生成树,开始时只包含根节点。同时,创建一个优先队列(通常使用最小堆)来存储候选边,该队列初始化为空。

找到候选边:将起始节点的所有相邻边(连接到未包含在最小生成树中的节点的边)添加到优先队列中。

选择最小权重的边:从优先队列中选择权重最小的边(即最小的边权重),并将其添加到最小生成树中。

标记节点:将该边连接的节点标记为已访问或已包含在最小生成树中。

重复步骤3至5:不断重复步骤3和步骤4,选择并添加下一条权重最小的边,直到最小生成树包含了图中的所有节点。

结束:当最小生成树包含了所有节点时,算法终止。

特点

Prim算法的时间复杂度取决于优先队列的实现方式,通常为O(E + VlogV),其中E是边的数量,V是节点的数量。Prim算法在稠密图(边数量较多)上效果较好,因为优先队列中的边数量较多时,操作的时间复杂度较低。它适用于带权重的图,用于解决网络设计、电路设计、城市规划等问题。

Kruskal算法

思想

Kruskal算法的关键思想是始终选择权重最小的边,并确保最小生成树不形成环。这种贪心策略保证了Kruskal算法的正确性。

步骤

初始化:将图中的所有边按照权重从小到大排序,并将它们存储在一个边集合中。同时,创建一个空的最小生成树,开始时不包含任何边。

遍历边集合:按照排序后的顺序遍历边集合。

检查边的端点:对于当前遍历到的边,检查它的两个端点是否已经在最小生成树中。如果边的两个端点都不在最小生成树中,说明将这条边添加到最小生成树不会形成环,因此可以安全地将这条边加入最小生成树中。

添加边:将通过步骤3确定可以添加的边,加入到最小生成树中。

重复步骤2至4:不断重复遍历边集合并添加边,直到最小生成树包含了图中的所有节点或者边集合已经遍历完。

结束:当最小生成树包含了所有节点时,算法终止。

特点

Kruskal算法的时间复杂度主要取决于排序边的操作,通常为O(ElogE),其中E是边的数量。由于Kruskal算法不需要对节点的度数进行操作,因此在稠密图(边数量较多)上表现较好。它适用于带权重的图,用于解决网络设计、电路设计、城市规划等问题。需要注意的是,Kruskal算法不一定会生成唯一的最小生成树,但它保证了生成的最小生成树的总权重是最小的。如果存在多个最小生成树,它们的总权重将相等。

最短路径

概念

最短路径问题是图论中的一个经典问题,其目标是找到从一个指定的起始节点到另一个指定的目标节点之间的最短路径,即具有最小权重(距离、代价等)的路径。最短路径问题分为单源最短路径和所有节点对最短路径。

单源最短路径问题(Single-Source Shortest Path):在单源最短路径问题中,给定一个起始节点,要找到该节点到图中所有其他节点的最短路径。最著名的算法是Dijkstra算法。

所有节点对最短路径问题(All-Pairs Shortest Path):在所有节点对最短路径问题中,需要找到图中任意两个节点之间的最短路径。最著名的算法是Floyd算法。

Dijkstra算法

思想

Dijkstra算法的核心思想是使用贪心策略,即始终选择当前距离最短的节点进行探索,并逐步更新距离数组。由于它不处理负权重边,因此适用于带有非负权重边的图。

步骤

初始化:首先,创建一个集合(通常是一个优先队列或最小堆),用于存储尚未确定最短路径的节点。同时,初始化一个距离数组,用于记录从起始节点到每个节点的当前最短距离。将起始节点的距离设置为0,表示到达起始节点的距离为0,而其他节点的距离设置为无穷大(或一个足够大的值)表示尚未确定的距离。

选择最近的节点:从集合中选择距离起始节点最近的节点,并将其标记为当前的最短路径节点。通常,这个选择过程使用优先队列或最小堆来实现,以确保每次选择的是距离最近的节点。

更新距离:对于当前选定的最短路径节点,遍历其所有未被确定最短路径的邻居节点。计算从起始节点经过当前节点到邻居节点的距离,并将这个距离与之前记录的距离进行比较。如果新计算得到的距离更短,就更新邻居节点的距离。

重复步骤2和步骤3:不断重复选择最近的节点和更新距离的步骤,直到所有节点都被标记为确定最短路径或集合为空。

结束:当所有节点都被标记为确定最短路径时,算法终止,距离数组中存储的就是从起始节点到所有其他节点的最短路径。

特点

Dijkstra算法的时间复杂度通常为O(V^2),其中V是节点的数量。但如果使用优先队列或最小堆来实现,时间复杂度可以优化为O(E + VlogV),其中E是边的数量。因此,对于大型图来说,使用优先队列实现Dijkstra算法更有效率。算法的正确性得益于贪心策略,它保证了每一步都选择了当前最优的路径。

Floyd算法

思想

使用动态规划的方法,通过考虑所有可能的中间节点,逐步更新节点对之间的最短路径距离。这个算法可以处理带有正、负权重边的图,以及检测负权重环路。

步骤

初始化距离矩阵:创建一个距离矩阵(通常用二维数组表示),其中元素d[i][j]表示从节点i到节点j的最短距离。初始时,将矩阵中的对角线元素(i等于j的元素)设置为0,表示从节点i到节点i的距离为0。对于其他元素,如果存在直接的边连接节点i和节点j,则将d[i][j]设置为边的权重,否则将其设置为无穷大。

更新距离矩阵:对于每对节点i和j,以及中间节点k,检查是否存在一条路径从节点i到节点j,经过节点k,比当前的最短距离更短。具体步骤如下:

对于每一对节点i和j(i ≠ j),遍历所有中间节点k(1到n,其中n是节点的总数)。
计算从节点i经过节点k到节点j的距离:d[i][k] + d[k][j]。
如果上述距离小于当前的最短距离d[i][j],则更新d[i][j]为这个新的更短距离。

重复步骤2:不断重复更新距离矩阵的步骤,直到不再存在更短的路径为止。这意味着每个节点对之间的最短路径已经被找到。

结束:当算法完成后,距离矩阵d包含了所有节点对之间的最短路径距离。

特点

Floyd算法的时间复杂度为O(V^3),其中V是节点的数量。虽然它在时间复杂度上比Dijkstra算法慢,但Floyd-Warshall算法具有一个重要的优点,即可以一次性计算所有节点对之间的最短路径,因此非常适合于计算密集型问题和需要计算所有节点对的情况。

你可能感兴趣的:(算法训练营,算法)