数学建模大赛中很多问题都能归约到下面的问题:从一副航线图中找到从一个城市到另外一个城市中代价最少的路线,这个代价可能是距离最近、花费最少或者时间最短,表示这些问题的数据结构是加权有向图。如下图所示,蓝色是从0到6的最短路径,是加权图和有向图的结合。
加权有向图的实现
和加权无向图的实现类似,程序见下面。
/**
* 加权有向边
* @author XY
*
*/
public class WeightedDiEdage {
private int v;
private int w;
private double we;
public WeightedDiEdage(int v,int w,double we){
this.v=v;
this.w=w;
this.we=we;
}
public int from(){
return v;
}
public int to(){
return w;
}
public double weight(){
return we;
}
@Override
public String toString() {
return "from:"+v+" to:"+w+" weight:"+we;
}
public static void main(String[] args) {
WeightedDiEdage dEdage=new WeightedDiEdage(0, 1, 0.2);
System.out.println(dEdage);
}
}
使用加权有向边实现加权有向图。
import java.io.File;
import java.util.ArrayList;
import java.util.Scanner;
/**
* 加权有向图的实现
* @author XY
*
*/
public class EdageWeightedDigraph {
private int V=0;
private int E=0;
private ArrayList<WeightedDiEdage>[] adj;
public EdageWeightedDigraph(int v){
this.V=v;
adj=(ArrayList<WeightedDiEdage>[])new ArrayList[v];
for (int i = 0; i < adj.length; i++) {
adj[i]=new ArrayList<WeightedDiEdage>();
}
}
public EdageWeightedDigraph(Scanner scanner){
this(scanner.nextInt());
int e=scanner.nextInt();
for (int i = 0; i <e; i++) {
int v=scanner.nextInt();
int w=scanner.nextInt();
double we=scanner.nextDouble();
WeightedDiEdage edage=new WeightedDiEdage(v, w, we);
addEdage(edage);
}
}
public void addEdage(WeightedDiEdage e){
int v=e.from();
adj[v].add(e);
E++;
}
public int V(){
return V;
}
public int E(){
return E;
}
public Iterable<WeightedDiEdage> adj(int v){
return adj[v];
}
@Override
public String toString() {
StringBuffer sb=new StringBuffer();
sb.append("V: "+V+" E: "+E+"\n");
for (int i = 0; i < V; i++) {
for(WeightedDiEdage e:adj[i])
sb.append(e+"\n");
}
return sb.toString();
}
public static void main(String[] args) throws Exception {
EdageWeightedDigraph dgraph=new EdageWeightedDigraph(new Scanner
(new File("E:"+File.separator+"wedigraph.txt")));
System.out.println(dgraph);
}
}
最短路径的普适算法
用两个数组表示最短路径,edageTo[]表示从起点到该顶点的最短路径中指向该顶点的有向边,distTo[]表示起点到该顶点的最短路径的距离。
最短路径:对任何一点最短路径的定义就是对任何路径e:v→w,都有distTo[w]<distTo[v]+e.weight。
顶点的放松:对于顶点v,将所有从v指出的边e:v→w,执行下面的程序,例如如果原来edageTo[5]=D,那么在放松顶点3的时候就会变为edageTo[5]=B,distTo[5]也会变小,在这个过程中舍弃了比较长的路径而为顶点5选择了较短的路径,因为我们在寻找最短路径。
private void relax(EdageWeightedDigraph g,int v){
for(WeightedDiEdage e:g.adj(v)){
int w=e.to();
if(disto[w]>disto[v]+e.weight()){
disto[w]=disto[v]+e.weight();
edageto[w]=e;
}
}
}
假设在放松顶点2的时候,distTo[5]=0.5,edageTo[5]=B,那么distTo[w]<distTo[v]+e.weight(),也就意味着边D是无效边,它不会影响到顶点5的最短路径。
普适最短路径算法:将起点s的distTo[s]初始化为0,其他元素的distTo[]初始化为无穷大,放松图中的任意边,直到不存在有效边为止。当不存在有效边的时候就意味着每个顶点的路径都是最短路径。
在讲顶点的放松的时候先放松顶点2还是先放松顶点3的情况是不一样的,所以在放松顶点的时候的顺序很重要,如果选择了好的顺序,执行if(disto[w]>disto[v]+e.weight())的时候dist[v]已经是顶点v的最短路径,那么顶点w不需要重新放松,如果顺序选择的不够好,disto[v]不是最小值,那么在放松完顶点v后放松其他顶点直到disto[v]是最小值后需要再次放松顶点v,才能得到顶点v的最短路径。
下面要讲到的Bellman-ford算法就是普适算法,它可以解决所有的最短路径问题,Dijkstra算法和无环加权有向图的最短路径算法都是在特定情况下选择好的顺序来使最短路径算法简单的算法。
Dijkstra算法
Dijkstra算法适合于所有权重都为正的加权有向图。
Dijksrtra算法和Prim算法相似,从起点开始,放松起点,将起点指向的其他顶点加入优先序列,选择距离最小的点进行放松。Prim算法和索引优先序列的实现见最小生成树
import java.io.File;
import java.util.ArrayList;
import java.util.Scanner;
import linmin.Indexpriority;
/**
* Dijkstra算法,和Prim的即时算法相似,同样的可以模仿延时Prim算法形成延时版的Dijkstra算法
* @author XY
*
*/
public class Dijkstra {
private WeightedDiEdage[] edageto;
private double[] disto;
private Indexpriority<Double> pq;
private int source;
public Dijkstra(EdageWeightedDigraph graph,int s){
edageto=new WeightedDiEdage[graph.V()];
disto=new double[graph.V()];
for (int i = 0; i < disto.length; i++) {
disto[i]=Double.POSITIVE_INFINITY;
}
disto[s]=0;
pq=new Indexpriority<Double>(graph.V());
pq.insert(s, 0.0);
while(!pq.isEmpty()){
relax(graph, pq.delMin());
}
}
private void relax(EdageWeightedDigraph g,int v){//顶点放松
for(WeightedDiEdage e:g.adj(v)){
int w=e.to();
if(disto[w]>disto[v]+e.weight()){
disto[w]=disto[v]+e.weight();
edageto[w]=e;
pq.insert(w, disto[w]);
//disto[w]变小,改变优先序列里面w对应的值。因为索引优先序列里面处理了插入和改变,所以这里
//没有执行“存在w则改变,不存在w则插入”的逻辑
}
}
}
public boolean hasPathTo(int v){
return disto[v]<Double.POSITIVE_INFINITY;
}
public double distTo(int v){
return disto[v];
}
public Iterable<WeightedDiEdage> pathTo(int v){
ArrayList<WeightedDiEdage> list=new ArrayList<WeightedDiEdage>();
for(int i=v;i!=source;i=edageto[i].from())
list.add(edageto[i]);
return list;
}
public static void main(String[] args) throws Exception {//测试用例
EdageWeightedDigraph dgraph=new EdageWeightedDigraph(new Scanner
(new File("E:"+File.separator+"wedigraph.txt")));
Dijkstra sp=new Dijkstra(dgraph, 0);
if(sp.hasPathTo(1))
for(WeightedDiEdage e:sp.pathTo(1))
System.out.println(e);
}
}
可以使用下面的加权有向图test.txt进行测试。
8
15
4 5 0.35
5 4 0.35
4 7 0.37
5 7 0.28
7 5 0.28
5 1 0.32
0 4 0.38
0 2 0.26
7 3 0.39
1 3 0.29
2 7 0.34
6 2 0.40
3 6 0.52
6 0 0.58
6 4 0.93
无环加权有向图的最短路径算法
针对无环加权图我们选择它的拓扑排序的顺序放松顶点,在拓扑顺序中,有向边只存在于位于前面的顶点到后面的顶点,也就是说位于前面的顶点v放松时的distTo[v]就是最小值,因为拓扑顺序中在v的后面再也不存在指向v的顶点,也就是distTo[v]不会被改变。位于拓扑排序前面的顶点的distTo[]不会改变意味着所有的顶点不需要重新放松。而如果存在环,就意味着存在后面的顶点到前面的顶点的有向边,那也就是前面的顶点的distTo[]会改变,那么所有的顶点都必须重新放松。
为了得到拓扑排序必须针对加权有向图改变有向图的拓扑排序的程序。有向图的拓扑排序见有向图
mport java.io.File;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;
import java.util.Stack;
/**
* 加权有向图的深度优选次序,逆后序就是拓扑排序
* @author XY
*
*/
public class WeightDFOrder {
private boolean[] marked;
private Queue<Integer> pre;//根据访问的顺序
private Queue<Integer> post;//深度优先搜索结束的顺序,先结束进
private Stack<Integer> reverse;//深度优先搜索结束的顺序的逆序,先结束进栈
public WeightDFOrder(EdageWeightedDigraph dgraph) {
marked = new boolean[dgraph.V()];
pre = new LinkedList<Integer>();
post = new LinkedList<Integer>();
reverse = new Stack<Integer>();
for (int i = 0; i < dgraph.V(); i++) {
if (!marked[i])
dfs(dgraph, i);
}
}
private void dfs(EdageWeightedDigraph dgraph, int s) {
marked[s] = true;
pre.add(s);
for (WeightedDiEdage x : dgraph.adj(s)) {
int w=x.to();
if (!marked[w])
dfs(dgraph, w);
}
post.add(s);
reverse.push(s);
}
public Iterable<Integer> pre() {
return pre;
}
public Iterable<Integer> post() {
return post;
}
public Iterable<Integer> reverse() {
Stack<Integer> stack=new Stack<Integer>();
//因为JAVA中Stack是Vector的子类,其迭代顺序是Vector的顺序,不是栈顶到栈底的顺序所以反转
while(!reverse.isEmpty()){
stack.push(reverse.pop());
}
return stack;
}
public static void main(String[] args) throws Exception {
EdageWeightedDigraph dgraph=new EdageWeightedDigraph(new Scanner
(new File("E:"+File.separator+"wedigraph.txt")));
WeightDFOrder order=new WeightDFOrder(dgraph);
for(int s:order.reverse())
System.out.println(s);
}
}
下面是无环加权有向图的最短路径算法。
import java.io.File;
import java.util.ArrayList;
import java.util.Scanner;
/**
* 无环加权有向图的最短路径算法
* @author XY
*
*/
public class NoCycleSP {
private double[] disto;
private WeightedDiEdage[] edageto;
private int source;
public NoCycleSP(EdageWeightedDigraph dgraph,int s){
disto=new double[dgraph.V()];
edageto=new WeightedDiEdage[dgraph.V()];
for (int i = 0; i < disto.length; i++) {
disto[i]=Double.POSITIVE_INFINITY;
}
disto[s]=0.0;
this.source=s;
Iterable<Integer> order=new WeightDFOrder(dgraph).reverse();//拓扑排序
for(int x:order){
relax(dgraph, x);
}
}
private void relax(EdageWeightedDigraph dgraph,int v){
for(WeightedDiEdage e:dgraph.adj(v)){
int w=e.to();
if(disto[w]>disto[v]+e.weight()){
disto[w]=disto[v]+e.weight();
edageto[w]=e;
}
}
}
public boolean hasPathTo(int v){
return disto[v]<Double.POSITIVE_INFINITY;
}
public double distTo(int v){
return disto[v];
}
public Iterable<WeightedDiEdage> pathTo(int v){
ArrayList<WeightedDiEdage> list=new ArrayList<WeightedDiEdage>();
for(int i=v;i!=source;i=edageto[i].from())
list.add(edageto[i]);
return list;
}
public static void main(String[] args) throws Exception {
EdageWeightedDigraph dgraph=new EdageWeightedDigraph(new Scanner
(new File("E:"+File.separator+"wedigraph.txt")));
NoCycleSP sp=new NoCycleSP(dgraph, 0);
if(sp.hasPathTo(6))
for(WeightedDiEdage e:sp.pathTo(6))
System.out.println(e);
}
}
Bellman-ford算法的实现
Bellman-ford算法是普适算法,可以解决所有的图的最短路径问题,包括环和负权重边。
Bellman-ford算法:将起点s的distTo[s]初始化为0,其他元素的distTo[]初始化为无穷大,以任意顺序放松有向图的所有边,重复V轮。此时不存在有效边。
Bellman-ford算法时间与VE成正比, 算法笔记17会介绍改进的Bellman-ford算法。