图论--最小生成树 kruskal prim

差不多就要CCF考试了,弱鸡我还是挺慌的(好想上400分保研~),最近开始来逐渐巩固自己所储备的知识。

CCF的第四题往往是图论,今天就来回顾一下经典的最小生成树算法:Kruskal && Prim。

Kruskal:

  • 遍历边,边的累加生成树--适用于边较稀疏的情况。
  • 边用结构体数组存储,属性有:两个端点,权值。

将所有边由小到大排序, 以最短的边为算法的起始。依次遍历剩下的边,结合并查集判断是否会形成强连通,不行成强连通的剩下边加入到树中。截止条件,边数 == 点数 - 1。

 

个人容易出错的地方:

1)并查集的Unite函数中,两个点的根节点应要通过FindRoot函数来查找,而不是直接用Root【】数组(数组存储的是父节点,单并不一定是祖宗节点),连通性是通过最祖宗的节点是否相连来判断的。

2)遇到多组测试用例时,要记得将重复使用的量重新初始化。

/*kruskal*/
#include <iostream>
using namespace std;
const int maxe = 1e4, maxv = 1e3;
struct Edge
{
    int u, v, w;
    int index;
}edge[maxe];
int root[maxv], e, v;
bool vis[maxe];
//初始的父节点就是自己
inline void ini(){
    for(int i=1; i<=v; ++i)
        root[i] = i;
    return;
}
//查找时,压缩路径,即父找爷,我找爷
inline int find_root(int q){
    while(q != root[q]){
        root[q] = root[root[q]];
        q = root[q];
    }return q;
}
//由于已经进行路径压缩,无需再考虑生成畸形树的情况
inline void unite(int a, int b){
    root[find_root(a)] = find_root(b);
}
inline void comp(const Edge& a, const Edge& b){
    return a.w < b.w;
}
void kruskal()
{
    sort(edge, edge + e, comp);

    for(int i=0; i<e; ++i){
        if(find_root(edge[i].u) != find_root(edge[i].v))
        {
            unite(edge[i].u, edge[i].v);
            sumw += edge[i].w;
            cnt += 1;
            if(cnt == (v - 1)) break;
        }
    }
    cout << sumw << endl;
    return;
}
int main()
{
    cin >> v >> e;
    ini();
    for(int i=0; i<e; ++i){
        cin >> edge[i].u >> edge[i].v >> edge[i].w;
        edge[i].index = i;
     }
     kruskal();
    return 0;
}

 

 

Prim:

  • 遍历点, 点的集合生成树--适用于边较密集的情况
  • 由于边较密集,用邻接矩阵table【】【】来存边,存边权时注意边的双向性

用minCnt【】数组存所有点到树的距离,个人习惯以1为初始的树根st(哪个点都无所谓,因为所有点都要取到),即初始的minCnt为所有点到st点的距离table[st][index]。

vis【】标记点进入树的情况。

  

先初始化minCnt,标记我们的st点(初始的树),因为已经找到一个点了,所以之后是一个总点数 - 1次的循环,即 n - 1 次, 找到剩下的 n - 1个点。在这个大循环中,每次遍历剩下的点,找到到树距离最短的(用minLen存最短距离,minNode存最小的点的编号,桶排思想),标记入树,同时累加树的总权值。接着,由于树的扩张,我们需要更新剩下点到树的最小距离minCnt, 又是一个循环。总体上就是,一个大循环,内嵌两个小循环。

 

个人容易出错的地方:

  1)这里各点到树的距离会随着树的生成不断变化,所以没有必要一开始sort排序,每次查找时用循环桶排就好。

  2)最初始的点就是初始的树,所以到树的距离为0,minCnt【st】 = 0。

 

这里贴一道CCF的第四题(这是比较早的CCF题,所以不难,竟然只是个模板题~),顺便扇自己一巴掌--我做题时,一看以为是求单源最短路径,上来就敲spfa,结果浪费了时间。真正考试时要稳一点,在纸上大致模拟一遍思路看是否符合题意再开始写。

//minimum spanning tree 
//边较多,用prim
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
using namespace std;
#define Inf 0x3f3f3f3f
#define ll long long
const int maxp = 1000 + 2, st = 1;
int table[maxp][maxp], minCnt[maxp]; // 到树的距离
bool vis[maxp];
int n, m, a, b, c, cntp;
ll sumw;

