自己总结的Dijkstra算法(未完成)

原始算法


思想
  利用贪心法则,每次从已发现节点中选取一个距离起点距离最小的节点,然后由此节点更新其周围节点信息,其正确性很容易证明,我们每次选到一个距离起点最近的点,那么下一个距离起点最近的点要么是之前更新的,要么是和此节点相连的。并且之后的节点不可能再更改此节点信息,因为我们以贪心法则选取结点,之后节点的距离一定大于此节点,故不可能更新。

实现
  我们设V为图的顶点集,dis[|V|]表示起点到各点的距离,w(u,v)为结点u和节点v之间的权值。初始化阶段中,令起点为start,则dis[start]=0,其余节点的dis值为INF,之后令point指示当前节点,初始为start,当dis[u]>dis[pos]+w(point,u)(edge(point,u)为图的一条边),则更新dis[u]。具体过程如下:

//此处我们求解的是起点到所有顶点的最短距离
dis[start]=0;
while(|V|){
	for i=V中每个节点
		pos=min(dis[i]);
	V=V-{pos};
	for u|u属于edge(pos,u)&&u属于V
		if(dis[u]>dis[pos]+w(u,v)) dis[u]=dis[pos]+w(u,v);
}

此时我们就已经得到了所有节点到start的最短路径长度。
使用范围
  dijkstra算法只适用于权值为非负的图中。


dijkstra算法优化


优化1   “盒子”优化
  我们分析上边的算法可知,原始dijkstra算法的时间复杂度为O(|V|^2),空间复杂度我们不去关心。很明显的一点是,我们在点集中搜索权值最小的节点时,连带着许多未发现(即dis[i]=INF)的节点也进行了扫描,一种解决方案是我们创建一个“盒子”,把所有发现的节点存储进来,这样我们只需要每次扫描盒子,更新盒子就可以了,减少了未发现节点的多余扫描。

优化2 二叉堆优化
  优化1的方案确实可以优化,但这种优化是非常不稳定的,在特殊数据(例如start节点直接把其他所有节点都更新进“盒子”)下,时间复杂度依然可能达到O(|V|^2)。那么,我们可以使用优先队列来保存已发现节点或所有为确定节点(时间差异几乎没有)。我们以将所有节点初始化进堆为例,堆中每个元素保存两项,key表示此节点代表图中的节点编号,dis表示此节点到起点的距离,我们以dis为关键字建立一个size=|V|的最小堆,r[|V|]表示各节点到起点的距离,初始化为INF。
  我们每次选出堆顶元素,将图中与其key值连接的节点尝试进行松弛,并且在每次松弛后对堆中对应元素进行调整即可。
  首先我们先定义堆中元素的类型

struct heapelem{
	int key,dis;
};
heapelem heap[810];


  按照上述过程,我们先定义初始化堆的操作

void init(int Size)
{
	size=Size;
	heap[0].dis=-INF;
	for(int i=1;i<=Size;i++){
		heap[i].dis=r[i]=INF;
		heap[i].key=i;
		pos[i]=i;//pos[i]指示i号节点在堆中的位置
	}
}

下面是堆的decrease操作

void decrease(int x,int ith)//修改第ith号节点权值为x
{
	int i,f;
	for(i=pos[ith];heap[f=i>>1].dis>x;i=f){
		heap[i]=heap[f];
		pos[heap[i].key]=i;
	}
	heap[i].dis=x;
	heap[i].key=ith;
	pos[ith]=i;
}

然后就是堆的delmin操作

void delmin()
{
	heapelem last=heap[size--];
	int i,c;
	for(i=1;(c=i<<1)<=size;i=c){
		if(c!=size&&heap[c+1].dis<heap[c].dis)
			c++;
		if(heap[c].dis<last.dis){
			heap[i]=heap[c];
			pos[heap[i].key]=i;
		}
		else break;
	}
	heap[i]=last;
	pos[last.key]=i;
}

而dijkstra算法的实现是基于上述操作和对图的存储的,所以在这里我们再次声明一下图的存储方式

struct EDGE{
	int to,dis;
	struct EDGE *next;
}edge[3000];
int totale=0;
EDGE *point[810];

其中point[i]保存的是一个指向i号节点连接的所有节点和其权值的链表,而加入边的操作为(类似于链表的头插法)

void addedge(int a,int b,int c)
{
	edge[totale].dis=c;
	edge[totale].to=b;
	edge[totale].next=point[a];
	point[a]=&edge[totale];
	totale++;
}//注:若为无向图,则应执行两次操作

那么接下来的dijkstra算法也就很容易编写了

void dijkstra(int start,int Size)
{
	init(Size);
	r[start]=0;
	decrease(0,start);
	int pos=start;
	while(size){
		delmin();
		EDGE *e;
		e=point[pos];
		while(e){
			if((r[pos]+e->dis)<r[e->to]){
				r[e->to]=r[pos]+e->dis;
				decrease(r[e->to],e->to);
			}
			e=e->next;
		}
		pos=heap[1].key;
	}
}

  这种优化的好处在于其使用范围广,只要能使用dijkstra算法,就一定可以这样优化,并且其时间复杂度非常稳定,为|V|log|V|。
