并查集、Kruskal算法与Prim算法

并查集:
【用途】并查集是用来整理图的连通分量的,并查集过程完成后,可以确定两点是否连通,也可得知有多少个连通分量。推广到集合论里就是已知若干对元素,a在某集合里能推知b在集合里,用并查集确定各个元素归属于哪个集合,也可以顺便确定集合的数量。
【思想】并查集的思想其实很简单,每次合并都是尝试把两棵树合并。
如果树根相同说明其实这是一棵树,不操作;如果树根不同说明这确实是两棵树,把其中一棵的根挂在另一棵的根上视作合并。
【优化】
路径压缩:并查集有一些优化方法,普遍使用的是路径压缩,就是每次在查询根结点是谁时,把一路上遇到的结点都直接挂在根上,下次查就能更快找到根。
秩优化:就是在合并两棵树时把结点更少的树挂在结点多的树下。设a树有n个子结点,b树有m个,n<m。
a挂b上,n个点找根时路径长度增加了1;
b挂a上,m个点找根时路径长度增加了1.
这样秩优化就省下m-n个找根步骤。
(实际上,路径压缩已经是一个足够强的优化以至于可以将秩优化的效果忽略)
int father[N];               //开始一系列并查集操作前把father数组初始化为全0
int get_father( int x )
{
    return (!father[x])?x:( father[x] = get_father(father[x]) );
}
void Union( int x , int y )  //并查集处理有向图,Union两个点时注意让x为前驱点,无向图无所谓
{
    int fx = get_father(x);
    int fy = get_father(y);
    if( fx != fy )
        father[fy] = fx;
}
既然说了秩优化我就忍不住手痒要写一下了:
int father[N] , rank[N];       //开始一系列并查集操作前把father和rank数组初始化为全0
int get_father( int x )
{
    return (!father[x])?x:( father[x] = get_father(father[x]) );
}
void Union( int x , int y )    //有向图就别用秩优化了
{
    int fx = get_father(x);
    int fy = get_father(y);
    if( fx != fy )
    {
        if( rank[fy] < rank[fx] )
        {
            father[fy] = fx;
            rank[fx] += rank[fy];
        } 
        else
        {
            father[fx] = fy;
            rank[fy] += rank[fx];
        } 
    }
}
Kruskal:
【用途】用来求稀疏图的最小生成树,比起prim来,只用排一次序,效率也不错,那么会了kruskal何必还要学prim呢?prim适合在描述图用邻接矩阵而不逐边描述时。但是既然是用邻接矩阵描述图,那就是个稠密图,图比较大时,逐边抽出来再排序,相比起来prim就占便宜一些了。
【思想】把所有的边都排序,从小到大挨个往图里放,放了会成环就跳过这条边,假设有n个点,那么放过n-1条边最小生成树就建好了。
【耗时点】所有边按权排序。
#include<cstdio>
#include<cstring>
#include<vector>
#include<algorithm>
using namespace std;
const int N = 100;
struct edge
{
    edge( int A , int B , int V ):a(A),b(B),v(V){}
    int a , b , v;
    bool operator < ( const edge &e )const
    {
        return v < e.v;
    }
};
vector<edge> list;
int father[N] , ans , eN;
int get_father( int x )
{
    return (!father[x]) ? x : ( father[x] = get_father( father[x] ) );
}
void Union( int x , int y , int v )
{
    int fx = get_father(x);
    int fy = get_father(y);
    if( fx == fy )
        return;
    father[fx] = fy;
    ans += v;
    eN++;
}
void kruskal( int n )
{
    eN = 0;
    memset( father , 0 , sizeof(father) );
    for( vector<edge>::iterator it = list.begin() ; it != list.end() ; it++ )
    {
        if( eN == n-1 )
            return;
        Union( (*it).a , (*it).b , (*it).v );
    }    
}
int main()
{
    int n , t , a , b , v;
    while( scanf("%d",&n) , n )
    {
        t = n*(n-1)/2;
        ans = 0;
        while(t--)
        {
            scanf("%d%d%d",&a,&b,&v);
            list.push_back( edge( a , b , v ) );
        }
        sort( list.begin() , list.end() );
        if( n > 1 )
            kruskal( n );
        printf("%d\n",ans);
        list.clear();
    }
    return 0;
}
Prim:
【用途】前面讲kruskal时说了,prim是用来求稠密图的最小生成树的。
【思想】建立一个点集,找任一点放入点集,遍历跟点集中点临接且不在点集中的点,找连边权值最小的那个点加入点集,然后再扩展。所有点都加入点集时最小生成树就完成了。
【耗时点】每一次扩展时的遍历。
下面是我按照算法思想写的朴素算法,O(N^3),适合用来理解,不适合实际使用:
#include<cstdio>
#include<vector>
using namespace std;
const int N = 100;
int g[N][N] , n;
int prim()
{
    bool ok[N] = {false};
    int sum = 0 , tmp , min_v;
    vector<int> s;
    s.push_back(0);
    ok[0] = true;
    while( s.size() < n )
    {
        tmp = -1;
        for( int i = 0 ; i < s.size() ; i++ )
        {
            for( int j = 0 ; j < n ; j++ )
            {
                if( ok[j] || s[i] == j )   
                    continue;
                if( tmp == -1 || tmp > 0 && g[s[i]][j] < tmp )
                {
                    tmp = g[s[i]][j];
                    min_v = j;
                }
            }
        }
        sum += tmp;
        s.push_back(min_v);
        ok[min_v] = true;
    }
    return sum;
}
int main()
{
    while( ~scanf("%d",&n) )
    {
        for( int i = 0 ; i < n ; i++ )
            for( int j = 0 ; j < n ; j++ )
                scanf("%d",&g[i][j]);
        printf("%d\n",prim());
    }
    return 0;
}
//这是我补充了邻接矩阵初始化的poj1789代码,O(N^2),可当模板
#include<cstdio>
const int INF = 8;               //边权值不超过7
const int N = 2000;              //最多可达2000个点
char t[N][8];
int g[N][N] , n;                 //建立邻接矩阵
int closestV[N];                 //closestV表示点状态和距离最近的点
int lowCost[N];                  //存储连边中的最小权值,跟closestV
                                 //一起描述每个点与邻接点的关系