void Prim()
{
    for(int i=2;i<=n;++i){
        minCnt[i] = table[st][i];
    }
    minCnt[st] = 0;
    vis[st] = true;    //第一个节点入树

    for(int t=1; t < n; ++t){//找到剩下的n-1个节点
        int minNode = 0, minLen = Inf;
        for(int j = 1; j <= n; ++j){
            if(!vis[j] && minCnt[j] < minLen){
                minLen = minCnt[j];
                minNode = j;
            }
            else continue;
        }
        sumw += minLen; //生成的一支的权值
        vis[minNode] = true;
        for(int j = 1; j<= n; ++j){
            if(!vis[j]){//更新到树的距离
                minCnt[j] = min(minCnt[j], table[minNode][j]);
            }
        }
    }
}
inline int read(){
    char ch = getchar();
    int ans = 0;
    while(ch < '0' || ch > '9') ch = getchar();
    while(ch >= '0' && ch <= '9'){
        ans = (ans << 3) + (ans << 1) + ch - '0';
        ch = getchar();
    }return ans;
}
int main()
{
    n = read(), m = read();
    memset(table, Inf, sizeof(table));
    while(m--){
        a = read(), b = read(), c = read();
        table[a][b] = table[b][a] = min(table[a][b], c);
    }
    Prim();
    printf("%lld\n", sumw);
    return 0;
}

Prim优化

和dijkstra类似,Prim也可以堆优化来提高效率,用链式向前星存图,用priority_queue来代替第一个小循环的桶排查找。由于使用链式向前星存图,所以在更新剩下未进入树的点到树的距离的操作变成了将当前入树的点的临边连接的点入队,这个很容易理解,当前点入树影响的必然是与之相连的点,相连的点到树的新距离(不一定是最小,但不用担心,priority_queue会自动将其排序好,我们只需要关注有新的距离出现即可)加入到priority_queue中,这样减少了许多不必要的遍历更新。仍然以上边的CCF的为例。有没有湖大的朋友啊,点个赞呀~

//边较密集邻接矩阵存图,Prim minimum spannning tree
//注意是双向图,存双向边
#include <cstdio>
#include <iostream>
#include <queue>
#include <cstring>
#include <algorithm>
using namespace std;
#define ll long long
#define Inf 0x3f3f3f3f
const int maxv = 1e3 + 5, maxe = 1e5 + 5, st = 1;
struct Node{
    int index;
    ll dis;        //到树的距离
    Node(int a=0, ll b=0):index(a), dis(b){}
    bool operator < (const Node& b) const{
        return this->dis > b.dis;
    }
};
struct Edge{
    int to, w;
    int next;
}e[maxe * 2];
int table[maxv][maxv], head[maxv];
int n, m, a, b, w, cnte;
ll dis[maxv], ans;
bool vis[maxv];    //标记是否入树
//堆优化版本的Prim
//用priority_queue中的优先弹出代替原来的桶排
//同时链式向前星优化邻接矩阵,这样更新节点的时候就只会遍历到相邻的点,省去了不必要的遍历
inline void Prim_que(){
    priority_queue<Node> pq;
    pq.push(Node(st, 0));
    int cntv = 0;
    while(cntv < n && pq.size()){
        Node cur = pq.top();
        pq.pop();
        if(vis[cur.index]) continue;
        vis[cur.index] = true;    
        cntv += 1, ans += cur.dis;
        for(int i = head[cur.index]; i; i = e[i].next){
            if(vis[e[i].to]) continue;
            pq.push(Node(e[i].to, e[i].w));
        }
    }
    return;
}
inline void add_edge(const int& from, const int& to, const int& w){
    ++cnte;
    e[cnte].to = to, e[cnte].w = w;
    e[cnte].next = head[from];
    head[from] = cnte;
    return;
}
inline int read(){
    char ch = getchar();
    int ans = 0;
    while(ch < '0' || ch > '9') ch = getchar();
    while(ch >= '0' && ch <= '9'){
        ans = (ans << 3) + (ans << 1) + ch - '0';
        ch = getchar();
    }
    return ans;
}
int main()
{
    n = read(), m = read();
    memset(table, Inf, sizeof(table));
    while(m--){
        a = read(), b = read(), w = read();
        add_edge(a, b, w);
        add_edge(b, a, w);
    }
    Prim_que();
    printf("%lld\n", ans);
    return 0;
}

 

转载于:https://www.cnblogs.com/GorgeousBankarian/p/11144382.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值