知识点四 图论:Prim算法 Kruskal算法 (最小生成树)

解决的问题

这两个算法都是用来在加权连通图里搜索最小生成树(MST)。

也就是一个图有很多边,这些边有权值,然后我们要从这些边中找到这样一个边的集合,使得全部的顶点都是连通的,并且边的权值之和最小。

适用情景:
Prim算法适用于稠密图 (边比较多的时候)
因为是每次加一个顶点进去。

Kruskal适用于稀疏图(边比较少的时候)
因为是每次查找最短的边。

Prim(普里姆算法)

算法描述:
1).输入:一个加权连通图,其中顶点集合为V,边集合为E;

2).初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {},为空;

3).重复下列操作,直到Vnew = V:
a.在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
b.将v加入集合Vnew中,将<u, v>边加入集合Enew中;

4).输出:使用集合Vnew和Enew来描述所得到的最小生成树。

也就是说,对于一张加权连通图,我们先用邻接矩阵读图,然后随意取一个点开始,取与这个点相连的边中,权值最短的那一条。
取完这条边,我们肯定会有两个点了。
然后看这两个点,取与它们相连的边中最短的那一条。
取完后肯定会有三个点(不可能成环),再取与这三个点连接的边中,最短的那一条。
然后一直重复上面的操作,直到我们把所有的点都连接起来了。

百度百科上有个例子:
https://baike.baidu.com/item/Prim/10242166?fr=aladdin

时间复杂度

不同的实现方法,时间复杂度不同。

最小边、权的数据结构时间复杂度(总计)
邻接矩阵、搜索O(V^2)
二叉堆(后文伪代码中使用的数据结构)、邻接表O((V + E) log(V)) = O(E log(V))
斐波那契堆、邻接表O(E + V log(V))

代码

邻接表 O( ElogV )

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 1e5+5;
const int INF = 0x3f3f3f3f;
int V, E;
int ans;
int mincost[MAXN];//
int used[MAXN];//这个点是否取过
struct edge
{
	int to,cost;
	//构造函数后面的冒号起分割作用,是类给成员变量赋值的方法,初始化列表,更适用于成员变量的常量const型。
	//在这里这么写是为了后面调用方便而已, 即G[u].push_back(edge(v, w));
	edge (){}
	edge (int to,int cost):to(to),cost(cost){}
};
vector <edge> G[MAXN];//存放从i出发的边
typedef pair<int,int> P;//first存放距离,second存放节点
priority_queue<P, vector<P>, greater<P> > que;//用优先队列,更快的取到下一个点。
void prim()
{
    //初始化
    memset(used, 0, sizeof(used));
    fill(mincost, mincost + V, INF);
    ans = 0;
    que.push(P(0, 0));

    while (!que.empty())
    {
        //取最短边
        P p = que.top();
        que.pop();
        int d = p.first;//距离
        int v = p.second;//节点

        if (used[v])
            continue;

        used[v] = 1;
        ans += d;
        //以最短边的节点为基础,更新最短距离
        for (int i = 0; i < G[v].size(); i += 1)
        {
            edge e = G [v][i];
            if (!used[i] && (mincost[e.to] > e.cost))
            {
                mincost[e.to] = e.cost;
                que.push(P(mincost[e.to], e.to));
            }
        }
    }
    printf("%d\n",ans);//MST的总权值
}
int main()
{
    scanf("%d %d", &V, &E);
    for(int i = 1; i <= E; i++)
	{
	    int u, v, w;
	    scanf("%d %d %d", &u, &v, &w);
	    G[u].push_back(edge(v, w));
	}
    prim();
    return 0;
}

邻接矩阵O(V^2)

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int maxn = 1e3 + 5;

int n, m;//n是点的个数,m是边的条数
int edge[maxn][maxn];//邻接矩阵,无向图,存的是边的权值
int lowcost[maxn];//lowcost[i]表示当前点到i点的最短的距离,在nearvex[]更新的时候也得随之更新
int nearvex[maxn];//nearvex[i]表示与i点连接的点中,距离最短的那个点

