最小生成树

参考:https://www.bilibili.com/video/BV1Ut411976J?from=search&seid=6406725088142163166

参考:https://baike.baidu.com/item/%E8%BF%9E%E9%80%9A%E5%9B%BE/6460995?fr=aladdin

 

 

一,MST 性质 (Minimum Spaning Tree)

1,定义:

  设 N =  {V,E} 是一个连通网,U 是顶点集 V 的一个非空子集。若 (u, v) 是一条具有最小权值的边,其中 u∈U,v∈V-U,则必存在一棵包含 (u, v) 的最小生成树。

2,证明:

  因为必然存在这么两颗未完成的最小生成树:一个顶点集为U;一个顶点集为 V-U。这两颗最小生成树要想得到完整的最小生成树,则最后一条边必然为 —— 其中一个顶点属于 U ,另一个顶点属于 V-U 的所有边中权值最小的边。

3,注意点

  ① MST 是构造最小生成树的基本性质

  ② 最小生成树并不唯一

  ③ 最小生成树只适用于无向图。若是有向图的话,选取不同的源点就会有不同的结果。

 4,补充概念 —— 连通图

  无向图中,若从 点i 到 点j 之间有路径,则称 点i 和 点j 之间是连通的;有向图中,若从 点i 到 点j 之间有路径,则称 点i 可达 点j 。

  若无向图中任意两个顶点都是连通的,则称该图是连通图。若有向图中任意两个顶点都是互相可达的,则称该图是强连通图。

 

 

二,Prim 算法

1,算法思想

  先利用 MST 性质,将图中的点分为两个集合:U (已落在最小生成树上的顶点集);V-U (未落在最小生成树上的顶点集)。

  然后在连通 U 和 V-U 的所有边中选取权值最小的边,加入当前的最小生成树。

  最后,不断循环选取,直至选取的边可以构成一颗最小生成树。

 

2,步骤

设 N = {V,E} 是一个连通图,M = {U,TE} 是 N 上的一棵最小生成树,源点为 u0,n = | V |。

再设属于 V-U 的 点i 与 U 中的所有点有直达边的所有边中的边长最短的边叫 —— 点i 到 U 的最短直达边。

  ①  初始化:令 U = { u0 },TE = { }。

  ②  更新距离:更新并记录 V-U 中的点到 U 的最短直达距离及对应的直达边。

  ③  找最短边:在 V-U 中的点到 U 的所有最短直达边中,找出边长最短的边 (ui,vi)。

  ④  更新 M:将 (ui,vi) 加入 TE 中,并将 vi 从 V-U 中拿出,加入 U。

  ⑤  迭代循环:重复 n-1 次  ②,③,④ 的循环。

  ⑥  判断:通过判断 U 是否等于 V 来判断是否能生成最小生成树。

 

  说明1:为什么 ⑤迭代循环 要循环 n-1 次?

    因为每次循环都要找到一条最小生成树的边,而 n 个顶点的最小生成树需要找到 n-1 条边。

  说明2:为什么需要 ②更新距离?

    因为每次 vi 加到 U 中,就可能会改变 V-U 中的点到 U 的最短直达边,所以要遍历 V-U 中的点到 vi 的距离,比较是否会比之前的最短直达边更短。

  说明3:为什么要先 ②,再 ③④?

    条件:

      每次进行 ④更新M 后,都必须进行 ②更新距离,具体见说明2。其实,①初始化 可以看成是一次 ④更新M,所以在 ①初始化 之后,需要进行一次 ②更新距离。

    原因:

      有些人的做法是在循环外进行 ②更新距离,然后按 ③④② 的顺序进行 n-1 次循环。但其实最后一次循环的 ④更新M 之后进行的 ②更新距离 是没有意义的,因为此时最小生成树已经生成了。

      于是,我们利用这点,将②提到③④前面,从而将最后一次无用功的 ②更新距离 提前到第一次循环开头。利用第一次循环的 ②更新距离 对 ①初始化 进行更新距离,从而不用在循环外面特意更新距离。

 

3,数据结构

存图常用的链式前向星或邻接表或邻接矩阵,效果一样,效率不同。

dis[]:

  区分点是属于 V-U 还是 U:

    若 dis[i] > 0,则 i 属于 V-U;

    若 dis[i] == 0,则 i 属于 U。    

  记录距离:

    若 i 属于 U,则 dis[i] 并无意义

    若 i 属于 V-U,代表 点i 到 U 的最短直达边距离

