1.应用场景:
上一篇博客学习了图论基础https://blog.youkuaiyun.com/zoroooooo/article/details/117461077介绍了无权图的算法实现以及路径计算获取,在实际生产场景中带权图往往更接近实际需求。最小生成树解决的是在n个顶点中找到n-1条边使得所有顶点全部连通并且n-1条边的权重之和最小(针对无向图)。
2.带权图的算法实现
上一篇博客介绍了在稠密图中,我们使用一个n*n(n为顶点数)的邻接矩阵(二维bool数组)g存储顶点之间的连接关系,g[i][j]为true表示图中含有由第i个顶点指向第j个顶点的边,反之则无;对于稀疏图我们使用邻接表g(二维int数组)来表示,g[i]数组中含有j元素表示图中含有由第i个顶点指向第j个顶点的边,否则表示没有由i指向j的边。带权边是一个相对复杂的数据结构不能用一个基本数据类型来表达,因此先将边Edge封装成一个类,在原有的稀疏图和稠密图的算法实现上邻接表和邻接矩阵改为Edge的二维数组,就实现了带权图。
Edge:
//带权图中边的表达
#include <iostream>
#include <cassert>
using namespace std;
template<typename Weight>
class Edge{
private:
int a,b;
Weight weight;
public:
Edge(int a, int b, Weight weight){
this->a = a;
this->b = b;
this->weight = weight;
}
Edge(){}
~Edge(){}
int v(){ return a;}//返回第一个顶点
int w(){ return b;}//返回第二个顶点
Weight wt(){ return weight;}
int other(int x){//返回边上的另一顶点
assert( x == a || x == b );
return x == a ? b : a;
}
friend ostream& operator<<(ostream &os, const Edge &e){
os<<e.a<<"-"<<e.b<<": "<<e.weight;
return os;
}
//边的大小比较,实际上是权重的比较
bool operator<(Edge<Weight>& e){
return weight < e.wt();
}
bool operator<=(Edge<Weight>& e){
return weight <= e.wt();
}
bool operator>(Edge<Weight>& e){
return weight > e.wt();
}
bool operator>=(Edge<Weight>& e){
return weight >= e.wt();
}
bool operator==(Edge<Weight>& e){
return weight == e.wt();
}
};
稠密图(DenseGraph):
#include <iostream>
#include <vector>
#include <cassert>
#include "Edge.h"
using namespace std;
// 稠密图 - 邻接矩阵
template <typename Weight>
class DenseGraph{
private:
int n, m;
bool directed;
vector<vector<Edge<Weight> *>> g;//里面的vector存的是指针(未命名),Weight是Edge这个类的模板,这个指针就是指向Edge对象的指针
public:
DenseGraph( int n , bool directed){
this->n = n;
this->m = 0;
this->directed = directed;
for( int i = 0 ; i < n ; i ++ ){
g.push_back( vector<Edge<Weight> *>(n,NULL) );
}
}
~DenseGraph(){
for( int i = 0 ; i < n ; i ++ )
for( int j = 0 ; j < n ; j ++ )
if( g[i][j] != NULL )
delete g[i][j];
}
int V(){ return n;}//返回边条数
int E(){ return m;}//返顶点的个数
void addEdge( int v, int w , Weight weight ){
assert( v >= 0 && v < n );
assert( w >= 0 && w < n );
if( hasEdge( v , w ) ){//添加边已存在,删除原存在边进行替换
delete g[v][w];
if( !directed )
delete g[w][v];
m --;//维护边条数
}
g[v][w] = new Edge<Weight>(v, w, weight);
if( !directed )
g[w][v] = new Edge<Weight>(w, v, weight);
m ++;
}
bool hasEdge( int v , int w ){
assert( v >= 0 && v < n );
assert( w >= 0 && w < n );
return g[v][w] != NULL;
}
void show(){
for( int i = 0 ; i < n ; i ++ ){
for( int j = 0 ; j < n ; j ++ )
if( g[i][j] )
cout<<g[i][j]->wt()<<"\t";
else
cout<<"NULL\t";
cout<<endl;
}
}
class adjIterator{
private:
DenseGraph &G;//目标顶点所在图
int v;//遍历的目标顶点
int index;//当前遍历位置
public:
adjIterator(DenseGraph &graph, int v): G(graph){
this->v = v;
this->index = -1;
}
Edge<Weight>* begin(){
index = -1;
return next();
}
Edge<Weight>* next(){//遍历邻接矩阵中v顶点所在行,不为NULL的下一条边
for( index += 1 ; index < G.V() ; index ++ )
if( G.g[v][index] )
return G.g[v][index];
return NULL;
}
bool end(){
return index >= G.V();
}
};
};
稀疏图(SparseGraph)
// 稀疏图 - 邻接表
template<typename Weight>
class SparseGraph{
private:
int n, m;
bool directed;
vector<vector<Edge<Weight> *>> g;
public:
SparseGraph( int n , bool directed){
this->n = n;
this->m = 0;
this->directed = directed;
for( int i = 0 ; i < n ; i ++ )
g.push_back( vector<Edge<Weight> *>() );
}
~SparseGraph(){
for( int i = 0 ; i < n ; i ++ )
for( int j = 0 ; j < g[i].size() ; j ++ )
delete g[i][j];
}
int V(){ return n;}
int E(){ return m;}
void addEdge( int v, int w , Weight weight){
assert( v >= 0 && v < n );
assert( w >= 0 && w < n );
g[v].push_back(new Edge<Weight>(v, w, weight));
if( v != w && !directed )
g[w].push_back(new Edge<Weight>(w, v, weight));
m ++;
}
bool hasEdge( int v , int w ){
assert( v >= 0 && v < n );
assert( w >= 0 && w < n );
for( int i = 0 ; i < g[v].size() ; i ++ )
if( g[v][i]->other(v) == w )
return true;
return false;
}
void show(){
for( int i = 0 ; i < n ; i ++ ){
cout<<"vertex "<<i<<":\t";
for( int j = 0 ; j < g[i].size() ; j ++ )
cout<<"( to:"<<g[i][j]->w()<<",wt:"<<g[i][j]->wt()<<")\t";
cout<<endl;
}
}
class adjIterator{
private:
SparseGraph &G;
int v;
int index;
public:
adjIterator(SparseGraph &graph, int v): G(graph){
this->v = v;
this->index = 0;
}
Edge<Weight>* begin(){
index = 0;
if( G.g[v].size() )
return G.g[v][index];
return NULL;
}
Edge<Weight>* next(){
index += 1;
if( index < G.g[v].size() )
return G.g[v][index];
return NULL;
}
bool end(){
return index >= G.g[v].size();
}
};
};
3.切分定理
切分:将图任意分成两部分;
横切边:如果一条边的两个顶点分别属于切分不同的两边,那么这条边称为横切边;
切分定理:给定任意切分,横切边中权值最小的边必定属于最小生成树。
4.最小生成树(LazyPrime算法,O(ElogE))
根据切分定理,我们选定一个起始点将图切分成两部分:起始点和其他点。这样就可以找到最小生成树中与该起始点相连的边,再将图切分为该边和其他边两部分,根据切分定理又可以找到最小生成树的又一条边,重复该过程即可找到最小生成树。在算法实现中我们使用最小堆(可参考https://blog.youkuaiyun.com/j_oop/article/details/107307066)来存储所有的横切边,使用vector来存储已确定的最小生成树中的边,使用bool数组来标记当前图的切分情况(为true表示在同一切分即最小生成树中,false为另一切分)。难点在于:随着顶点加入最小生成树所在切分,横切边队列中原先满足横切边的边可能不再是横切边,解决方法代码中已经给出注释。
LazyPrime:
#include <iostream>
#include <vector>
#include <cassert>
#include "Edge.h"
#include "MinHeap.h"
using namespace std;
template<typename Graph, typename Weight>
class LazyPrimMST{
private:
Graph &G;//图
MinHeap<Edge<Weight>> pq;//横切边队列
bool *marked;//标记切分,为true表示属于切分中的一部分,false为另一部分
vector<Edge<Weight>> mst;//最小生成树
Weight mstWeight;//最小生成树的权值
void visit(int v){//访问节点,将其邻接节点放入优先队列中
assert( !marked[v] );
marked[v] = true;
typename Graph::adjIterator adj(G,v);遍历V所有的邻边
for( Edge<Weight>* e = adj.begin() ; !adj.end() ; e = adj.next() )
if( !marked[e->other(v)] )
pq.insert(*e);//横切边进队列
}
public:
LazyPrimMST(Graph &graph):G(graph), pq(MinHeap<Edge<Weight>>(graph.E())){
marked = new bool[G.V()];
for( int i = 0 ; i < G.V() ; i ++ )
marked[i] = false;
mst.clear();
// Lazy Prim
visit(0);
while( !pq.isEmpty() ){//时间复杂度E(图有多少条边)
Edge<Weight> e = pq.extractMin();//取出权值最小的横切边,时间复杂度logE
if( marked[e.v()] == marked[e.w()] )//如果e的两个端点v和w都变红了,就不管了,这就是LazyPrim,后续改进就要管
continue;
mst.push_back( e );//否则e就是横切边,放入mst
if( !marked[e.v()] )//接着找蓝色边
visit( e.v() );
else
visit( e.w() );
}
mstWeight = mst[0].wt();
for( int i = 1 ; i < mst.size() ; i ++ )
mstWeight += mst[i].wt();
}
~LazyPrimMST(){
delete[] marked;
}
vector<Edge<Weight>> mstEdges(){//返回最小生成树的所有边
return mst;
};
Weight result(){// 返回最小生成树的权值
return mstWeight;
};
};
对LazyPrime进行算法效率分析:最外层循环while(!pq.isEmpty())的时间复杂度是E级别的,循环内pq.extractMin()时间复杂度是logE级别,visit方法中pq.insert(*e)也是logE级别的,因此LazyPrime算法的时间复杂度是O(ElogE)。
5.LazyPrime优化,Prime算法O(ElogV)
LazyPrime算法中通过将横切边存入最小堆中,每次取出权值最小的边来获取最小生成树的一条边,即根据边来找顶点。
而事实上最小生成树就是找到E-1条边来连接E个顶点使得权值之和最小,每次循环确定连接一个顶点的路径。也就是说我们可以每次遍历顶点(即visit)维护未确定顶点(marked为false的顶点)的候选横切边,再根据切分定理依次确定每个顶点的路径。
先上代码:
/************************************************************************/
/* 对lazyPrime进行改进 */
/************************************************************************/
template<typename Graph,typename Weight>
class PrimeMST
{
private:
//图
Graph &G;
//最小生成树
vector<Edge<Weight>> mst;
//最小生成树的权值
Weight mstWeight;
//最小索引堆,存储横切边权值
IndexMinHeap<Weight> ipq;
//候选横切边列表
vector<Edge<Weight>*> edgeTo;
//标记切分
bool* marked;
void visit(int v)
{
Graph::adjIterator adj(G,v);
for(Edge<Weight>* e = adj.begin();!adj.end();e = adj.next())
{
int w = e->other(v);
if(!marked[w])
{
if(!edgeTo[w]) //当前顶点没有侯选边.直接赋值
{
edgeTo[w] = e;
ipq.insert(w,e->wt());
}
else if(e->wt()< edgeTo[w]) //已经存在候选边,如果当前边权值小于侯选边,进行替换
{
edgeTo[w] = e;
ipq.change(w,e->wt());
}
}
}
}
public:
PrimeMST(Graph &graph):G(graph),ipq(IndexMinHeap<double>(graph.V()))
{
assert(graph.E() >= 1);
marked = new bool[graph.V()];
for(int i=0;i<graph.V();i++)
{
marked[i] = false;
edgeTo.push_back(NULL);
}
mst.clear();
visit(0);
while(!ipq.isEmpty()) //O(E)
{
int v = ipq.extractMinIndex(); //O(logE)
if(edgeTo[v])
{
mst.push_back(edgeTo[v]);
visit(v);
mstWeight+=edgeTo[v]->wt();
}
}
}
~PrimeMST(void);
};
可以看到算法的时间复杂度变成了O(ElogE),除此之外,由于每次遍历顶点维护的是顶点对应的侯选边列表且需要进行频繁替换操作,最小堆无法实现替换指定索引的值,因此使用了最小索引堆,可以参考https://blog.youkuaiyun.com/j_oop/article/details/107307066
6.Kruskal算法
Kruskal算法的思想是,依次取出图中权值最小的边加入到最小生成树,如果当前最小生成树中构成了环,则该边不在最小生成树中,未构成环则该边存在在最小生成树中。代码实现中使用并查集来判断最小生成树中是否存在环,并查集可参考https://blog.youkuaiyun.com/j_oop/article/details/107558581
template<typename Graph,typename Weight>
class KruskalMST
{
private:
//最小生成树
vector<Edge<Weight>> mst;
//最小生成树权值
Weight mstWeight;
public:
KruskalMST(Graph &g)
{
MinHeap<Edge<Weight>> pq (g.E());
for(int i=0;i<g.V();i++)
{
Graph::adjIteratorz adj(g,i);
for(Edge<Weight>*e=adj.begin();!adj.end();e=adj.next())
if(e->V()<e->W()) //无向图所有的边都存了两遍,此处保证所有的边只加入最小堆一次
pq.insert(*e);
}
//并查集
UF5::UnionFind uf(g.V());
while(!pq.isEmpty() && mst.size()<g.V()-1) //当最小生成树已确定,提前终止循环
{
Edge<Weight> e = pq.extractMin(); //取出权值最小的边
if(uf.isConnected(e.V(),e.W())) //形成了环,跳过
{
continue;
}
//不形成环,加入最小生成树并合并顶点
mst.push_back(e);
uf.unionElements(e.V(),e.W());
}
mstWeight = mst[0].wt();
for( int i = 1 ; i < mst.size() ; i ++ )
mstWeight += mst[i].wt();
}
~KruskalMST(void);
};
Kruskal算法时间复杂度分析:Kruscal首先对所有的边进行了排序,时间复杂度ElogE,取最小边时间复杂度ElogE,并查集合并顶点时间复杂度ElogV,所以综合起来Kruskal算法效率要低于Prime算法。
最短路径尚未实现