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中的边,从中选择一条代价最小的边
普里姆算法的贪心准则是:在保证S所代表的子图是一棵树的前提下选择一条最小代价的边
普里姆(prim)算法
设
//普里姆算法 //图采用邻接表存储 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,很明显,普里姆算法的时间复杂度是
库鲁斯卡尔(Kruskal)算法
设
//克鲁斯卡尔算法 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条边,一般有
算法正确性
定理:设图
定理:普里姆算法和克鲁斯卡尔算法都将产生一个带权无向连通图的最小代价生成树。
比较Prim算法和Kruskal算法
Prim算法:保证S所代表的子图是一棵树的前提下,选择一条最小代价的边
Kruskal算法:构造生成树的过程中,边集S代表的子图不一定是连通的;按边代价的非减次序考察E中的边,从中选择一条代价最小的边
Prim算法:由于Prim算法中每次选取的边两端总是一个已连通顶点和一个未连通顶点,故这个边选取后一定能将该未连通点连通而又保证不会形成回路。因此每选择一条边后,无须再判断边集
Kruskal算法:为了确保最终得到生成树,每选择一条边时,都需要判定边集
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)