优化三   扇形优化
  当一个图可以直接抽象为一个平面时,我们可以将dijkstra算法理解为以起点为圆心画一个圆,其半径逐渐增大,每当圆周遇到一个点,就标记其为确定节点,并画出此节点所连接的未发现节点与更新与其连接的已发现节点,那么可以清楚的看到,此时的搜索有很大一部分“南辕北辙”了,
例如我们的目标节点在起点的正北方向,而我们的搜索却把2pi的角度全进行了,那么此时,可以采取有损算法来提高效率,虽然并不能保证一定可以得到最优解,但多数情况下不会发生误差,即使发生也不会很大。
  有损算法核心在于限定一个扇形,起点与目标点的连线为扇形圆心角的角平分线,圆心角的大小可以根据图的具体情况而定,而判断一个顶点是否在扇形区域内可以采用余弦定理来实现。
  对于整个算法流程,其余大部分和上述代码一致(可能变量名字或者类型需要稍微修改),我们假设对于每个顶点的定义如下:

struct Point{
	int x,y;
}point[800];

  那么接下来的工作就是求出需要处理的点,即在限定角度内的顶点,之后执行dijkstra算法即可,让我们定义sift函数,并假设start是起点,target是目标顶点,range是设定的扇形的角度的一般:

void sift()
{
	for i=V中每一个顶点
		double dis_si,dis_st,dis_it;//表平面上距离,不一定等于路径长度
		dis_si=sqrt((point[start].x-point[i].x)^2+(point[start].y-point[i].y)^2);
		dis_st=sqrt((point[start].x-point[t].x)^2+(point[start].y-point[t].y)^2);
		dis_it=sqrt((point[i].x-point[target].x)^2+(point[i].y-point[target].y)^2);
		double angle=arccos((dis_si^2+dis_st^2-dis_it^2)/(2*dis_si*dis_st));
		if(angle>range) V=V-{i};
	end for;
}

  注意,此算法的前提是图可抽象成平面的,即每个点都是有二维坐标的,是固定的,而且当利用dijkstra求全点对最短路径是,此优化是没有必要的。
  实际应用中还可以通过A*算法或十字链表等方法进行优化,而以上三点(优化1可忽略)基本已满足竞赛中的时间要求,因此个人认为重心应该在dijkstra的功能上,因为dijkstra算法的强大不仅仅在其能求单源最短路径,以下是dijkstra算法的一些拓展功能。
拓展一
  记录最短路径,生成最短路径树,即保存每个最短路径节点的父节点,这一点实现起来比较简单,我们只需在dijkstra函数中第二层while循环的if语句中加入 path[e->to]=pos; 即可完成。
拓展二
  路径统计。两顶点间不一定只有一条最短路径,例如有a,b,c三个顶点和三条边(a,b)=3,(b,c)=4,(a,c)=7,此时a到c的最短路径长为7,有两条。对于这类问题,我们选择好起点和终点,先利用普通dijkstra算法求出起点到达各点的最短路径,之后我们需要承认这样一个事实,起点和终点的任意一条最短路径上,到达每一个内点的距离一定是最短的,这个很容易证明,若到达某个内点时所走距离大于该点最短距离,则一定不是最短路径。所以我们假设当前处于点i,则我们只能选择dis[j]=dis[i]+w(i,j),此处的w(i,j)表点i和点j间存在一条边,且其权值为w(i,j)。那么点i到终点的最短路径数为dp[i]=sum{dp[j]|dis[j]=dis[i]+w(i,j)},使用记忆化搜索即可在O(|V|)时间完成统计。
  在实现的时候我们有多种方法。我们可以根据dp方程的条件构造新图,也可以在原图的基础上判断条件再dp。后者的工作量明显小于前者。

//在调用记忆化搜索函数前需初始化dp数组为0
//调用此函数时格式为 int ans=dfs(start);
//
int dfs(int u)
{
	if(u==target) return 1;
	if(dp[u]) return dp[u];
	EDGE *e;
	for(e=point[u];e;e=e->next) if(dis[e->to]==dis[u]+e->dis)
		dp[u]+=dfs(e->to);
	return dp[u];
}

拓展三
  求出两点间所有最短路径。由拓展一可知,当图中存在多条最短路径是,path数组中保存的为第一次更新的父节点,而之后的父节点没有保存,我们有两种方案求出所有路径,一种是在拓展一的基础上构造向量存储父节点,当然需要增加一个相等情况的判断,此时时间复杂度不变,但由于最坏情况下会有|V|-2条路径,所以最坏空间复杂度为|V|^2,对于大多数的计算机内存来说,这是巨大的。值得庆幸的是,我们还有另一种方案,这种方案是基于拓展二的,我们先用dijkstra算法对图进行预处理,之后从起点开始进行深度优先搜索,搜索时只允许拓展dis[j]|dis[j]=dis[i]+w(i,j)的节点,每次遇到目标节点,就输出路径。

拓展四

  求第K短路长度,这个还没有找到什么特别可靠的资料,传说有个曹氏短边算法,具体不太清楚,等自己弄明白了在补充。







评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值