最小生成树 prim算法和kruscal算法
背景:用最小的路径把所有的节点连接起来,即在所有节点间构建一颗最小生成树。
怎么构造一条最小生成树呢,大概有两种思路。1.逐步添加点到树中 2.逐步添加边到树中
前者即是prim算法的思路,而后者则对应kruscal算法。
prim算法
prim算法的核心在于用一个数组表示所有节点到树的距离(minDist数组),然后依次把离节点最近的节点添加到树中,添加节点的同时,其他的节点离树的最短距离可能因为新节点的加入而改变。
因此prim算法的大致步骤分为:
1.选择离树最近的节点
2.将树添加到树中
3.更新minDist数组
下面以例题对prim算法进行分析,具体细节见代码
#include<iostream>
using namespace std;
#include<vector>
#include<climits>
int main() {
int v, e;//分别表示定点数和边数
cin >> v >> e;
//,用二维数组来存储边的权值,题目说两点之间距离最大为10000
vector<vector<int>>val(v + 1, vector<int>(v + 1, 10001));
for (int i = 1; i <= e; i++) {
int a, b, c;
cin >> a >> b >> c;
val[a][b] = c;
val[b][a] = c;
}
vector<int>minDist(v + 1, 10001);
vector<bool>inTree(v + 1, false);
//当进行v-1次遍历时,已经添加了v-1个节点,所有节点离树的距离已更新完毕
for (int i = 1; i < v; i++) {
//第一步,找到离树最近的节点,已经在树中的节点不用更新
int pos = -1;
int dist = INT_MAX;
for (int j = 1; j <= v; j++) {
if (!inTree[j] && minDist[j] < dist) {
pos = j;
dist = minDist[j];
}
}
//第二步,将节点加入树中
inTree[pos] = true;
//第三步,更新节点离树的距离
for (int j = 1; j <= v; j++) {
if (!inTree[j] && val[pos][j] < minDist[j]) {
minDist[j] = val[pos][j];
}
}
}
int res = 0;
//注意我们第一个节点初始为10001,而事实上第一个节点到树的距离为0,从
for (int i = 2; i <= v; i++) {
res += minDist[i];
}
cout << res << endl;
return 0;
}
思考:怎么把树的每条边输出呢?
回忆prim算法的三个步骤,我们是在第三个步骤中更新了节点到树的最短距离,是把pos节点到j节点的距离赋值给了minDist[j],因此我们只需再用一个parent数组,在更新j节点到树最短距离的同时,指明j节点连接的是哪个节点即可。
minDist[j] = val[pos][j];
parent[j]=pos;
kruscal算法
kruscal算法的思想是先对所有的边进行排序,然后逐次选择最短且符合条件的边不断放入集合。
如何选择最短的边,可以使用sort和compare()来对边的权值进行排序,也可以使用c++自带的数据结构multimap,我们知道map可以根据键值在插入时自动进行排序,为什么用multimap而不用map呢,因为我们这里是根据键值,即边的权值进行排序,比如有多条边权值为1,用map就只会保留最后一条权值为1的边。还有一个区别,因为可以有多个相同的键值,因此multimap不可以使用map[1]=val的形式输入数据,只能用insert。
这里的条件就是不能成环,例如已经添加了ac和bc,那么ab就不能再添加。看到这里,是不是思路的实现刚好符合并查集的功能。回顾一下并查集的功能:1.判断两个节点是否在同一个集合2.将两个节点添加到同一个集合。
下面以例题对kruscal算法进行分析,具体细节见代码
#include<iostream>
using namespace std;
#include<vector>
#include<climits>
#include<map>
int v,e;//表示顶点和边
vector<int>parent;//并查集
//判断两节点是否在同一集合
bool issame(int i,int j){
while(i!=parent[i])i=parent[i];
while(j!=parent[j])j=parent[j];
if(i==j)return true;
return false;
}
//初始化
void init(){
for(int i=0;i<=v;i++)parent[i]=i;
}
//将节点添加到并查集
void add(int i,int j){
while(i!=parent[i])i=parent[i];
while(j!=parent[j])j=parent[j];
parent[i]=j;
}
int main(){
cin>>v>>e;
parent.resize(v+1);
init();
multimap<int,pair<int,int>>val;//存储边
for(int i=0;i<e;i++){
int a,b,c;
cin>>a>>b>>c;
val.insert(make_pair(c,make_pair(a,b)));
}
int res=0;
for(auto it=val.begin();it!=val.end();it++){
int a=it->second.first;
int b=it->second.second;
if(issame(a,b))continue;
add(a,b);
res+=it->first;
}
cout<<res<<endl;
return 0;
}
总结
我们知道prim算法是点优先,而kruscal算法是边优先,主要的时间复杂度来自于对边进行快速排序,时间复杂度为O(n*log n),所以对于边相对来说比较少的情况,用kruscal算法。当边相对较多时,图更趋近于完全图,而我们实现prim算法时的时间复杂度和空间复杂度都只与点有关,时间复杂度为O(n²),用prim算法更优。