普里姆算法
上一篇关于克鲁斯卡尔算法。那么另一个对应的算法为普里姆算法。
思想:
同样在算法开始前对所有的边的权重进行排序。但是与克鲁斯卡尔算法不同的是该算法要求整个过程中至始至终都是一棵树。即边应该连接已经在树中的一个顶点。什么意思呢?下图所示为原来的一个图。
其算法的建立过程为以下几个步骤:
(1) | (2) | (3) |
(4) | (5) |
(6) |
但是前者要求的是每次都是插入权重最短的边。尽管结果与Kruskal算法是一样的。仔细的分析建立过程。同样的要避免回路的产生,但是因为是在同一棵树中,因此没有必要再使用根节点来区分处于不同的集合,单用个标记边上两个节点是否都已经被访问过,也就是插入了。就可以判断能否产生回路。因此新的数据结构的定义为:
struct NodeExtend//节点的结构体
{
int key;//值
bool visited;//表示访问与否
};
struct EdgeExtend//边的定义
{
NodeExtend *startNode;//起始节点
NodeExtend *endNode;//结束节点
int weight;//权重
bool visited;//表示是否已经加入到树中
};
对应的在程序中若边已经被访问过了,那么就遍历edges中的下一条边。
算法复杂度:
本程序中使用了递归算法。那么递归的次数是由节点的个数决定,但是在每次递归运行中需要对edges中的边进行遍历,寻找满足下列几个条件的边:1)值最短,2)有一个顶点已经在图中,3)该条边的加入不会使树产生回路。所以算法的复杂度为:O(v*e)。
主要代码:
//普里姆算法
void JarnikPrimAlgorithm()
{
numTime++;//迭代次数
for (int i=0;i<edgeNum;i++)
{
if (edges[i].visited==false)//若边已经被访问过了,则不需要再次的遍历
{
if (edges[i].startNode->visited&&edges[i].endNode->visited)
{
edges[i].visited=true;//避免产生回路,多次的访问未加入树中的节点,该边对应的两个节点均已经被放入了树中
continue;
}
else//若边中节点没有被访问过,这里分为3中,需要排除的是两个节点都没有被访问过
{
bool add=false;
if (size==0)//除了首次加入的节点,其它均直接被排除
{
edges[i].startNode->visited=true;
edges[i].endNode->visited=true;
size++;
add=true;//以标记来说明该边能否加入树中
}
else if(edges[i].startNode->visited||edges[i].endNode->visited)
{ //两个节点只有一个未被访问过
if (edges[i].startNode->visited==false)//若没有被访问过,将其进行标记
{
edges[i].startNode->visited=true;
}
else
{
edges[i].endNode->visited=true;
}
add=true;
}
if (add)
{
edges[i].visited=true;//将边的访问设为true。
cout<<endl<<"起始点:"<<edges[i].startNode->key
<<", 终止点:"<<edges[i].endNode->key
<<", 值:"<<edges[i].weight;
JarnikPrimAlgorithm();//继续从第一个节点开始查找。
}
}
}
else
{
continue;
}
}
}
};
运行结果:
分析:
从程序的运行结果中,可以看出,当第一条边3被加入到树中寻找的是下一条其顶点已经在树中的且权重较小的边,对应程序中的为7。当建立对应于Kruskal算法中另一棵子树的连接时,边5,6才逐步的加入到树的集合中。Prim算法减少了在Kruskal算法中对根节点的判断,也即联合的过程。算法更为的简单,容易理解,但是其复杂度要高于前者。
或许可以找到在寻找满足前面提到的3个条件的边上有更快的算法,使Prim算法得以改进。