在一个连通图中,从一个节点出发,试图找到一种连接方式,使得连接所有节点的边的权重总和是最小的。这样的问题就可以通过最小生成树来解决,它能保证从一个特定的节点出发连接整个连通图节点的边权重的和是最小的。下面介绍并实现最小生成树的两个经典算法Kruskal和Prim算法。
1、最小生成树概念:
图H是连通图G的子图,它们的顶点集合相同,且H是以源节点为根的一棵树,并且这组连接所有节点V边的权重是最小的,则称H是G的一棵最小生成树。显然H含有G中的|V|-1条边连接着相同的V个顶点,这一组边是连接这V个顶点中权重是最小的,既没有其它|V|-1条边的权重和小于这组边的权重和(可能相等),并且H是没有环路的无向连通图(即一棵树)。
为了找到最小生成树引入两个概念,切割和轻量级边
切割:无向图G=(V,E)的一个切割(S,V-S)是集合V的一个划分。
轻量级边:在一个变得集合中权重最小的边。
若(S,V-S)是G的一个划分,在横跨S和V-S顶点的边中的轻量级边是安全边,即这条边是最小生成树中的一条边。
2、最小生成树的Kruskal算法
思想:Kruskal算法找到安全边的方法是,在所有连接的森林中两棵不同树的边里面,找到权重最小的边(u,v)。设C1和C2为边(u,v)所连接的两棵树。由于边(u,v)一定是连接C1和其它某棵树的一条轻量级边,因此边(u,v)是C1的一条安全边。很显然Kruskal算法属于贪心算法,每次都选择一条权重最小的边加入到森林。下面是Kruskal算法的伪代码:
//最小生成树 Kruskal算法
MST-KRUSKAL(G,w)
A=null
for each vertex v belong G.V
MAKE-SET(v)
sort the edges of G.E into nondecreasing order by weight w
for each edge (u,v)belong G.E,taken in nondecreasing order by weight
if(FIND-SET(v)!=FIND-SET(v)
A =A U {u,v}
UNION(u,v)
return A
对于下图无向图,算法选择边的过程中会遇到4中情况。1:边的起始节点不在现有的森林中,这种情况边是安全的,这时新建一棵树并将这条边加入到这棵树中;2:边的起始有一个顶点在森林中,另一个顶点不在森林中,这条边也是安全边,并将这条边添加到含有起始节点树中;3:边连接边的起始都在森林中并且连接着两棵树,这条边也是安全的,这时合并两棵树为一棵树,删除另一棵树,并将这条边加入到这棵合并的树中;4:就是边的起始都在森林的一棵树中,这条边是不安全的,直接跳过到下一条边。是边算法选择边的过程为:
顺序 | 边 | 情况
1 (g,h) 1
2 (c,i ) 1
3 (f,g) 2
4 (a,b) 1
5 (i,g) 3
6 (c,d) 2
7 (h,i) 4
8 (b,c) 3
9 (a,h) 4
10 (e,d) 2
11 (e,f) 4
12 (d,f) 4
3、最小生成树的Prim算法
思想:Prim算法随机选取一个节点或指定节点作为算法的起始节点加入图A,算法每次选取横跨A.V和{G.V-A.V}的边中的轻量级边加入集合A,直到A.V等于G.V,这时A就是最小生成树。A.V与{G.V-A.V}形成一个切割,而选取的边是轻量级边,因此该边是安全边,因此最后结果是最小生成树;显然Prim算法总能保证A是一棵树。下面是Prim算法的伪代码:
//最小生成树Prim算法
MST-PRIM(G,w,r)
for each u belong G.V
u.key= infinity //无穷大
u.pi=NIL //无前驱结点
r.key=0
Q=G.V
while Q!=null
u=EXTRACT-MIN(Q) //从队列中取出u
for each v belong G.Adj[u] //扫描u的邻接链表
if v belong G and w(u,v)<v.key
v.pi=u
v.key=w(u,v)
顺序 | 边
1 (a,b)
2 (b,c)
3 (c,i)
4 (i,g)
5 (g,h)
6 (g,f)
7 (c,d)
8 (d,e)
结束
4、java实现Kruskal算法和Prim算法
java实现Kruskal算法,算法使用到了堆排序对边进行排序,因此还自建了堆:
定义边:
package Structure;
/*
* @vamesary
* 4/2017
* 定义边
*/
public class Edge {
public int start;//边的起点
public int end;//边的终点
public int weight;//边的权重
//无参构造函数
public Edge(){
start=0;
end=0;
weight=0;
}
//设置边的起点
public Edge(int theStart,int theEnd){
start=theStart;
end=theEnd;
}
//设置边
public Edge(int theStart,int theEnd,int theWeight){
start=theStart;
end=theEnd;
weight=theWeight;
}
}
边的节点:
package Structure;
/*
* @vamesary
* 4/2017
* 测试边节点,这里主要用于构建小根堆和边链表
*/
public class EdgeNode {
public Edge element;//边元素
public EdgeNode next;//下一个元素
//新建空节点
public EdgeNode(){
element=null;
next=null;
}
//新建指向空的新节点
public EdgeNode(Edge theElement){
element=theElement;
next=null;
}
//新建指向特定节点的节点
public EdgeNode(Edge theElement,EdgeNode theNext){
element=theElement;
next=theNext;
}
}
边的链表:模拟一个边的集合,在算法中就是森林中的一棵树
package Structure;
/*
* @vamesary
* 4/2017
* 边的链表,动态存储边
*/
public class EdgeList {
//根节点
private EdgeNode root;
public EdgeList(){
root=null;
}
//获得根节点指针
public EdgeNode getRoot() {
return root;
}
//插入一条边
public void insert(Edge e){
if(root==null)
root=new EdgeNode(e);
else{
EdgeNode p=new EdgeNode(e,root);
root=p;
}
}
//查找一条边是否存在
public boolean findEdge(Edge e){
if(root==null)
return false;
else{
boolean find=false;
EdgeNode p=root;
while(p!=null){
if(p.element.start==e.start&&
p.element.end==e.end){
find=true;
break;
}
p=p.next;
}
return find;
}
}
//查找一个顶点是否存在
public boolean findVertex(int v){
if(root==null)
return false;
else{
boolean find=false;
EdgeNode p=root;
while(p!=null){
if(p.element.start==v||p.element.end==v){
find=true;
break;
}
p=p.next;
}
return find;
}
}
//合并两个链表,用于合并两个集合
public void insertMerge(EdgeList list) {
if(root==null){
EdgeNode q=list.getRoot();
if(q!=null){
root=new EdgeNode(q.element);
EdgeNode p=root;
while(q!=null){
p.next=new EdgeNode(q.element);
p=p.next;
q=q.next;
}
}
}else{
EdgeNode p=root;
while(p.next!=null){
p=p.next;
}
EdgeNode q = list.getRoot();
while (q != null) {
p.next = new EdgeNode(new Edge(q.element.start,
q.element.end,q.element.weight));
p = p.next;
q = q.next;
}
}
}
//打印函数
public String toString(){
String content="";
EdgeNode p=root;
while(p!=null){
content+=p.element.start+"->"+p.element.end+" "+p.element.weight+"\t";
p=p.next;
}
return content;
}
}
堆节点
package Structure;
/*
* @vamesary
* 4/2017
* 二叉树的节点,用于构建二叉树。
*/
import Structure.Edge;
//定义树节点
public class TreeNode{
protected Edge element;//节点域
protected TreeNode leftChild,//左子树指针和右子树指针
rightChild;
//无参构造函数,构建空树
public TreeNode(){
leftChild=rightChild=null;
}
//新建节点,插入末尾
public TreeNode(Edge theElement){
element=theElement;
leftChild=rightChild=null;
}
//赋值构造函数,替换节点
public TreeNode(Edge theElement,TreeNode theleftchild,
TreeNode therightchild){
element=theElement;
leftChild=theleftchild;
rightChild=therightchild;}
};
数据结构堆
package Structure;
/*
* @vamesary
* 4/2017
* 小根堆,这里用于对边进行排序
*/
public class MinHeap {
TreeNode root;
int size;
public MinHeap() {
root = null;
size = 0;
}
public Edge top() {
return root.element;
}
public TreeNode getRoot() {
return root;
}
public void visit(TreeNode t) {
System.out.print(t.element + " ");
}
public void perOrder(TreeNode t) {
if (t != null) {
visit(t);
perOrder(t.leftChild);
perOrder(t.rightChild);
}
}
public void insert(Edge theElement) {
if (root == null) {
root = new TreeNode(theElement);
size++;
} else {
Edge tem = theElement;
Edge tem1;
int a = size + 1, i = 0;
int[] b = new int[20];
while (a > 1) {
i++;
b[i] = a % 2;
a = a / 2;
}
TreeNode p = root;
if (root.element.weight>tem.weight) {
tem = p.element;
root.element = theElement;
}
while (i > 1) {
if (b[i] == 0)
p = p.leftChild;
else
p = p.rightChild;
i--;
if (p.element.weight > tem.weight) {
tem1 = p.element;
p.element = tem;
tem = tem1;
}
}
if (b[1] == 0)
p.leftChild = new TreeNode(tem);
else
p.rightChild = new TreeNode(tem);
size++;
}
}
public Edge pop() {
if (root == null) {
//System.out.println("当前堆为空");
return null;
}
if (size == 1) {
TreeNode popElement = root;
root = null;
size--;
return popElement.element;
} else {
TreeNode popElement = root;
int a = size, i = 0;
int[] b = new int[20];
Edge tem, tem1;
while (a > 1) {
i++;
b[i] = a % 2;
a = a / 2;
}
TreeNode p = root;
while (i > 1) {
if (b[i] == 0)
p = p.leftChild;
else
p = p.rightChild;
i--;
}
if (b[1] == 0) {
tem = p.leftChild.element;
p.leftChild = null;
} else {
tem = p.rightChild.element;
p.rightChild = null;
}
p = root;
while (p.leftChild != null && p.rightChild != null) {
if (p.leftChild.element.weight <= p.rightChild.element.weight) {
if (p.leftChild.element.weight <= tem.weight)
p.element = p.leftChild.element;
else {
tem1 = p.leftChild.element;
p.element = tem;
tem = tem1;
}
p = p.leftChild;
} else {
if (p.rightChild.element.weight <= tem.weight)
p.element = p.rightChild.element;
else {
tem1 = p.rightChild.element;
p.element = tem;
tem = tem1;
}
p = p.rightChild;
}
}
if (p.leftChild == null)
p.element = tem;
else {
if (p.leftChild.element.weight >= tem.weight)
p.element = tem;
else {
p.element = p.leftChild.element;
p.leftChild.element = tem;
}
}
size--;
return popElement.element;
}
}
//打印前序遍历的堆
public String toString(){
System.out.print("堆的前序遍历结果是:");
perOrder(root);
return "";
}
}
Kruskal算法和Prim算法
package Algorithm;
/*
* @vamesary
* 4/2017
* 最小生成树算法,包括Kruskal和Prim算法
*/
import java.util.LinkedList;
import Structure.Graph;
import Structure.Edge;
import Structure.EdgeList;
import Structure.EdgeNode;
import Structure.LinkList;
import Structure.MinHeap;
import Structure.Node;
public class SpanningTree {
private LinkList [] list;//邻接链表
private int vertex=0;//图的顶点个数
LinkedList<EdgeList> assemble;//最小生成树边的集合T,
//T可能由于图不是连通的形成森林,每一棵树是一个分支
//自己随机生成图
public SpanningTree(int theVertex,int theEdge){
Graph graph=new Graph(theVertex,theEdge,false);
list=graph.getList();
vertex=graph.getVertex();
System.out.println(graph.toString());;
}
//使用现有图构造函数
public SpanningTree(Graph graph){
list=graph.getList();
vertex=graph.getVertex();
System.out.println(graph.toString());;
}
//返回最小生成树
public LinkedList<EdgeList> getSpanningTree()
{
return assemble;
}
//最小生成树Kruskal算法
public void Kruskal(){
//排序使用的小根堆);
MinHeap heap=new MinHeap();
//对图G的每条边进行排序
int i=0;
while(i<vertex){
Node p=list[i].getRoot();
while(p!=null){
heap.insert(new Edge(i,p.id,p.weight));
p=p.next;
}
i++;
}
//连通分支
assemble=new LinkedList();
int number=0;//连通分支集合数目
//从小根堆中取出的元素是从小到大的一个序列
Edge e=heap.pop();
while(e!=null){
int findstart=-2;
int findend=-1;
i=0;
//在现有的所有连通分支中查看是否存在这条边连接的顶点
while(i<number){
EdgeList temp=assemble.get(i);
//找到边的起点顶点
if(temp.findVertex(e.start)){
findstart=i;
if(findend!=-1)
break;
}
//找到边的终点顶点
if(temp.findVertex(e.end)){
findend=i;
if(findstart!=-2)
break;
}
i++;
}
//如果起点和终点不是在一个连通分支中发现,则这是有效边,将它加入
if(findstart!=findend){
if(findstart>=0){
if(findend>=0){
//如果在两个不同的连通分支发现起点和终点,则合并这两个连通分支
EdgeList temp1=assemble.get(findstart);
EdgeList temp2=assemble.get(findend);
temp1.insertMerge(temp2);
temp1.insert(new Edge(e.start,e.end,e.weight));
assemble.remove(findend);
number--;
}else{
//如果仅发现起点没有发现终点,则将终点加入到起点连通分支中
EdgeList temp1=assemble.get(findstart);
temp1.insert(new Edge(e.start,e.end,e.weight));
}
}else{
if(findend>=0){
//如果仅发现终点没有发现起点,则将起点加入到终点连通分支中
EdgeList temp2=assemble.get(findend);
temp2.insert(new Edge(e.start,e.end,e.weight));
}else{
//如果起点和终点都没有发现,则建立一个新的连通分支,加入这两个顶点
EdgeList temp=new EdgeList();
temp.insert(new Edge(e.start,e.end,e.weight));
assemble.add(temp);
number++;
}
}
}
//取下一条边
e=heap.pop();
}
}
public void Prim(){
//prim算法使用的队列
LinkList queue =new LinkList();
int []key=new int [vertex];
int []pi=new int [vertex];
for(int i=0;i<vertex;i++){
key[i]=Integer.MAX_VALUE;
pi[i]=-1;
queue.insertOnEnd(i, 0);
}
key[0]=0;
Node u=queue.removeFromStart();
while(u!=null){
Node v=queue.getRoot();
while(v!=null){
Node e=list[u.id].getRoot();
while(e!=null){
if(e.weight<key[v.id]&&e.id==v.id){
pi[v.id]=u.id;
key[v.id]=e.weight;
}
e=e.next;
}
v=v.next;
}
u=queue.removeFromStart();
}
//连通分支
assemble=new LinkedList();
EdgeList temp=new EdgeList();
for(int i=1;i<vertex;i++){
if(pi[i]!=-1)
temp.insert(new Edge(i,pi[i],key[i]));
}
assemble.add(temp);
}
//打印函数
public String toString(){
String content="";
int size=assemble.size();
int i=0;
while(i<size){
content+="\n第 "+i+" 棵最小生成树:";
EdgeList list=assemble.get(i);
EdgeNode p=list.getRoot();
while(p!=null){
content+=p.element.start+"->"+
p.element.end+" "+p.element.weight+"\t";
p=p.next;
}
i++;
}
return content;
}
}
测试:
package Test;
/*
* @vamesary
* 4/2017
* 测试图的最小生成树算法,包含Kruuskal和Prim算法
*/
import Algorithm.SpanningTree;
import Structure.Graph;
public class SpanningTreeTest {
public static void main(String[] args) {
int [][]array=new int[9][9];
for(int i=0;i<9;i++){
for(int j=0;j<9;j++){
array[i][j]=-1;
}
}
array[0][1]=4;array[0][2]=8;
array[1][0]=4;array[1][2]=11;array[1][3]=8;
array[2][0]=8;array[2][1]=11;array[2][4]=7;array[2][5]=1;
array[3][1]=8;array[3][4]=2;array[3][6]=7;array[3][7]=4;
array[4][2]=7;array[4][3]=2;array[4][5]=6;
array[5][2]=1;array[5][4]=6;array[5][7]=2;
array[6][3]=7;array[6][7]=14;array[6][8]=9;
array[7][3]=4;array[7][5]=2;array[7][6]=14;array[7][8]=10;
array[8][6]=9;array[8][7]=10;
Graph graph =new Graph(array,9);
//System.out.println(graph);
SpanningTree tree=new SpanningTree(5,8);
tree.Kruskal();
System.out.println(tree);
System.out.println();
tree.Prim();
System.out.println(tree);
}
}
输出:
Kruskal的最小生成树:8->6 90->2 8
0->1 46->3 7
7->3 45->7 2
5->2 13->4 2
Prim的最小生成树:8->6 9 7->5 26->3 7
5->2 14->3 2
3->1 82->0 8
1->0 4