Kruskal算法思想
假设连通网 N={V,{E}},令最小生成树的初始状态只有n个顶点而无边的非连通图T={V,{}},然后在E中选择代价最小的边,若加入该边后不会形成环,则将该边加入到T中,否则舍去此边而选择下一条边,以此类推,直到所有顶点都被包含为止。结合下图将会对这个过程有更深刻的理解。
在对上述过程有简单的了解后,其实现步骤可分为以下两个过程:(1)找出代价最小的边,(2)判断加入该边后是否构成了环,如果构成了环则舍弃。那我们又该如何判断是否构成了环呢?这便要用到另一种数据结构------并查集(disjointSet)。
并查集(disjointSet)
算法思想: 当我们将通信网络上各边的信息存储后,起始时我们假设各个点孤立,又可以理解为起始时各个点没有根结点。我们设置一个数组parent[],表示各个结点的根节点,当我们要加入一条边时,就要判断该边的两个结点其根节点是否相同,如果相同,那么我们将该边加入后必定会形成环,故舍弃此边。反之,如果该边两个根节点不相同,说明加入改变后不会形成环,那么我们就将改变加入。那么又该如何表示加入了该边呢?我们只需要将其中一个结点设置为另一个结点的父结点即可。但是在将一个结点设置为另一个结点的父结点时,为了避免树退化形成一种链式结构(这样会降低查询父结点的效率),我们应当将深度较小哪一个二叉树的连接到深度较大的那一颗二叉树上。
例题剖析
如下图所示的联通网络,请你利用并查集判断是否有环。
过程一:
过程二:
明显对于过程二所示的方式,由于树的深度不断加长,那么查询根节点的效率将会越来越低。为了解决这个问题,使所构成的树的深度尽可能的浅(过程1所示的方式),那么我们需要设定一个rank[]数组来记录数的深度,在每次连接时比较rank的值,将深度较小的树连接到深度较大的树上。
代码
import java.util.Scanner;
/*
并查集
测试数据:
6
6
0 2
0 3
1 3
2 4
2 5
3 5
*/
public class Main{
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
int nodesTotalNumber = sc.nextInt(); //输入网络中结点的个数
int edgeTotalNumber = sc.nextInt(); //输入网络中边的条数
//录入数据,当存在一条边时,我们只需录入其两个端点即可
int[][] graph = new int[edgeTotalNumber][2];
for(int i=0;i<edgeTotalNumber;i++){
graph[i][0] = sc.nextInt();
graph[i][1] = sc.nextInt();
}
sc.close();
//创建根结点数组parent和深度数组rank,并初始化
int[] parent = new int[nodesTotalNumber];
int[] rank = new int[nodesTotalNumber];
//初始化每个结点的父结点为-1,每个结点所属的树的长度为1
for(int i=0;i<nodesTotalNumber;i++){
parent[i] = -1;
rank[i] = 1;
}
//测试
for(int i=0;i<edgeTotalNumber;i++){
if(union(graph[i][0],graph[i][1],parent,rank) == false){
System.out.println("存在环");
return;
}
}
System.out.println("不存在环");
}
//查询根结点
public static int find_root(int x,int[] parent){
int x_root = x;
while(parent[x] != -1){
x_root = parent[x];
x = parent[x];
}
return x_root;
}
//连接
public static boolean union(int x,int y,int[] parent,int[] rank){
int x_root = find_root(x,parent);
int y_root = find_root(y,parent);
//如果两个点的根节点一样,连接就会形成环
if(x_root == y_root){
return false;
} else {
if(rank[x] > rank[y]){
parent[y_root] = x_root;
} else if(rank[x] == rank[y]){
parent[y_root] = x_root;
rank[x]++;
} else {
parent[x_root] = y_root;
}
return true;
}
}
}
Kruskal算法实现
import java.util.Scanner;
/*
kruskal算法
测试数据:
6
10
0 1 6
0 2 1
0 3 5
1 2 5
2 3 5
1 4 3
2 4 6
2 5 4
3 5 2
4 5 6
输出结果:
0--2
3--5
1--4
2--5
1--2
最小生成树的总花费: 15
*/
public class Main{
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
int nodesTotalNumber = sc.nextInt(); //图中的顶点数
int edgesTotalNumber = sc.nextInt(); //图中的边数
edge[] graph = new edge[edgesTotalNumber]; //存储网络图
for(int i=0;i<edgesTotalNumber;i++){
int pointOne = sc.nextInt();
int pointTwo = sc.nextInt();
int cost = sc.nextInt();
graph[i] = new edge(pointOne,pointTwo,cost);
}
sc.close();
int[] parent = new int[nodesTotalNumber]; //记录各个结点的根节点
int[] rank = new int[nodesTotalNumber]; //记录结点所树的深度
for(int i=0;i<nodesTotalNumber;i++){ //由于刚开始均为互相孤立的点,所以parent初始化为-1,rank初始化为1
parent[i] = -1;
rank[i] = 1;
}
Kruskal(nodesTotalNumber,edgesTotalNumber,graph,parent,rank);
}
//kruskal算法
public static void Kruskal(int nodesTotalNumber,int edgesTotalNumber,edge[] graph,int[] parent,int[] rank){
int sum=0,count=0; //sum用来记录最短路径的总花费,count用来记录当前加入的边数
quickSort(0,edgesTotalNumber-1,graph);
for(int i=0;i<edgesTotalNumber;i++){
if(union(graph[i].endpointOne,graph[i].endpointTwo,parent,rank)){
sum += graph[i].cost;
count++;
System.out.println(graph[i].endpointOne+"--"+graph[i].endpointTwo);
}
if(count==nodesTotalNumber-1){
break;
}
}
System.out.println("最小生成树的总花费: "+sum);
}
//寻找根节点
public static int find_root(int x,int[] parent){
int x_root = x;
while(parent[x] != -1){
x_root = parent[x];
x = parent[x];
}
return x_root;
}
//连接两个树
public static boolean union(int x,int y,int[] parent,int[] rank){
int x_root = find_root(x,parent);
int y_root = find_root(y,parent);
if(x_root == y_root){
return false;
}
if(rank[x]>rank[y]){
parent[y_root] = x_root;
} else if(rank[x] == rank[y]){
parent[y_root] = x_root;
rank[x]++;
} else {
parent[x_root] = y_root;
}
return true;
}
//利用快速排序,将图中的各个元素边按"边的花费"由小到大排列
public static edge[] quickSort(int low,int high,edge[] graph){
int l = low,h = high;
while(l<h){
int keyPoint = graph[l].cost;
while(l<h && graph[h].cost>=keyPoint){
h--;
}
edge temph = graph[h];
graph[h] = graph[l];
graph[l] = temph;
while(l<h && graph[l].cost<=keyPoint){
l++;
}
edge templ = graph[l];
graph[l] = graph[h];
graph[h] = templ;
}
if(low<high){
quickSort(low,l-1,graph);
quickSort(l+1,high,graph);
}
return graph;
}
}
//创建一个"边"类,其成员变量为"边两端的顶点"和"边的花费"
class edge{
int endpointOne;
int endpointTwo;
int cost;
//全参数构造方法
public edge(int endpointOne,int endpointTwo,int cost){
this.endpointOne = endpointOne;
this.endpointTwo = endpointTwo;
this.cost = cost;
}
}
算法分析
Kruskal的最佳时间复杂度为0(eloge) (e为网中的边数),因此相对于prim算法而言,适合于求边稀疏的网中的最小生成树。
以上即是对kruskal算法的分析。