最小生成树

算法思想 来自:https://www.bilibili.com/video/BV1Ut411976J?from=search&seid=6406725088142163166

一,MST 性质 (Minimum Spaning Tree)

应该知道 最小生成树 并不唯一。 (•‾̑⌣‾̑•)✧˖°

定义:设 N = (V,E )是一个连通网,U 是顶点集V的一个非空子集。

若(u , v )是一条具有最小权值(代价)的边,其中u∈U, v∈V - U,则必存在一棵包含边(u,v)的最小生成树。 

这是构造最小生成树的基本性质。

二,Prim 算法

① 算法思想

利用 MST 性质,将图中的点分为两个集合:

U:已落在生成树上的顶点集、

U-V:未落在生成树上的顶点集

接下来就在所有 连通U中的顶点 和 V-U中的顶点 的边中选取 权值最小的边

(解释:U 是未完成的最小生成树,要想所有点连接,则最小生成树必存在边 (U 中一点,V-U 中一点),所以他们中最小的一定在树中)

② 步骤

设 N = (V,E )是一个连通网,TE 是 N 上最小生成树中 边的集合

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

2,选择:在所有 u 属于 U,v 属于 V-U 的边(u,v)中,找一条代价最小的边(u0,v0)

3,更新:将(u0,v0)并入集合 TE 中,同时 v0 并入 U

4,重复 2,3 操作,直至 U=V,则 T = { V,TE } 为 N 的最小生成树

③ 例题

 

步骤:其中 星号:表示该边已经加入最生,打勾:表示在本次比较中找到的 要加入最生的

④ 代码

用链式前向星

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define inf 0x3f3f3f3f
#define N 10010
int last[N], tot;
struct edge
{
    int to;
    int w;
    int pre;
}e[N];
void init()
{
    memset(last, -1, sizeof(last));
    tot = 0;
}
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;
}
int vis[N];  //(vis[i]==1)为  集合 U  ,(vis[i]==0)为 集合 V-U
int dis[N];  // 记录 U 集合中的点 到 V-U 的所有点的 最短距离.  如 dis[2]=8 代表所有在 U 中的点 到 点2 的最短距离是 8,这里边的另外一点为 p[2]
int p[N];
int Prim(int n)
{
    // 1,初始化 :令 U={ u0 },TE={ },用 p[]记录路径, 这里没用
    for (int i = 1; i <= n; i++) 
    {
        vis[i] = 0;
        dis[i] = inf;   // 既然是要记录最短的距离,则初始化 为 inf
        p[i] = 0;
    }
    int s = 1;      //  开始的结点 u0
    vis[s] = 1;  // U={ u0 }
    p[s] = -1;    // 标记 根

    // 4,重复 2,3 操作,直至 U=V,则 T = { V,TE } 为 N 的最小生成树
    int sum = 0;     // 记录总代价,可以看作是 TE
    int m = n - 1;  // 最小生成树的边数
    while (m--)    // 最小生成树的边数为原来图的点数减一,即  U == V,
    {
        for (int i = last[s]; ~i; i = e[i].pre)  // 2,选择:在所有 u 属于 U,v 属于 V-U 的边(u,v)中,找一条代价最小的边(u0,v0)
        {
            int to = e[i].to;
            int w = e[i].w;
            if (!vis[to] && dis[to] > w)   // 更新 U 集合中的点 到 V-U 的所有点的 最短距离 如果这条路走到尽头, 下面还是会选出 最短的那条路径,直接跳过去
            {
                dis[to] = w;
                p[to] = s;    // 记录路径
            }
        }   
        int min = inf;
        for (int j = 1; j <= n; j++)   // 找到 在所有 u 属于 U,v 属于 V-U 的边(u,v)中,代价最小的边( p[j] ,j)
        {
            if (!vis[j] && dis[j] < min)
            {
                min = dis[j];
                s = j;
            }
        }
        if (min == inf)   // 如果没有这条边 说明不是 连通图
            return 0;

        //   3,更新:将(u0,v0)并入集合 TE 中,同时 v0 并入 U;
        vis[s] = 1;     // v0 并入 U
        sum += min;    // (u0,v0)并入集合 TE 中
    }
    return sum;
}

