前言
上一篇文章我们讲到了图的基础,相信有了无权图的基础很快就能理解有权图了。有权图其实也是符合我们生活实际情况的,距离最短并不说明这条路线是最佳的路线,因为可能会堵车。这时候,有权图就可以帮助我们解决问题,每条道路都有一定的代价,我们的代价最小时,这条路就是最优的。
有权图
邻接矩阵
稠密图我们依然使用邻接矩阵,不过和无权图不同的是我们就不是0、1表示了,而是使用权重进行代替。
邻接表
稀疏图也是老样子采用邻接表,但是存储的就不是简单的数字了,它应该存储一个结构,表明方向和权重。所以这里我们就采用了一个类Edge表示,方便我们进行操作。
最小生成树
最小生成树MST(Minimum Spanning Tree),是我们这一篇主要想解决的问题。
维基百科定义:
最小生成树其实是最小权重生成树的简称。
一个连通图可能有多个生成树。当图中的边具有权值时,总会有一个生成树的边的权值之和小于或者等于其它生成树的边的权值之和。广义上而言,对于非连通无向图来说,它的每一连通分量同样有最小生成树,它们的并被称为最小生成森林。
在有权图中,我们保证以最小代价连接图中的所有节点,当然前提保证图是连通图。
切分定理
切分定理是实现最小生成树的理论支持。
维基定义:
在一幅连通加权无向图中,给定任意的切分,它的横切边中权值最小的边必然属于图的最小生成树。
反证法证明:
令e为权重最小的横切边, T为图的最小生成树。假设T不包含e,那么如果将e加入T,得到的图必然含有一条经过e的环,且这个环只是含有一条横切边–设为f,f的权重必然大于e,那么用e替换f可以形成一个权值小于T的生成树,与T为最小生成树矛盾。所以假设不成立。
我一开始纠结于怎么就形成了一个环呢,但是重新看了一下最小生成树的定义,茅塞顿开。因为所有节点都包含在最小生成树中,添加一条边必然有环。
LazyPrimMST
我们这就进入核心代码部分。Prim是一个人名,为什么叫Lazy?因为还有比他更优化的Prim算法。言归正传,我们首先介绍一下这个算法的原理。
我们一开始将0作为一个切分,然后从它的横切边找到最小的边,扩展到7;然后0、7作为一个切分,继续寻找,扩展到1,不断扩展,直到所有节点都被连接。
代码实现
Edge类
Edge主要存储边的两个点和权重,这里的Weight是模板类。
private:
int a,b;
Weight weight;
稠密图
这里我们依然可以采用vector表示,但是里面的类型我们采用了类的指针。这里需要注意的是,初始化时需要强制转换 (Edge<Weight>*)NULL
,这里绝对是一个大坑,因为我使用code::blocks,在编译时报错invalid conversion from int to 'std::vector<Edge<double>*, std::allocator<Edge<double>*> >::value_type {aka Edge<double>*}
。而且报错定位是在系统文件中,我卡在这里的时间也不多,也就个把小时吧。。。。
typedef vector<Edge<Weight>* > EDGE;
private:
int n, m;
bool directed;
vector<EDGE> g;
public:
DenseGraph( int n , bool directed){
this->n = n;
this->m = 0;
this->directed = directed;
g = vector< EDGE >(n, EDGE(n, (Edge<Weight>*)NULL));
}
其他都和无权图的很类似这里就不贴了,有需要直接去文尾的仓库下载。
稀疏图
稀疏图也采用vector表示二维矩阵。
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> *>() );
}
LazyPrimMST
因为我们要找到边的最小值,这里我们就使用了之前学习过的最小堆进行快速查找。同时需要标记节点是否已经被访问,使用marked数组。还需要添加一个向量存放所有的边,以及存储最终的总权值。
private:
Graph &G; // 图的引用
MinHeap< Edge<Weight> > pq; // 最小堆, 算法辅助数据结构
bool *marked; // 标记数组, 在算法运行过程中标记节点i是否被访问
vector< Edge<Weight> > mst; // 最小生成树所包含的所有边
Weight mstWeight; // 最小生成树的权值
访问节点,将其所有横切边放入堆中。
// 访问节点v
void visit(int v){
assert( !marked[v] );
marked[v] = true;
// 将和节点v相连接的所有未访问的边放入最小堆中
typename Graph::adjIterator adj(G,v);
for( Edge<Weight>* e = adj.begin() ; !adj.end() ; e = adj.next() )
if( !marked[e->other(v)] )
pq.insert(*e);
}
构造函数如下:
// 构造函数, 使用Prim算法求图的最小生成树
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() ){
// 使用最小堆找出已经访问的边中权值最小的边
Edge<Weight> e = pq.extractMin();
// 如果这条边的两端都已经访问过了, 则扔掉这条边
if( marked[e.v()] == marked[e.w()] )
continue;
// 否则, 这条边则应该存在在最小生成树中
mst.push_back( e );
// 访问和这条边连接的还没有被访问过的节点
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();
}
优化Prim
之前的LazyPrimMST,有几个地方没有考虑:
1. 很多边成为最小生成树的一部分之后,不必再加入最小堆中
2. 我们只需要关注最短的横切边即可
因此我们只需要不断更新横切边中的最短边即可,这里也使用了一个之前学习数据结构——最小索引堆,它可以很好地对堆中进行修改。
我们将0到相邻节点的边的各个权值放入索引堆中,找到0-7这条边最小,扩展到7;查找7到除0外各个节点的边的各个权值,与原有的进行比较,如果比原来索引堆中的小,则更新索引堆,然后找到最短的。
更新后的索引堆:
代码实现
当然我们存储在索引堆中的应该是Edge,而不是简单的数值。
private:
Graph &G; // 图的引用
IndexMinHeap<Weight> ipq; // 最小索引堆, 算法辅助数据结构
vector<Edge<Weight>*> edgeTo; // 访问的点所对应的边, 算法辅助数据结构
bool* marked; // 标记数组, 在算法运行过程中标记节点i是否被访问
vector<Edge<Weight>> mst; // 最小生成树所包含的所有边
Weight mstWeight; // 最小生成树的权值
根据之前的操作,访问节点v,并对最小索引堆进行维护:
// 访问节点v
void visit(int v){
assert( !marked[v] );
marked[v] = true;
// 将和节点v相连接的未访问的另一端点, 和与之相连接的边, 放入最小堆中
typename 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]->wt() ){
edgeTo[w] = e;
ipq.change(w, e->wt());
}
}
}
}
构造函数,完成最小生成树:
// 构造函数, 使用Prim算法求图的最小生成树
PrimMST(Graph &graph):G(graph), ipq(IndexMinHeap<double>(graph.V())){
assert( graph.E() >= 1 );
// 算法初始化
marked = new bool[G.V()];
for( int i = 0 ; i < G.V() ; i ++ ){
marked[i] = false;
edgeTo.push_back(NULL);
}
mst.clear();
// Prim
visit(0);
while( !ipq.isEmpty() ){
// 使用最小索引堆找出已经访问的边中权值最小的边
// 最小索引堆中存储的是点的索引, 通过点的索引找到相对应的边
int v = ipq.extractMinIndex();
assert( edgeTo[v] );
mst.push_back( *edgeTo[v] );
visit( v );
}
mstWeight = mst[0].wt();
for( int i = 1 ; i < mst.size() ; i ++ )
mstWeight += mst[i].wt();
}
经测试,在节点和边较多的情况下,Prim算法会比LazyPrim快两到三倍。
Kruskal算法
这个也是发明者的名字。。
我们每次都选取最短的边,如果这条边加入之后不构成环,那么就可以成为最小生成树的一部分。当所有节点加入之后,最小生成树就完成了。
我们可以先对边进行排序;查看是否形成一个环我们可以使用UnionFind并查集来判断。
代码实现
我们首先使用最小堆,将所有边加入;然后创建并查集,查看两节点是否已经连通,如果已经连通,则放弃,如果未连通,则将该边加入最小生成树中。
private:
vector< Edge<Weight> > mst; // 最小生成树所包含的所有边
Weight mstWeight; // 最小生成树的权值
public:
// 构造函数, 使用Kruskal算法计算graph的最小生成树
KruskalMST(Graph &graph){
// 将图中的所有边存放到一个最小堆中
MinHeap< Edge<Weight> > pq( graph.E() );
for( int i = 0 ; i < graph.V() ; i ++ ){
typename Graph::adjIterator adj(graph,i);
for( Edge<Weight> *e = adj.begin() ; !adj.end() ; e = adj.next() )
if( e->v() < e->w() )
pq.insert(*e);
}
// 创建一个并查集, 来查看已经访问的节点的联通情况
UnionFind uf = UnionFind(graph.V());
while( !pq.isEmpty() && mst.size() < graph.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();
}
对比
Prim算法明显快了不少,但是Kruskal加上排序和查找的时间,效率并不高
图片引用百度图片
代码实现参照liuyubobobo慕课网教程