最短路径Dijkstra算法优化实战

最近做的系统中要实现一个功能,求地图中两点之间的最短路径,用Java编写。自然想到了Dijkstra算法,这个算法的时间复杂度为O(n^2),另外我们的系统中还需要将路径中经过的所有点都保存起来,这就会引入额外的复杂度。
Dijkstra算法描述传送门:[url]http://baike.baidu.com/view/7839.htm?fr=ala0_1_1[/url]
第一步,先不讨论复杂度,先将第一步迈出去,实现最基本的功能,于是有了第一版的代码。

/**
* 点
* @author Jason Wen
*
*/
public class Point {
private String name;
//经度
private double x = 0;
//纬度
private double y = 0;
private int lu = 0;
//是否为交叉点
private boolean isCross;
//是否为红点,如果源点到该点的最短路径已经求出,该点变为红点
private boolean isRed;
//是否为源点
private boolean isSource;

constructor & setter/getter
}

/**
* 边,任意相连的两点均构成一条边
* 属性包括起始,结束点,长度
* @author Jason Wen
*
*/
public class Edge {
private Point start;
private Point end;
private double length;

constructor & setter/getter
}

/**
* 最短路径,属性包括源点,终点,路径所经过所有点的集合,长度
* @author Jason Wen
*
*/
public class Path {
private Point source;
private Point end;
private List<Point> points;
private double length;

constructor & setter/getter
}

public class Dijkstra {
private List<Point> points;
private List<Edge> edges = new ArrayList<Edge>();;
private List<Path> paths = new ArrayList<Path>();
//当前变为红点的点
private Point currentPoint;
private final double INFINITY = 999999;
//源点到当前红点的长度
private double startToCurrent;

/**
* 初始化源点到其他各点的路径,即Path
*/
public void init(){
Point source = null;
for (Point point : points) {
if (point.isSource()) {
source = point;
}
}
//设置路径的终点
for (Point point : points) {
List<Point> redPoints = new ArrayList<Point>();
redPoints.add(source);
Path path = new Path(source,point,redPoints,INFINITY);
paths.add(path);
}
//设置路径的长度,当存在这样的边:起始,结束点分别和路径的源点,终点相同
for (Path path : paths) {
for (Edge edge: edges) {
if (path.getSource().getName().equals(edge.getStart().getName())&&path.getEnd().getName().equals(edge.getEnd().getName())) {
path.setLength(edge.getLength());
}
}
}
}

public void dijkstra(){
dijkstra(null);
}

public void dijkstra(Point end){
init();
for (int i = 0; i < paths.size(); i++) {
int indexMin = getMin();
double minDist = paths.get(indexMin).getLength();
if (minDist==INFINITY) {
System.out.println("有无法到达的顶点");
}else if(i!=paths.size()-1){
//获取当前循环已经求出最短路径的点
currentPoint = paths.get(indexMin).getEnd();
paths.get(indexMin).getPoints().add(currentPoint);
startToCurrent = minDist;
}
//将当前点设为红点
for (Point point : points) {
if (point.getName().equals(currentPoint.getName())) {
point.setRed(true);
if (end!=null&&end.getName().equals(point.getName())) {
return;
}
}
}
resetPaths();
}
}

/**
* 在路径集合中获取长度最小的索引值
* @return
*/
private int getMin() {
double minDist = INFINITY;
int indexMin = 0;
for (int i = 0; i < paths.size(); i++) {
Path path = paths.get(i);
if (!path.getEnd().isRed()&&path.getLength()<minDist) {
minDist = path.getLength();
indexMin = i;
}
}
return indexMin;
}

/**
* 在当前红点发生变化后,源点到其他点的路径也相应变化,通过当前红点,
* 之前不可到的点有可能变为可到达,
* 之前可到达的点路径长度会发生改变
* 所以要重置其他路径的长度
*/
private void resetPaths() {
for (Path path : paths) {
if (path.getEnd().isRed()) {
continue;
}
for (Edge edge: edges) {
if (edge.getStart().getName().equals(currentPoint.getName())&&edge.getEnd().getName().equals(path.getEnd().getName())) {
double currentToFringe = side.getLength();
double startToFringe = startToCurrent + currentToFringe;
double pathLength = path.getLength();
if(startToFringe<pathLength) {
path.getPoints().add(currentPoint);
path.setLength(startToFringe);
}
}
}
}

}

public void display(){
for (Path path : paths) {
System.out.print(path.getSource().getName()+"-->"+path.getEnd().getName()+":");
for (Point point : path.getPoints()) {
System.out.print(point.getName()+" ");
}
System.out.print(path.getLength());
System.out.println();
}
}


setter/getter
}


测试一下