int main(void)
{
    int n, m;    // m 是边数, n 是点数
    while (scanf("%d%d", &m, &n) == 2 && m)
    {
        init();
        for (int i = 0; i < m; i++)
        {
            int u, v, w; scanf("%d%d%d", &u, &v, &w);
            add(u, v, w);
            if (u != v)
                add(v, u, w);
        }
        int sum = Prim(n);
        if (sum == 0)
            puts("?");
        else
            printf("%d
", sum);
    }
    return 0;
}
/*
测试数据;长方形
4 4
1 2 1
1 3 4
2 4 1
3 4 4
*/
View Code

 用优先队列

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<queue>
#include<string.h>
using namespace std;
#define N 105
struct Edge
{
    int pre, to, w;
}e[N];
int last[N], vis[N], p[N];
int tot = 0, n, m;    // m 是边数, n 是点数
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;
}
void init()
{
    memset(last, -1, sizeof(last));
    memset(vis, 0, sizeof(vis));
    memset(p, 0, sizeof(p));
    tot = 0;
}
struct Node
{
    int to;
    int cost;
    int from;  /* 因为 找 最小的边 在队列里面完成了,只有出队列后,我们才知道是哪条边,不过那时候我们也不知道这条边的 起点 是哪一个
                  所以为了可以记录路径,在这里增加一个变量,记录 边的起点  */
    Node() {}
    Node(int tt, int cc,int qq) :to(tt), cost(cc), from(qq){}
    friend bool operator < (const Node &a, const Node &b)
    {
        return a.cost > b.cost;
    }
};
void prim()
{
    int cnt = 0, ans = 0;

    priority_queue<Node>q;
    q.push(Node(1, 0, -1));
    p[1] = -1;   // 标记 根结点
    while (q.size())  // BFS
    {
        Node vertex = q.top();
        q.pop();

        if (vis[vertex.to] == 0)
        {
            vis[vertex.to] = 1;
            ans += vertex.cost;
            p[vertex.to] = vertex.from;
            cnt++;
            for (int i = last[vertex.to]; ~i; i = e[i].pre)   // 起点在 点集 T 中,终点在 E-T 中 
            {
                Node next = { e[i].to,e[i].w ,vertex.to };
                if (vis[next.to] == 0)
                {
                    q.push(next);
                }
            }
        }
    }
    if (cnt != n)
        puts("?");
    else
        printf("%d
", ans);
}
int main(void)
{
    while (scanf("%d%d", &m, &n) == 2 && m)
    {
        init();
        for (int i = 0; i < m; i++)
        {
            int u, v, w; scanf("%d%d%d", &u, &v, &w);
            add(u, v, w);
            if (u != v)
                add(v, u, w);
        }
        prim();

    }
    return 0;
}
View Code

用 邻接矩阵

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define inf 0x3f3f3f3f
#define N 105
int a[N][N], vis[N], dis[N], p[N];
int n, m;  // m 是边数, n 是点数
void init() 
{
    memset(vis, 0, sizeof(vis));
    memset(dis, 0x3f, sizeof(dis));
    memset(a, 0x3f, sizeof(a));
    memset(p, 0, sizeof(p));
}
int prim()
{
    int s = 1;
    dis[1] = 0, vis[s] = 1, p[s] = -1;
    int sum = 0;
    for (int i = 1; i < n; i++)  // 边数 为点数减一
    {
        for (int j = 1; j <= n; j++)  // 更新距离,标记起点
        {
            if (vis[j] == 0 && dis[j] > a[s][j])
            {
                dis[j] = a[s][j];
                p[j] = s;
            }
        }
        int min = inf;
        for (int j = 1; j <= n; j++)  // 找到 最小的那条边
        {
            if (vis[j] == 0 && dis[j] < min)
            {
                min = dis[j];
                s = j;
            }
        }
        if (min == inf)
            return 0;
        vis[s] = 1;
        sum += min;
    }
    return sum;
}
int main(void)
{
    while (scanf("%d%d", &m, &n) == 2 && m)
    {
        init();
        for (int i = 1; i <= m; i++)
        {
            int u, v, w; scanf("%d%d%d", &u, &v, &w);
            a[u][v] = a[v][u] = w;
        }
        int sum = prim();
        if (sum == 0)
            puts("?");
        else
            printf("%d
", sum);
    }
    system("pause");
    return 0;
}
View Code

 在这里提醒一句:注意起点的判断,虽然这边都可以省略这一步。

 三,Kruskal

 ①  算法思想

设 连通网 N = {V,E }, 令 最小生成树 初始状态为 只有 n 个顶点 而 没有边 的非连通图 T = {V,{ }},

每个顶点自成一个连通分量 ,

然后在 E 中,选取 代价最小 的边,若 该边的顶点 落在 T 中的不同的连通分量上(即不能形成 环)

则将该边加入到  T 中,否则,舍去该边,选择下一条边。

依次类推,直至 T 中的所有顶点 都在  同一个连通分量上。

 ② 步骤

顶点数可以直接用 cnt 数,所以可以不用初始化处理。

1,排序  将所有的边 按照权值 由小到大 排序

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

3,判断  判断最后会不会形成 最小生成树(即 最小生成树 的边数 是否是 该图 的点数-1 )

③ 例题

④ 代码

#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 from, to;
    int w;
}e[5000];
int cmp(edge &a, edge &b)  // 好像有取值 会更快
{
    return a.w < b.w;
}
int find(int x)
{
    int a = x;
    while (x != p[x])
        x = p[x];
    while (a != x)
    {
        int t = p[a];
        p[a] = x;
        a = t;
    }
    return 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;    // 边数
    int m;    // 点数
    while (scanf("%d%d", &n, &m), n)
    {
        for (int i = 1; i <= n; i++)
            scanf("%d%d%d", &e[i].from, &e[i].to, &e[i].w);
        for (int i = 1; i <= m; i++)
        {
            p[i] = i;
            num[i] = 1;
        }
        sort(e + 1, e + n + 1, cmp);    // 1,排序  将所有的边 按照权值 由小到大 排序

        int sum = 0;   // 算 最小生成树 的代价
        int t = 0;     // 数 最小生成树 的边数
        for (int i = 1; i <= n; i++)    // 2,选择  将排序好的所有边依次加入生成树中,会形成 环 的跳过(用并查集判断)
        {
            if (join(e[i].from, e[i].to) == 1)  // if 不加大括号,只读取一句,即使你写同一行,后面的语句也不属于 if
            {
                sum += e[i].w; 
                t++;
            }
        }
        if (num[find(1)] == m)   // 3,判断 生成树的边数 是否 等于 点数
        //if (t == m - 1)   
            printf("%d
", sum);
        else
            puts("?");
    }

    system("pause");
    return 0;
}
/*
测试数据:
10 6
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
*/
View Code

 四,prim 和 Kruskal 比较  (҂‾ ▵‾)︻デ═一 - - -  - -  -

Prim :   通过 点 去选择   O( n 的平方 )(n  为顶点数) 适合稠密图

Kruskal:通过 边 去选择   O( eloge )  ( e 为边数 )    适合稀疏图

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

  所以我时常害怕,愿中国青年都摆脱冷气,只是向上走,不必听自暴自弃者流的话。

能做事的做事,能发声的发声。有一分热,发一分光,就令萤火一般,也可以在黑暗里发一点光,不必等候炬火。 

  此后如竟没有炬火:我便是唯一的光。倘若有了炬火,出了太阳,我们自然心悦诚服的消失,不但毫无不平,

而且还要随喜赞美这炬火或太阳;因为他照了人类,连我都在内。

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

原文地址:https://www.cnblogs.com/asdfknjhu/p/13020254.html