该系列文章是本人整理的有关带权无向图的数据结构和算法的分析与实现,若要查看源码可以访问我的github仓库,如有问题或者建议欢迎各位指出。
目录
基于C++的带权无向图的实现 (一)- 数据结构
基于C++的带权无向图的实现 (二)- 遍历算法
基于C++的带权无向图的实现 (三)- Prim最小生成树算法
基于C++的带权无向图的实现 (四)- Dijkstra最短路径算法
基于C++的带权无向图的实现 (五)- 连通图和连通分量
基于C++的带权无向图的实现 (六)- 关节点算法
最小生成树(MST)
概念推导
由于分析的是最小生成树(Minimum Spanning Tree)算法,所以这里假设这个图是一个连通图,即每个顶点可以通过一条路径到达其他的顶点。根据维基百科对最小生成树的描述和我在课上做的笔记,将最小生成树概念推导如下:
- 子图是形成图的图的顶点和边缘的子集。
- 如果该子图包含所有在原来的图中的顶点,那么该子图是生成的(spanning)。
- 如果生成子图不包含回路,那么这个生成子图是生成树。
- 如果生成树在该图的所有可能生成树上具有最小的总(边缘)权重,则它是最小生成树。
- 当连通图中存在权值相等的情况下, 最小生成树可能是唯一的也可能是不唯一的。
- 当连通图上各边权值均不相同的情况下,该图的最小生成树是唯一的。
最小生成树算法
对于带权图,最小生成树算法主要有两种,分别是:
- 普里姆算法(Prim’s Algorithm)
- 克鲁斯卡尔算法(Kruskal’s Algorithm)
这两个算法都属于贪心算法,根据百度百科的描述, 贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解。
本节内容将对普里姆算法进行详解。
Prim算法步骤
给定一个连通图G
- 创建一个空的图T,用来作为最小生成树。
- 在图G中任意选择一个起始顶点v,把v加入到最小生成树T中。
- 当最小生成树中顶点的数量小于原图中的顶点数量时:
- 让E’作为两个顶点u,v的边 edge<u,v>的集合,其中顶点u∈G且u∈T,顶点v∈G但v∉T 。
- 从集合E’中选择顶点u和顶点v之间权重最小的边: edge<u,v> = min(E’) 。
- 在T创建新的顶点v,然后形成边edge<u,v> 。
- 返回T。
此外,遍历顶点需要用到广度优先算法,找到权重最小的边需要用到优先队列(priority_queue)。
Prim算法图解
继续使用前两节用的那张图:
对于该连通图,假定原图为G,最小生成树为T,对于同时存在于G和T的顶点u,和访问过的顶点v,我用红色标记。对于当前只存在于图G的顶点v,和未访问过的顶点v,我用蓝色标记,图和队列的初始情况如下:
据下图所示,队列首元素出队,此时u = A, v = D。把D标记为已访问,然后把D的所有未访问过的邻居顶点放入到优先队列后,队列和图的变化如下所示:
据下图所示,队列首元素出队,此时u = D, v = F。把F标记为已访问,然后把F的所有未访问过的邻居顶点放入到优先队列后,队列和图的变化如下所示:
据下图所示,队列首元素出队,此时u = A, v = B。把B标记为已访问,然后把B的所有未访问过的邻居顶点放入到优先队列后,队列和图的变化如下所示:
据下图所示,队列首元素出队,此时u = B, v = E。把E标记为已访问,然后把E的所有未访问过的邻居顶点放入到优先队列后,队列和图的变化如下所示:
据下图所示,队列首元素出队,此时u = E, v = C。把C标记为已访问,由于C的所有邻居都已经访问过,所以将不会入队任何新元素,队列和图的变化如下所示:
据下图所示,队列首元素出队,此时u = B, v = C。由于此时C已经访问过,则跳过本次循环。再出队下一个元素,此时u = F, v = E,由于此时E已经访问过,则跳过本次循环。再出队下一个元素,此时u = E, v = G,由于G还未访问过,则把G标记为已访问,由于G的所有邻居都已经访问过,所以将不会入队任何新元素,队列和图的变化如下所示:
然后依次弹出队列首元素,发现B,G,E已经访问过了,队列为空,此时最小生成树构建完成:
代码实现
在Graph类中除了上节内容实现的功能外,额外添加了prim最小生成树算法,T为提前定义好的模板:
函数名 | 用途 |
---|---|
Graph prim(T v); | Prim最小生成树算法 |
- 边的定义(edge.hpp):
template <typename T>
class Edge {
public:
T vertex;
int weight;
Edge(T neighbour_vertex) {
this->vertex = neighbour_vertex;
this->weight = 0;
}
Edge(T neighbour_vertex, int weight) {
this->vertex = neighbour_vertex;
this->weight = weight;
}
bool operator<(const Edge& obj) const {
return obj.vertex > vertex;
}
bool operator==(const Edge& obj) const {
return obj.vertex == vertex;
}
};
- 图的定义(graph.hpp)
#include<iostream>
#include<string>
#include<vector>
#include<map>
#include<set>
#include<queue>
#include<stack>
#include "edge.hpp"
using namespace std;
template <typename T>
class Graph {
public:
map<T, set<Edge<T>>> adj; /* 邻接表 */
bool contains(const T& u); /* 判断顶点u是否在图中 */
bool adjacent(const T& u, const T& v); /* 判断顶点u和v是否相邻 */
void add_vertex(const T& u); /* 添加顶点 */
void add_edge(const T& u, const T& v, int weight); /* 添加边和权重 */
void change_weight(const T& u, const T& v, int weight); /* 修改权重 */
void remove_weight(const T& u, const T& v); /* 移除权重 */
void remove_vertex(const T& u); /* 移除顶点 */
void remove_edge(const T& u, const T& v); /* 移除边 */
int degree(const T& u); /* 求顶点的度数 */
int num_vertices(); /* 求图中顶点的总数 */
int num_edges(); /* 求图中边的总数*/
int largest_degree(); /* 求图中的最大度数 */
int get_weight(const T& u, const T& v); /* 得到某两个顶点之间边的权重 */
vector<T> get_vertices(); /* 得到图中所有顶点 */
map<T, int> get_neighbours(const T& u); /* 得到顶点u的所有边 */
void show();
void dft_recursion(const T& u, set<T>& visited, vector<T>& result); /* 深度优先遍历递归辅助函数 */
vector<T> depth_first_rec(const T& u); /* 深度优先遍历递归法 */
vector<T> depth_first_itr(const T& u); /* 深度优先遍历迭代法*/
vector<T> breadth_first(const T& u); /* 广度优先遍历迭代法 */
Graph<T> prim(T v); /* prim最小生成树算法 */
};
由于图的函数声明除了最后一个函数其他的都在前两节中实现了,所以这里只放prim算法的实现代码(graph.hpp):
template <typename T> Graph<T> Graph<T>::prim(T v) {
// 最小生成树的创建
Graph<T> min_spanning_tree;
// 在生成树中添加顶点v
min_spanning_tree.add_vertex(v);
// 设置带权重的队列,按第一个元素(权值)进行从小到大的排列
priority_queue<pair<int, pair<T, T>>, vector<pair<int, pair<T, T>>>, greater<pair<int, pair<T, T>>>> q;
// 设置集合visited来存放已经访问过的顶点
set<T> visited;
// 入队:入队的元素是一个pair类型,第一个值是权重,第二个值也是pair
// 第二个值的pair里面第一个值是u(只在生成树中存在的顶点), 第二个值是v(只在在原图中存在的点)
for (auto neighbour : adj[v]) {
q.push(make_pair(neighbour.weight, make_pair(v, neighbour.vertex)));
}
while (!q.empty()) {
// 队首元素出队
auto front = q.top();
q.pop();
// 获得已在生成树中的顶点u
T u = front.second.first;
// 获得在原图中, 但不在生成树中的顶点v
T v = front.second.second;
// 如果顶点v已经访问过则跳过本此循环
if (visited.find(v) != visited.end()) continue;
else visited.insert(v);
// 在生成树中添加新的顶点v以及v和u之间的边
min_spanning_tree.add_vertex(v);
min_spanning_tree.add_edge(u, v, front.first);
// 依次将顶点v尚未访问过的邻居放入优先队列中
for (auto neighbour : adj[v]) {
if (visited.find(neighbour.vertex) == visited.end()) {
q.push(make_pair(neighbour.weight, make_pair(v, neighbour.vertex)));
}
}
}
return min_spanning_tree;
}
测试
测试案例(graph_testing.cpp):
#include "graph.hpp"
void test03(Graph<char> g) {
cout << "生成的最小生成树如下:" << endl;
Graph<char> result = g.prim('A');
result.show();
}
int main()
{
Graph<char> g;
g.add_vertex('A');
g.add_vertex('B');
g.add_vertex('C');
g.add_vertex('D');
g.add_vertex('E');
g.add_vertex('F');
g.add_vertex('G');
g.add_edge('A', 'B', 7);
g.add_edge('A', 'D', 5);
g.add_edge('B', 'C', 8);
g.add_edge('B', 'D', 9);
g.add_edge('B', 'E', 7);
g.add_edge('C', 'E', 5);
g.add_edge('D', 'E', 15);
g.add_edge('D', 'F', 6);
g.add_edge('E', 'F', 8);
g.add_edge('E', 'G', 9);
g.add_edge('F', 'G', 11);
g.add_vertex('H');
g.add_edge('B', 'H', 9);
g.add_edge('A', 'H', 10);
g.add_edge('D', 'H', 11);
g.add_edge('A', 'H', 12);
g.remove_vertex('H');
cout << "打印图中顶点及其邻接表的详细信息如下" << endl;
g.show();
cout << endl;
test03(g);
return 0;
}
输出结果:
跟之前演示的图做个对比,发现完全一致。