public class Client {
public static void main(String[] args) {
long initBegin = System.currentTimeMillis();
Point A = new Point("A");
Point B = new Point("B");
Point C = new Point("C");
Point D = new Point("D");
Point E = new Point("E");
Point F = new Point("F");
Point G = new Point("G");
Point H = new Point("H");
Point I = new Point("I");
Point J = new Point("J");
Point K = new Point("K");
Point L = new Point("L");
A.setRed(true);
A.setSource(true);

List<Point> points = new ArrayList<Point>();
points.add(B);
points.add(A);
points.add(C);
points.add(D);
points.add(E);
points.add(F);
points.add(G);
points.add(H);
points.add(I);
points.add(J);
points.add(K);

// for (int i = 0; i < 10000; i++) {
// Point point = new Point(""+i);
// points.add(point);
// }

List<Edge> edges = new ArrayList<Edge>();
edges.add(new Edge(A,B,19));
edges.add(new Edge(B,C,25));
edges.add(new Edge(B,D,8));
edges.add(new Edge(C,E,10));
edges.add(new Edge(D,A,20));
edges.add(new Edge(D,C,4));
edges.add(new Edge(D,E,12));

edges.add(new Edge(A,E,13));
edges.add(new Edge(B,F,25));
edges.add(new Edge(C,F,2));
edges.add(new Edge(D,F,10));

edges.add(new Edge(F,I,20));
edges.add(new Edge(F,K,16));
edges.add(new Edge(C,G,5));
edges.add(new Edge(G,I,10));
edges.add(new Edge(G,K,1));
edges.add(new Edge(G,H,7));
edges.add(new Edge(H,J,20));
edges.add(new Edge(H,K,45));
edges.add(new Edge(I,J,4));
edges.add(new Edge(J,K,3));

// for (int i = 0; i < points.size(); i++) {
// for (int j = i; j < points.size()-i+1; j++) {
// if (j<points.size()&&(j==i+1||j==10*i+1)) {
// edges.add(new Edge(points.get(i),points.get(j),100*Math.random()));
// }
// }
// }

System.out.println(edges.size());
long initEnd = System.currentTimeMillis();
System.out.println("init time:"+(initEnd-initBegin)/60);

long computeBegin = System.currentTimeMillis();
Dijkstra dijkstra = new Dijkstra();
dijkstra.setPoints(points);
dijkstra.setEdges(edges);
dijkstra.dijkstra(A);
dijkstra.display();
long computeEnd = System.currentTimeMillis();
System.out.println("compute time:"+(computeEnd-computeBegin)/1000);
}
}

恩恩,结果出来了,万里长征卖出了第一步。那咱们再看看性能如何,打开注释部分,模拟1w个点,6k条边,运行。(N分钟后)咦,这Eclipse的小红灯怎么还不灭掉,这这这,啥也不说了,改吧。
看看之前闷头写的代码,dijkstra(Point end);和resetPaths();总共3层嵌套for循环,这复杂度可就是O(n^3),这比最基本的Dijkstra算法复杂度还高,改吧。
分析了一下,决定从resetPaths()这个方法下手,因为这个方法的初始设计就有问题,重置所有的路径不需要每次都和所有的边进行比较,我所需要的只是当红点发生变化的时候,将所有以红点为起点的边的终点所匹配的路径作出修改即可,这样,用List存储Path就变得不可行了,而Map则是很好的存储结构,value为Path,key为这个Path的终点。
这样,代码有了第一次优化

