以前学图论的时候感觉prim和kruskal理论都理解了,prim不就是基于点,kruskal不就是基于边嘛,自以为掌握了,但某一天发现需要自己写的时候却无从下手,所以还是决定把这两个算法写一遍。
最小生成树,就是以最小的代价将所有的点连通起来,打个比方就是将各个独立的村落修道路将所有村落连接起来,待会看图就可理解了。
关于这两算法的详细介绍可参考图的基本算法解析(最小生成树) - 综合编程类其他综合 - 红黑联盟,本文图是他的,代码也是参考他的依照自己的理解进行了一些小优化,下文会标出。
1、PrimPrim我个人的理解就是集合开始往外扩张。也就是部落从一个小村庄开始,他们想最有效的征服周围的村落,那么就把目标定为离该部落最近的村落,收复一个村落后把该点也加进来考虑下一个最近的村落,直至全部村落都在部落内(理解为修铁路也可以,将所有村落连起来)。
(1)选择起点
(2)从可移动的路径中找到最短路径
(3)判断此最短路径的两点是否已经在集合中,若在则删除此路径,不在则加入集合
(4)重复(2)(3)步骤直到所有点都已经在集合内,得出总路径
假设从a点开始,目前可移动的路径有7,4,那么选4,连接ac;
现在集合内拥有的点有a和c,可移动路径有7,6,8,9,选最小值6,连接cb;
重复执行,最后边的顺序为4,6,2,4,1,点的顺序为acbdfe;
#include
#include
#define INF 10000
using namespace std;
const int N = 6;//6个点嘛
bool visit[N];//此点是否已在集合内
int dist[N] = {0};//当前集合离各个点的最小距离
int graph[N][N] = { {INF,7,4,INF,INF,INF}, //邻接矩阵,INF代表两点之间不可达
{7,INF,6,2,INF,4},//一维数组代表点,二维代表该点到其他点的距离
{4,6,INF,INF,9,8},//对角线为自身到自身,所以都是INF
{INF,2,INF,INF,INF,7},
{INF,INF,9,INF,INF,1},
{INF,4,8,7,1,INF}
};
int prim(int cur)
{
int index = cur;//起始点
int sum = 0;//总路径
int i = 0;
int j = 0;
cout << index << " ";//debug输出起始点
memset(visit,false, sizeof(visit));//所有点未在集合内
visit[cur] = true;//起点加入集合
for(i = 0; i < N; i++)
dist[i] = graph[cur][i];//初始化,每个起点邻接的点的距离存入dist,这里存入 {INF,7,4,INF,INF,INF}
for(i = 1; i < N; i++)//因为起点已经初始化过,故少遍历一次,i=1,而非i=0
{
int minor = INF;//dist内的最小值
for(j = 0; j < N; j++)
{
if(!visit[j] && dist[j] < minor)//找到未访问的点中,距离当前最小生成树距离最小的点
{
minor = dist[j];//更改最小值
index = j;//最小边所连接的点
}
}
visit[index] = true;//该点加入集合
cout << index << " ";//debug输出,该点
sum += minor;//总路程更新
for(j = 0; j < N; j++)
{
if(!visit[j] && dist[j]>graph[index][j])//执行更新,更新集合内到每个点的最小路径,对比一下新加入的点是否有更近路径,只判断集合外的点
{
dist[j] = graph[index][j];
}
}
}
cout << endl;//换行
return sum;//返回最小生成树的总路径值
}
int main()
{
cout << prim(0) << endl;//从顶点a开始
return 0;
}
graph[N][N]二维邻接矩阵代表点(一维)和点到每个点的距离(二维),
visit[N]代表是否在集合内,
dist[N]代表当前集合到各个点的最小距离,
总体流程为:
(1)初始化,将起点加入集合,visit=true,并获取其各个路径加入dist
(2)遍历N-1次(起点已经单独判断过,所以次数-1),在dist找到最短路径(只判断集合外的点,即!visit[j]),将最短的路径所连接的点加入集合,即visit=true。
(3)加入了新点后,获取该点到其他点的距离(只判断集合外的点,即!visit[j]),若有到其他点的更短路径,更新dist。
(4)重复(2)(3)直至全部点加入集合。
注释应该写的比较详细,结合代码和注释应该不难看懂。
2、KruskalPrim理解为集合从一个点开始慢慢吸纳其它的点进来。Kruskal则是以边为出发点,最短的路径先连起来,连接成好几个小集合再合并。举上文的例子就是,各个点分别为一个部落,其中距离相近的两个部落先进行合并,合并成了几个中等规模部落,然后这几个部落再按照距离近的进行合并。怪怪的,看下流程吧:
(1)每个点作为一个集合,将边的距离进行升序排序
(2)找到最短的路径,判断该边的两顶点是否在同一集合内,若在则跳过,将该边踢出检测队列,若不在则合并两集合
(3)重复(2)直到所有顶点都在同一集合内
注意图跟我放的链接文章不太一样,仍然为Prim的图,9条边。
第一步找到最短的边,长度为1,连接ef;
第二步找到最短的边,长度为2,连接bd;
第三步找到最短的边,长度为4,连接ac(bf也可,看排序细节了);
第四步找到最短的边,长度为4,连接bf;
注意:此时若de存在长度为5的边不会连接,因为在同一集合内,已经连同了!!!
第五步找到最短的边,长度为6,连接bc;
#include
#include
#include
#define N 9
using namespace std;
class Node{
public://class默认private
int start;
int end;
int val;
};
vector V(N);//边
int edge[N][3] = { {0,1,7},//start起点,end终点,val路径长度
{0,2,4},
{1,2,6},
{1,3,2},
{1,5,4},
{2,4,8},
{2,5,9},
{3,5,7},
{4,5,1}
};
int father[N] = {0};//属于哪个集合,比如father[4]=0,4节点是属于0这个集合内的
int cap[N] = {0};//可以理解为集合优先级,也可以理解为吞并其它集合的次数,并非集合的元素个数,详细见下文
void make_set()//初始化集合,让所有的点都各成一个集合,每个集合都只包含自己
{
for(int i = 0; i < N; i++)
{
father[i] = i;//每个点各自为一个集合
cap[i] = 1;//优先级都为1
}
}
int find_set(int x)//判断一个点属于哪个集合,点如果都有着共同的祖先结点,就可以说他们属于一个集合
{
if(x != father[x])//如果点不在原始的集合,即已经加入了其它集合
{
father[x] = find_set(father[x]);//获取该点所在的上级集合,上级若也并非原始集合,则递归找到最终的集合
}
return father[x];
}
void Union(int x, int y)//将x,y合并到同一个集合
{
x = find_set(x);//找到最终集合
y = find_set(y);
if(x == y)//同一个集合则返回,无需合并
return;
if(cap[x] < cap[y])//优先级比他小,则归入他的集合
father[x] = y;//原来是find_set(y),但直接写y也一样
else
{
if(cap[x] == cap[y])//如果优先级相等
cap[x]++;//那么X的优先级增加,也可以理解成x吞并其它集合的次数,cap[x]不能理解为集合内元素的数量,不然此处应该为cap[x]+=cap[y];
father[y] = x;//y归入x ,x原为find_set(x)
}
}
int Kruskal(int n)
{
int sum = 0;
make_set();
for(int i = 0; i < N; i++)//将边的顺序按从小到大取出来
{
if(find_set(V[i].start) != find_set(V[i].end))//如果边的两个顶点不在一个集合中,就并到一个集合里,生成树的长度加上这条边的长度
{
Union(V[i].start, V[i].end); //合并两个顶点到一个集合
sum += V[i].val;//总路径增加
}
}
return sum;
}
int main()
{
for(int i = 0; i < N; i++)//初始化边的数据,在实际应用中可根据具体情况转换并且读取数据,这边只是测试用例
{
V[i].start = edge[i][0];
V[i].end = edge[i][1];
V[i].val = edge[i][2];
}
sort(V.begin(),V.end(),[](Node x,Node y)->bool{return x.val
edge[N][3]:N指的是9条边,3是边的三个属性,左边的顶点和右边的顶点、长度(无向图,并非单向,两点反过来也一样)
father[N]:该点属于哪个集合,需要递归判断,比如a属于b,但b又属于c,也就是a其实是属于c
cap[N] :可以理解为集合优先级,也可以理解为吞并其它集合的次数,并非集合的元素个数
(1)初始化,依照边的长度进行升序排序。各个点分别为自己的集合
(2)排序后遍历每条边,若边两点不在一个集合内,则合并,若在一个集合内则不处理直接跳过
(3)重复执行(2)直到所有点都在同一集合内
注意:如果报错应该是匿名函数执行不了,这部分是C++11的新特性,老版本的编译器可能执行不了,需要在编译器上更改配置,或者直接不用匿名函数,按照下文进行修改即可
//sort(V.begin(),V.end(),[](Node x,Node y)->bool{return x.val
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)