采用C++实现了一个简单的图类(有权/无权,有向/无向),封装部分操作,包括深度、广度遍历,获取指定顶点的度,判断两个顶点是否连通,图的连通分量,求无向有权连通图的最小生成树(prim算法,kruskal算法),有向有权图的最短路径(dijkstra单源最短路径算法,floyd所有点对点之间的最短路径)等等,详细见如下实现
const int INTMAX = std::numeric_limits<int>::max();
enum class GraphType {
DUWG, // 有向无权图
DWG, // 有向有权图
UDUWG, // 无向无权图
UDWG, // 无向有权图
UNKNOW, // 未知类型
};
// 代价最小的边数据结构,用来辅助进行prim算法求解
struct CloseNode {
int adjvex = 0;
int lowcost = INTMAX;
};
class Graph {
public:
// 图的构造,通过传递顶点数量,边的集合(边通过两个顶点(点1,点2)(无权图)或者三个数据(有权图表示为(点1,点2,权值))表示,存储在vector<int>中),图的类型等
Graph(const int n, const vector<vector<int>> &edges, const GraphType gType) : len(n), type(gType), count(0),
hasBfs(false), hasDfs(false) {
assert(gType != GraphType::UNKNOW);
if (type == GraphType::DUWG || type == GraphType::UDUWG) {
// 无权图构造
matrix.resize(len + 1, vector<int>(len + 1, 0)); // 无权图邻接矩阵初始化为0
// for(int i=0; i<=len; ++i) {
// // 无权图i-i可达
// matrix[i][i] = 1;
// }
for (auto &edge : edges) {
assert(edge.size() == 2);
int v = edge[0];
int w = edge[1];
matrix[v][w] = 1; // 无权图赋值1表示两点之间连通
if (type == GraphType::UDUWG) {
// 无向图
matrix[w][v] = 1;
}
}
} else {
// 有权图构造
matrix.resize(len + 1, vector<int>(len + 1, INTMAX)); // 有权图邻接矩阵初始化为INT_MAX
for(int i=0; i<=len; ++i) {
// 有权图i-i权值为0
matrix[i][i] = 0;
}
for (auto &edge : edges) {
assert(edge.size() == 3);
int v = edge[0];
int w = edge[1];
int weight = edge[2]; // 权值
matrix[v][w] = weight; // 有权图赋权值
if (type == GraphType::UDWG) {
// 无向图
matrix[w][v] = weight;
}
}
}
}
// floyd算法求解有向有权图每一对顶点之间的最短路径, O(N*N*N)
vector<vector<int>> floydShortPath() {
auto distance = matrix;
for(int i=0; i<=len; ++i) {
distance[i][i] = 0;
}
for (int i = 0; i <= len; ++i) {
for (int v = 0; v <= len; ++v) {
for (int w = 0; w <= len; ++w) {
if (distance[v][i] == INTMAX || distance[i][w] == INTMAX) {
// v-i不可达或者i-w不可达,显然不可能通过i为中继优化v-w之间的最短路径距离
continue;
}
if (distance[v][i] + distance[i][w] < distance[v][w]) {
// 通过i为中继优化v-w之间的最短路径距离
distance[v][w] = distance[v][i] + distance[i][w];
}
}
}
}
return distance;
}
// dijkstra算法求解有向有权图的最短路径,从某个源点到其余顶点的最短路径, O(N*N)
vector<int> dijkstraShortPath(int v0) {
vector<int> distance(len + 1, INTMAX); // 记录v0到点i的最短路径距离
visited.clear();
visited.resize(len + 1, false);
visited[v0] = true;
distance[v0] = 0;
for (int i = 0; i <= len; ++i) {
if (matrix[v0][i] != INTMAX) {
// 初始化,如果存在从v0到i的边,则用其权值初始化
distance[i] = matrix[v0][i];
}
}
for (int i = 1; i <= len; ++i) {
int min = INTMAX;
int index;
for (int j = 0; j <= len; ++j) {
if (!visited[j] && distance[j] < min) {
min = distance[j];
index = j;
}
}
visited[index] = true;
for (int k = 0; k <= len; ++k) {
if (!visited[k] && matrix[index][k] != INTMAX && distance[k] > min + matrix[index][k]) {
distance[k] = min + matrix[index][k];
}
}
}
return distance;
}
// kruskal算法求解无向有权连通图的最小生成树,返回最小生成树的权值和
int kruskalMinTree() {
std::multimap<int, std::pair<int, int>> closedge;
for (int i = 1; i <= len; ++i) {
for (int j = 1; j <= len; ++j) {
closedge.insert(std::make_pair(matrix[i][j], std::pair<int, int>{i, j}));
}
}
UnionFindSpace::UnionFind uf(len); // 并查集,用于判断两个顶点是否已经关联,即已添加的路径中可以使两个顶点相通
vector<std::pair<int, int>> minTreeEdgeSet; // 逐步往里面添加最小生成树的边
int weight = 0;
while (minTreeEdgeSet.size() < len - 1) {
auto &lowEdge = *closedge.begin(); // <weight, pari<v, w>>
auto &points = lowEdge.second;
if (!uf.isConnected(points.first, points.second)) {
minTreeEdgeSet.push_back({points.first, points.second});
uf.unionElements(points.first, points.second);
weight += lowEdge.first;
}
// 删除当前最小权值得边,该边可能已经被添加到最小树中,也可能不满足条件(不在两个不通的连通分量重)而没有添加
closedge.erase(closedge.begin());
}
return weight;
}
// prim算法求解无向有权连通图的最小生成树,返回最小生成树的权值和
int primMinTree() {
assert(getConComponent() == 1); // 保证当前的图为连通图且只有一个连通分量,否则无法进行Prim算法
closedge.clear();
closedge.resize(len + 1, CloseNode());
visited.clear();
visited.resize(len + 1, false);
visited[1] = true;
// 添加顶点1后初始化closedge
for (int i = 1; i <= len; ++i) {
if (!visited[i]) {
closedge[i].adjvex = 1;
closedge[i].lowcost = matrix[1][i];
}
}
int weight = 0;
vector<std::pair<int, int>> minTreeEdgeSet; // 逐步往里面添加最小生成树的边
for (int i = 2; i <= len; ++i) {
int index = 0;
int min = INTMAX;
// 从余下未添加的节点中寻找一个最小权值
for (int k = 1; k <= len; ++k) {
if (!visited[k] && min > closedge[k].lowcost) {
index = k;
min = closedge[k].lowcost;
}
}
visited[index] = true;
weight += min;
minTreeEdgeSet.push_back({closedge[index].adjvex, index});
// 新添加一个顶点后,刷新closedge集合
for (int j = 1; j <= len; ++j) {
if (!visited[j] && matrix[index][j] < closedge[j].lowcost) {
closedge[j].lowcost = matrix[index][j];
closedge[j].adjvex = index;
}
}
}
assert(len - 1 == minTreeEdgeSet.size()); // 最小生成树的边为n-1(n为节点数量)
std::for_each(minTreeEdgeSet.cbegin(), minTreeEdgeSet.cend(),
[](std::pair<int, int> p) { std::cout << p.first << " - " << p.second << std::endl; });
return weight;
}
// 求图中指定顶点的度,注意有向图和无向图的区别
int getDegree(int v) {
int degree = 0;
for (int flag : matrix[v]) {
degree += flag;
}
if (type == GraphType::DUWG) {
// 有向图需要加上入度
int inDegree = 0;
for (int i = 1; i <= len; ++i) {
inDegree += matrix[i][v];
}
degree += inDegree;
}
return degree;
}
// 获取指定顶点(存储点)的邻接点(存储点)
vector<int> getAdjPoint(const int v) {
vector<int> adjPoints;
auto &ws = matrix[v]; // 顶点v和其他定点是否有边的数组结合
for (int i = 1; i <= len; ++i) {
if (ws[i] == 0) {
// 与v不邻接
continue;
}
adjPoints.push_back(i);
}
return adjPoints;
}
// 图的深度遍历
void dfsTraverse() {
hasDfs = true;
count = 0; // 连通分量初始化
visited.clear(); // 数据清理,非常重要
visited.resize(len + 1, false); // 访问标志初始化
id.clear(); // 数据清理,非常重要
id.resize(len + 1, 0); // 每个顶点的连通分量初始化,从下标1开始使用
std::cout << "start dfsTraverse: ";
for (int v = 1; v <= len; ++v) {
if (!visited[v]) {
++count; // 连通分量
dfs(v);
}
}
std::cout << "END\n" << std::flush;
}
// 获取连通分量
int getConComponent() {
if (!hasDfs) {
dfsTraverse();
}
return count;
}
// 判断两个定点是否相连
bool isConnected(int v, int w) {
if (!hasDfs) {
dfsTraverse();
}
return id[v] == id[w];
}
// 图的广度遍历
void bfsTraverse() {
visited.clear(); // 数据清理,非常重要
visited.resize(len + 1, false); // 访问标志初始化
from.clear();
from.resize(len + 1, 0);
std::queue<int> q; // 队列初始化,逻辑点
std::cout << "start bfsTraverse: ";
for (int v = 1; v <= len; ++v) {
if (!visited[v]) {
std::cout << v << " -> "; // 访问并打印该节点,注意,应该在最靠近visited判断的地方进行访问,且在访问后立即进行visited标记
visited[v] = true;
q.push(v);
while (!q.empty()) {
int w = q.front();
q.pop();
auto adjPoints = getAdjPoint(w);
for (auto adjp : adjPoints) {
// 将v的邻接点加入到队列
if (!visited[adjp]) {
std::cout << adjp << " -> ";
from[adjp] = w; // (w, adjp)存在一条边
visited[adjp] = true;
q.push(adjp);
}
}
}
}
}
std::cout << "END\n" << std::flush;
}
// 返回v -> w的最短路径[v, ..., w],如果不存在返回空
vector<int> getShortPathFromUnWeighted(int v, int w) {
if (!isConnected(v, w)) {
return vector<int>();
}
if (!hasBfs) {
bfsTraverse();
}
std::stack<int> record;
record.push(w);
while (from[w] != v) {
record.push(from[w]);
w = from[w];
}
record.push(v);
vector<int> res;
std::cout << "start output unweighted graph shorted path: \n";
while (!record.empty()) {
int t = record.top();
std::cout << t << " -->> ";
res.push_back(t);
record.pop();
}
std::cout << "END\n" << std::flush;
return res;
}
private:
// 深度遍历递归公共函数,从顶点v(逻辑点)开始遍历
void dfs(const int v) {
id[v] = count; // 记录当前访问顶点的连通分量
visited[v] = true;
std::cout << v << " -> "; // 访问并打印该节点
auto adjPoints = getAdjPoint(v);
for (int w : adjPoints) {
// 与v邻接的定点
if (!visited[w]) {
// 对v的尚未访问邻接节点递归调用dfs
dfs(w); // 存储点 +1 转换为逻辑点
}
}
}
private:
vector<vector<int>> matrix; // 图的邻接矩阵存储,从1行1列开始存储,0行0列保留不使用
GraphType type; // 图类型
int len = 0; // 顶点数量
vector<bool> visited; // 是否被访问标志,从下标1开始使用
int count = 0; // 连通分量
vector<int> id; // 顶点的连通分量,如果id[v] == id[w],则v,w两点间连通,从下标1开始使用
bool hasDfs = false; // 是否已进行深度遍历
bool hasBfs = false; // 是否已进行广度遍历
vector<CloseNode> closedge; // prim算法辅助数组
vector<int> from; // 记录广度优先遍历中每个节点的上一个被访问节点,即如果(v,w)存在表,则from[w] = v,用于无权图的最短路径记录
};
测试
1)无权图一般测试
void graphTest() {
Graph dg1(4, vector<vector<int>>{{1, 2},
{1, 3},
{3, 4},
{4, 1}}, GraphType::DUWG);
Graph udg2(5, vector<vector<int>>{{1, 2},
{1, 4},
{2, 3},
{2, 5},
{3, 4},
{3, 5}}, GraphType::UDUWG);
Graph udg3(13, vector<vector<int>>{{1, 2},
{1, 3},
{1, 6},
{1, 12},
{2, 13},
{4, 5},
{7, 8},
{7, 9},
{7, 11},
{8, 11},
{10, 12},
{10, 13},
{12, 13}}, GraphType::UDUWG);
Graph udg4(8, vector<vector<int>>{{1, 2},
{1, 3},
{2, 4},
{2, 5},
{4, 8},
{5, 8},
{3, 6},
{3, 7},
{6, 7}}, GraphType::UDUWG);
Graph udg5(6, vector<vector<int>>{{1, 2},
{1, 3},
{2, 4},
{2, 5},
{3, 6}}, GraphType::UDUWG);
// 测试顶点的度
assert(3 == dg1.getDegree(1));
assert(1 == dg1.getDegree(2));
assert(2 == dg1.getDegree(3));
assert(2 == dg1.getDegree(4));
assert(2 == udg2.getDegree(1));
assert(3 == udg2.getDegree(2));
assert(3 == udg2.getDegree(3));
assert(2 == udg2.getDegree(4));
assert(2 == udg2.getDegree(5));
assert(4 == udg3.getDegree(1));
assert(1 == udg3.getDegree(3));
assert(2 == udg3.getDegree(2));
assert(3 == udg3.getDegree(7));
assert(2 == udg3.getDegree(10));
// 图的遍历,连通分量
dg1.dfsTraverse();
dg1.bfsTraverse();
udg2.dfsTraverse();
udg2.bfsTraverse();
udg5.dfsTraverse();
udg5.bfsTraverse();
udg4.dfsTraverse();
udg4.bfsTraverse();
assert(1 == udg4.getConComponent());
assert(udg4.isConnected(1, 2));
assert(udg4.isConnected(2, 3));
assert(udg4.isConnected(3, 4));
assert(udg4.isConnected(4, 5));
assert(udg4.isConnected(5, 6));
assert(udg4.isConnected(6, 7));
udg3.dfsTraverse();
udg3.bfsTraverse();
assert(3 == udg3.getConComponent());
assert(udg3.isConnected(1, 2));
assert(udg3.isConnected(2, 3));
assert(udg3.isConnected(3, 6));
assert(udg3.isConnected(6, 12));
assert(udg3.isConnected(12, 10));
assert(udg3.isConnected(10, 13));
assert(udg3.isConnected(4, 5));
assert(udg3.isConnected(7, 8));
assert(udg3.isConnected(8, 9));
assert(udg3.isConnected(9, 11));
return;
}
2)无向有权图最小生成树测试
void minTreeTest() {
Graph udwg1(6, vector<vector<int>>{{1, 2, 6},
{1, 3, 1},
{1, 4, 5},
{2, 5, 3},
{2, 3, 5},
{4, 3, 5},
{4, 6, 2},
{3, 5, 6},
{3, 6, 4},
{5, 6, 6}}, GraphType::UDWG);
assert(15 == udwg1.primMinTree());
assert(15 == udwg1.kruskalMinTree());
return;
}
3)有向有权图最短路径测试
void shortPathTest() {
Graph dwg(6, vector<vector<int>>{{0, 5, 100},
{0, 4, 30},
{0, 2, 10},
{1, 2, 5},
{2, 3, 50},
{3, 5, 10},
{4, 3, 20},
{4, 5, 60}}, GraphType::DWG);
auto d = dwg.dijkstraShortPath(0);
assert(0 == d[0]);
assert(INTMAX == d[1]);
assert(10 == d[2]);
assert(50 == d[3]);
assert(30 == d[4]);
assert(60 == d[5]);
auto dis = dwg.floydShortPath();
assert(0 == dis[0][0]);
assert(INTMAX == dis[0][1]);
assert(10 == dis[0][2]);
assert(50 == dis[0][3]);
assert(30 == dis[0][4]);
assert(60 == dis[0][5]);
assert(INTMAX == dis[1][0]);
assert(0 == dis[1][1]);
assert(5 == dis[1][2]);
assert(55 == dis[1][3]);
assert(INTMAX == dis[1][4]);
assert(65 == dis[1][5]);
assert(INTMAX == dis[2][0]);
assert(INTMAX == dis[2][1]);
assert(0 == dis[2][2]);
assert(50 == dis[2][3]);
assert(INTMAX == dis[2][4]);
assert(60 == dis[2][5]);
assert(INTMAX == dis[3][0]);
assert(INTMAX == dis[3][1]);
assert(INTMAX == dis[3][2]);
assert(0 == dis[3][3]);
assert(INTMAX == dis[3][4]);
assert(10 == dis[3][5]);
assert(INTMAX == dis[4][0]);
assert(INTMAX == dis[4][1]);
assert(INTMAX == dis[4][2]);
assert(20 == dis[4][3]);
assert(0 == dis[4][4]);
assert(30 == dis[4][5]);
assert(INTMAX == dis[5][0]);
assert(INTMAX == dis[5][1]);
assert(INTMAX == dis[5][2]);
assert(INTMAX == dis[5][3]);
assert(INTMAX == dis[5][4]);
assert(0 == dis[5][5]);
return;
}