public class DijkstraO {
private List<Point> points;
private List<Edge> edges = new ArrayList<Edge>();
//Point is the end of the Path
private Map<Point, Path> pathMap = new HashMap<Point, Path>();
//当前变为红点的点
private Point currentPoint;
private final double INFINITY = 999999;
//源点到当前红点的长度
private double startToCurrent;

/**
* 初始化源点到其他各点的路径,即Path
*/
public void init(Point start){
start.setSource(true);
start.setRed(true);
Point source = start;
//设置路径的终点
for (Point point : points) {
List<Point> redPoints = new ArrayList<Point>();
redPoints.add(source);
Path path = new Path(source,point,redPoints,INFINITY);
pathMap.put(point, path);
}
//设置路径的长度,当存在这样的边:起始,结束点分别和路径的源点,终点相同
for (Edge edge : edges) {
if (source.getName().equals(edge.getStart().getName())) {
pathMap.get(edge.getEnd()).setLength(edge.getLength());
}
}
}

public void dijkstra(){
dijkstra(null,null);
}

public void dijkstra(Point start,Point end){
long startInit = System.currentTimeMillis();
init(start);
System.out.println("dijkstra init time:"+(System.currentTimeMillis()-startInit));
for (int i = 0; i < points.size(); i++) {
int indexMin = getMin();
double minDist = pathMap.get(points.get(indexMin)).getLength();
if (minDist==INFINITY) {
System.out.println("有无法到达的顶点");
}else if(i!=points.size()-1){
//获取当前循环已经求出最短路径的点
currentPoint = points.get(indexMin);
points.get(indexMin).setRed(true);
if (end!=null&&end.equals(currentPoint)) {
return;
}
pathMap.get(points.get(indexMin)).getPoints().add(currentPoint);
startToCurrent = minDist;
}

resetPaths();
}
}

private int getMin() {
double minDist = INFINITY;
int indexMin = 0;
for (int i = 0; i < points.size(); i++) {
Path path = pathMap.get(points.get(i));
if (!path.getEnd().isRed()&&path.getLength()<minDist) {
minDist = path.getLength();
indexMin = i;
}
}
return indexMin;
}

/**
* 在当前红点发生变化后,源点到其他点的路径也相应变化,通过当前红点,
* 之前不可到的点有可能变为可到达,
* 之前可到达的点路径长度会发生改变
* 所以要重置其他路径的长度
*/
private void resetPaths() {
for (Edge edge : edges) {
if (edge.getEnd().isRed()) {
continue;
}
Path path = pathMap.get(edge.getEnd());
if (edge.getStart().getName().equals(currentPoint.getName())&&edge.getEnd().getName().equals(path.getEnd().getName())) {
double currentToFringe = edge.getLength();
double startToFringe = startToCurrent + currentToFringe;
double pathLength = path.getLength();
if(startToFringe<pathLength) {
List<Point> points = pathMap.get(currentPoint).getPoints();
List<Point> copyPoints = new ArrayList<Point>();
for (Point point : points) {
copyPoints.add(point);
}
path.setPoints(copyPoints);
path.setLength(startToFringe);
}
}
}
}

public void display(){
for (Point point : pathMap.keySet()) {
Path path = pathMap.get(point);
System.out.print(path.getSource().getName()+"-->"+path.getEnd().getName()+":");
for (Point point2 : path.getPoints()) {
System.out.print(point2.getName()+" ");
}
System.out.print(path.getLength());
System.out.println();
}
}


setter/getter
}

新的程序中,path的存储结构从List改为Map<Point, Path> pathMap,这样在resetPaths()这个方法中只需对所有的边循环一次即可,每次循环时,通过边的结束点作为key查找path,然后对其进行长度的重置。这个方法中有一个嵌套for,这是对路径所经过点集的一个深拷贝,数据很少,对复杂度的影响可以忽略不计。这样,算法的复杂度降低为O(n^2)。测试,15秒出结果。虽然这个结果不令人满意,但总算是不会让人望眼欲穿了。
那就跑一下真实数据吧,3w个点,8w条边。start-->还没end-->还没end-->还没end-->...15分钟后-->end,我很伤心,这东西能交给BOSS吗?答案当然是否定的。
那就继续吧,google到一篇论文[url="http://www.docin.com/p-28300368.html"]《快速Dijkstra 最短路径优化算法的实现》[/url],反复看了几遍,比对我的代码,再进行分析。如何能降低复杂度,就目前的功能来讲,减少循环嵌套难度很大,既然这样,能不能减少循环的次数,也就是减少dijkstra()方法中的points.size(),以及resetPaths()中的edges.size()。再来分析一下,我们为什么要对这两个集合进行循环,我们的目的是什么?关键在于红点的变化,再回过头来看看resetPaths()的注释“在当前红点发生变化后,源点到其他点的路径也相应变化”,这意味着在红点发生变化后,与红点相关的点,也就是以红点为起点的边的结束点发生变化,这个变化是什么呢?再看一下注释“之前不可到的点有可能变为可到达,之前可到达的点路径长度会发生改变”。这个思路就比较清晰了,我们需要循环的是这些改变的点,其他的点我们根本不需要管它。
按照这个思路进行优化,我们将这些变化的点称为蓝点,存储在Set<Point> bluePoints中。蓝点集,存放两种点,1.初始化阶段Path.length!=INFINITY,2.resetPath 方法中重置过的点。再构造两个Map<Point, List<Edge>>,分别以起始点和结束点为key。以起始点为key的Map的作用是在红点发生变化时,找到以这个红点为起始点的所有边的结束点,并装入蓝点集。以结束点为key的Map的作用是在resetPaths中,找到所有以这个点为结束点的所有边,再进行后续的重置操作。
优化过的代码:

