最小生成树(prim和kruskal)

问题引入:
假设要在n个城市之间建立通信网络,则连通n个城市至少需要n-1条线路。n个城市之间,最多可能设置n*(n-1)/2条线路。这时,自然会考虑一个问题:如何在这些可能的线路中选择n-1条,使得在最节省费用的前提下建立该网络通信?
解决方法:
1.prim算法
算法思想:
G=(V,E)是无向连通带权图,V=(1,2,3,…,n);设最小生成树T=(U,TE),算法结束时U=V,TE属于E。构造最小生成树T的prim算法思想是:首先令U={u0},u0属于V,TE={}。然后,只要U是V的真子集,就做如下贪心选择:选取满足条件i属于U,j属于V-U,且边(i,j)是连接U和V-U的所有边中的最短边,即该边的权值最小。然后,将顶点j加入集合U,边(i,j)加入结合TE。继续上面的贪心选择一直到U=V为止,此时选取到的所有的边恰好构成G的一棵最小生成树T。(看成是一个集合到另外一个集合,逐次从中选出一个路线)
算法设计

  1. 将数据存放在数组,如果两个点不通,则设置为无穷大(0x33f3f3f)
  2. 初始化数组lowcost[],假设从1号点(下标1,lowcost[0]=0)开始,将与1相连的边存放到数组lowcost[]中
  3. 寻找到与1相连的最小的边,记录其在lowcost数组中的小标和权值,
  4. 将最小权值加入total,更新lowcost数组
    nyoj题:http://acm.nyist.net/JudgeOnline/problem.php?pid=38
    代码:
#include<cstdio>
#include<cstring>
using namespace std;
int lou[506][506],v,e,total;
const int inf=0x3f3f3f3f;
void prim()
{
   int lowcost[506],flag,minn;//用于记录每次加入一个点后连通其他点的最小权值,
   lowcost[1]=0;
   for(int i=2;i<=v;i++)//假设加入了点1,然后给lowcost赋值与1连接的边权值
   {
       lowcost[i]=lou[1][i];
   }
   for(int i=1;i<v;i++)//只需要寻找n-1条边
   {
       flag=0,minn=inf;
       for(int j=1;j<=v;j++)//找出当前的点集合中最小的边
       {
           if(lowcost[j]&&lowcost[j]<minn)
           {
               minn=lowcost[j];//记录最小的权值
               flag=j;//标记位置
           }
       }
       if(flag!=0)
       total+=minn;
   lowcost[flag]=0;
   for(int i=2;i<=v;i++)  //更新lowcost[]数组
   {
       if(lowcost[i]&&lowcost[i]>lou[flag][i])
       {
           lowcost[i]=lou[flag][i];
       }
   }

   }

}
int main()
{
  int n,out[506],a,b,c;
  scanf("%d",&n);
  while(n--)
  {
      memset(out,0,sizeof(out));
      memset(lou,inf,sizeof(lou));//开始赋初值为整型无穷大量
      scanf("%d%d",&v,&e);
      total=0;
      for(int i=0;i<e;i++)
      {
          scanf("%d%d%d",&a,&b,&c);
          lou[a][b]=c;
          lou[b][a]=c;
      }
     prim();
     int sm=inf;
     for(int i=0;i<v;i++)
     {
         scanf("%d",&out[i]);
         if(sm>out[i])sm=out[i];
     }
     printf("%d\n",total+sm);
  }
 return 0;
}

2.kruskal算法

