1. 介绍

Dijsktra算法是大牛Dijsktra于1956年提出,用来解决有向图单源最短路径问题;但是不能解决负权的有向图,若要解决负权图则需要用到Bellman-Ford算法。Dijsktra算法思想:在DFS遍历图的过程中,每一次取出离源点的最近距离的点,将该点标记为已访问,松弛与该点相邻的结点。

有向图记为\(G=(n, m)\),其中,\(n\)为顶点数,\(m\)为边数;且\(e[a,b]\)表示从结点\(a\)到结点\(b\)的边。\(d[i]\)记录源点到结点i的距离,\(U\)为未访问的结点集合,\(V\)为已访问的结点集合。Dijsktra算法具体步骤如下:

  • 从集合\(U\)中寻找离源点最近的结点\(u\),并将结点\(u\)标记为已访问(从集合\(U\)中移到集合\(V\)中)

\[u = \mathop {\arg \min } \limits_{i \in U} d[i]
\]

  • 松弛与结点\(u\)相邻的未访问结点,更新d数组

\[\mathop {d[i]} \limits_{i \in U} = \min \lbrace d[i]\ , \ d[u] + e[u,i] \rbrace
\]

  • 重复上述操作\(n\)次,即访问了所有结点,集合\(U\)为空

Dijsktra算法的Java实现

/**
* Dijkstra's Algorithm for finding the shortest path
*
* @param adjMatrix adjacency matrix representation of the graph
* @param source the source vertex
* @param dest the destination vertex
* @return the cost for the shortest path
*/
public static int dijkstra(int[][] adjMatrix, int source, int dest) {
int numVertex = adjMatrix.length, minVertex = source;
// `d` marks the cost for the shortest path, `visit` marks whether has been visited or not
int[] d = new int[numVertex], visit = new int[numVertex];
Arrays.fill(d, Integer.MAX_VALUE);
d[source] = 0;
for (int cnt = 1; cnt <= numVertex; cnt++) {
int lowCost = Integer.MAX_VALUE;
// find the min-vertex which is the nearest among the unvisited vertices
for (int i = 0; i < numVertex; i++) {
if (visit[i] == 0 && d[i] < lowCost) {
lowCost = d[i];
minVertex = i;
}
}
visit[minVertex] = 1;
if (minVertex == dest) return d[dest];
// relax the minVertex's adjacency vertices
for (int i = 0; i < numVertex; i++) {
if (visit[i] == 0 && adjMatrix[minVertex][i] != Integer.MAX_VALUE) {
d[i] = Math.min(d[i], d[minVertex] + adjMatrix[minVertex][i]);
}
}
}
return d[dest];
}

复杂度分析

  • 时间复杂度:重复操作(即最外层for循环)n次,找出minNode操作、松弛操作需遍历所有结点,因此复杂度为\(O(n^2)\).
  • 空间复杂度:开辟两个长度为n的数组d与visit,因此复杂度为\(T(n)\).

2. 优化

从上述Java实现中,我们发现:(里层for循环)寻找距离源点最近的未访问结点\(u\),通过遍历整个数组来实现的,缺点是重复访问已经访问过的结点,浪费了时间。首先,我们来看看堆的性质。

堆是一种完全二叉树(complete binary tree);若其高度为h,则1~h-1层都是满的。如果从左至右从上至下从1开始给结点编号,堆满足:

  • 结点\(i\)的父结点编号为\(i/2\).
  • 结点\(i\)的左右孩子结点编号分别为\(2*i\), \(2*i+1\).

如果结点\(i\)的关键值小于父结点的关键值,则需要进行上浮操作(move up);如果结点\(i\)的关键值大于父结点的,则需要下沉操作(move down)。为了保持堆的整体有序性,通常下沉操作从根结点开始。

小顶堆优化Dijsktra算法

我们可以用小顶堆来代替d数组,堆顶对应于结点\(u\);取出堆顶,然后删除,如此堆中结点都是未访问的。同时为了记录数组\(d[i]\)中索引\(i\)值,我们让每个堆结点挂两个值——顶点、源点到该顶点的距离。算法伪代码如下:

Insert(vertex 0, 0)  // 插入源点
FOR i from 1 to n-1: // 初始化堆
Insert(vertex i, infinity) FOR k from 1 to n:
(i, d) := DeleteMin()
FOR all edges ij:
IF d + edge(i,j) < j’s key
DecreaseKey(vertex j, d + edge(i,j))
  1. Insert(vertex i, d)指在堆中插入堆结点(i, d)。
  2. DeleteMin()指取出堆顶并删除,时间复杂度为\(O(\log n)\)。
  3. DecreaseKey(vertex j, d + edge(i,j))是松弛操作,更新结点(vertex j, d + edge(i,j)),需要进行上浮,时间复杂度为\(O(\log n)\)。

