0x06算法设计与分析复习(二):算法设计策略-贪心法3

参考书籍:算法设计与分析——C++语言描述(第二版)

算法设计策略-贪心法

最小代价生成树

问题描述

一个无向连通图的生成树是一个极小连通子图,它包括图中全部的结点,并且尽可能少的边。遍历一个连通图得到图的一颗生成树。

一颗生成树的代价是树中各条边上的代价之和。一个网络的各个生成树中,具有最小代价的生成树称为该网络的最小代价生成树(minimum-cost spanning tree)

贪心法求解

一个无向图的所有生成树都可看成是问题的可行解,其中代价最小的生成树就是所求的最优解,生成树的代价是问题的目标函数。

//最小代价生成树的贪心算法
ESetType SpanningTree(ESetType E, int n)
{
  //G=(V,E)为无向图,E是图G的边集,n是图中结点个数
  ESetType S = EmptySet;//S为生成树上边的集合
  int u,v,k=0;
  EType e;//e=(u,v)为一条边
  //选择生成树的n-1条边
  while(k<n-1 && E中尚有未检查的边){
    //按照最优量度标准选择一条边
    e=select(E);
    //判定可行性
    if(S U e不包含回路){
      //在生成树边集S中添加一条边
      S=S U e;
      k++;
    }
  }
  return S;
}

最简单的最优量度标准是:选择使得迄今为止已入选S中边的代价这和增重最小的边。对于最优量度标准的不同解释产生不同的构造最小代价生成树算法,对于上述最优量度标准有两种可能的理解,它们是普里姆(Prim)算法克鲁斯卡尔(Kruskal)算法

库鲁斯卡尔算法的贪心准则是:按边代价的非减次序考察E中的边,从中选择一条代价最小的边e=(u,v)。这种做法使得算法在构造生成树的过程中,边集S代表的图不一定是连通的。这种算法对边数较少的带权图有较高的效率,时间复杂度为O(eloge)

普里姆算法的贪心准则是:在保证S所代表的子图是一棵树的前提下选择一条最小代价的边e=(u,v)。这种算法特别适用于边数相对较多,即比较接近于完全图的图,算法的时间复杂度为O(n2)

普里姆(prim)算法

G=(V,E)是带权的连通图,F=(U,S)是图G的子图,它是正在构造中的生成树。普里姆算法从U={v0}S=开始构造最小代价生成树,其中v0,v0V是任意选定的结点。普里姆算法的具体选边准则是:寻找一条边(u,v),它是所有一个端点u在构造的生成树上(uU),而另一个端点v不在该树上(vVU)的这些边中代价最小的边。算法按照上述选边准则,选取n-1条满足条件的最小边加到生成树上,最终U=V。这时,T=(V,S)是图G的一颗最小代价生成树。

//普里姆算法
//图采用邻接表存储

template<class T>
struct ENode
{//带权图的边结点
    int adjVex;
    T w;
    ENode* nextArc;
};

template<class T>
class Graph
{
    public:
    Graph (int mSize);
    void Prim(int s);
    ...
    protected:
    void Prim(int k,int* nearest, T* lowcost);
    ...
    ENode<T>** a;
    int n;
};
template<class T>
void Graph<T>::Prim(int s)
{//共有成员函数
    int *nearest=new int[n], *lowcost=new int[n];
    //对于尚未加入生成树的一个顶点v∈V-V’,当前可能存在若干条边与生成树上的顶点相邻接。
    //若边(u,v)是其中权值最小者,那么lowcost[v]= w(u,v),nearest[v]=u。
    Prim(s,nearest,lowcost);
    for(int j=0;j<n;j++)
        cout<<"("<<nearest[j]<<","<<j<<","<<lowcost[j]<<")";
    cout<<endl;
    delete []nearest;
    delete []lowcost;
}