p[]:

  表示 最小生成树

    若 p[i] == -1,则代表 i 是 u0 ;

    若 p[i] != -1,则代表 i 的父节点是 p[i] ;

 

4,代码及注意事项

Ⅰ,源点的选取是任意的,可以选取图中任意点作为源点。

 

Ⅱ,初始化时 s 是源点。经过后面的更新和选择后,s 就代表循环中每次加入 U 中的新的点。

 

Ⅲ,在 ②更新距离 中,p[] 是将 V-U 中的点到 U 的最短直达边全都记录下来。虽然,每次记录下来的时候,只有1条边是正确的。但在进行 ⑤迭代循环 后,p[] 记录的就都是最小生成树的边了。

 

Ⅳ,如果在某次 ③找最短边 中,如果找到的边的权值是 inf,说明此时 U 和 V-U 之间不存在连通的边,即此时的 U 和 V-U 是两个不同的连通分量,即该图不是连通图,即该图不存在最小生成树。

 

Ⅴ,如果是运行了多次的 Prim() 算法的话,p[] 的初始化只需要 p[u0] = -1 就可以了。因为最小生成树是必然连通的,所以即使 p[] 的值在前一次 Prim() 算法中被赋值了,也会在 ②更新距离 中重新赋值。

 

Ⅵ,p[] 表示的是小生成树,其根节点是源点。

  如果代码中是 p[to] = s; —— V-U 中的点指向 U 中的点,则树的方向是叶子节点指向根节点。

  如果代码中是 p[s] = to; —— U 中的点指向 V-U 中的点,则树的方向是根节点指向叶子节点。

 

Ⅶ,用优先队列时,无法根据 BFS 的结束条件判断图是否为连通图。所以,需要根据 dis == 0 的个数判断是否等于图的点数,从而判断是否能生成最小生成树。

 

Ⅷ,用邻接矩阵时要把矩阵初始化为 inf,表示所有点之间皆不可达,不能初始化为 0

 

1,链式前向星

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define inf 0x3f3f3f3f
#define N 110
struct Chain_forward_star // 链式前向星
{
    int last[N], tot;
    struct Edge {
        int pre, to, w;
    }e[N*N];
    void init()
    {
        tot = 0;
        memset(last, -1, sizeof(last));
    }
    void add(int from, int to, int w)
    {
        tot++;
        e[tot].to = to;
        e[tot].w = w;
        e[tot].pre = last[from];
        last[from] = tot;
    }
}star;
int dis[N], p[N];
int Prim(int n)
{
    // ① 初始化
    memset(dis, 0x3f, sizeof(dis));
    memset(p, -1, sizeof(p));
    int s = 1;
    dis[s] = 0; p[s] = -2;

    // ⑤ 迭代循环
    int m = n - 1;
    while (m--)
    {
        // ② 更新距离
        for (int i = star.last[s]; ~i; i = star.e[i].pre)
        {
            int to = star.e[i].to, w = star.e[i].w;
            if (dis[to] > 0 && w < dis[to])
            {
                dis[to] = w;
                //④ 更新 M:加边入TE
                p[to] = s;
            }
        }

        // ③ 找最短边:(s, p[s])
        int min = inf;
        for (int i = 1; i <= n; i++)
        {
            if (dis[i] && dis[i] < min)
            {
                min = dis[i];
                s = i;
            }
        }
        // ④ 更新 M:加点入 U
        dis[s] = 0;

        // ⑥ 判断:如果找不到最短边 说明不是连通图
        if (min == inf)
            return 0;
    }
    // ⑥ 判断:如果全部找到,说明可以生成最小生成树
    return 1;
}
int main(void)
{
    int n, m;    // m 是边数, n 是点数
    while (scanf("%d%d", &n, &m) != EOF)
    {
        star.init();
        for (int i = 0; i < m; i++)
        {
            int u, v, w; scanf("%d%d%d", &u, &v, &w);
            star.add(u, v, w);
            if (u != v)
                star.add(v, u, w);
        }
        Prim(n);

        // 输出最小生成树
        printf("Prim: \n");
        for (int i = 1; i <= n; i++)
        {
            if (p[i] == -2)
                continue;
            printf("%d %d\n", i, p[i]); // 链式前向星找一条边很麻烦
        }
    }
    return 0;
}
View Code

