【算法】深入理解Dijsktra算法

Dijsktra算法介绍

Dijsktra算法是大牛Dijsktra于1956年提出,用来解决有向图单源最短路径问题。但不能解决负权的有向图,若要解决负权图则需要用 到Bellman-Ford算法。算法思想是,在dfs遍历图的过程中,每一次取出离源点的最近距离的点,将该点标记为已访问,松弛与该点相邻的节点。约 定:对有向图(n, m),n为顶点数,m为边数,d[i]记录源点到节点i的距离,U为未访问的节点集合,V为已访问的节点集合。具体步骤如下:

  1. 在U中寻找离源点最近的节点minNode,并将节点minNode标记为已访问(从集合U中移到集合V中)

    d[minNode]=mini∈Ud[i]

  2. 松弛与minNode相邻的未访问节点,更新d数组

    d[i]i∈U=min{d[i] , d[minNode]+edge[minNode][i]}
  3. 重复上述操作n次,即访问了所有节点,集合U为空

代码实现

visit数组记录节点访问情况

void dijkstra(int n)
{
	int lowcost, minNode;
	int d[n], visit[n];

	/*初始化d数组*/
	for(int i = 0; i < n; i++) {
		d[i] = inf;
		visit[i] = 0;
	}
	d[0] = 0;  

	/*重复操作n次*/
	for(int count = 1; count <= n; count++) {
		lowcost = inf;             

		//找出minNode
		for(int i = 0; i < n-1; i++) {
			if(!visit[i] && d[i] < lowcost) {
				lowcost=d[i];
				minNode=i;
			}
		}
		visit[minNode]=1;		   

		//松弛操作
		for(int i=0; i<n; i++) {
			if(!visit[i])
				d[i]=min(d[i],d[minNode]+edge[minNode][i]);
		}
	}
}

复杂度分析

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

算法优化

我们可以观察到最外层的循坏没法再做优化,因为操作就是得重复n次才能访问到所有节点;只有针对里层的两个操作进行优化了:

  • 找出minNode操作通过遍历来查找,缺点是效率太慢了,并且有一些节点是已访问的。因此,我们可以用小顶堆来维护d数组,堆顶对应就是minNode;取出堆顶,然后删除,如此堆中节点都是未访问的。
  • 通过建立有向图的邻接表,松弛操作不需要遍历所有节点

堆的性质

是一种完全二叉树(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[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);
	}
}

参考资料

[1] FRWMM, ALGORITHMS - DIJKSTRA WITH HEAPS.

时间: 2024-11-03 03:25:33

【算法】深入理解Dijsktra算法的相关文章

POJ1523(求连用分量数目,tarjan算法原理理解)

SPF Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 7406   Accepted: 3363 Description Consider the two networks shown below. Assuming that data moves around these networks only between directly connected nodes on a peer-to-peer basis, a

deep learning 自编码算法详细理解与代码实现(超详细)

在有监督学习中,训练样本是有类别标签的.现在假设我们只有一个没有带类别标签的训练样本集合 ,其中 .自编码神经网络是一种无监督学习算法,它使用了反向传播算法,并让目标值等于输入值,比如 .下图是一个自编码神经网络的示例.通过训练,我们使输出 接近于输入 .当我们为自编码神经网络加入某些限制,比如限定隐藏神经元的数量,我们就可以从输入数据中发现一些有趣的结构.举例来说,假设某个自编码神经网络的输入 是一张 张8*8 图像(共64个像素)的像素灰度值,于是 n=64,其隐藏层 中有25个隐藏神经元.

趣写算法系列之--匈牙利算法(真的很好理解)

