最小生成树算法 prime算法和克鲁斯卡尔算法
克鲁斯卡尔算法
思路 优先队列+并查集
Kuskal算法
【算法简介】:上一篇中的Prime算法是一种“加点式的算法”,而Kuskal算法是一种“加边式的算法”;Kuskal算法与Prime算法都是一种贪心算法,但Kruskal算法对图中存在相同权值的边时也有效。
【算法思想】:算法对权值从小到大排序,每次选取当前权值最小的边加入结点,直到所有的结点都已加入结点。算法中用到了并查集的思想(并查集),通过并查集来判断两个结点是否有共同的父节点,如果有,则表明两个结点已经联通。如果没有,就将两个结点联通,记录路径。
Kruskal算法也是采用贪心算法的思想,运行时间为O(nlogn)。
代码设计
1、利用优先级队列将权值小的边放到队列最前,优先出对,保证了每次选择的都是权值最小的边。
2、利用并查集的查找及结合把同处同一连通分量中的顶点连到同一父节点下。这样,每次判断是否构成回路,只要判断父节点是否相同的即可。
1.1 存图方式
使用结构体数组来存图;
//因为每条边需要保存数据 起始节点 ,到达节点 ,花费(路的长度)
struct edge{
int start;//出发节点
int target;//目标节点
int cost;//花费(路径的长度)
};
//因为定义了边类型,需要使用优先队列,即需要比大小,需要重新定义<
bool operator<( edge a, edge b ){//升序
if(a.cost>b.cost)
return true;
else
return false;
}
1.2 并查集思想(重点)
我们可以把每个连通分量看成一个集合,该集合包含了连通分量的所有点。而具体的连通方式无关紧要,好比集合中的元素没有先后顺序之分,只有“属于”与“不属于”的区别。图的所有连通分量可以用若干个不相交集合来表示。
分为三部分:
(1)初始化:使每个结点的初始根节点为自己,并且每个结点构成一颗树,树的深度是1;
(2)查找:使用递归来查找每个结点的父亲结点;
(3)合并:将不同父节点的结点合并;
注:这里的并查集是优化后的,即:进行了路径压缩。如果题目中无要求,可以只写简单的并查集算法
void inin(){//为每个节点初始化它的父节点
for(int i=0;i<nodeNum;i++){
father[i]=i;//初始化一下寻找父节点的数组
}
}
int find(int x){//查看节点x的最终父节点,即并查集,方便查看是否属于同一集合中
if(x == father[x])//此时该结点孤立没有父节点
return x;
else
find(father[x]);
}
bool join(int node_start,int node_target){//用于判断这俩结点是否在同一连通分支中,是,true,不是 false
//并查集的使用
int start_father = find(node_start);
int target_father = find(node_target);
if( start_father == target_father)//这俩结点在同一个连通分支中
return true;
if( start_father != target_father){//这俩结点不在同一个连通分支中
father[target_father] = find(start_father);//将两个点放到同一个集合中去
return false;
}
}
1.3 Kuskal算法主体
变量每个结点;
int Kruskal(){
inin();
int num=0;//已经处理了几个结点
while(!myqueue.empty()&& num != nodeNum - 1 ){
edge temp = myqueue.top();//返回队列顶部的元素
myqueue.pop();//出队队顶元素
if(!join( temp.start, temp.target)){//两个结点不在同一个并查集中(俩结点不在同一个集合中)
ans+=temp.cost;//最终答案总花费+连接这俩点的花费
num++;//被处理的节点数++
}
}
//多此一举的,更新一下每个节点的最终父节点
for (int i = 1; i <= nodeNum; i++) {
father[i] = find(father[i]);
}
return ans;
}
Kuskal算法完整代码实现:
#include<iostream>
#include<queue>
#define max 10000
using namespace std;
//最小生成树算法 prime算法和克鲁斯卡尔算法
//克鲁斯卡尔算法
//思路 优先队列+并查集
int edgeNum;//边数
int nodeNum;//结点数
int ans;//返回的最终答案
int father[max];//并查集用到的父节点 方便查看是否属于同一个集合中
//因为每条边需要保存数据 起始节点 ,到达节点 ,花费(路的长度)
struct edge{
int start;//出发节点
int target;//目标节点
int cost;//花费(路径的长度)
};
//因为定义了边类型,需要使用优先队列,即需要比大小,需要重新定义<
bool operator<( edge a, edge b ){//升序
if(a.cost>b.cost)
return true;
else
return false;
}
//定义关于边类型的优先队列,需要让花费小的边出队
priority_queue<edge> myqueue;
void inin(){//为每个节点初始化它的父节点
for(int i=0;i<nodeNum;i++){
father[i]=i;//初始化一下寻找父节点的数组
}
}
int find(int x){//查看节点x的最终父节点,即并查集,方便查看是否属于同一集合中
if(x == father[x])//此时该结点孤立没有父节点
return x;
else
find(father[x]);
}
bool join(int node_start,int node_target){//用于判断这俩结点是否在同一连通分支中,是,true,不是 false
//并查集的使用
int start_father = find(node_start);
int target_father = find(node_target);
if( start_father == target_father)//这俩结点在同一个连通分支中
return true;
if( start_father != target_father){//这俩结点不在同一个连通分支中
father[target_father] = find(start_father);//将两个点放到同一个集合中去
return false;
}
}
int Kruskal(){
inin();
int num=0;//已经处理了几个结点
while(!myqueue.empty()&& num != nodeNum - 1 ){
edge temp = myqueue.top();//返回队列顶部的元素
myqueue.pop();//出队队顶元素
if(!join( temp.start, temp.target)){//两个结点不在同一个并查集中(俩结点不在同一个集合中)
ans+=temp.cost;//最终答案总花费+连接这俩点的花费
num++;//被处理的节点数++
}
}
//多此一举的,更新一下每个节点的最终父节点
for (int i = 1; i <= nodeNum; i++) {
father[i] = find(father[i]);
}
return ans;
}
bool judge(){//判断最后是否生成一个连通分支
int flag = father[1];
for (int i = 2; i != nodeNum + 1; i++) {
if (flag != find(father[i])) {
return false;
}
}
return true;
}
int main()
{
cin>>nodeNum;
cin>>edgeNum;
while (!myqueue.empty()) {//防止队列不为空
myqueue.pop();
}
for(int i=0;i<edgeNum;i++){
edge temp;
cin>>temp.start>>temp.target>>temp.cost;
myqueue.push(temp);
}//到此优先队列没有问题
ans = Kruskal();
if (judge()) {
cout << ans << endl;
} else {
cout << "Don't exist!" << endl;
}
for (int i = 1; i <= nodeNum; i++) {
cout<<"结点"<<i<<"的根节点是"<<father[i]<<" "<<endl;
}
return 0;
}
/*
It daesn't matter the order of edge.
standard sample:
8
11
2 1 3
3 1 2
3 2 4
2 4 2
3 4 3
4 5 5
3 5 2
5 6 4
7 6 5
8 5 5
7 8 4
5 8
1 2 3
1 3 7
2 3 10
2 4 4
2 5 8
3 4 6
3 5 2
4 5 17
9 14
1 2 4
2 3 8
3 4 7
4 5 9
5 6 10
6 7 2
7 8 1
8 9 7
2 8 11
3 9 2
7 9 6
3 6 4
4 6 14
1 8 8
*/