2,邻接表 + 优先队列

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<queue>
#include<string.h>
using namespace std;
#define inf 0x3f3f3f3f
#define N 110
struct Node
{
    int id;   // 当前顶点的id
    int dis;  // 当前顶点到 集合U 的最短距离 

    Node(int a, int b) :id(a), dis(b) {}  // 构造函数
    friend bool operator <(const Node &a, const Node &b) {
        return a.dis > b.dis;
    }
};
vector<Node>v[N]; // 邻接表哦
int dis[N], p[N];
int Prim(int n)
{
    // ① 初始化
    memset(dis, 0x3f, sizeof(dis));
    memset(p, -2, sizeof(p));
    priority_queue<Node>q;
    int s = 1;
    p[s] = -2; dis[s] = 0;
    q.push(Node(s, inf));

    // ⑤ 迭代循环
    while (q.size())
    {
        Node vertex = q.top();
        q.pop();
        // ④ 更新M
        dis[vertex.id] = 0;

        // ② 更新距离
        for (int i = 0; i < v[vertex.id].size(); i++)
        {
            Node next = { v[vertex.id][i].id, v[vertex.id][i].dis };
            if (dis[next.id] && next.dis < dis[next.id])
            {
                dis[next.id] = next.dis;
                q.push(next);
                // ④ 更新M
                p[next.id] = vertex.id;
            }
        }
    }

    // ⑥ 判断
    for (int i = 1; i <= n; i++)
        if (dis[i] != 0)
            return 0;
    return 1;
}
int main(void)
{
    int n, m;;
    while (scanf("%d%d", &n, &m) != EOF)
    {
        // 清空邻接表
        for (int i = 0; i <= n; i++)
            v[i].clear();
        // 存图
        for (int i = 0; i < m; i++)
        {
            int x, y, w; scanf("%d%d%d", &x, &y, &w);
            v[x].push_back(Node(y, w));
            if (x != y)
                v[y].push_back(Node(x, w));
        }
        Prim(n);

        // 输出最小生成树
        printf("Prim: \n");
        for (int i = 1; i <= n; i++)
        {
            if (p[i] == -2)
                continue;
            printf("%d %d\n", i, p[i]);
        }
    }
}
View Code

3,邻接矩阵

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define N 110
#define inf 0x3f3f3f3f
int a[N][N];
int dis[N], p[N];
void Prim(int n)
{
    memset(dis, 0x3f, sizeof(dis));
    memset(p, -1, sizeof(p));
    int s = 1;
    dis[s] = 0; p[s] = -2;

    int m = n - 1;
    while (m--)
    {
        for (int i = 1; i <= n; i++)
        {
            if (dis[i] && a[s][i] < dis[i])
            {
                dis[i] = a[s][i];
                p[i] = s;
            }
        }
        int min = inf;
        for (int i = 1; i <= n; i++)
        {
            if (dis[i] && dis[i] < min)
            {
                min = dis[i];
                s = i;
            }
        }
        dis[s] = 0;
    }

    printf("Prim: \n");
    for (int i = 1; i <= n; i++)
    {
        if (p[i] == -2)
            continue;
        printf("%d %d %d\n", i, p[i], a[i][p[i]]);
    }
}
int main(void)
{
    int n, m;
    while (scanf("%d%d", &n, &m) != EOF)
    {
        memset(a, 0x3f, sizeof(a));
        for (int i = 0; i < m; i++)
        {
            int u, v, w; scanf("%d%d%d", &u, &v, &w);
            a[u][v] = a[v][u] = w;
        }
        Prim(n);
    }
}
/*
输入1:
4 4
1 2 1
1 3 4
2 4 1
3 4 3
输出2:
2 1 1
3 4 3
4 2 1

输入2:
6 10
1 2 6
1 4 5
1 3 1
2 3 5
2 5 3
3 5 6
3 6 4
3 4 5
4 6 2
5 6 6
输出2:
2 3 5
3 1 1
4 6 2
5 2 3
6 3 4
*/
View Code

 

 

 

三,Kruskal

 1,算法思想

通过选择边来构建最小生成树。首先根据边权从小到大对边进行循环。然后根据当前边会不会构成环进行选择。

 

2,步骤

