最小生成树
定义
定义:图的生成树,是它的一颗含有其所有顶点的无环连通子图,一幅加权无项图的的最小生成树(MST)是它的一颗权值(树中所有边的权值之和)最小的生成树。
事实上,通过对于最小生成树的理解,我们可以很容易得出以下结论:
- 最小生成树求解过程中要求图中必然只含有一个连通分量;
- 图的边的权值可以为0或者是负值;
- 最小生成树必然是无环的类树结构
- 对于含n个顶点的图而言,最小生成树中的边必为n-1条;
- 如果图中含有权值相同的边,那么最小生成树就不一定唯一,因此在一开始的最小生成树算法中假设所有边的权值都不一样;
- 最小生成树要求图必须为加权无向图;
切分定理
图的一种切分是将图的所有顶点分为两个非空且不重叠的集合,横切边是一条连接两个属于不同集合的顶点的边。
切分定理
在一幅加权图中,给定任意的切分,它的横切边中的权值最小者必然属于图的最小生成树。
证明就略过,可见切分定理是解决最小生成树问题的所有算法的基础。事实上,先确定一个点,通过切分定理,找到所有横切边中最小的那条边,即为MST的一条边,将该边加入树中,并将该边指向的顶点加入初始集合,不断重复该操作,直到所有顶点都在同一集合,这体现的是MST解法中的一种贪心解法。
两种MST算法
Prim算法
prim算法就是基于上述贪心的一种算法,核心思想就是每次都找到最小的横切边,然后加入到mst中去,这里给出的是prim算法的延时版本,所谓的延时版本,是指所有邻接边加入优先队列中时,不管这条边是否有效都加入,不管这条边能不能让某个顶点到树mst的距离变小都加入,后续取出最小边的时候可能这条边就因为上述原因而不满足要求而舍去,即所谓延时。还有个瞬时版本,是加入优先队列的边都是能有效松弛顶点的边,而且加入的是一对信息,即还未在mst中的顶点下标和它距离mst的距离(经过了初次松弛以后),并且如果能再次松弛同一个还未加入mst的顶点,需要把优先队列里的信息给进行修改,注意这里的键值对信息是顶点下标为键,边权值为值,并且每次取出的最小边还是以权值进行排序以后的最小边,这里就不多赘述,下面给出延时版本代码如下;
package PAT;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Scanner;
public class PrimTest {
static PrimTest instance = new PrimTest(); // 用于生成内部类Edge的实例
static int N, M; // N为顶点总数,M为边的总数
static List<Edge>[] bag; // 用于存放所有边信息的邻接表
static boolean[] marked; //
static PriorityQueue<Edge> pq; // 优先队列
static Queue<Edge> mst; // 用于存放最小生成树中的所有边
/**
* 用于测试主函数,输入形式如下,
* 5 6
* 0 1 2
* 0 2 3
* 0 3 1
* 1 2 4
* 2 4 5
* 3 4 6
* @param args
*/
public static void main(String[] args) {
Scanner s = new Scanner(System.in);
String[] temp1 = s.nextLine().trim().split(" ");
N = Integer.valueOf(temp1[0]);
bag = new ArrayList[N];
for(int i=0; i<N; i++)
bag[i] = new ArrayList<>();
M = Integer.valueOf(temp1[1]);
for(int i=0; i<M; i++) {
temp1 = s.nextLine().trim().split(" ");
int u = Integer.valueOf(temp1[0]);
int v = Integer.valueOf(temp1[1]);
int weigh = Integer.valueOf(temp1[2]);
Edge e = instance.new Edge(u, v, weigh);
bag[u].add(e);
bag[v].add(e); // 因为是无向边,所以一条边得存在两个地方
}
lazyPrimMST(); // Prim算法的延时实现
// 打印mst中的所有边信息
List<Edge> list = new ArrayList<>();
while(!mst.isEmpty())
list.add(mst.poll());
System.out.println(list);
}
private static void lazyPrimMST() {
pq = new PriorityQueue<>();
marked = new boolean[N];
mst = new LinkedList<>();
visit(0); // 先从第0个顶点开始加入最小生成树中
while(!pq.isEmpty()) {
Edge e = pq.poll(); // 取出优先队列中最短边
int u = e.either();
int v = e.other(u);
if(marked[u] && marked[v]) continue; //若两个顶点都在mst中,则边失效
mst.add(e); // 否则即为有效边,加入mst中
if(!marked[u]) visit(u); // 访问还未在mst中的另一个顶点
if(!marked[v]) visit(v);
}
}
private static void visit(int i) {
marked[i] = true; // 代表已经加入mst
for(Edge e : bag[i])
if(!marked[e.other(i)]) pq.add(e);
}
/**
* 最小生成树中的带权无向边的类
* @author Gastby
*
*/
private class Edge implements Comparable<Edge>{
private final int weigh; //权值
final int u, v; //两个顶点
Edge(int u, int v, int weigh) {
this.u = u;
this.v = v;
this.weigh = weigh;
}
public int weigh() {
return weigh;
}
/**
* 返回其中一个顶点
* @return
*/
public int either() {
return u;
}
public int other(int k) {
if(k == u) return v;
else return u;
}
/**
* 因为要放入优先队列中,所以得实现Comparable接口
*/
@Override
public int compareTo(Edge that) {
if(this.weigh() < that.weigh()) return -1;
else if(this.weigh() > that.weigh()) return 1;
else return 0;
}
public String toString() {
return "边长是" + weigh();
}
}
}
Kruskal算法
kruskal算法其实非常好容易理解,就是把所有边都放入优先队列里,每次取出最小边,然后判断边中的顶点是否已经连通了(一开始默认都是不连通的),如果未连通则将边加入mst中,如果已经连通,则说明这条边失效,选择继续。注意这里采用了并查集算法,关于并查集算法,这里不再赘述。实现的代码如下;
package PAT;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Scanner;
public class KruskalTest {
static KruskalTest instance = new KruskalTest(); // 用于生成内部类Edge的实例
static int N, M; // N为顶点总数,M为边的总数
static List<Edge>[] bag; // 用于存放所有边信息的邻接表
static boolean[] marked; //
static PriorityQueue<Edge> pq; // 优先队列
static Queue<Edge> mst; // 用于存放最小生成树中的所有边
static int[] id; // 并查集算法中的基数组
public static void main(String[] args) {
Scanner s = new Scanner(System.in);
String[] temp1 = s.nextLine().trim().split(" ");
N = Integer.valueOf(temp1[0]);
bag = new ArrayList[N];
for(int i=0; i<N; i++)
bag[i] = new ArrayList<>();
id = new int[N];
for(int i=0; i<N; i++)
id[i] = i;
pq = new PriorityQueue<>();
M = Integer.valueOf(temp1[1]);
for(int i=0; i<M; i++) {
temp1 = s.nextLine().trim().split(" ");
int u = Integer.valueOf(temp1[0]);
int v = Integer.valueOf(temp1[1]);
int weigh = Integer.valueOf(temp1[2]);
Edge e = instance.new Edge(u, v, weigh);
bag[u].add(e);
bag[v].add(e); // 因为是无向边,所以一条边得存在两个地方
pq.add(e);
}
krusal();
// 打印mst中的所有边信息
List<Edge> list = new ArrayList<>();
while(!mst.isEmpty())
list.add(mst.poll());
System.out.println(list);
}
/**
* kruskal主算法思想
*/
private static void krusal() {
mst = new LinkedList<>();
while(!pq.isEmpty() && mst.size() < N-1) {
Edge e = pq.poll();
int u = e.either(), v = e.other(u);
if(connected(u, v)) continue; // 采用并查集算法,如果两个顶点已经联通
union(u, v); // 联通两个点
mst.add(e); // 将边e加入MST队列中
}
}
private static void union(int u, int v) {
int r1 = find(u), r2 = find(v);
if(r1 == r2) return;
id[r1] = r2;
}
private static boolean connected(int u, int v) {
return find(u) == find(v);
}
private static int find(int u) {
while(u != id[u]) u = id[u];
return u;
}
/**
* 最小生成树中的带权无向边的类
* @author Gastby
*
*/
private class Edge implements Comparable<Edge>{
private final int weigh; //权值
final int u, v; //两个顶点
Edge(int u, int v, int weigh) {
this.u = u;
this.v = v;
this.weigh = weigh;
}
public int weigh() {
return weigh;
}
/**
* 返回其中一个顶点
* @return
*/
public int either() {
return u;
}
public int other(int k) {
if(k == u) return v;
else return u;
}
/**
* 因为要放入优先队列中,所以得实现Comparable接口
*/
@Override
public int compareTo(Edge that) {
if(this.weigh() < that.weigh()) return -1;
else if(this.weigh() > that.weigh()) return 1;
else return 0;
}
public String toString() {
return "边长是" + weigh();
}
}
}
最小生成树算法介绍到此,接下来就是Dijkstra算法了,关于这个大名鼎鼎的算法,其实核心思想和Prim算法非常相近,不过具体是什么样的,等下一篇详述把。