该系列文章是本人整理的有关带权无向图的数据结构和算法的分析与实现,如有问题或者建议欢迎各位指出。
由于分析的是最小生成树(Minimum Spanning Tree)算法,所以这里假设这个图是一个连通图,即每个顶点可以通过一条路径到达其他的顶点。根据维基百科对最小生成树的描述和我在课上做的笔记,将最小生成树概念推导如下:
对于带权图,最小生成树算法主要有两种,分别是:
这两个算法都属于贪心算法,根据百度百科的描述, 贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解。
本节内容将对普里姆算法进行详解。
给定一个连通图G
此外,遍历顶点需要用到广度优先算法,找到权重最小的边需要用到优先队列(priority_queue)。
继续使用前两节用的那张图:
对于该连通图,假定原图为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类中除了上节内容实现的功能外,额外添加了遍历算法,T为提前定义好的模板:
函数名 | 用途 |
---|---|
Graph prim(T v); | Prim最小生成树算法 |
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;
}
};
#include
#include
#include
#include
#include
#include
#include
#include "edge.hpp"
using namespace std;
template <typename T>
class Graph {
public:
map<T, set<Edge<T>>> adj;
bool contains(const T& u);
bool adjacent(const T& u, const T& 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();
vector<T> get_neighbours(const T& u);
void show();
void dft_recursion(const T& u, set<T>& visited, vector<T>& result);
vector<T> depth_first(const T& u);
vector<T> depth_first_itr(const T& u);
vector<T> breadth_first(const T& u);
Graph<T> prim(T v);
};
由于图的函数声明除了最后一个函数其他的都在前两节中实现了,所以这里只放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;
}