我们需要n次DeleteMin,m次DecreaseKey,优化版的算法时间复杂度为\(O((n+m)\log n)\).

代码实现

邻接表

每一个邻接表的表项包含两个部分:头结点、表结点,整个邻接表可以用一个头结点数组表示。下面给出其Java实现

public class AdjList {
private int V = 0;
private HNode[] adjList =null; //邻接表 /*表结点*/
class ArcNode {
int adjvex, weight;
ArcNode next; public ArcNode(int adjvex, int weight) {
this.adjvex = adjvex;
this.weight = weight;
next = null;
}
} /*头结点*/
class HNode {
int vertex;
ArcNode firstArc; public HNode(int vertex) {
this.vertex = vertex;
firstArc = null;
}
} /*构造函数*/
public AdjList(int V) {
this.V = V;
adjList = new HNode[V+1];
for(int i = 1; i <= V; i++) {
adjList[i] = new HNode(i);
}
} /*添加边*/
public void addEdge(int start, int end, int weight) {
ArcNode arc = new ArcNode(end, weight);
ArcNode temp = adjList[start].firstArc;
adjList[start].firstArc = arc;
arc.next = temp;
} public int getV() {
return V;
} public HNode[] getAdjList() {
return adjList;
} }

小顶堆

public class Heap {
public int size = 0 ;
public Node[] h = null; //堆结点 /*记录Node中vertex对应堆的位置*/
public int[] index = null; /*堆结点:
* 存储结点+源点到该结点的距离
*/
public class Node {
int vertex, weight; public Node(int vertex, int weight) {
this.vertex = vertex;
this.weight = weight;
}
} public Heap(int maximum) {
h = new Node[maximum];
index = new int[maximum];
} /*上浮*/
public void moveUp(int pos) {
Node temp = h[pos];
for (; pos > 1 && h[pos/2].weight > temp.weight; pos/=2) {
h[pos] = h[pos/2];
index[h[pos].vertex] = pos; //更新位置
}
h[pos] = temp;
index[h[pos].vertex] = pos;
} /*下沉*/
public void moveDown( ) {
Node root = h[1];
int pos = 1, child = 1;
for(; pos <= size; pos = child) {
child = 2*pos;
if(child < size && h[child+1].weight < h[child].weight)
child++;
if(h[child].weight < root.weight) {
h[pos] = h[child];
index[h[pos].vertex] = pos;
} else {
break;
}
}
h[pos] = root;
index[h[pos].vertex] = pos;
} /*插入操作*/
public void insert(int v, int w) {
h[++size] = new Node(v, w);
moveUp(size);
} /*删除堆顶元素*/
public Node deleteMin( ) {
Node result = h[1];
h[1] = h[size--];
moveDown();
return result;
} }

优化算法


public class ShortestPath {
private static final int inf = 0xffffff; public static void dijkstra(AdjList al) {
int V = al.getV();
Heap heap = new Heap(V+1);
heap.insert(1, 0);
for(int i = 2; i <= V; i++) {
heap.insert(i, inf);
} for(int k =1; k <= V; k++) {
Heap.Node min = heap.deleteMin();
if(min.vertex == V) {
System.out.println(min.weight);
break;
}
AdjList.ArcNode arc = al.getAdjList()[min.vertex].firstArc;
while(arc != null) {
if((min.weight+ arc.weight) < heap.h[heap.index[arc.adjvex]].weight) {
heap.h[heap.index[arc.adjvex]].weight = min.weight+ arc.weight;
heap.moveUp(heap.index[arc.adjvex]);
}
arc = arc.next;
}
}
} /*main方法用于测试*/
public static void main(String[] args) {
AdjList al = new AdjList(5);
al.addEdge(1, 2, 20);
al.addEdge(2, 3, 30);
al.addEdge(3, 4, 20);
al.addEdge(4, 5, 20);
al.addEdge(1, 5, 100);
dijkstra(al);
}
}

3. 参考资料#

[1] FRWMM, ALGORITHMS - DIJKSTRA WITH HEAPS.

