图论系列文章
前言
在学习了前两章的内容后,接下来学习最小生成树算法。
最小生成树的意思是将所有的顶点都连通,所有的边加起来的权值最小的图(成形为树)。此类算法广泛用于城市建设中,例如让所有的城市全部连通,使其造价最小。
- 测试图
-
最小生成树
-
测试数据
7 10
0 1 5
0 2 2
1 3 1
2 3 6
1 4 6
3 4 1
3 5 2
2 5 8
4 6 7
5 6 3
一.Prim算法
普利姆算法(Prim)可以在加权连通图中搜索最小生成树,其最小生成树包含了连通图中的所有顶点,并且其所有边的权值之和最小。其时间复杂度为O(N2),比较适合稠密图,也就是弧比较多的图。
Prim算法核心
:
创建新的顶点集合U,以某一个顶点为起始顶点(通常以第0个顶点开始)。然后:
- 找到新集合U中所有顶点的相邻的权值最小的边,并加入该边相邻的新顶点;
- 将新加入的顶点的相邻边更新。
重复以上1,2步骤直到所以的顶点都加入到了新的顶点集合U中。
/*
Prim算法思量:
lowcost:记录的是每个顶点所在边的权重,以及顶点是否已经被访问;0代表该顶点已经访问,INT_MAX代表暂不连通,
正常数值代表该顶点连通的最小权值边
adjvex;记录目前加入顶点的相邻顶点
*/
int N, C;//N个顶点,C条边
void Prim(vector<vector<int>>& vertex)
{
vector<int> lowcost(N, INT_MAX), adjvex(N, 0);
for (int i = 0; i < N; ++i)
lowcost[i] = vertex[0][i];
for (int i = 0; i < N - 1; ++i) //将剩下的N-1个顶点依次加入集合
{
//找到现有集合中权值最小的边
int Min = INT_MAX, k = 0; //k是即将加入集合的下一个顶点,Min用来保存权值最小的边
for (int j = 0; j < N; ++j)
{
if (0 != lowcost[j] && lowcost[j] < Min) //顶点j没有被访问过,找到最小权值边相连的顶点
{
Min = lowcost[j];
k = j;
}
}
cout << "(" << adjvex[k] << "," << k << ") ";
//将k顶点加入集合,并且更新k顶点相邻的边
lowcost[k] = 0;
for (int j = 0; j < N; ++j)
{
if (0 != lowcost[j] && vertex[k][j] < lowcost[j])//顶点未被访问,k-j之间连通
{
lowcost[j] = vertex[k][j];
adjvex[j] = k;
}
}
}
}
int main()
{
cin >> N >> C;
vector<vector<int>> vertex(N, vector<int>(N, INT32_MAX));//由于是加权图,不连通则用INT32_MAX表示
for (int i = 0; i < C; ++i)
{
int m, n, weight;
cin >> m >> n >> weight;
vertex[m][n] = weight;//顶点m到顶点n连通
vertex[n][m] = weight;//无向图,顶点n到顶点m连通
}
for (int i = 0; i < N; ++i)
for (int j = 0; j < N; ++j)
if (i == j)
vertex[i][j] = 0;
Prim(vertex);
return 0;
}
二.Kruskal算法
Kruskal算法较为简单,只需要不断找到权值较小的边,依次连接好顶点即可;过程中需要注意顶点之间不能够成环。时间复杂度为O(eloge),适合稀疏图,也就是边比较少的图。
算法核心
:
- 将输入的边进行存储;
- 找到权值最小的边,连接好首尾顶点;
- 通过并查集判断有没有成环;
- 不断重复2,3步骤直到遍历完所有的边。
不断加入权值最小的弧,并判断该弧连接的顶点是否属于并查集中的同一个根,如果不是,那么加入该顶点。
- 并查集
为了防止最小生成树中没有环,我们需要使用到一种数据结构:并查集
并查集下标指向该顶点,存放的元素是其根,可以用来帮助我们合并两个源点不同的集合, 并且也可以当作树的路径生成来使用。
int find(vector<int> &union_find, int i)
{
if (union_find[i] != -1)
return find(union_find, union_find[i]);
return i;
}
vector<int> union_find(_vertexNum, -1); //并查集
- 实现程序
/*
Kruskal算法
*/
struct Edge
{
int beg;
int end;
int weight;
Edge(int a, int b, int c):beg(a), end(b), weight(c){}
};
int N, C;//N个顶点,C条边
//并查集查找,用于判断两个顶点是否在同一个集合中,最后返回的是集合根顶点的位置
int find(vector<int>& parent, int i)
{
while (parent[i] != -1)
i = parent[i];
return i;
}
void Kruskal(vector<Edge>& edges)
{
vector<int> parent(N, -1);//并查集,用于判断两个顶点是否在同一个集合中
for (int i = 0; i < C; ++i) //循环遍历每一条边,因为有并查集的存在,所以可以保证不成环
{
int m = find(parent, edges[i].beg);
int n = find(parent, edges[i].end);
if (m != n)//如果根节点不相同
{
cout << "(" << edges[i].beg << "," << edges[i].end << ") ";
parent[m] = n;
}
}
}
int main()
{
cin >> N >> C;
vector<Edge> edges;//对于Kruscal算法,只需要对边进行操作即可.
for (int i = 0; i < C; ++i)
{
int m, n, weight;
cin >> m >> n >> weight;
edges.push_back(Edge(m, n, weight));
}
//根据边的权值大小进行排序
sort(edges.begin(), edges.end(), [&](Edge& lhs, Edge& rhs) {
return lhs.weight < rhs.weight;
});
Kruskal(edges);
return 0;
}