原始算法
思想
利用贪心法则,每次从已发现节点中选取一个距离起点距离最小的节点,然后由此节点更新其周围节点信息,其正确性很容易证明,我们每次选到一个距离起点最近的点,那么下一个距离起点最近的点要么是之前更新的,要么是和此节点相连的。并且之后的节点不可能再更改此节点信息,因为我们以贪心法则选取结点,之后节点的距离一定大于此节点,故不可能更新。
实现
我们设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短路长度,这个还没有找到什么特别可靠的资料,传说有个曹氏短边算法,具体不太清楚,等自己弄明白了在补充。