最短路径
所谓的最短路径算法,当然得是图中的一个顶点s到另一个顶点w的最短路径,这个w可以是图中的任意的顶点,当然可能并不是所有点都满足存在最短路径,最短路径的存在是需要条件的,其实这个最短即为最优解,所有的最优解问题如果解有限,那么都可以通过枚举来找到最优的那个解,如果解无限而且不能剪枝呢?在无限的解里还存在更优解?那么最短路径问题就是无解的,具体来看,我想大家很容易想到,那就是如果图中存在环,那么s到有的顶点w的最短路径势必会经过该环,如果环为正,那么最短路径仅仅是经过而不会陷入环中,如果环所有边权值为负,那么所谓权值最低的路径就可以有无数条(绕环走五无数圈就行),所以在这种情况下出发点s到凡是和环连通的顶点的最短路径问题均无解。
那么最短路径具有一种怎么样的特性?令G为一副加权有向图,顶点s是G中起点,dis[G.v()]为一个顶点索引的数组,保存的是G中路径的长度,如s到顶点v的距离就用dis[v]表示,一开始设置dis[]数组都为+∞,并设置dis[s]为0,当单源点s到图中任意顶点的最短路径计算完毕后,对于v到w的任意一条边e,dis数组都满足dis[v] <= dis[w] + e.weigh();理解了这一点,是解决最短路径问题的核心;
- 所以通用的最短路径算法,都是基于这样的一种思想进行的,不断放松图中的任意边,直到不存在任何有效的边为止。何为放松?其实对任意一条边e执行上述插图以及核心中的判别,并修改dis数组上的数值,直到所有边的放松操作都没法修改dis数组为止。关键是,如何先选取哪些边进行松弛?
*
Dijkstra算法
迪杰斯特拉算法,其实算导里面的老师并不这么叫的,j是不发音的,不过许多人都这么叫,无所谓。Dijkstra算法就提供了寻找放松边的一种思路,它先将dis[]数组中最小的(意味着此时离s最近)还未加入到最小路径树的顶点加入树中,直到所有顶点都在最短路径树中或者是所有的非树顶点的dis[]数组中的值为无穷大。
下面我将通过测试一个加权有向无环图的输入并计算第0个顶点到其他任意顶点的最短距离并输入结果的实例进行演示:
代码如下:
package PAT;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Scanner;
public class DijkstraTest {
static DijkstraTest instance = new DijkstraTest(); // 用于生成内部类Edge的实例
static int N, M; // N为顶点总数,M为边的总数
static List<Edge>[] bag; // 用于存放所有边信息的邻接表
static int[] dis; // 每一个顶点到出发点的距离数组
static Edge[] edgeTo; // 使得每一个顶点加入最短路径树的边数组,若没加入,则为空
static PriorityQueue<IndexMinPQ> pq; // 优先队列
/**
* 用于测试主函数,输入形式如下,
* 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];
dis = new int[N];
edgeTo = new Edge[N];
for(int i=0; i<N; i++)
dis[i] = Integer.MAX_VALUE;
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);
}
//以上都是图的邻接表的构建
int s0 = 0; // 选定0号顶点作为出发点
dijkstra(s0); // 计算从s0号顶点出发到其余各点的最短路径
// 打印所有可达的最短路径及加入最短路径树的边信息
for(int i=0; i<N; i++) {
if(edgeTo[i] == null && i != s0)
System.out.println(s0 + "->" + i + ": 不可达;");
else
System.out.println(s0 + "->" + i + ": " + dis[i] + ", 最后使该顶点加入树的是:" + edgeTo[i]);
}
}
/**
* dijkstra算法的主核心代码
* @param s
*/
private static void dijkstra(int s) {
dis[s] = 0;
pq = new PriorityQueue<>();
pq.add(instance.new IndexMinPQ(s, 0));
while(!pq.isEmpty())
relax(pq.poll().v);
}
/**
* 对顶点进行u的所有邻接边进行松弛
* @param u
*/
private static void relax(int u) {
for(Edge e : bag[u]) {
int v = e.other(u);
if(dis[v] > dis[u] + e.weigh()) {
dis[v] = dis[u] + e.weigh();
edgeTo[v] = e; // 使得顶点v加入最短路径树的边e保存在数组上;
check(v, dis[v]);
}
}
}
/**
* check函数是用来判断当前的顶点v是否在队列pq中
* 如果不存在就得在pq中添加进v及此时他的最短距离。
* 如果v已经在,那么就得将修改后的dis[v]重新存入队列中
* 因为java是值传递,所以队列里的this.e仅仅是和dis[v]相等,若外部的dis[v]修改了
* 那么this.e也得进行修改,做到即时更新顶点v到初始点s的距离
* @param v 被松弛的顶点v
* @param d 松驰后的距离d
*/
private static void check(int v, int d) {
Iterator<IndexMinPQ> it = pq.iterator();
while(it.hasNext()) {
IndexMinPQ temp = it.next();
if(temp.v == v) {
temp.distance = d;
return; // 一旦查找到队列中含有该点就立马停止返回
}
}
pq.add(instance.new IndexMinPQ(v, d)); // 优先队列加入该点和距离
}
/**
* 这个类主要实现了对每次松弛后顶点序号,及此时顶点到初始点s的距离的存储
* 主要用于后续实现对距离e的排序,并可以及时取出最小距离的顶点序号key
* @author Gastby
*
*/
private class IndexMinPQ implements Comparable<IndexMinPQ> {
int v; // 保存顶点下标
int distance; // 保存v顶点到出发点s的距离
IndexMinPQ(int k, int e) {
this.v = k;
this.distance = e;
}
/**
* 因为要放入优先队列中,所以必须实现这个接口
*/
@Override
public int compareTo(IndexMinPQ p) {
if(distance > p.distance) return 1;
else if(distance < p.distance) return -1;
return 0;
}
}
/**
* 带权有向边的类,其实在最短路径中这个边类和最小生成树中的边是一样的
* 而且这条边不管用来表示有向还是无向图,加权或者不加权,都是一样的(不加权无非都是1咯);
* @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() + "的边";
}
}
}
测试数据:
5 4
0 1 1
0 3 2
2 4 4
2 3 3
输出结果:
0->0: 0, 最后使该顶点加入树的是:null
0->1: 1, 最后使该顶点加入树的是:长为1的边
0->2: 不可达;
0->3: 2, 最后使该顶点加入树的是:长为2的边
0->4: 不可达;
思考
这里的工具类IndexMinPQ工具类是我自己写的,按照算法书中的写法,它实际上船创建了一个非常巨大的实现键值对存储而且可以对值进行堆有序处理的工具类,笔者看来非常复杂,一开始想使用java自己提供的工具类,但是PriorityQueue却只能实现单值的堆有序存储,而TreeMap是可以完成对键值对的存储,但是红黑树实现的对键的全排,况且,如果将距离作为键,不可避免的包含重复,综上,决定自己实现三方类,为达目的而已,将顶点序号v,和顶点v到初始点s的距离distance,一起作为一个实现了Comparable类并重写了比较方法的三方类的两个成员变量存储,其中的比较方法为distance的比较。于是这样就可以利用java的工具类PriorityQueue,省去了大量的构造最小堆和添加删除元素的代码。
以上为基于优先队列的做法,但实际上Dijkstra算法,按照我们一开始的理解,可以不需要这么优化,为了更好理解,我写了下面这个简单的版本;
下面这个版本就是上面的Dijkstra经典算法尚未经过优先队列优化之前的核心代码,阐述如下(把上面的那一段再次摘抄如下):
它先将dis[]数组中最小的(意味着此时离s最近)还未加入到最小路径树的顶点加入树中,直到所有顶点都在最短路径树中或者是所有的非树顶点的dis[]数组中的值为无穷大。
代码如下:仅需要将上述的代码里的dijkstra函数替换为下面即可,简洁明了。
private static void dijkstra(int s) {
dis[s] = 0;
while(true) {
int index = -1, INF = Integer.MAX_VALUE;
for(int i=0; i<N; i++)
if(!marked[i] && dis[i] < INF)
index = i;
if(index != -1) {
marked[index] = true;
for(Edge e : bag[index]) {
int v = e.other(index);
if(dis[v] > dis[index] + e.weigh()) {
dis[v] = dis[index] + e.weigh();
edgeTo[v] = e;
}
}
} else
break;
}
}
相比上述经过优先队列优化后的代码,这段代码就简单了非常非常多, 首先少了我们的三方类IndexMinPQ,以及PriorityQueue,直接采用每次都对当前dis数组遍历,找还未加入最短路径树的点中距离s最近的那个点。不得不说,这段代码非常简洁明了,让人一目了然。
那么最短路径算法的局限性就在于?
事实上,一开始分析的一样,对于含有负权环的图的最短路径问题无解。但其实对于Dijkstra算法而言,它因为每次从dis数组中取出的都是此时莉源点最近的尚未被标记的顶点,而取出来之后松弛完所有的边,立马标记为已在最小路径树上,按照这个流程,依次标记的顶点顺序是按照离源点距离从小到大排列的,一旦后续出现负权边,是可能让已经被标记了的顶点再次被松弛的,但是就因为已经被标记,所以失去了再次松弛的机会,最终的结果肯定是错误的。下有图示~
当你真正了解了Dijkstra算法的核心思想,你可以在无论是基于邻接表存储的图或者是邻接矩阵存储的图,亦或者是基于优先队列进行优化之后的代码,都可以将其核心代码写出来。
下面最后再附上基于数组存储的图的Dijkstra算法,邻接矩阵比较适合存储稠密图,但虽然可以说空间上有点奢侈,但依次换来的整体上的代码,非常的简洁,美!
代码如下:
/**
* s为初发点
*/
private static void dijkstra(int s) {
//初始化dis数组
for(int i=0; i<N; i++)
dis[i] = edge[s][i]; // 初始化dis数组,直接从初始点的邻接边开始
marked[s] = true; // 标记s点已经加入最小路径树了;
//Dijkstra算法核心代码
int min, u;
for(int i=0; i<N; i++) {
u = -1;
min = Integer.MAX_VALUE;
// 找到不在树中的最小距离点
for(int j=0; j<N; j++) {
if(!marked[j] && dis[j] < min) {
min = dis[j];
u = j;
}
}
if(u == -1) break; // 可能存在有点不连通,i未达到N-1;
marked[u] = true; // 找出的最小距离点标记加入最小路径树中
// 对新加入的点的邻接边逐个松弛~
for(int j=0; j<N; j++) {
if(edge[u][j] < INF) {
if(dis[j] > dis[u] + edge[u][j])
dis[j] = dis[u] + edge[u][j];
}
}
}
}
整体上来看,最短路径算法,还是掌握核心思想,逐个松弛即可。实际中的许多题目中,Dijkstra算法只会是其中的很小一部分,所以为了解决好相应的问题,需要随机应变,怎么方便,怎么高效,怎么简洁怎么来,掌握了核心思想也就不难了啦。