一定要看这个链接的讲解视频:强烈推荐!!!【图-最小生成树-Prim(普里姆)算法和Kruskal(克鲁斯卡尔)算法】
文章目录
- 1.题目
- 2.Kruskal基本思想:
- 3.逐行解释代码:
1.题目
2.Kruskal基本思想:
Kruskal算法是一种用于求解加权连通图的最小生成树问题的贪心算法。它的基本思想可以概括为以下几个步骤:
-
边排序:将图中的所有边按照权重从小到大进行排序。
-
初始化并查集:创建一个并查集来跟踪图中的各个连通分量。开始时,每个顶点都是一个独立的连通分量,即每个顶点的父节点指向自己。
-
选择边:从排序后的边列表中选择边,每次选择时,都选择当前最小的边。为了确保添加的边不会形成环,使用并查集来检查当前选择的边的两个顶点是否属于同一个连通分量。
-
检查连通分量:在添加边之前,使用并查集的
find
操作来确定边的两个顶点是否已经在同一个连通分量中。如果两个顶点已经在同一个连通分量中,则跳过这条边;如果不在同一个连通分量中,则将它们合并到同一个连通分量中。 -
构建最小生成树:重复步骤3和步骤4,直到添加了 ( n-1 ) 条边(其中 ( n ) 是图中顶点的数量),这时就构成了一棵包含所有顶点的最小生成树。
-
结束条件:如果在选择边的过程中,发现剩余的边不足以连接所有顶点(即连通分量的数量大于1),则说明图不连通,无法构建最小生成树。
Kruskal算法的效率主要依赖于边排序和并查集的操作。边排序通常使用归并排序或其他高效的排序算法来完成,而并查集提供了快速的连通分量检查和合并操作。算法的时间复杂度主要取决于排序和并查集操作的时间复杂度,通常为 O ( E log E ) O(E \log E) O(ElogE),其中 E E E是边的数量。
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010, M = 200010, INF = 0x3f3f3f3f;
int n, m;
int p[N];
struct Edge
{
int a, b, w;
//对边权排序的时候会进行Edge的比较,因此需要重载operator<
bool operator< (const Edge &W)const
{
return w < W.w;
}
}edges[M];
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int kruskal()
{
sort(edges, edges + m);
for (int i = 1; i <= n; i ++ ) p[i] = i; // 初始化并查集
int res = 0, cnt = 0;
for (int i = 0; i < m; i ++ )
{
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
a = find(a), b = find(b);
if (a != b)//不在一个连通分量里面,没有成环,可以在ab点之间添加进去一条边,
{
p[a] = b;
res += w;
cnt ++ ;
}
}
//n个点,最小生成树里面应该包含n-1条边,如果cnt小于n-1说明有的不连通,只能放弃
if (cnt < n - 1) return INF;
return res;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 0; i < m; i ++ )
{
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
edges[i] = {a, b, w};
}
int t = kruskal();
if (t == INF) puts("impossible");
else printf("%d\n", t);
return 0;
}
3.逐行解释代码:
当然,下面是对代码中每一行的详细解释:
#include <cstring>
包含 C 标准库中的字符串处理函数。
#include <iostream>
包含 C++ 标准库中的输入输出流相关函数。
#include <algorithm>
包含 C++ 标准库中的算法相关函数,比如排序函数 sort
。
using namespace std;
使用标准命名空间,这样在使用标准库中的函数和对象时就不需要加 std::
前缀。
const int N = 100010, M = 200010, INF = 0x3f3f3f3f;
定义常量 N
为顶点的最大数量,M
为边的最大数量,INF
为一个很大的数,表示无法连接的状态。
int n, m;
定义变量 n
存储顶点数量,m
存储边的数量。
int p[N];
定义数组 p
用于存储并查集的父节点信息。
struct Edge
{
int a, b, w;
定义结构体 Edge
用于存储边的信息,包括两个顶点 a
和 b
,以及边的权重 w
。
bool operator< (const Edge &W)const
{
return w < W.w;
}
重载小于运算符,用于比较两条边的权重,以便后续排序。
}edges[M];
定义数组 edges
存储所有的边。
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
定义 find
函数,用于并查集的查找操作,如果 x
不是其自己的父节点,则递归查找其父节点,并进行路径压缩。
int kruskal()
{
sort(edges, edges + m);
定义 kruskal
函数,并在函数内部对所有边按照权重进行排序。
for (int i = 1; i <= n; i ++ ) p[i] = i;
初始化并查集,每个顶点的父节点都是自己。
int res = 0, cnt = 0;
定义变量 res
用于存储最小生成树的总权重,cnt
用于存储已经加入最小生成树的边的数量。
for (int i = 0; i < m; i ++ )
{
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
遍历所有边,提取每条边的两个顶点和权重。
a = find(a), b = find(b);
对每条边的两个顶点进行查找,获取它们的根节点。
if (a != b)
{
p[a] = b;
res += w;
cnt ++ ;
}
}
如果两个顶点的根节点不同,说明它们不属于同一个集合,可以将它们合并,更新并查集,累加权重到 res
,并增加 cnt
。
if (cnt < n - 1) return INF;
return res;
}
如果加入最小生成树的边的数量小于顶点数量减一,说明无法构成最小生成树,返回 INF
;否则返回最小生成树的总权重。
int main()
{
scanf("%d%d", &n, &m);
在 main
函数中,使用 scanf
函数读取顶点和边的数量。
for (int i = 0; i < m; i ++ )
{
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
edges[i] = {a, b, w};
}
读取每条边的信息,包括两个顶点和边的权重,并将它们存储到 edges
数组中。
int t = kruskal();
调用 kruskal
函数计算最小生成树的权重。
if (t == INF) puts("impossible");
else printf("%d\n", t);
根据 kruskal
函数的返回值判断是否能够构成最小生成树,如果不能则输出 “impossible”,否则输出最小生成树的总权重。
return 0;
}
程序正常结束,返回 0。