template<class T>
void Graph<T>::Prim(int k,int* nearest, T* lowcost)
{//私有成员函数
    bool* mark=new bool[n];//创建mark数组
    //用于表示某个顶点是否已被选入生成树。如果mark[v]=false,表示v未加入生成树;反之,v已选入。
    ENode<T>* p;
    if(k<0||k>n-1)
        throw OutofBounds;
    for(int i=0;i<n;i++){
        //初始化
        nearest[i]=-1;
        mark[i]=false;
        lowcost[i]=INFTY;
    }
    //源点k加入生成树
    lowcost[k]=0;
    nearest[k]=k;
    mark[k]=true;

    for(i=1;i<n;i++){
        //修改lowcost和nearest
        for(p=a[k];p;p=p->nextArc){
            int j=p->adjVex;
            if((!mark[j]) && (lowcost[j]>p->w)){
                lowcost[j]=p->w;
                nearest[j]=k;
            }
        }
        T min=INFTY;//求下一条最小权边的值
        for(int j=0;j<n;j++)
            if((!mark[j])&&(lowcost[j]<min)){
                min=lowcost[j];
                k=j;
            }
            mark[k]=true;//将结点k加到生成树上
    }
}

设无向图中结点数为n,很明显,普里姆算法的时间复杂度是O(n2)

库鲁斯卡尔(Kruskal)算法

G=(V,E)是带权的连通图,F=(U,S)是正在构造中的生成树。初始时,U=VS=克鲁斯卡尔的选边准则是:在E中选择一条权重最小的边(v,u),并将其从E中删除;若在S代表的子图中加入边e=(u,v)后不形成回路,则将该边加入S中。这就要求结点u和v分属于生成森林F的两颗不同的树上,当边(u,v)加入S后,这两颗树合并成一棵树。如果在S中加入e会形成回路,则应舍弃此边,继续选择下一条边,直到S中包含n-1条边,此时F=(V,S)便是图G的一颗最小代价生成树。也可能在所有的边都考察后|S|<n1,这种情况说明原图不是连通的。

//克鲁斯卡尔算法
template<class T>
void Graph<T>::Kruskal(PrioQueen<eNode<T>>& pq)
{
    //优先权队列pq中保存无向图边的集合,n是无向图的结点个数
    eNode<T> x;
    //建立一个并查集s
    UFSet s(n);
    int u,v,k=0;
    //生成生成树的n-1条边
    while(k<n-1 && !pq.IsEmpty()){
        //从pq中取出最小代价的边x
        pq.Serve(x);
        //分别取找出x.u和x.v所在的树根
        u=s.Find(x.u);
        v=s.Find(x.v);
        if(u!=v){
            //若u和v不在同一树中
            //合并两颗根为u和v的树
            s.Union(u,v);
            k++;
            //输出生成树的一条边
            cout << "("<<x.u<<","<<x.v<<","<<x.w<<")";
        }
    }
    cout <<endl;
    if(k<n-2)
        throw NonConnected;//若边数少于n-1,则原图非连通
}

设无向图有n个结点和e条边,一般有en,所以克鲁斯卡尔算法的时间复杂度为O(eloge)。克鲁斯卡尔算法对边数较少的带权图有较高效率。

算法正确性

定理:设图G=(V,E)是一个带权连通图,U是V的一个真子集。若边(u,v)E是所有uU,vVU的边中权值最小者,那么一定存在G的一颗最小代价生成树T=(V,S),(u,v)S。这一性质称为MST(minimum spanning tree)性质。

定理:普里姆算法和克鲁斯卡尔算法都将产生一个带权无向连通图的最小代价生成树。

比较Prim算法和Kruskal算法

Prim算法:保证S所代表的子图是一棵树的前提下,选择一条最小代价的边e=(u,v);

Kruskal算法:构造生成树的过程中,边集S代表的子图不一定是连通的;按边代价的非减次序考察E中的边,从中选择一条代价最小的边e=(u,v);

Prim算法:由于Prim算法中每次选取的边两端总是一个已连通顶点和一个未连通顶点,故这个边选取后一定能将该未连通点连通而又保证不会形成回路。因此每选择一条边后,无须再判断边集Se是否包含回路

Kruskal算法:为了确保最终得到生成树,每选择一条边时,都需要判定边集Se是否包含回路

posted @ 2018-01-08 20:41  main_c  阅读(347)  评论(0编辑  收藏  举报