加权图是指一种每条边都有权重的图。图的生成树是它的一棵含有其所有顶点的无环连通子图。最小生成树是指所有边的权重最小的树。如下图所示,该图的最小生成树在图中蓝色标出,本文的目的就是设计算法得到加权图的最小生成树。 切分定理:在一副加权图中,给定一种切分,它的横切边中的权重最小边必定属于图的最小生成树。横切边是指将图分为没有交集的两部分,连接这两部分的边。
所有最小生成树算法的本质都是重复的利用切分定理的贪心算法:初始状态下所有边均为灰色,找到一种切分,其产生的横切边均不为黑色,将权重最小的横切边标记为黑色,反复执行直到标记了V-1条黑色的边。
为了实现这种算法,先实现带权重的边和加权图。
/** * 带权重的边 * @author XY * */ public class WeightedEdage implements Comparable<WeightedEdage>{ private int v; private int w; private double weight; public WeightedEdage(int v,int w,double weight){ this.v=v; this.w=w; this.weight=weight; } public int either(){ return v; } public int other(int vertex){ if(v==vertex) return w; if(w==vertex) return v; else throw new IllegalArgumentException("no this vertex"); } public double weight(){ return this.weight; } @Override public int compareTo(WeightedEdage o) {//为了权重比较方便直接实现Comparable接口 if(this.weight>o.weight) return 1; if(this.weight<o.weight) return -1; return 0; } @Override public String toString() { return "vertex: "+v+" "+w+",weight: "+weight; } public static void main(String[] args) {//测试用例 WeightedEdage edage=new WeightedEdage(0, 2, 0.52); WeightedEdage edage1=new WeightedEdage(3, 2, 0.2); int v=edage.either(); int w=edage.other(v); System.out.println(v); System.out.println(w); System.out.println(edage); System.out.println(edage.compareTo(edage)); } }下面是加权图的实现
import java.io.File; import java.util.ArrayList; import java.util.Scanner; /** * 加权图 * 和无向图的实现完全一致,唯一不同是邻接表中存储的是连接该顶点的边而不是顶点 * @author XY * */ public class EdageWeightedGraph { private int V; private int E; private ArrayList<WeightedEdage>[] adj; public EdageWeightedGraph(int v){ this.V=v; adj=(ArrayList<WeightedEdage>[])new ArrayList[v]; for (int i = 0; i < adj.length; i++) { adj[i]=new ArrayList<WeightedEdage>(); } } public EdageWeightedGraph(Scanner scan){ this(scan.nextInt()); int e=scan.nextInt(); for (int i = 0; i < e; i++) { int v=scan.nextInt(); int w=scan.nextInt(); double weight=scan.nextDouble(); addEdage(v, w, weight); } } public void addEdage(int v,int w,double weight){ WeightedEdage edage=new WeightedEdage(v, w, weight); adj[v].add(edage); adj[w].add(edage); E++; } public Iterable<WeightedEdage> adj(int v){//返回相邻边 return adj[v]; } public Iterable<WeightedEdage> edages(){//返回所有边 ArrayList<WeightedEdage> list=new ArrayList<WeightedEdage>(); for (int i = 0; i < adj.length; i++) { for(WeightedEdage edage:adj[i]){ if(!list.contains(edage)) list.add(edage); } } return list; } public int V(){ return this.V; } public int E(){ return this.E; } @Override public String toString() { StringBuffer sb=new StringBuffer(); sb.append("V: "+V+" E: "+E+"\n"); for(WeightedEdage edage:edages()){ sb.append(edage+"\n"); } return sb.toString(); } public static void main(String[] args) throws Exception {//测试用例 EdageWeightedGraph wgraph=new EdageWeightedGraph(new Scanner (new File("E:"+File.separator+"wedagegraph.txt"))); for(WeightedEdage edage:wgraph.adj(0)) System.out.println(edage); System.out.println(wgraph.V()); } }根据前面讲到的切分定理和最小生成树算法的本质,我们需要一个boolean[]来标记顶点是否进入最小生成树,如果一条边的两个顶点都被标记意味着该边不属于进入最小生成树的边的候选:横切边。使用一个集合保存进入最小生成树的边。使用优先序列动态加入边和得到权重最小边。 Prim算法是从任意一个顶点开始,此顶点加入最小生成树,其所有边加入优先序列,删除得到权重最小边,判断权重最小边是否是横切边,如果是该边和另一个顶点加入最小生成树,如果最小边不是横切边则判断次小边直到找到属于横切边的边并加入最小生成树。将新加入的顶点的相邻边全部加入优先序列,取出最小边重复前述步骤。
延迟Prim算法的内容就是前述的Prim算法。
import java.io.File; import java.util.LinkedList; import java.util.PriorityQueue; import java.util.Scanner; /** * 演示Prim算法:最小生成树 * 从0开始每次寻找横切边中的最小权重边加入最小生成树,两个顶点都在最小生成树中的边不是横切边 * @author XY * */ public class LazyPrim { private PriorityQueue<WeightedEdage> pq;//保存边,得到最小权重边 private LinkedList<WeightedEdage> queue=null;//最小生成树结果 private boolean[] marked;//标记是否进入最小生成树 private double weight; public LazyPrim(EdageWeightedGraph wgraph){ pq=new PriorityQueue<WeightedEdage>(); queue=new LinkedList<WeightedEdage>(); marked=new boolean[wgraph.V()]; for(WeightedEdage edage:wgraph.adj(0)) pq.add(edage); marked[0]=true; while(!pq.isEmpty()){ WeightedEdage edage=pq.poll(); int v=edage.either(); int w=edage.other(v); if(marked[v] && marked[w]) continue;//非横切边,失效 queue.add(edage); weight+=edage.weight(); if(marked[v]) { marked[w]=true; visit(wgraph, w); }else { marked[v]=true; visit(wgraph, v); } } } private void visit(EdageWeightedGraph wgraph,int v){ for(WeightedEdage edage:wgraph.adj(v)) if(!pq.contains(edage)) pq.add(edage); } public double weight(){ return this.weight; } public Iterable<WeightedEdage> edages(){//最小生成树结果 return queue; } public static void main(String[] args) throws Exception { EdageWeightedGraph wgraph=new EdageWeightedGraph(new Scanner (new File("E:"+File.separator+"wedagegraph.txt"))); LazyPrim prim=new LazyPrim(wgraph); for (WeightedEdage edage:prim.edages()) { System.out.println(edage); } System.out.println(prim.weight()); } }即使Prim算法和延时Prim算法的唯一区别就是延迟Prim算法的优先序列中保存了每个顶点的所有横切边(非横切边会失效),那么优先序列的大小就是E。但实际上只有每个顶点的最小横切边才会有机会进入最小生成树,其他变迟早会失效,这样的话就可以减少优先序列的大小:每个顶点的最小横切边进入优先序列,为此使用索引优先序列,索引位顶点,内容为对应顶点的最小横切边。索引优先序列的实现参考索引优先序列中的最小索引优先序列。
import java.io.File; import java.util.LinkedList; import java.util.Scanner; import linmin.Indexpriority; /** * 即使Prim算法:最小生成树 * 和演示Prim算法的区别就是优先序列中只存在V个元素,对应的是连接每个顶点的边的最小权重, * 而延时Prim中优先序列保存了所有的边,E个元素。因为对于每个顶点而言只会选择权重最小的 * 横切边进入最小生成树,那么权重更大的边会失效,但依然在优先序列中参与优先序列的堆有序 * 过程。这里使用了edages[]和weights[]保存每个顶点的权重最小边,他们的作用是得到最后的 * 最小生成树,改变索引优先序列Indexpriority的API,实现get方法去实现visit()方法中 * 的更新的if语句,再使用队列保存进入最小生成树的边,也可以实现本算法 * @author XY * */ public class Prim { private Indexpriority<Double> pq;//索引优先序列,保存edages[]中的边的权重。 private boolean[] marked;//是否进入最小生成树 private WeightedEdage[] edages;//最小生成树到索引点的边中权重最小的边。 private double[] weights;//edages[]对应索引边的权重 public Prim(EdageWeightedGraph wgraph){ pq=new Indexpriority<Double>(wgraph.V()); marked=new boolean[wgraph.V()]; edages=new WeightedEdage[wgraph.V()]; weights=new double[wgraph.V()]; for (int i = 0; i < edages.length; i++) { edages[i]=null;//初始为0 weights[i]=Double.POSITIVE_INFINITY;//初始为无穷大 } pq.insert(0, 0.0);//从0开始,第一个进入最小生成树,边的权重为0; weights[0]=0.0; while(!pq.isEmpty()){ visit(wgraph, pq.delMin()); } } public void visit(EdageWeightedGraph wgraph,int v){ marked[v]=true; for(WeightedEdage edage:wgraph.adj(v)){ int w=edage.other(v); if(marked[w]) continue; if(edage.weight()<weights[w]){//如果连接该点的边存在更小权重的边则更新 weights[w]=edage.weight(); edages[w]=edage; pq.insert(w, weights[w]); } } } public Iterable<WeightedEdage> edages(){ LinkedList<WeightedEdage> queue=new LinkedList<WeightedEdage>(); for(WeightedEdage e:edages) if(e!=null) queue.add(e); return queue; } public double weight(){ double w=0; for (double i:weights) { w+=i; } return w; } public static void main(String[] args) throws Exception { EdageWeightedGraph wgraph=new EdageWeightedGraph(new Scanner (new File("E:"+File.separator+"wedagegraph.txt"))); Prim prim=new Prim(wgraph); System.out.println(prim.weight()); } }Prim算法是从一个顶点开始连续的往下走完成最小生成树,和Prim算法不一样,Kruskal算法是分开的,首先形成的是最小生成树的各个部分,最后各个部分连接完成最小生成树。Kruskal算法的原理是将所有的边依次删除取最小边,判断最小边是否属于横切边,如果是那么最小边肯定会进入最后的最小生成树,由于最小生成树的生长过程不连续,需要使用union-find算法来判断是否是横切边(union-find为最小生成树实现,选取的边的两个定点在union-find中连接,如果边的两个定点已经连接说明该边不是横切边(没有连接最小生成树和非最小生成树))并将顶点加入最小生成树。union-find算法见union-find算法。
import java.io.File; import java.util.LinkedList; import java.util.PriorityQueue; import java.util.Scanner; import linmin.QUuf; /** * 最小生成树算法:Kruskal算法 * 所有边进入优先队列,每次都选择最小边进入最小生成树,为了确定最小边 * 是横切边,所以使用union-find * @author XY * */ public class Kruskal { private PriorityQueue<WeightedEdage> pq;//优先队列取出权重最小边 private LinkedList<WeightedEdage> queue;//保存最小生成树的边 private QUuf uf;//最小生成树的union-find,判断边是否会在最小生成树中形成环 private double weight=0.0;//最小生成树的权重 public Kruskal(EdageWeightedGraph wgraph){ pq=new PriorityQueue<WeightedEdage>(); queue=new LinkedList<WeightedEdage>(); uf=new QUuf(wgraph.V()); for (int i = 0; i < wgraph.V(); i++) { for(WeightedEdage e:wgraph.adj(i)) if(!pq.contains(e)) pq.add(e); } while(!pq.isEmpty()){ WeightedEdage e=pq.poll();//取出权重最小边 int v=e.either(); int w=e.other(v); if(uf.isconnect(v, w)) continue; //判断最小生成树中两点是否联通,若连通则此边形成环,失效 uf.union(v, w); queue.add(e); weight+=e.weight(); } } public Iterable<WeightedEdage> edages(){ return queue; } public double weight(){ return weight; } public static void main(String[] args) throws Exception { EdageWeightedGraph wgraph=new EdageWeightedGraph(new Scanner (new File("E:"+File.separator+"wedagegraph.txt"))); Kruskal k=new Kruskal(wgraph); for (WeightedEdage e:k.edages()) { System.out.println(e); } System.out.println(k.weight()); } }最小生成树算法解决最大生成树问题的3种方法: 1 将所有的权重取相对值,得到的最小生成树就是最大生成树,得到的最小生成树的权重的相反数是最大生成树的权重 2 改变WeightedEdage的compareTo()方法,改变两个返回值 3 改变最小生成树算法中的if()语句的不等式的方向。 后两种方法的原理是一致的。