[书本上的算法往往讲得非常复杂,我和我的朋友计划用一些简单通俗的例子来描述算法的流程] 匈牙利算法是由匈牙利数学家Edmonds于1965年提出,因而得名.匈牙利算法是基于Hall定理中充分性证明的思想,它是部图匹配最常见的算法,该算法的核心就是寻找增广路径,它是一种用增广路径求二分图最大匹配的算法. -------等等,看得头大?那么请看下面的版本: 通过数代人的努力,你终于赶上了剩男剩女的大潮,假设你是一位光荣的新世纪媒人,在你的手上有N个剩男,M个剩女,每个人都可能对多名异性有好感(-_-

一步一步理解Paxos算法

一步一步理解Paxos算法 背景 Paxos 算法是Lamport于1990年提出的一种基于消息传递的一致性算法.由于算法难以理解起初并没有引起人们的重视,使Lamport在八年后重新发表到 TOCS上.即便如此paxos算法还是没有得到重视,2001年Lamport用可读性比较强的叙述性语言给出算法描述.可见Lamport对 paxos算法情有独钟.近几年paxos算法的普遍使用也证明它在分布式一致性算法中的重要地位.06年google的三篇论文初现“云”的端倪,其中的chubby锁服务使用p

KMP算法 --- 深入理解next数组

KMP算法的前缀next数组最通俗的解释 我们在一个母字符串中查找一个子字符串有很多方法.KMP是一种最常见的改进算法,它可以在匹配过程中失配的情况下,有效地多往后面跳几个字符,加快匹配速度. 当然我们可以看到这个算法针对的是子串有对称属性,如果有对称属性,那么就需要向前查找是否有可以再次匹配的内容. 在KMP算法中有个数组,叫做前缀数组,也有的叫next数组,每一个子串有一个固定的next数组,它记录着字符串匹配过程中失配情况下可以向前多跳几个字符,当然它描述的也是子串的对称程度,程度越高,值

01背包算法的理解

01背包问题: 有N件物品和一个最大重量限制为V的背包.第i件物品的重量是c[i],价值是w[i].求解将哪些物品装入背包可使这些物品的重量总和不超过V,且价值总和最大.每个物品只有1份,且不可分割 看了01背包算法,言简意赅,但理解起来头昏脑胀,不得要领.尝试解释下对该算法的理解,加深记忆. 假设最优解已经存在,怎么判断一个物品i是否在背包里?  简单,只要知道, 1.c[i]是否大于V, 2.F[i-1][V-c[i]],即没有i物品的情况下,最大重量限制为V-c[i]的最优解. 3.F[i

我对最近公共祖先LCA(Tarjan算法)的理解

LCA 最近公共祖先 Tarjan(离线)算法的基本思路及我个人理解 首先是最近公共祖先的概念(什么是最近公共祖先?): 在一棵没有环的树上,每个节点肯定有其父亲节点和祖先节点,而最近公共祖先,就是两个节点在这棵树上深度最大的公共的祖先节点. 换句话说,就是两个点在这棵树上距离最近的公共祖先节点. 所以LCA主要是用来处理当两个点仅有唯一一条确定的最短路径时的路径. 有人可能会问:那他本身或者其父亲节点是否可以作为祖先节点呢? 答案是肯定的,很简单,按照人的亲戚观念来说,你的父亲也是你的祖先,而

KNN算法的理解

一.算法 1.kNN算法又称为k近邻分类(k-nearest neighbor classification)算法. 最简单平庸的分类器或许是那种死记硬背式的分类器,记住全部的训练数据.对于新的数据则直接和训练数据匹配,假设存在同样属性的训练数据,则直接用它的分类来作为新数据的分类.这样的方式有一个明显的缺点,那就是非常可能无法找到全然匹配的训练记录. kNN算法则是从训练集中找到和新数据最接近的k条记录.然后依据他们的主要分类来决定新数据的类别.该算法涉及3个主要因素:训练集.距离或相似的衡量

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

[经典算法]——KMP,深入讲解next数组的求解 前言 之前对kmp算法虽然了解它的原理,即求出P0···Pi的最大相同前后缀长度k:但是问题在于如何求出这个最大前后缀长度呢?我觉得网上很多帖子都说的不是很清楚,总感觉没有把那层纸戳破,后来翻看算法导论,32章 字符串匹配虽然讲到了对前后缀计算的正确性,但是大量的推理证明不大好理解,没有与程序结合起来讲.今天我在这里讲一讲我的一些理解,希望大家多多指教,如果有不清楚的或错误的请给我留言. 1.kmp算法的原理: 本部分内容转自:http://w