设 N = {V,E} 是一个连通网,M = {U,TE} 是 N 上的一棵最小生成树。

  ①  初始化:令 U = V,TE = {}

  ②  排序:将所有的边按照权值 由小到大 排序。

  ③  选择:将排序好的所有边依次加入生成树中,会形成环的跳过。(用并查集)

  ④  判断:判断最后会不会形成最小生成树。(即 最小生成树的边数 是否等于 该图的点数-1 ) (或者 最小生成树的点数 是否等于 该图的点数)

  注意:最小生成树的点数可以利用并查集计算。

  

3,代码

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<algorithm>
using namespace std;
#define N 110
int p[N], num[N];
struct edge // 边集
{
    int u, v; // 起点 终点
    int w;    // 权重
}e[5000];
int cmp(edge &a, edge &b) // 好像有取值符会更快
{
    return a.w < b.w;
}
int find(int x)
{
    if (p[x] != x)
        p[x] = find(p[x]);
    return p[x];
}
int join(int x, int y)
{
    x = find(x), y = find(y);
    if (x == y)
        return 0;
    p[x] = y;
    num[y] += num[x];
    return 1;
}
int main(void)
{
    int n, m; // 点数 边数
    while (scanf("%d%d", &n, &m), n)
    {
        for (int i = 1; i <= n; i++)
        {
            p[i] = i;
            num[i] = 1;
        }

        // ② 排序
        for (int i = 1; i <= m; i++)
            scanf("%d%d%d", &e[i].u, &e[i].v, &e[i].w);
        sort(e + 1, e + m + 1, cmp);    

        //③ 选择
        int sum = 0;   // 代价
        for (int i = 1; i <= m; i++)    
            if (join(e[i].u, e[i].v) == 1)
                sum += e[i].w;

        // ④ 判断
        if (num[find(1)] == n)   
            printf("%d\n", sum);
        else
            puts("?");
    }
    return 0;
}
/*
测试数据:
6 10
1 2 6
1 4 5
1 3 1
2 3 5
3 4 5
2 5 3
3 5 6
3 6 4
4 6 2
5 6 5
答案:
15
*/
/*
下面这种计算边数的方法,只适用于连通图,如果存在多个连通分量,它会计算到别的连通分量的边数

        int sum = 0, t = 0;   // 代价 边数
        for (int i = 1; i <= n; i++)    //② 选择
            if (join(e[i].u, e[i].v) == 1)
            {
                sum += e[i].w;
                t++;
            }
        if (t == m - 1)    // ③ 判断
            printf("%d\n", sum);
        else
            puts("?");
*/
View Code

 

 

四,prim 和 Kruskal 时间复杂度比较

  Prim:通过 点 去选择,时间复杂度O( n 的平方)( n 为顶点数),适合边多点少的稠密图

  Kruskal:通过 边 去选择,时间复杂度O( eloge )( e 为边数 ),适合边少点多的稀疏图

 

 

五,Dijkstra 与 Prim 比较

相同点:

  Dijkstra 和 Prim 都是从一个源点开始,不断在剩下的点中选取满足一定条件的点,加入自身,并以此将图的点集分成两个点集。

  其中,对于“一定条件”也都是寻求两个集合的某种距离关系,并将距离最短的点加入源点归属的集合中。

差别点:

  Dijkstra 的结果是 —— 最短路径树;

  Prim      的结果是 —— 最小生成树。

  Dijkstra 选择的条件是 —— V-S 中的点到 s 的所有借助 S 最短路径中的最短路径中连通 V-S 和 S 的边;

  Prim      选择的条件是 —— V-U 中的点到 U 的所有最短直达边中边长最短的边。

 

 

========= ======== ======= ======= ====== ===== ==== === == =

所以我时常害怕,愿中国青年都摆脱冷气,只是向上走,不必听自暴自弃者流的话。能做事的做事,能发声的发声。有一分热,发一分光,就令萤火一般,也可以在黑暗里发一点光,不必等候炬火。 

此后如竟没有炬火:我便是唯一的光。倘若有了炬火,出了太阳,我们自然心悦诚服的消失,不但毫无不平,而且还要随喜赞美这炬火或太阳;因为他照了人类,连我都在内。

                                                      ——《热风·随感录四十一》 鲁迅

 

posted @ 2020-05-31 18:02  叫我妖道  阅读(325)  评论(0编辑  收藏  举报
~~加载中~~