该系列文章是本人整理的有关带权无向图的数据结构和算法的分析与实现,若要查看源码可以访问我的github仓库,如有问题或者建议欢迎各位指出。
目录
基于C++的带权无向图的实现 (一)- 数据结构
基于C++的带权无向图的实现 (二)- 遍历算法
基于C++的带权无向图的实现 (三)- Prim最小生成树算法
基于C++的带权无向图的实现 (四)- Dijkstra最短路径算法
基于C++的带权无向图的实现 (五)- 连通图和连通分量
基于C++的带权无向图的实现 (六)- 关节点算法
关节点(Articulation Points)
如果移除顶点和关联的边会使连通分量的数量增加,则将该顶点称为图中的割点,或叫关节点。 拿上节内容的图G来举例,如下所示:
由于图G为非连通图,具有两个连通分量,所有关节点即为每个连通分量的关节点之和。左边的连通分量的关节点为9,右边的连通分量的关节点为1,3,4。如果任意删除这四个顶点中的一个顶点及其相邻所有边,该图的连通分量数量将会增加。比如删除顶点9或者顶点4或顶点3的时候,图中的连通分量会变成三个;删除顶点1的时候,图中的连通分量会变成四个。
求关节点的算法
暴力求解
暴力求解理解起来非常简单。
- 记录图中原有的连通分量的数量N
- 遍历图G中的每一个顶点u:
- 删除顶点 u及其相邻的所有边。
- 计算删除顶点u及其相邻的所有边后连通分量 M。如果M > N,则说明顶点u是分割点。
- 将顶点u重新添加到图中。
这种方法的缺点就是时间复杂度非常高,由于要依次删除和还原每一个顶点,并且每次删除顶点后都要进行深度优先遍历,所以其时间复杂度为O(N ∗ (N+ M)) 。
Tarjan算法
这种算法也是当下求关节点的最优算法,时间复杂度为O(N + M),对于每个连通分量,只需要进行一次深度优先遍历即可。在介绍这种算法之前,我们先了解几个概念:
- 树边(Tree edge): 指向未访问过的顶点的边,也叫父子边。
- 回边(Back edge):指向已访问过的祖先顶点的边。
- 深度优先搜索树(DFS trees)
深度优先搜索树是指在图中按照深度优先遍历顺序产生的树。对于图1,其深度优先生成树如下(蓝色实线表示树边,红色虚线表示回边,箭头指向的是可回溯到的祖先顶点):
对于Tarjan算法,有两类顶点可以作为关节点:
- 对于根节点u,若在根节点下有两颗或两颗以上的子树,那么该根节点可视为关节点。上图中的两颗DFS搜索树的根节点分别为1和7,由于他们都只有一颗子树,所以在这个例子中这两个根节点都不为关节点。
- 当u不为根节点并且当u不为叶子结点时,如果u的子树均没有指向u的祖先顶点的回边,则说明删除u之后,图中的连通分量数量会增加,那么此时u为关节点/割点。对于上图中的顶点3,他只有一颗子树(顶点6),且基于顶点6的子树没有指向顶点3祖先的回边,所以顶点3是图中的关节点。
在Tarjan算法中中,需要使用两个数组dfn[u]和 low[u],他们的用途分别为:
- dfn[u]:深度优先遍历的次序序号。
- low[u]:顶点u或其子树能回溯到的最早的祖先结点ID。
dfn[u]的记录非常容易,只需要从1开始,每遍历一个未访问过的顶点时加1即可。
对于low[u]的记录和更新,则需要通过以下几步:
- 每遍历到一个未访问过的顶点时,将其初始化为深度优先遍历的次序序号,即low[u] = dfn[u], 表示以u为根节点的子树能回溯到的最早的祖先是其自己。
- 对于u的邻居结点v:
- 如果(u,v)为树边,那么对于v能回溯到的最早的祖先,u肯定也能回溯到。此时:low[u] = min(low[u], low[v])
- 如果(u,v)为回边,即v已经访问过,但v不是u的父亲,那么此时基于顶点u的子树可以回溯到祖先v。此时low[u] = min(low[u], dfn[v])
如果(u,v)为树边,且low[v] >= dfn[u]时,即可判断u为关节点,这条公式的含义如下:
- 当low[v] > dfn[u]时,表示以v为根节点的子树能回溯到的最早的祖先为顶点v自己。如图2(b)中的顶点6相当于v,顶点3相当于u。由于顶点6没有任何回边(红色虚线)指向顶点6之前的顶点,所以可以判断顶点3为关节点。
- 当low[v] == dfn[u] 时,表示以v为根节点的子树只能回溯到顶点u为止,那么既然他只能回溯到顶点u,当你移除顶点u的时候,以v为根节点的子树不就凉了?所以此时顶点u即为关节点。
下面用一组图示和表格的方式来表示图2中各个顶点dfn和low记录的值:
当u = 1,v = 3时, low[v] = 4,dfn[u] = 2,low[v] > dfn[u],所以顶点1为关节点。
当u = 3,v = 6时,low[v] = 5, dfn[u] = 4,low[v] > dfn[u],所以顶点3为关节点。
当u = 4,v = 5时,low[v] = 7, dfn[u] = 6,low[v] > dfn[u],所以顶点4为关节点。
当u = 9,v = 10时,low[v] = 10, dfn[u] = 10,low[v] == dfn[u],所以顶点9为关节点。
代码实现
在Graph类中除了上节内容实现的功能外,额外添加了求关节点的算法,T为提前定义好的模板:
函数名 | 用途 |
---|---|
vector articulation_points(int choice) | 求关节点的函数 |
void violent_solution(vector& articulation_point_collection) | 暴力求解法 |
void dft(T u, T root, T parent, set& visited_vertices, int& dfn_cnt, map<T, int>& dfn, map<T, int>& low, vector& articulation_point_collection) | Tarjan算法 |
- 边的定义(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<limits.h>
#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最小生成树算法 */
map<T, int> dijkstra(T start); /* dijkstra最短路径算法 */
vector<vector<T>> get_connected_components(); /* 获得图中的连通分量 */
void print_connected_components(const vector<vector<T>>& connected_components); /* 打印连通分量 */
vector<T> articulation_points(int choice); /* 获得图中的关节点(分割点)*/
private:
void dft(T u, T root, T parent, set<T>& visited_vertices, /* 获得关节点的Tarjan算法 */
int& dfn_cnt, map<T, int>& dfn, map<T, int>& low, vector<T>& articulation_point_collection);
void violent_solution(vector<T>& articulation_point_collection); /* 获得关节点的暴力求解法 */
};
函数实现代码:
template <typename T> vector<T> Graph<T>::articulation_points(int choice)
{
// 计算深度优先遍历的次数
int dfn_cnt = 0;
// 记录图中出现的分割点
vector<T> articulation_point_collection;
// 记录深度优先遍历顺序
map<T, int> dfn;
// 记录以某个特定顶点为根的子树能回溯到的最早的祖先顶点
map<T, int> low;
// 记录已访问过的顶点
set<T> visited_vertices;
if (choice == 1) {
violent_solution(articulation_point_collection);
}
else if (choice == 2) {
// 对未访问过的顶点进行深度优先遍历求分割点(实际上是在每一个连通分量中使用一次深度优先遍历)
for (auto u : adj) {
if (visited_vertices.find(u.first) == visited_vertices.end())
dft(u.first, u.first, u.first, visited_vertices, dfn_cnt, dfn, low, articulation_point_collection);
}
int a = 1;
}
return articulation_point_collection;
}
template <typename T> void Graph<T>::violent_solution(vector<T>& articulation_point_collection)
{
// 获得原来的图的连通分量数量
unsigned original_number = get_connected_components().size();
// 获得图中的所有顶点
vector<T>vertices = get_vertices();
for (T vertex : vertices) {
// 暂存要删除的顶点附近的邻居
map<T, int> temp_neighbours = get_neighbours(vertex);
// 删除顶点
remove_vertex(vertex);
// 将删除后的连通分量数量与删除前的比较
unsigned current_number = get_connected_components().size();
if (current_number > original_number) articulation_point_collection.push_back(vertex);
// 添加回顶点及对应的边
add_vertex(vertex);
for (auto neighbour : temp_neighbours) {
add_edge(vertex, neighbour.first, neighbour.second);
}
}
}
template <typename T> void Graph<T>::dft(T u, T root, T parent, set<T>& visited_vertices,
int& dfn_cnt, map<T, int>& dfn, map<T, int>& low, vector<T>& articulation_point_collection)
{
// 记录深度优先遍历次序
dfn_cnt++;
dfn[u] = dfn_cnt;
// 初始化low[u]
low[u] = dfn[u];
// 标记当前顶点为已访问
visited_vertices.insert(u);
// 记录子树数量
int n_subtree= 0;
// 记录该顶点是否为关节点
bool is_cut = false;
for (auto edge : adj[u]) {
T v = edge.vertex;
// 当(u,v)边为树边时
if (visited_vertices.find(v) == visited_vertices.end()) {
n_subtree++;
// 对u的孩子v进行深度优先遍历,此时u作为parent
dft(v, root, u, visited_vertices, dfn_cnt, dfn, low, articulation_point_collection);
// 以v为根节点的子树能访问到的祖先必然也能从u结点出发访问到,依此来更新u值
low[u] = min(low[u], low[v]);
// 以v为根节点的子树能访问到的最早的祖先为u或者v时,则可判断出顶点u(非根节点)为关节点
if (u != root && low[v] >= dfn[u]) is_cut = true;
}
// 当(u,v)边为回边时
// 使用v的深度优先遍历次序来更新low[u]
else if (v != parent) low[u] = min(low[u], dfn[v]);
}
// u为根节点且子树数量大于等于2的情况
if (n_subtree >= 2 && u == root) is_cut = true;
// 记录关节点
if (is_cut) articulation_point_collection.push_back(u);
}
测试
测试案例(graph_testing.cpp):
void test06(Graph<int> g) {
cout << "暴力求解得到的分割点为:";
auto articulation_points2 = g.articulation_points(1);
for (auto u : articulation_points2) cout << " " << u;
cout << endl;
cout << "Targan算法求得的分割点为:";
auto articulation_points1 = g.articulation_points(2);
for (auto u : articulation_points1) cout << " " << u;
cout << endl;
}
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;
// test01(g);
// test02(g);
// test03(g);
// test04(g);
Graph<int> g;
g.add_vertex(0);
g.add_vertex(1);
g.add_vertex(2);
g.add_vertex(3);
g.add_vertex(4);
g.add_vertex(5);
g.add_vertex(6);
g.add_vertex(7);
g.add_vertex(8);
g.add_vertex(9);
g.add_vertex(10);
g.add_vertex(11);
g.add_vertex(12);
g.add_edge(0, 1 , 1);
g.add_edge(0, 2, 1);
g.add_edge(1, 2, 1);
g.add_edge(1, 3, 1);
g.add_edge(1, 4, 1);
g.add_edge(3, 6, 1);
g.add_edge(4, 5, 1);
g.add_edge(7, 8, 1);
g.add_edge(7, 9, 1);
g.add_edge(7, 12, 1);
g.add_edge(8, 9, 1);
g.add_edge(8, 12, 1);
g.add_edge(9, 10, 1);
g.add_edge(9, 11, 1);
g.add_edge(9, 12, 1);
g.add_edge(10, 11, 1);
cout << "打印图中顶点及其邻接表的详细信息如下" << endl;
g.show();
cout << endl;
test06(g);
return 0;
}
运行结果: