图经典算法第五篇| 豆包MarsCode AI刷题

101 阅读6分钟

最小生成树(MST)和最短路算法是图算法中重要的两类算法,需要加以掌握,本文总结图的算法中的重要方法。

MST

在一个连通的加权无向图中,包含图中所有顶点的一棵子树,且这棵树的边的权重之和(即总权重)是所有可能的生成树中最小的。 Prim算法和Kruskal算法在寻找最小生成树时有以下几个主要区别:

  1. 构建最小生成树的方法不同

    • Prim算法以顶点为中心扩展,从一个顶点开始,逐步将其他顶点加入到生成树中,直到所有顶点都被包含在生成树中。
    • Kruskal算法以边为中心,按照边的权重递增的顺序选择边,并将其加入到生成树中,直到生成树包含所有顶点。
  2. 适用场景不同

    • Prim算法适用于稠密图,因为它每次添加的是与已选顶点集合距离最近的顶点。
    • Kruskal算法适用于稀疏图,因为它每次选择的是所有未选边中权重最小的边。
  3. 算法的时间复杂度不同

    • Prim算法的时间复杂度与顶点数相关,而Kruskal算法的时间复杂度与边数相关。
    • Prim算法的时间复杂度通常为O(V^2),其中V为顶点数,但可以使用斐波那契堆优化到O(E log V),E为边数。
    • Kruskal算法的时间复杂度为O(E log E),其中E为边数。
  4. 实现方式不同

    • Prim算法通过维护一个优先队列(最小堆)来选择最小权重边。
    • Kruskal算法通过并查集来判断是否形成环。
  5. 结果的形式不同

    • Prim算法得到的最小生成树是以一个顶点为起点的树形结构。
    • Kruskal算法得到的最小生成树是以边为基础的森林,需要进行额外的处理才能形成树。

这些区别使得在不同的应用场景和图的特性下,可以选择更适合的算法来求解最小生成树问题。

Prim求MST

P联想Poinit,Prim算法是基于结点的算法,初始化时任选一个节点,之后循环加结点到当前集合,每次选择到当前集合的距离最近的那个结点,直到所有结点都加入集合。以下是C语言的模板代码。

int n;      // n表示点数
int g[N][N];        // 邻接矩阵,存储所有边
int dist[N];        // 存储其他点到当前最小生成树的距离
bool st[N];     // 存储每个点是否已经在生成树中


// 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
int prim()
{
    memset(dist, 0x3f, sizeof dist);

    int res = 0;
    for (int i = 0; i < n; i ++ )
    {
        int t = -1;
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;

        if (i && dist[t] == INF) return INF;

        if (i) res += dist[t];
        st[t] = true;

        for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
    }

    return res;
}
Kruskal求MST

克鲁斯卡尔算法基于边求最小生成树,每次选取能连接两个未连通的结点的最短的边。

int n, m;       // n是点数,m是边数
int p[N];       // 并查集的父节点数组

struct Edge     // 存储边
{
    int a, b, w;

    bool operator< (const Edge &W)const
    {
        return w < W.w;
    }
}edges[M];

int find(int x)     // 并查集核心操作
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int kruskal()
{
    sort(edges, edges + m);

    for (int i = 1; i <= n; i ++ ) p[i] = i;    // 初始化并查集

    int res = 0, cnt = 0;
    for (int i = 0; i < m; i ++ )
    {
        int a = edges[i].a, b = edges[i].b, w = edges[i].w;

        a = find(a), b = find(b);
        if (a != b)     // 如果两个连通块不连通,则将这两个连通块合并
        {
            p[a] = b;
            res += w;
            cnt ++ ;
        }
    }

    if (cnt < n - 1) return INF;
    return res;
}

最短路

Dijkstra求最短路

原理

  • Dijkstra算法是一种贪心算法,用于在带权图中找到单个源点到其他所有顶点的最短路径。
  • 它使用一个优先队列(通常是最小堆)来存储所有顶点,并在每一步中选择距离源点最近的未访问顶点。

步骤

  1. 初始化源点到所有其他顶点的距离,源点到自身的距离为0,到其他顶点的距离为无穷大。
  2. 创建一个优先队列,将所有顶点按照到源点的距离排序。
  3. 从优先队列中取出距离最小的顶点,更新其相邻顶点的距离。
  4. 重复步骤3,直到优先队列为空或找到目标顶点。

适用场景

  • 适用于边权为非负数的图。
  • 适合稀疏图,因为需要频繁地更新距离。

时间复杂度

  • 使用数组实现时,时间复杂度为O(V^2),其中V是顶点数。
  • 使用优先队列(最小堆)实现时,时间复杂度为O((V+E) log V),其中E是边数。
int g[N][N];  // 存储每条边
int dist[N];  // 存储1号点到每个点的最短距离
bool st[N];   // 存储每个点的最短路是否已经确定

// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    for (int i = 0; i < n - 1; i ++ )
    {
        int t = -1;     // 在还未确定最短路的点中,寻找距离最小的点
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;

        // 用t更新其他点的距离
        for (int j = 1; j <= n; j ++ )
            dist[j] = min(dist[j], dist[t] + g[t][j]);

        st[t] = true;
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}
Floyd求最短路

原理

  • Floyd算法是一种动态规划算法,用于在带权图中找到所有顶点对之间的最短路径。
  • 它通过考虑所有顶点作为中间顶点,逐步更新顶点对之间的最短距离。

步骤

  1. 初始化一个距离矩阵,矩阵的元素表示顶点对之间的直接距离。
  2. 对于每个顶点k,检查通过k作为中间顶点是否可以找到更短的路径。
  3. 更新距离矩阵,如果通过k作为中间顶点的路径更短,则更新对应的距离值。
  4. 重复步骤2和3,直到所有顶点都被考虑过。

适用场景

  • 适用于边权可以为负数的图,但不能包含负权重环。
  • 适合稠密图,因为需要计算所有顶点对之间的最短路径。

时间复杂度

  • 时间复杂度为O(V^3),其中V是顶点数。
for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            if (i == j) d[i][j] = 0;
            else d[i][j] = INF;

// 算法结束后,d[a][b]表示a到b的最短距离
void floyd()
{
    for (int k = 1; k <= n; k ++ )
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}