【基础算法笔记】Prim,Kruskal,匈牙利算法

 在acwing上学习算法的一点思考与总结


匈牙利算法

这个算法是个很有趣的算法,也很好用。引用acwing一句高赞评论:匈牙利算法准则:待字闺中,据为己有;名花有主,求他放手。还有来自人生导师y总的总结【doge】:一定要坚持不懈,就算前面有一个困难,也不要直接退缩,直接退缩是完全没有希望的。只有当尝试各种各样的方法都达不到后,我们才考虑放弃。

简单来说就是如果两个点(a, b)同时连到一个点 c 时,如果先前占据的点(原本是a占据b)有备选项的话,他就会让位占领另一个点(假设是d),让点b占据点c。

代码实现上,最巧妙的一部分就是st数组。每一次对一个点进行匹配都重新将st数组刷新为false。之所以这样就是为了好让先前的占据者(海王)让步,让他去重新搜索有没有其他可选择的点。所以这个st的true和false只是表示一个暂时的状态,并不代表永久的占据。

#include<iostream>
#include<algorithm>
#include<cstring>

using namespace std;
const int N =510, M = 100010;

int h[M], e[M], ne[M], idx;
int n1,n2, m;
int match[N];
bool st[N];

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

bool find(int x)
{
    for(int i = h[x]; i != -1; i = ne[i])
    {
        int j = e[i];
        if(!st[j])
        {
            st[j] = true;
            // 若没人占据或先前占据的人有备选项
            if( !match[j] || find(match[j]) )
            {
                match[j] = x;
                return true;
            }
        }
    }
    return false;
}

int main()
{
    memset(h, -1, sizeof h);
    cin>>n1>>n2>>m;
    while(m--)
    {
        int a,b;
        cin>>a>>b;
        add(a,b);
    }
    int res =0;
    for(int i = 1; i <= n1; i ++)
    {
        memset(st, false, sizeof st); //刷新状态数组
        if(find(i)) res ++; 
    }
    
    cout<<res;
}

Prim算法求最小生成树

实现思路:在prim中先选择一个点(比如1)作为最小生成树的开始,然后从这个点出发去找与它相连的边(假设有到2,3,4的边),在这些边中,找出权重最小的点(假设是3),和开始的点形成一个子树,这个子树就用当前的点3来代表,然后继续去找与这个子树(3)有连接的最小权重边,重复上述过程,直到所有点都被找完。

对比:代码的实现形式和Dijkstra很像,二者的本质都是用贪心的算法实现。区别就在于一个是用于求起点的点n的距离,一个是用于求最小生成子树到点的距离。在代码上的核心区别: Dijkstra  dis[ j ] =dis[ t ] + g[ t ][ j ];Prim dis[ j ] = g[ t ][ j ]; 

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;
const int N =510, M = 100010;
int g[N][N];
int dis[N];
int st[N];
int n, m, res;

void prim()
{
    memset(dis, 0x3f, sizeof dis);
    
    dis[1] = 0;
    
    for(int i = 0; i < n; i ++)
    {
        int t = -1; 
        for(int j = 1; j <= n; j ++)
        {
            if(!st[j] && (t == -1 || dis[j] < dis[t] ))
                t = j;
        }
        
        if(dis[t] >= 0x3f3f) //如果这个点没有被任何一个点连通,也就意味着不可能有最小生成树
        {
            cout<<"impossible";
            return;
        }
        
        st[t] = 1; //标记:已被加到最小生成树中
        res += dis[t]; //更新答案
        for(int k = 1; k <= n; k ++)
        {
            if(dis[k] > g[t][k] && !st[k])
            {
                dis[k] = g[t][k];
            }
        }
    }   

    cout<<res;
}

int main()
{
    
    memset(g, 0x3f, sizeof g);
    cin>>n>>m;
    while(m--)
    {
        int a,b,c;
        cin>>a>>b>>c;
        g[b][a] =g[a][b] = min(g[a][b],c); //处理重边,取最小值
    }
    
    prim();

}

Kruskal算法

当点数和边数相当时,就要用到这个算法,事实上也可以用Prim的堆优化算法来实现,但Kruskal算法更加简洁,清晰。所以就用Kruskal来处理。

思路:Kruskal实现的思路非常巧妙,先将所有边的权重排个序,然后从最短的边开始连接。这里就涉及到很关键的一步,就是判断连接的点之间是否成环了(这样就不符合最小生成树的定义)。我们这时就用到并查集里的函数,即如果相连的两个点有一个共同的祖宗节点,那就代表这两个点已经在一颗最小生成树里了,不能将这两条线连接起来。最后的判断就是连接边数cnt等于点数n-1

#include<iostream>
#include<algorithm>
#include<cstring>

using namespace std;
const int N = 100010, M = 200010;
struct E  //只用逐一枚举每条边,所以直接用结构体存边信息
{
    int a;
    int b;
    int c;
    bool operator < (const E& rhs) //通过边长进行排序
    {
        return this->c < rhs.c;
    }
}edge[N*2];

int p[N];
int n,m, cnt, res;

int find(int u)
{
    if(p[u] != u) p[u] = find(p[u]);
    
    return p[u];
}

void kruskal()
{
    for(int i = 1; i <= m; i ++)
    {
        int pa = find(edge[i].a); //查找祖宗节点
        int pb = find(edge[i].b);
        if(pa != pb )
        {
            res += edge[i].c;
            p[pa] = pb; //合并a,b
            cnt ++;
        }
    }
}

int main()
{
    cin>>n>>m;
    
    for(int i = 1; i <= n; i ++ ) p[i] = i;
    for(int i = 1; i <= m; i ++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        edge[i] = {a, b, c };
    }
    sort(edge + 1, edge + m + 1 );
    kruskal();
    
    if(cnt != n-1)
    { 
        cout<<"impossible";
    
        return 0;
    }
    cout<<res;
    
}

  • 24
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值