本文所有代码都基于Java实现图的存储和创建一文所实现的带权无向图
普里姆算法
普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树。意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点(英语:Vertex (graph theory)),且其所有边的权值之和亦为最小。
Prim算法构造最小生成树的过程如下图所示:
初始时从图中任取一顶点(如顶点A)加入树T,此时树中只有一个顶点,之后选择一个与当前T中顶点集合距离最近的顶点,并将该顶点和相应的边加入T,每次操作后T中的顶点数和边数都加1。以此类推,直至图中所有顶点都并入T,得到的就是最小生成树。初始T必有n-1
条边。
Prim算法的步骤如下:
假设
G={V,E}
是连通图,其最小生成树T={U,Et}
,Et
是最小生成树中边的集合。
初始化: 向空树T={U,Et}
中添加图G={V,E}
的任一顶点u0,使U={u0}
,Et≠空集
。
循环: (重复下列操作直至U=V
),从图G中选择满足{(u,v)|u∈U,v∈U-V}
且具有最小权值的边(u,v)
,加入树T,置U=U∪{v},Et = Et∪{(u,v)}
。
简单来说就是先往空树中添加任一顶点,然后把这颗树以及图去掉被添加的顶点和边剩余的部分分别视为两部分,然后不断选取连接两部分且权值最小的边,直至所有顶点都被加入树中。
代码实现
public graph_t getMSTbYPrim(){
//普里姆算法求最小生成树
//初始化空树
graph_t res = new graph_t();
//从当前图的第一个顶点开始生成
VNode curNode = this.vertices.get(0);
res.addNode(new VNode(curNode.getName()));
//初始化边集合
ArrayList<ArcNode> arcList = new ArrayList<>();
//将添加的顶点所连接的所有边添加进边集合,后序被添加的顶点也需要此操作
ArcNode tmpArcNode = curNode.first;
while (tmpArcNode != null){
arcList.add(new ArcNode(tmpArcNode));
tmpArcNode = tmpArcNode.next;
}
while (res.vertices.size() < this.vertices.size()){
//选出符合条件的边
ArcNode curArc = this.getMinimumSpanArc(res,arcList);
//找到新加入的顶点,根据我的设计逻辑需要先添加顶点,再添加边
//因为每条边在添加时都会被添加两次,若先添加边,则添加时另一
// 个顶点还不在图中,导致邻接表中每条边只出现一次(应该出现两次)
if (res.containVnode(curArc.node1.getName()) >= 0){
curNode = curArc.node2;
}else{
curNode = curArc.node1;
}
//加入顶点
res.addNode(new VNode(curNode.getName()));
//将此边加入生成树
res.addArc(new ArcNode(curArc));
//将添加的顶点所连接的所有边添加进边集合,后序被添加的顶点也需要此操作
tmpArcNode = curNode.first;
while (tmpArcNode != null){
arcList.add(new ArcNode(tmpArcNode));
tmpArcNode = tmpArcNode.next;
}
}
return res;
}
private ArcNode getMinimumSpanArc(graph_t g,ArrayList<ArcNode> arcList){
//prim算法的辅助函数,从边集合中选出连接res和其余顶点的权值最小的边
ArcNode res = new ArcNode(arcList.get(0));
//将初始权重设为极大值,否则可能出现以下情况:
//该边不满足连接条件但是其权值比所有满足连接条件的边都小,从而导致无法找到正确的边
res.setWeight(Integer.MAX_VALUE);
for (ArcNode arc:arcList) {
//两种情况,满足其一即满足连接两边的条件
arc = new ArcNode(arc);
boolean linked1 = g.containVnode(arc.node1.getName()) >= 0 && g.containVnode(arc.node2.getName()) < 0;
boolean linked2 = g.containVnode(arc.node2.getName()) >= 0 && g.containVnode(arc.node1.getName()) < 0;
if((linked1||linked2)&&arc.getWeight() < res.getWeight()){
res = arc;
}
}
//找到的同时要将此边从边集合中去除
arcList.remove(res);
//处理该边,使其不与其他边有联系
res.next = null;
return res;
}
代码主要涉及两个方法,其中public graph_t getMSTbYPrim()
是Prim算法的主方法,另外一个方法private ArcNode getMinimumSpanArc(graph_t g,ArrayList<ArcNode> arcList)
用来从边集合中选取符合条件的边,每次被选中的都是将被加入生成树的边。
克鲁斯卡尔算法
与Prim算法从顶点开始扩展最小生成树不同,Kruskal(克鲁斯卡尔)算法是一种按权值的递增次序选择合适的边来构造最小生成树的方法。
克鲁斯卡尔算法构造最小生成树的过程如下图所示:
初始时为只有n个顶点而无边的非连通图T={V,{}},每个顶点自成一个连通分量,然后按照边的权值由小到大的顺序,不断选取当前未被选取且权值最小的边若该边依附的顶点落在T中不同的联通分量上,则将此边加入T,否则舍弃此边而选择下一条权值最小的边。以此类推,直至T中所有顶点都在一个联通分量上。
Kruskal算法的步骤如下:
假设
G={V,E}
是连通图,其最小生成树T={U,Et}
。
初始化:U = V, Et = 空集
。即每个顶点构成一棵独立的树,T
此时是一个仅含|V|
个顶点的森林。
循环 (重复下列操作直至T
是一棵树): 按G的边的权值递增顺序依次从E-Et
中选择一条边,若这条边加入T
后不构成回路,则将其加入Et
,否则舍弃,知道Et中含有n-1
条边。
代码实现
public graph_t getMSTByKruskal(){
//克鲁斯卡尔算法求最小生成树
//初始化生成树
graph_t res = new graph_t();
//初始化边集合
ArrayList<ArcNode> arcList = new ArrayList<>();
//将图的所有边加入边集合
for (VNode vnode:this.vertices) {
ArcNode tmpArc = new ArcNode(vnode.first);
while (tmpArc != null){
arcList.add(tmpArc);
tmpArc = tmpArc.next;
}
}
while (res.getVexNum() < this.getVexNum() || res.getConnNum() > 1){
//当生成树中顶点数小于图中定点数或者生成树还不是一个连通图时说明生成还未完成
//选出满足条件的权值最小的边
ArcNode selectedArc = getArcForKruskal(res,arcList);
if(res.containVnode(selectedArc.node1.getName()) < 0){
//若顶点1未被连接则添加该顶点
res.addNode(new VNode(selectedArc.node1.getName()));
}
if(res.containVnode(selectedArc.node2.getName()) < 0){
//若顶点2未被连接则添加该顶点
res.addNode(new VNode(selectedArc.node2.getName()));
}
//添加该边
res.addArc(new ArcNode(selectedArc));
}
return res;
}
private ArcNode getArcForKruskal(graph_t g,ArrayList<ArcNode> arcList){
//克鲁斯卡尔算法的辅助函数,从边集合中选出符合条件的边
ArcNode res = new ArcNode(arcList.get(0));
res.setWeight(Integer.MAX_VALUE);
for (ArcNode arc:arcList) {
arc = new ArcNode(arc);
//若顶点1已被连接
boolean linked1 = g.containVnode(arc.node1.getName()) >= 0 ;
//若顶点2已被连接
boolean linked2 = g.containVnode(arc.node2.getName()) >= 0 ;
if(((!(linked1&&linked2))||(!g.canReach(arc.node1,arc.node2)))&&arc.getWeight() < res.getWeight()){
//只要不是两个顶点都已被连接或者二者不属于同一联通分量就满足连接条件
res = arc;
}
}
//找到的同时要将此边从边集合中去除
arcList.remove(res);
//处理该边,使其不与其他边有联系
res.next = null;
//不这样处理的话,该边所引用的两个顶点还是之前图中的顶点
res.setNode1(new VNode(res.node1.getName()));
res.setNode2(new VNode(res.node2.getName()));
return res;
}
public boolean canReach(VNode node1,VNode node2){
//通过dfs判断从node1出发是否能到达node2,如果能则二者属于同一连通分量,否则不属于
//该方法与BFS思路一样只是将队列改为栈
//访问标记数组初始化
graph_t.visited = new boolean[this.vexNum];
for (int i = 0; i < this.vexNum; i++) {
visited[i] = false;
}
Stack<VNode> stack = new Stack<>();
stack.push(this.vertices.get(this.containVnode(node1.getName())));
while (!stack.isEmpty()){
VNode node = stack.pop();
int curIndex = this.containVnode(node.getName());
if (!graph_t.visited[curIndex]){
// tmpRes.add(node.getName());
graph_t.visited[curIndex] = true;
//如果node后面没有边,直接调用new ArcNode(node.first)会导致空指针异常
ArcNode tmpArcNode = node.first == null ? null : new ArcNode(node.first);
while (tmpArcNode != null){
if (node.getName().equals(tmpArcNode.getNode1().getName())){
//此时node2试邻接点
stack.push(this.getVNodeByName(tmpArcNode.getNode2().getName()));
}else{
//否则node1为邻接点
stack.push(this.getVNodeByName(tmpArcNode.getNode1().getName()));
}
tmpArcNode = tmpArcNode.next;
}
}
}
return graph_t.visited[this.containVnode(node2.getName())];
}
public int getConnNum(){
//通过获取图的连通分量个数来判断图是否为来连通图
//通过改造DFSTraverse方法来获取连通分量个数,DFS函数执行的次数对应着连通分量个数
graph_t.visited = new boolean[this.vexNum];
for (int i = 0; i < this.vexNum; i++) {
visited[i] = false;
}
ArrayList<String> res = new ArrayList<>();
int connNum = 0;
for (int i = 0; i < this.vexNum; i++) {
if(!graph_t.visited[i]){
res = this.DFS(i,res);
//每执行一个该函数,连通分量个数加一
connNum++;
}
}
return connNum;
}
克鲁斯卡尔算法虽然逻辑上比普里姆算法更加简单清晰,但其实现起来要比普里姆算法稍难一些,主要是因为克鲁斯卡尔算法在选择边时需要判断两个顶点是否属于同一连通分量,于是我又额外添加了两个方法public int getConnNum()
和private ArcNode getArcForKruskal(graph_t g,ArrayList<ArcNode> arcList)
,其中前者用来计算一个图中共有几个连通分量,借助它可以判断算法何时结束,后者用来判断两个顶点是否属于同一连通分量,这两个方法的实现思路都已写在注释里了。
运行效果
测试运行的代码:
public static void main(String[] args) {
graph_t g = new graph_t();
g.create();
g.showGraph();
System.out.println("---------------克鲁斯卡尔算法-----------------------");
graph_t MST = g.getMSTByKruskal();
MST.showGraph();
System.out.println("---------------普里姆算法-----------------------");
graph_t mst_p = g.getMSTbYPrim();
mst_p.showGraph();
}
输入输出
输入图即为上面描述两种算法构造过程的图片中的带权无向图
输出结果如下:
如有错误恳请指正