并查集、Kruskal算法与Prim算法

来源:互联网 发布:ubuntu系统联网 编辑:程序博客网 时间:2024/05/18 09:22
并查集:
【用途】并查集是用来整理图的连通分量的,并查集过程完成后,可以确定两点是否连通,也可得知有多少个连通分量。推广到集合论里就是已知若干对元素,a在某集合里能推知b在集合里,用并查集确定各个元素归属于哪个集合,也可以顺便确定集合的数量。
【思想】并查集的思想其实很简单,每次合并都是尝试把两棵树合并。
如果树根相同说明其实这是一棵树,不操作;如果树根不同说明这确实是两棵树,把其中一棵的根挂在另一棵的根上视作合并。
【优化】
路径压缩:并查集有一些优化方法,普遍使用的是路径压缩,就是每次在查询根结点是谁时,把一路上遇到的结点都直接挂在根上,下次查就能更快找到根。
秩优化:就是在合并两棵树时把结点更少的树挂在结点多的树下。设a树有n个子结点,b树有m个,n<m。
a挂b上,n个点找根时路径长度增加了1;
b挂a上,m个点找根时路径长度增加了1.
这样秩优化就省下m-n个找根步骤。
(实际上,路径压缩已经是一个足够强的优化以至于可以将秩优化的效果忽略)
int father[N];               //开始一系列并查集操作前把father数组初始化为全0int 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数组初始化为全0int 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;               //边权值不超过7const 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;}

0 0
原创粉丝点击