public class DijkstraO3 {
private List<Point> points;
private List<Edge> edges = new ArrayList<Edge>();
//Point is the end of the Path
private Map<Point, Path> pathMap = new HashMap<Point, Path>();
//Point is the start of all Edge in the List
private Map<Point, List<Edge>> edgesMapByStart = new HashMap<Point, List<Edge>>();
//Point is the end of all Edge in the List
private Map<Point, List<Edge>> edgesMapByEnd = new HashMap<Point, List<Edge>>();
//蓝点集,存放两种点,1.初始化阶段Path.length!=INFINITY
//2.resetPath 方法中重置过的点
private Set<Point> bluePoints = new HashSet<Point>();
private Point currentPoint;
private final double INFINITY = 999999;
private double startToCurrent;

public void init(Point start){
start.setSource(true);
start.setRed(true);
Point source = start;
for (Point point : points) {
List<Point> redPoints = new ArrayList<Point>();
redPoints.add(source);
Path path = new Path(source,point,redPoints,INFINITY);
pathMap.put(point, path);
}
for (Edge edge : edges) {
Point s = edge.getStart();
Point e = edge.getEnd();
if (source.equals(s)) {
pathMap.get(e).setLength(edge.getLength());
bluePoints.add(e);
}
if (edgesMapByStart.get(s)==null) {
edgesMapByStart.put(s, new ArrayList<Edge>());
edgesMapByStart.get(s).add(edge);
}else{
edgesMapByStart.get(s).add(edge);
}
if (edgesMapByEnd.get(e)==null) {
edgesMapByEnd.put(e, new ArrayList<Edge>());
edgesMapByEnd.get(e).add(edge);
}else{
edgesMapByEnd.get(e).add(edge);
}
}
}

public void dijkstra(){
dijkstra(null,null);
}

public void dijkstra(Point start,Point end){
long startInit = System.currentTimeMillis();
init(start);
System.out.println("dijkstra init time:"+(System.currentTimeMillis()-startInit));
while (bluePoints.size()>0) {
Point point = getMin();
if (point==null) {
continue;
}
double minDist = pathMap.get(point).getLength();
if (minDist==INFINITY) {
System.out.println("有无法到达的顶点");
}else {
currentPoint = point;
point.setRed(true);
List<Edge> edges = edgesMapByStart.get(point);
if (edges!=null) {
for (Edge edge : edges) {
if (!edge.getEnd().isRed()) {
bluePoints.add(edge.getEnd());
}
}

}
bluePoints.remove(point);
if (end!=null&&end.equals(currentPoint)) {
return;
}
pathMap.get(point).getPoints().add(currentPoint);
startToCurrent = minDist;
}

resetPaths();
}
}

private void resetPaths() {
Iterator<Point> it = bluePoints.iterator();
while (it.hasNext()) {
Point bluePoint = it.next();
List<Edge> edges = edgesMapByEnd.get(bluePoint);
for (Edge edge : edges) {
if (edge.getEnd().isRed()) {
continue;
}
Path path = pathMap.get(edge.getEnd());
if (edge.getStart().equals(currentPoint)&&edge.getEnd().equals(path.getEnd())) {
double currentToFringe = edge.getLength();
double startToFringe = startToCurrent + currentToFringe;
double pathLength = path.getLength();
if(startToFringe<pathLength) {
List<Point> points = pathMap.get(currentPoint).getPoints();
List<Point> copyPoints = new ArrayList<Point>();
for (Point point : points) {
copyPoints.add(point);
}
path.setPoints(copyPoints);
path.setLength(startToFringe);
}
}
}
}
}

public void display(){
for (Point point : pathMap.keySet()) {
Path path = pathMap.get(point);
System.out.print(path.getSource().getX()+"-->"+path.getEnd().getX()+":");
for (Point point2 : path.getPoints()) {
System.out.print(point2.getX()+" ");
}
System.out.print(path.getLength());
System.out.println();
}
}

private Point getMin() {
double minDist = INFINITY;
Point point = null;
for (Point bluePoint : bluePoints) {
Path path = pathMap.get(bluePoint);
if (!path.getEnd().isRed()&&path.getLength()<minDist) {
minDist = path.getLength();
point = bluePoint;
}

}
return point;
}


setter/getter
}

测试一下,Bingo,以迅雷不及掩耳盗铃儿响叮当仁不让世界充满爱你没商靓颖靓颖我爱你之势,一秒搞定。
记录一下优化的结果,在1w个点,6k条边的测试数据量下,计算时间从N分钟-->15秒。以15秒的优化结果转战真实数据3w个点,8w条边,计算时间飙升至15分钟,最后优化至1秒钟。这个性能的提升应该说是很高了。
回过头来看看这个算法的实现过程,首先,不论性能的先实现功能。这为后续的优化提供了正确性的测试保障。然后,用Map来替代部分List,直接用key获取value,直接减少一层嵌套。最后,分析优化的可行性,这可以用排除法,再分析可行的优化,抓住问题的关键点,排除不需要的运算。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值