本篇介绍解决最小生成树问题的另一个贪心算法:Kruskal算法
Kruskal算法
什么是Kruskal算法
输入:图G=(V,E,W)
输出:G的最小生成树T
1、按照长度从小到大排序
2、以此考察当前最短边e,如果e与T的边不构成回路,则把e加入树T,否则跳过e,直到选择了n-1条边为止
下面我们看个例子:
还是以上一篇那个实例进行说明
首先对边进行从小到大排序,然后考虑当前最短边。从图中看长度为1是最短边,所以把那个长度为1的边加到T中
然后再从剩下的边中进行选择,找到了最短的边长度为2。将这个边加入到T中
再从剩下的边中找到了最短的边长度为3,将这个边加入到T中
同样的,将长度为4的边加入到T中
最后我们考虑长度为5的边,我们看到如果连接3,4则3,4,6会构成回路;如果连接1,4则1,3,6,4会构成回路。那么只能连接2和3。将这条边加入到T中
至此红色的边及其顶点所构成的就是一棵最小生成树。
下面用数学归纳法证明Kruskal算法
用数学归纳法证明Kruskal算法
对于任意n,算法对n个顶点构成的图(n阶图)找到一棵最小生成树
n=2时,两个顶点只能连成一条边,即G中只有一条边。最小生成树就是G。所以归纳基础是正确的。
下面进行归纳步骤
假设算法对于n阶图是正确的,证明对于n+1阶图,算法也能找到一棵最小生成树
证明如下:
我们对任意的n+1阶图G,将其中含有最短边e的两个顶点合二为一(这种合二为一的操作称作短接操作),得到一个n阶图G’
我们上面的图是短接之前的图,即n+1阶图,然后将最短边e的两个顶点合二为一,就成为了下面那个图,当然两个点分别相连的边,经合二为一后变成一个点与这些边相连。
那么对于下面的这个n阶图来说,可以用归纳假设让算法找到一个最小生成树T’。
然后我们再拉伸回来,让那个合二为一的点分开成两个点
分开之后,刚才由一个点连的边再还回去,得到这一个生成树T
下面证明T是一棵最小生成树
如若不是,存在一棵最小生成树T1,这个T1是包含e的(如果e不在T’里面,那么可以加上边e构成回路,去掉回路中任意别的边所得的生成树的权仍旧最小)。那么就会有T1的权值小于T
下面我们对T和T’通过短接e的操作,将e这条边去掉
T1-{e}<T-{e}=T’
也就证明出,这个T1短接e后的权值是小于T’,这就会与刚才通过归纳假设证明的T’的权值最小产生了矛盾。故不存在这个T1。T就是n+1阶图的最小生成树,所以Kruskal算法是正确的。
证毕!
下面我们分析代码:
首先按照权值进行升序排序,然后就是判断两个点是否可以连接。
我们可以通过建立一个数组a,初始化时:a[i]=i,这个表示的是将每一个点设为根。
连接边时:是要先判断两个点是不是属于同一个根元素点。就是说顶点1与顶点4连接,我们根据小的顶点把大的顶点作为根结点。我们看到这个图
比如说:1与3连接,根据小的顶点把大的顶点作为根结点为准则,那么a[1]=3。也就是说3作为根连接1这个点。然后4和6相连,同理我们把a[4]=6。不管2和5相连,3和6相连时,a[3]=6,随后更新a[1]=6。如果遇到这种父结点的根改变了,那么子节点的根结点也要随着父结点一起改变。
接下来看1和4连接。因为a[1]=6,a[4]=6。他们两个的根都是6。故不可以相连,因为此时相连会产生回路。同样的3和4也不能相连。因为a[3]=6,a[4]=6。这样的拥有同一个根结点,则两顶点是不能相连的,因为会产生回路。这样的算法是在《算法导论》中有证明,而且也证明了,建立和更新a数组的时间复杂度是O(eloge)。大家有兴趣的可以去看一看。
至于a[i]的更新。比如说a数组中是{1,2,3,4,5,6}。那么我想找到顶点1的根结点是多少,我们可以通过回溯原理进行寻找。我们将查找算法写成find(int i),i=find(a[i])
我们举个例子:假如说1和5连接。1的根结点是4,而在其他连接中4的根结点是9,那么寻找1的根结点就得是i=a[1]=4,然后再回溯a[i]=9。进而找到1的根结点。
如果想不透的可以将a数组定义成连通分支标记。如果标记一样的,则不能连接
大家或者可以通过代码再想想
public class Graph {
int v1; //v1顶点
int v2; //v2顶点
int len; //权值
public Graph() {
// TODO Auto-generated constructor stub
}
public Graph(int v1, int v2, int len) {
super();
this.v1 = v1;
this.v2 = v2;
this.len = len;
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
int m=9; //总共9个顶点
//设置权重:自己和自己连的是0,没有边的是-1,其他的都是自己权重值
int[][] a = {
{0,10,-1,-1,-1,11,-1,-1,-1},
{10,0,18,-1,-1,-1,16,-1,12},
{-1,18,0,22,-1,-1,-1,-1,8},
{-1,-1,22,0,20,-1,24,16,21},
{-1,-1,-1,20,0,26,-1,7,-1},
{11,-1,-1,-1,26,0,17,-1,-1},
{-1,16,-1,24,-1,17,0,19,-1},
{-1,-1,-1,16,7,-1,19,0,-1},
{-1,12,8,21,-1,-1,-1,-1,0}
};
Graph tempGraph[]=CreateGraph(m, a);
//按权值进行升序排序
Arrays.sort(tempGraph,new Comparator<Graph>() {
public int compare(Graph o1, Graph o2) {
return o1.len-o2.len;
};
});
kruskal(tempGraph, m, tempGraph.length);
}
//贪心算法解最小生成树(克鲁斯卡尔法)
public static void kruskal(Graph tempGraph[],int m,int k) {
int numPoint=m; //未结点数
int v1; //访问点1
int v2; //访问点2
List<Graph> minST=new ArrayList<Graph>(); //寄存选取的最小生成树的边
int a[]=new int[m]; //将根记录下来
//将每个点设为一个根
for(int i=0;i<m;i++)
a[i]=i;
for(int i=0;i<k&&numPoint>1;i++) {
v1=tempGraph[i].v1-1;
v2=tempGraph[i].v2-1;
//判断两个点是不是属于同一个根元素点
if(UnitRoot(v1, v2, a)) {
minST.add(tempGraph[i]);
numPoint--;
}
}
if(numPoint==1)
TracingMST(minST);
else
System.out.println("不存在最小生成树");
}
//访问根节点
public static int FindRoot(int a[],int root) {
if(root==a[root])
return root; //如果访问点就是根则直接返回
return a[root]=FindRoot(a, a[root]); //否则递归找到访问节点的根元素
}
//将根元素合并
public static Boolean UnitRoot(int root1,int root2,int a[]) {
int temp1=FindRoot(a, root1);
int temp2=FindRoot(a, root2);
if(temp1!=temp2) {
a[temp2]=temp1; //若两个节点根元素不一样,则将标号大的点作为根赋给标号为小的点
return true;
}
else
return false; //若两个节点根元素一样,则说明这两个点再连接就会是一个闭合的图,而不再是树
}
//遍寻最小生成树的边
public static void TracingMST(List<Graph> minST) {
int sum = 0;
System.out.println("生成的最小生成树是:");
for (int i=0;i<minST.size();i++) {
System.out.println((minST.get(i).v1-1)+"<-->"+(minST.get(i).v2-1));
sum+=minST.get(i).len;
}
System.out.println("得到的最小生成树的权值和是:"+sum);
}
//创建图
public static Graph[] CreateGraph(int m,int a[][]) {
int k=0;
Graph graph[]=new Graph[m*a.length];
for(int i=0;i<m;i++)
for(int j=0;j<i;j++) {
if(a[i][j]>0) {
graph[k]=new Graph(i+1,j+1,a[i][j]);
k++;
}
}
Graph tempGraph[]=new Graph[k];
for(int i=0;i<k;i++)
tempGraph[i]=graph[i];
return tempGraph;
}
下面对Kruskal算法进行分析
Kruskal算法分析
令e为边数
我们在上面说了建立和更新数组是O(eloge)
排序算法的时间复杂度是O(eloge)
而一层for循环中的遍历e条边的时间复杂度是O(e)
所以Kruskal算法的时间复杂度就是O(eloge)
以上就是Kruskal算法的内容介绍