算法思想:
kruskal算法将n个点看成是n个孤立的连通分支。它首将所有的边按权从小到大排序。然后,只要T中的连通分支数目不为1,就做如下贪心选择:在边集E中选取权值最小的边(i,j),如果将边(i,j)加入集合TE中不产生回路(环),则将边(i,j)加入边集TE中,即用边(i,j)将这两个连通分支合并连接成一个连通分支,否则继续选择下一条最短边。在这两种情况下,把边(i,j)从集合中删除。继续上面的贪心选择知道T中的所有顶点都在同一个连通分支上为止。此时,选取到的n-1条边恰好构成G的一棵最下生成树T。
算法设计:

  1. 初始化,将图G的边集E中的所有边按权从大到小排序,边集TE={},把每个顶点都初始化为一个孤立的分支,即一个顶点对应的集合。
  2. 在E中寻找权值最小的边(i,j)。
  3. 如果顶点i和j位于两个不同的连通分支,则将边(i,j)加入边集TE,并执行合并操作将两个连通分支进行合并。
  4. 将边(i,j)从集合E中删去,即E=E-{(i,j)}
  5. 如果连通分支数目不为1,转步骤2;否则算法结束,生成最小生成树T。
    代码:
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
int fa[2000];
struct building
{
    int a,b,cost;
}build[130000];
bool cmp(building x,building y)//让点之间的距离按照花费从小到大排序
{
    return x.cost<y.cost;
}
int main()
{
    int t,v,e;
    scanf("%d",&t);
    while(t--)
    {
        int wei[506],weight=0;
        memset(build,0,sizeof(build));
        memset(fa,0,sizeof(fa));
        scanf("%d%d",&v,&e);
        for(int i=0;i<e;i++)
        {
            scanf("%d%d%d",&build[i].a,&build[i].b,&build[i].cost);
        }
        sort(build,build+e,cmp);//用sort排序
//        for(int j=0;j<e;j++)
//        {
//            printf("%d %d\n",build[j].a,build[j].cost);
//        }
        for(int i=0;i<v;i++)
        {
            scanf("%d",&wei[i]);
            fa[i]=i;
        }
        sort(wei,wei+v);
        for(int k=0;k<e;k++)
        {
          int x=fa[build[k].a-1];//此处也不是很明白
          int y=fa[build[k].b-1];
          if(x!=y)//当前最小边,如果不为环,那么加入该集合
          {
              weight+=build[k].cost;
              //printf("====weight=%d\n",weight);
              for(int i=0;i<v;i++)
              {
                  if(fa[i]==y)//把可以相连的点并入一个集合,通过设置它们的fa[]值相同实现
                  {
                      fa[i]=x;
                  }
              }
          }
        }
        printf("%d\n",wei[0]+weight);


    }
    return 0;
}

如果G中的边比较少,可以采用kruskal,如果边数较多,则适用prim算法。时间上,kruskal时间复杂度为O(eloge),prim算法时间复杂度为O(n^2),prim适用于稠密图,kruskal适用于稀疏图。
另外kruskal算法另外一种通过并查集的方式写
并查集:
我们可以把每个连通分量看成一个集合,该集合包含了连通分量的所有点。而具体的连通方式无关紧要,好比集合中的元素没有先后顺序之分,只有“属于”与“不属于”的区别。图的所有连通分量可以用若干个不相交集合来表示。

而并查集的精妙之处在于用数来表示集合。如果把x的父结点保存在p[x]中(如果没有父亲,p[x]=x),则不难写出结点x所在树的递归程序:

find(int x) {return p[x]==x?x:p[x]=find(p[x]);}

意思是,如果p[x]=x,说明x本身就是树根,因此返回x;否则返回x的父亲p[x]所在树的根结点。

既然每棵树表示的只是一个集合,因此树的形态是无关紧要的,并不需要在“查找”操作之后保持树的形态不变,只要顺便把遍历过的结点都改成树根的儿子,下次查找就会快很多了
代码:


#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
int fa[2000];
struct building
{
    int a,b,cost;
}build[130000];
bool cmp(building x,building y)
{
    return x.cost<y.cost;
}
int find(int x)
{
    //printf("-->%d\n",x);
    if(x!=fa[x-1])fa[x-1]=find(fa[x-1]);//并查集
    return fa[x-1];
}
int main()
{
    int t,v,e;
    scanf("%d",&t);
    while(t--)
    {
        int wei[506],weight=0;
        memset(build,0,sizeof(build));
        memset(fa,0,sizeof(fa));
        scanf("%d%d",&v,&e);
        for(int i=0;i<e;i++)
        {
            scanf("%d%d%d",&build[i].a,&build[i].b,&build[i].cost);
        }
        sort(build,build+e,cmp);
//        for(int j=0;j<e;j++)
//        {
//            printf("%d %d\n",build[j].a,build[j].cost);
//        }
        for(int i=0;i<v;i++)
        {
            scanf("%d",&wei[i]);
            fa[i]=i+1;
        }
        sort(wei,wei+v);
        for(int k=0;k<e;k++)
        {
          int x=find(build[k].a);
          int y=find(build[k].b);
          //printf("%d %d\n",x,y);
          if(x!=y)
          {
              weight+=build[k].cost;
              fa[x-1]=y;
             // printf("%d\n",weight);
          }
        }
        printf("%d\n",wei[0]+weight);
    }
    return 0;
}



猜你喜欢

转载自blog.csdn.net/pan_xi_yi/article/details/52853020