//Prim算法
int Prim()
{
    //初始化
    int selectVNum = 1;
    int ans = 0;
    int newV = 0;                //将点0作为新选中的点
    for( int i = 0 ; i < n ; i++ )
        closestV[i] = 0;         //最近点都先指向起点等待更新
    closestV[0] = -1;            //closestV[i]=-1表示选中点i
    for( int i = 0 ; i < n ; i++ )
        lowCost[i] = INF;        //连边最小权值置为无限等待更新
    //扩展选中点集
    while(selectVNum < n)
    {
        int minCost = INF , VtoAdd;
        for( int i = 0 ; i < n ; i++ )
            if(closestV[i] != -1)//不在选中点集中
            {
                int cost = g[newV][i];
                if(cost < lowCost[i])
                {                //更新邻接信息,O(N^3)到O(N^2)的关键
                    lowCost[i] = cost;
                    closestV[i] = newV;
                }
                if(lowCost[i] < minCost)
                    minCost = lowCost[VtoAdd=i];
            }
        ans += minCost;
        closestV[newV=VtoAdd] = -1;
        selectVNum++;
    }
    return ans;
}

void initG()
{
    for( int i = 0 ; i < n ; i++ )
        for( int j = 0 ; j < n ; j++ )
            g[i][j] = INF;
}

int main()
{
    while( scanf("%d", &n) , n )
    {
        for( int i = 0 ; i < n ; i++ )
            scanf("%s", t[i]);
        //initG();          //如果邻接矩阵没被权值填满就需要初始化
        for( int i = 0 ; i < n ; i++ )
            for( int j = i+1 ; j < n ; j++ )
            {
                int d = 0;
                //获得边权值
                for( int k = 0 ; k < 7 ; k++ )
                    if( t[i][k] != t[j][k] )
                        d++;
                //向邻接矩阵赋边权值,看得出来是稠密图,相当稠密
                g[i][j] = d;
                g[j][i] = d;
            }
        printf("The highest possible quality is 1/%d.\n", Prim());
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值