最小生成树
加权图
一种为每条边关联一个权值或是成本的图模型
生成树
图的生成树是一棵含有其所有顶点的无环连通子图
最小生成树(MST)
加权图的最小生成树就是它的生成树中权值之和最小的一棵生成树
约定
为了避免一些难以理解的过程而对问题做出一些限制与简化,实际并不影响整体流程
- 只考虑连通图。根据我们对生成树的定义可知,一棵最小生成树只可能存在于连通图中,而如果我们对并非连通的图使用算法计算,最后得到的结果也只能是各个连通分量内的最小生成树组成的最小生成森林。
- 边的权重不一定表示距离。这很好理解,边的权重应该视具体问题而赋予实际意义,它可以表示很多距离以外的物理含义,例如费用成本,数量等等
- 边的权重可能是0或者是负数。这使得我们解决的问题更富有实际意义与可操作性
- 所有边的权重各不相同。这主要是由于如果存在相同权重的边,就可能导致最小生成树不唯一。这会使后续算法的证明困难,所以我们主动排除这种情况,实际它的存在并不会影响算法运行与最后的正确
图的两个重要性质:
- 用一条边连接图中两个任意顶点都会产生一个新的环
- 从树中删去任意一条边都会生成两个新的树
切分定理
图的一种切分是将所有顶点分为不重叠的两个子集,横切边就是一条两个属于不同集合的顶点的边
切分定理内容:
在一幅加权图中,给定任意切分,它的权重最小横切边必然在该加权图的最小生成树中
证明:
e表示权重最小的横切边,T为加权图中最小生成树,假设T中不包含e,将e加入T,则根据树的性质必然生成一个新环,且这个新环内至少包含另外一条横切边f,且f权重一定大于e,那么删掉f不影响最小生成树的连通性且权重变小,所以假设不成立权重最小的横切边一定在最小生成树中
贪心算法
最小生成树的贪心算法
在一个含V个顶点的连通加权图中,找到一种切分,将它的最小权重的横切边标记(归入最小生成树的边中),重复找到另一组切分,并继续标记最小横切边,指导有V-1个横切边被标记表示已经形成最小生成树
加权无向图数据类型
由于加权无向图中每个边都包含权重,所以定义Edge类,用来表示边
边由于内含权重,所以理应是可以比较的,所以继承Comparable
加权边的代码实现:
package cn.ywrby.Graph;
//定义权重图中的边结构,含有左右顶点和边所占权重
public class Edge implements Comparable<Edge> {
private final int v; //顶点
private final int w; //顶点
private final double weight; //权重
//构造方法
public Edge(int v,int w,double weight){
this.v=v;
this.w=w;
this.weight=weight;
}
public double weight(){return weight;}
//返回一个顶点
public int either(){return v;}
//返回另一个顶点
public int other(int vertex){
if(vertex==v) return w;
else if (vertex==w) return v;
else throw new RuntimeException("Inconsistent Edge");
}
//重写继承的比较方法
public int compareTo(Edge that){
if(this.weight()<that.weight()) return -1;
else if(this.weight()>that.weight()) return 1;
else return 0;
}
//将边转化为字符串形式
public String toString(){
return String.format("%d-%d %.2f",v,w,weight);
}
}
加权无向图的代码实现
package cn.ywrby.Graph;
//加权无向图
import edu.princeton.cs.algs4.Bag;
import edu.princeton.cs.algs4.In;
import java.util.NoSuchElementException;
public class EdgeWeightedGraph {
private final int V; //顶点数
private int E; //边数
private Bag<Edge>[] adj;
public EdgeWeightedGraph(int V){
this.V=V;
this.E=0;
for(int i=0;i<V;i++){
adj[i]=new Bag<Edge>();
}
}
//读入数据创建加权无向图
public EdgeWeightedGraph(In in){
if(in==null) throw new IllegalArgumentException("argument is null");
try{
V=in.readInt();
adj=(Bag<Edge>[]) new Bag[V];
for(int i=0;i<V;i++){
adj[i]=new Bag<Edge>();
}
int E=in.readInt();
if(E<0)throw new IllegalArgumentException("Number of edges must be nonnegative");
for(int i=0;i<E;i++){
int v=in.readInt();
int w=in.readInt();
double weight=in.readDouble();
Edge e=new Edge(v,w,weight);
addEdge(e);
}
}
catch(NoSuchElementException e) {
throw new IllegalArgumentException("invalid input format in EdgeWeightedGraph constructor", e);
}
}
public int V(){return V;}
public int E(){return E;}
public void addEdge(Edge e){
int v=e.either(),w=e.other(v);
adj[v].add(e);
adj[w].add(e);
E++;
}
public Iterable<Edge> adj(int v){return adj[v];}
//返回加权无向图中的所有边
public Iterable<Edge> edges(){
Bag<Edge> b=new Bag<Edge>();
for(int v=0;v<V;v++){
for(Edge e:adj[v]){
if(e.other(v)>v) b.add(e);
}
}
return b;
}
}
最小生成树实现
Prim算法
Prim算法结合上边所讲的贪心算法与横切边概念,在逻辑上很好理解
首先从树中随机选出一个顶点,此时树中的顶点和边都是未被标记过的,然后将该顶点看作一个集合,图中另外所有顶点看作一个集合,对横切边进行比较,将最小横切边标记,此时原顶点和刚加入的横切边连接的另一顶点组成新的集合,如此不断重复,直至边数达到V-1表示一棵最小生成树形成
整个过程中维护的数据结构
- 顶点:使用布尔数组marked[]表示
- 边:延时版本,采用队列mst表示(保存最小生成树的边)。即时版本,采用由一个顶点索引的Edge对象组成的数组edgeTo[]表示(edgeTo[v]表示将v连接到树中的Edge对象)
- 横切边,使用优先队列MinPQ表示,方便根据权重比较所有边
整个过程中的难点在于新加入横切边的对应顶点M后,如何确定新的横切边队列,整个过程分为两部分,首先需要将新加入顶点M所连接的不在生成树中的边(x,y,z)加入进来,其次需要删除队列中所有连接新加入顶点M和各个旧顶点(A,B,C…)之间的边(因为这两个顶点都已经加入生成树中,对应边也就不是横切边了)
第一点可以通过marked[]数组维护,第二点可以采用即时删除,或是延时删除两种方法实现
Prim算法延时实现:
package cn.ywrby.Graph;
//最小生成树的Prim算法的延时实现
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.MinPQ;
import edu.princeton.cs.algs4.Queue;
import edu.princeton.cs.algs4.StdOut;
public class LazyPrimMST {
private boolean[] marked; //标记最小生成树的顶点,在树中的为true
private Queue<Edge> mst; //最小生成树的边
private MinPQ<Edge> pq; //横切边队列,盛放现有横切边
//初始化算法
public LazyPrimMST(EdgeWeightedGraph G){
marked=new boolean[G.V()];
mst=new Queue<Edge>();
pq=new MinPQ<Edge>();
visit(G,0);
//只要队列不空就要进行判断
while(!pq.isEmpty()){
Edge e=pq.delMin(); //从队列取出权重最小边
int v=e.either(),w=e.other(v); //获得边对应的两个顶点
if(marked[v]&&marked[w]) continue; //如果两个顶点已经标记过了,表示这已经不是横切边了,直接跳过
mst.enqueue(e); //否则就将该边入队
if(!marked[v]) visit(G,v); //将没标记过的边加入树中
if(!marked[w]) visit(G,w);
}
}
//向树中增添顶点
private void visit(EdgeWeightedGraph G,int v){
marked[v]=true;
for(Edge e:G.adj(v)){
if(!marked[e.other(v)]) pq.insert(e); //将新的横切边加入队列
}
}
public Iterable<Edge> edges(){
return mst;
}
public double weight() {
double weight = 0.0;
for (Edge e : edges())
weight += e.weight();
return weight;
}
public static void main(String[] args) {
In in=new In(args[0]);
EdgeWeightedGraph G;
G=new EdgeWeightedGraph(in);
LazyPrimMST mst=new LazyPrimMST(G);
for(Edge e:mst.edges()){
StdOut.println(e);
}
StdOut.println("weight = "+mst.weight());
}
}
可以看到延时算法将横切边加入到队列中后,只在取出的时候才会判断这个横切边是否仍然存在,否则会一直保留这条可能已经失效的横切边
Prim算法即时实现
在我们将顶点v加入到树中时,对于其他所有非树顶点w产生的变化只可能使w到树的权重更小,所以,整个过程中我们不需要保存所有从树顶点到w的边,只需要保存一条权重最小的边就可以,我们在加入新顶点后不断更新这些值,直到所有顶点都进入树中
在即时实现的算法中,对数据结构做出了调整。将marked[]替换为顶点索引数组edgeTo[]。将mst[]替换为distTo[]顶点索引数组
对于一个非树顶点v来说,edgeTo[v]表示v到树中权重最小的边,distTo[v]表示这条边的权重
所有这类顶点都被保存在一条索引优先队列中,索引v关联的值使edgeTo[v]的边的权重,保证了每次总是将横切边中最短的边加入到树中
代码实现:
package cn.ywrby.Graph;
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.IndexMinPQ;
import edu.princeton.cs.algs4.Queue;
import edu.princeton.cs.algs4.StdOut;
//最小生成树Prim算法即时实现
public class PrimMST {
private Edge[] edgeTo; //距离树最近的边
private double[] distTo; //距离树最近的边的权重
private boolean[] marked; //是否在树中
private IndexMinPQ<Double> pq; //存放横切边的优先索引队列
public PrimMST(EdgeWeightedGraph G){
edgeTo=new Edge[G.V()];
distTo=new double[G.V()];
marked=new boolean[G.V()];
//初始化distTo数组
for(int i=0;i<G.V();i++){
distTo[i]=Double.POSITIVE_INFINITY;
}
pq=new IndexMinPQ<Double>(G.V());
distTo[0]=0.0;
pq.insert(0,0.0);
while(!pq.isEmpty()){
visit(G,pq.delMin());
}
}
private void visit(EdgeWeightedGraph G,int v){
marked[v]=true;
for(Edge e:G.adj(v)){
int w=e.other(v);
if(marked[w])continue;
if(e.weight()<distTo[w]){ //比较并更新非树顶点到树的最小权重
edgeTo[w]=e;
distTo[w]=e.weight();
if(pq.contains(w)) pq.change(w,distTo[w]);
else pq.insert(w,distTo[w]);
}
}
}
public Iterable<Edge> edges(){
Queue<Edge> mst = new Queue<Edge>();
for (int v = 0; v < edgeTo.length; v++) {
Edge e = edgeTo[v];
if (e != null) {
mst.enqueue(e);
}
}
return mst;
}
public double weight(){
double weight = 0.0;
for (Edge e : edges())
weight += e.weight();
return weight;
}
public static void main(String[] args) {
In in=new In(args[0]);
EdgeWeightedGraph G;
G=new EdgeWeightedGraph(in);
PrimMST mst=new PrimMST(G);
for(Edge e:mst.edges()){
StdOut.println(e);
}
StdOut.println("weight = "+mst.weight());
}
}
Prim算法找到加权无向图的最小生成树所需时间和ElogV成正比,空间和V成正比
优先队列中的顶点数最多为V,且使用了三条由顶点索引的数组,所以所需的空间上限和V成正比,算法会进行V次插入,V次删除和E次改变优先级(最坏情况下)操作,所以整体时间和ElogV成正比
Kruskal算法
Kruskal算法在最开始就将所有的边加入队列中,利用优先队列这种数据结构的优势,每次从队列中取出一条权重最小的边,然后利用Union-Find算法检查这条边对应的两个顶点是否已经连通,如果连通说明这条边不是横切边,如果没有连通,结合这条边的权重最小可以确定它就是下一个要插入的横切边,并将其加入,如此循环,直到含有V-1条边时表示已经成功完场一棵最小生成树的构造
代码实现:
package cn.ywrby.Graph;
import cn.ywrby.dataStructure.Queue;
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.MinPQ;
import edu.princeton.cs.algs4.StdOut;
import edu.princeton.cs.algs4.UF;
//Kruskal算法解决最小生成树问题
public class KruskalMST {
private Queue<Edge> mst;
public KruskalMST(EdgeWeightedGraph G){
mst=new Queue<Edge>();
MinPQ<Edge>pq=new MinPQ<Edge>();
for(Edge e:G.edges()) pq.insert(e);
UF uf=new UF(G.V());
while (!pq.isEmpty()&&mst.size()<G.V()-1){
Edge e=pq.delMin(); //从队列中得到权重最小的边
int v=e.either(),w=e.other(v);
if(uf.connected(v,w)) continue; //判断是不是横切边
uf.union(v,w); //连接两边
mst.enqueue(e); //入队
}
}
public Iterable<Edge> edges(){return mst;}
public double weight(){
double weight = 0.0;
for (Edge e : edges())
weight += e.weight();
return weight;
}
public static void main(String[] args) {
In in=new In(args[0]);
EdgeWeightedGraph G;
G=new EdgeWeightedGraph(in);
KruskalMST mst=new KruskalMST(G);
for(Edge e:mst.edges()){
StdOut.println(e);
}
StdOut.println("weight = "+mst.weight());
}
}