【图论】深入理解Dijsktra算法的更多相关文章

  1. 深入理解KMP算法

    前言:本人最近在看<大话数据结构>字符串模式匹配算法的内容,但是看得很迷糊,这本书中这块的内容感觉基本是严蔚敏<数据结构>的一个翻版,此书中给出的代码实现确实非常精炼,但是个人 ...

  2. KMP算法详解 --- 彻头彻尾理解KMP算法

    前言 之前对kmp算法虽然了解它的原理,即求出P0···Pi的最大相同前后缀长度k. 但是问题在于如何求出这个最大前后缀长度呢? 我觉得网上很多帖子都说的不是很清楚,总感觉没有把那层纸戳破, 后来翻看 ...

  3. 一步一步理解Paxos算法

    一步一步理解Paxos算法 背景 Paxos 算法是Lamport于1990年提出的一种基于消息传递的一致性算法.由于算法难以理解起初并没有引起人们的重视,使Lamport在八年后重新发表到 TOCS ...

  4. Dijsktra算法C++实现

    Dijsktra算法解决了有向图G=(V,E)上带权的单源最短路径问题.但要求所有边的权值非负. 思想:Dijkstra算法中设置了一顶点集合S,从源点s到集合中的顶点的最终最短路径的权值均已确定.算 ...

  5. 简单的理解deflate算法

    简单的理解deflate算法 最近做压缩算法. 用到了deflate压缩算法,  找了很多资料,  这篇文章算是讲的比较易懂的, 这篇文章不长,但却浅显易懂, 基本上涵盖了我想要知道的所有要点. 翻译 ...

  6. Hdu-2112 HDU Today (单源多点最短路——Dijsktra算法)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=2112 题目大意:给你N个公交车站,起点,终点,各站之间的距离,求起点到终点之间的最短距离.(起点终点相 ...

  7. 理解 KMP 算法

    KMP(The Knuth-Morris-Pratt Algorithm)算法用于字符串匹配,从字符串中找出给定的子字符串.但它并不是很好理解和掌握.而理解它概念中的部分匹配表,是理解 KMP 算法的 ...

  8. 理解DeepBox算法

    理解DeepBox算法 基本情况 论文发表在ICCV2015,作者是Berkeley的博士生Weicheng Kuo: @inproceedings{KuoICCV15DeepBox, Author ...

  9. 如何感性地理解EM算法?

    https://www.jianshu.com/p/1121509ac1dc 如果使用基于最大似然估计的模型,模型中存在隐变量,就要用EM算法做参数估计.个人认为,理解EM算法背后的idea,远比看懂 ...

随机推荐

  1. iOS常用的忽略警告

    在iOS开发过程中,偶尔会碰到一些编译器警告,如果能够确定该警告不会影响到程序的正常运行,则可以手动告诉编译器忽略掉这个警告 iOS常用的忽略警告类型: 1.方法弃用警告 #pragma clang ...

  2. Replication的犄角旮旯(七)-- 一个DDL引发的血案(下)(聊聊logreader的延迟)

    <Replication的犄角旮旯>系列导读 Replication的犄角旮旯(一)--变更订阅端表名的应用场景 Replication的犄角旮旯(二)--寻找订阅端丢失的记录 Repli ...

  3. 移动app框架inoic功能研究

    原生移动app框架inoic功能研究 本篇只侧重框架提供的功能和能力的研究,请关注后续实际部署使用体验. 一.inoic是什么? inoic是一个可以使用Web技术以hybird方式开发移动app的前 ...

  4. Lucene.net 实现近实时搜索(NRT)和增量索引

    Lucene做站内搜索的时候经常会遇到实时搜索的应用场景,比如用户搜索的功能.实现实时搜索,最普通的做法是,添加新的document之后,调用 IndexWriter 的 Commit 方法把内存中的 ...

  5. nginx(2、反向代理)

    反向代理是nginx最重要的特性之一,与正向代理相反,它代理的不是客户端,而是目标源,即我代理目标源满足客户端给出的请求. 在nginx中反向代理的简单配置如下: server { listen 80 ...

  6. 浅谈Excel开发:九 Excel 开发中遇到的常见问题及解决方法

    Excel开发过程中有时候会遇到各种奇怪的问题,下面就列出一些本人在开发中遇到的一些比较典型的问题,并给出了解决方法,希望对大家有所帮助. 一 插件调试不了以及错误导致崩溃的问题 在开发机器上,有时可 ...

  7. [ACM_动态规划] 数字三角形(数塔)

    递归方法解决数塔问题 状态转移方程:d[i][j]=a[i][j]+max{d[i+1][j],d[i+1][j+1]} 注意:1\d[i][j]表示从i,j出发的最大总和;2\变界值设为0;3\递归 ...

  8. js模版引擎handlebars.js实用教程——if-判断的基本用法

    返回目录 <!DOCTYPE html> <html> <head> <META http-equiv=Content-Type content=" ...

  9. IOS Animation-CABasicAnimation例子(简单动画实现)

    这些例子都是CABasicAnimation的一些简单实现的动画,例如移动.透明度.翻转等等.方法里面传入一个CALayer类或者子类就可以了. 下面是用swift实现的,这些方法我们也可以用作公共类 ...

  10. Java-面向对象基础练习

    1.编写一个Java应用程序,该应用程序包括2个类:Print类和主类E.Print 类里有一个方法output()功能是输出100 ~ 999之间的所有水仙花数(各位数字的 立方和等于这个三位数本身 ...