void prim(int u0)//u0为起点
{
    int i, j;
    int sumweight = 0;//最小生成树所有边的权值之和。
    for(i = 1; i <= n; i++)
    {
        lowcost[i] = edge[u0][i];//将u0到其他点的权值都存到lowcost数组中
        nearvex[i] = u0;//nearvex[1-n]从都是起始点u0
    }

    nearvex[u0] = -1;//nearvex[起始点] 等于 -1,因为起始点已经取过了。

    for(i = 1; i < n; i++)//执行 n - 1 次
    {
        int min = INF;//最小值赋值为INF
        int v = -1;//v点赋值为 -1
        for(j = 1; j <= n; j++)//遍历所有的点
        {
            if(nearvex[j] != -1 && lowcost[j] < min)//找下一个点,如果这个点没有被取过,并且lowcost[这个点]小于min
            {
                v = j;//更新v,表示下一个点
                min = lowcost[j];//最小值更新为lowcost[这个点]
            }
        }
        //循环走完,我们找到了下一个点以及这个点与上一个点的最短距离。

        if(v != -1)//如果上面的那个循环找到了下一个点
        {
            //cout<<nearvex[v]<<" "<<v<<" "<<lowcost[v]<<endl;//这里输出了 上一个点  这个点  上一个点到这个点的距离
            nearvex[v] = -1;//这个v点已经取过了,所以nearvex[v]赋值成-1。
            sumweight += lowcost[v];//加上这条边
            for(j = 1; j <= n; j++)//遍历所有的点
            {
                if(nearvex[j] != -1 && edge[v][j] < lowcost[j])//如果这个点没有被取过,并且v点到这个点的距离小于lowcost[j],那么更新
                {
                    lowcost[j] = edge[v][j];//距离修改为v到j的距离
                    nearvex[j] = v;//v点变为到j点距离最短的点
                }
            }
        }
    }
    //cout<<"weight of mst is "<<sumweight<<endl;//输出了最小生成树的权值之和
}

int main()
{
    int u, v, w;//u为起点,v为终点,w为权值
    cin>>n>>m;//点的个数,边的条数
    memset(edge, 0, sizeof(edge));//初始化所有的距离等于0

    for(int i = 1; i <= m; i++)//读所有的边
    {
        cin>>u>>v>>w;
        edge[u][v] = edge[v][u] = w;//邻接矩阵存图,存的是无向图
    }

    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= n; j++)
        {
            if(i == j)//自己到自己等于0
                edge[i][j] = 0;
            else if(edge[i][j] == 0)//如果两个点之间的距离为初始值(0),那么说明题目没有给这两个点之间的边,也就是说这两个点不相连
                edge[i][j] = INF;//那我们设置它们之间的距离为INF
        }
    }
    prim(1);//随便取一个顶点开始就可以。
    return 0;
}

Kruskal 算法

Kruskal就是在不形成环的情况下,尽可能优先取短的边。
Kruskal算法的思路和Prim算法的思路基本相同。

Kruskal算法按照边的权值的顺序,从小到大查看一遍,如果不产生圈(重边等也算在内),就把当前这条边加入到生成树中。
这个算法要判断是否产生圈,也就是不能形成环。
对于两个顶点u,v,如果他们在一个联通分量里面,我们连接这两个顶点,那么必定成环,反之则不会。所以我们判断两个点在不在同一个连通分量里面即可,这里用并查集就能快速判断。

算法复杂度是O(ElogV)

代码

O(ElogV)

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int max_E = 1e5+5;
const int max_V = 1e5+5;
int V, E;
int arr[max_V];//并查集
struct edge
{
    int u, v, cost;
}es[max_E];//记录边的信息
bool cmp(const edge& e1, const edge& e2)
{
    return e1.cost < e2.cost;//按照边的权值从小到大排序
}
void init_union_find(int V)//初始化并查集
{
    for(int i = 0; i < V; i++)
        arr[i] = i;
}
int find_parent(int a)//查找某个节点的祖先
{
    while(a != arr[a])
        a = arr[a];
    return a;
}
int same(int a, int b)//判断两个节点是否在同一个强连通分量里
{
    int x = find_parent(a);
    int y = find_parent(b);
    return x == y ? 1 : 0;
}
void unite(int a,int b)//合并两个连通分量
{
    int a_parent = find_parent(a);
    int b_parent = find_parent(b);
    if(a_parent != b_parent)
        arr[a_parent] = b_parent;
}
int kruskal()
{
    sort(es, es + E, cmp);//按照edge.cost的顺序从小到大排序
    init_union_find(V);//并查集的初始化
    int res = 0;
    for(int i = 0; i < E; i++)//遍历所有的边
    {
        edge e = es[i];//对于当前边
        if(!same(e.u, e.v))//如果该边的两个节点不在同一个联通分量里面(那么连起来肯定不会成环)
        {
            unite(e.u, e.v);//那么就取这条边(把这两个节点连起来)
            res += e.cost;
        }
    }
    return res;
}
int main()
{
    scanf("%d %d", &V, &E);
    for(int i = 0; i < E; i++)
    {
        scanf("%d %d %d", &es[i].u, &es[i].v, &es[i].cost);
    }
    printf("%d\n", kruskal());
    return 0;
}

参考来源

Prim
百度百科
https://baike.baidu.com/item/Prim/10242166?fr=aladdin
参考来源
https://www.cnblogs.com/ggzhangxiaochao/p/9070873.html

Kruscal
百度百科
https://baike.baidu.com/item/最小生成树/5223845

挑战程序设计竞赛 P107

博客
https://blog.csdn.net/qq_41754350/article/details/81460643

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值