Kruskal算法在找最小生成树结点之前,需要对权重从小到大进行排序。将排序好的权重边依次加入到最小生成树中(如果加入时产生回路就跳过这条边,加入下一条边),当所有的结点都加入到最小生成树中后,就找到了这个连通图的最小生成树~
y总的模板代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1e5+10,M=2*N;
const int INF=0x3f3f3f3f;
int n,m;// n是点数,m是边数
int p[N]; // 并查集的父节点数组
struct Edge{
int a,b,w;
// 重载小于运算符
bool operator< (const Edge &W) const {
return w<W.w;
}
}edges[M];
int find(int x) {
if(p[x]==x)
return p[x];
else
return p[x]=find(p[x]);
}
int kruskal()
{
sort(edges, edges + m);
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 ++ ;
}
}
if (cnt < n - 1) return INF;
return res;
}
int main() {
cin>>n>>m;
for(int i=0;i<m;i++) {
int a,b,w; cin>>a>>b>>w;
edges[i]={a,b,w};
}
int ans=kruskal();
if(ans==INF)
cout<<"impossible"<<endl;
else
cout<<ans<<endl;
return 0;
}
本文详细介绍了数据结构中的最小生成树算法之一——克鲁斯卡尔算法,强调了算法的核心思想是优先队列和并查集。通过权值排序和并查集判断边的连接,避免形成回路,最终构造最小生成树。文章包含存图方式、并查集的原理及其在算法中的应用,以及Kruskal算法的具体步骤和代码实现。
484

被折叠的 条评论
为什么被折叠?



