Dijkstra算法实现、链式前向星及一个变体的最短路径问题

Dijkstra算法解决的是带权重的有向图上单源最短路径问题,所谓单源最短路径指的是从一个给定的点到目标点之间的最短路径。Dijkstra算法其实就是从源点出发广度优先遍历图,在遍历的同时计算源点到遍历到的点的距离并保存下来,如果从新的路径到达某一点的距离比原来计算出的距离还要小,就更新这个距离值,最终,源点到所有点的最短距离就都可以求出来了。

一般采用优先队列来暂存距离值,相当于是一种贪心算法,每次都从已经访问过的点中选择距离最小的点出发,有可能可以更快的找到最短路径,同时也能尽快的减少待计算距离的点,不然的话,一个点只有当它的所有入边都“松驰”过才能肯定不再需要暂存。当然,也不一定非得采用优先队列,可以采取双向队列SLF(Small Label First),即插入队列时如果比队首还小就直接插入队首也是可以的。还可以LLL(Large Label Last),即待出队的如果比队伍中间的还要大,就直接踢到队尾。如果不用优先队列,LLL似乎是必须的,因为先入队的点不一定就不在最短路径上。但是如果每一步都选择当前最短距离的出队,是可以证明该点的距离确实已经不可能再短了的。

具体实现的时候存在一个问题,即:遍历过的点需要加入队列,而该点的距离可能会被更新,那么如何更新队列中的元素。采用索引的队列,可以简单地避开这个问题,但在采用优先队列的时候,即使队列的实际内容只是一个索引,由于索引指向的内容发生变化,索引在队列中的位置也应该随着发生变动。而查找一个队列中的元素除了顺序查找这外似乎并没有什么更好的办法,因为队列通常是无序的。关键是这个动作在每个点都要执行若干次,如果不提高在队列中查找元素的效率,整个程序的效率是不行的。

但实际上这个问题是不存在的。因为表面上是在遍历节点,但实际上是顺着一条一条的边去找的,所以实际上遍历的是边而不是点,入队、出队的也应该是边而不是点。对于边来说,就不存在起点到它的距离这样的概念,就不存在更新队列的问题。但是,边和点是相关的,边只不过是两点之间的连线而已,边没有距离的概念,但是边的终点有,那么,如果起点到已经入队的一条边的终点的距离发生了变化,不同样也需要调整该边在优先队列中的位置吗?实际上并不需要这样做,因为根据算法,要发生一个距离变化,必然是发现了一条新的路径,必然就会有一条新的边加入队列,起点到这条新加入的边的终点的距离是更新了的,它加入优先队列后自然会代表该终点排到合适的位置上去,而已经在队列中的边不需要调整位置,调整了也没意义,因为实际上这些边已经失效(松驰)了。当然,在出队的时候就需要判断一下边是否失效,取到失效边就直接放弃继续取下一条边,直到取到有效边才继续。这么一来,顶点入队还是边入队就没有什么区别了,顶点入队的话,就重复入队(这一点容易迷惑,因为一般使用队列的时候,通常是不会重复入队的,这个地方重复入队的作用,要把点当成边来看才好理解),而后入队的会自动排到合适的位置。不管怎样,都得采取一定的办法来标记队列中已失效的点或边。通常,图的顶点是用数组存储,可以用固定的序号来表示,所以通常还是采取的顶点入队外加一个mark数组来标记失效的方法。

当然,边也可以采用数组存储,也可以同样用邻接表法来存储一张图,只不过数据结构中存储的不是指向下一条边的指针而是下一条边的索引。这种数据结构叫做链式前向星,采用了预先分配内存代替了传统链表的动态分配内存,空间肯定浪费了但些,但是不用再动态申请内存,时间要节省很多。

OJ4TH上有一道题,是2018年校ACM选拔赛的题,题目描述很简单。“一棵 n 个点的有根树,以 1 号点为根,走一条边需要花费相应的代价,任意深度相差为 1 的点之间可以相互跳跃,花费代价为 p ,求 s 走到 t 的最小代价”。给出的n的范围是[1, 1 0 5 10^5 105]。这道题很烧脑,隐含着任意深度相差为 1 的点之间有2条边,极端情况下可能达到 n 2 n^2 n2这个数量级,非常恐怖。完全忽视这些边,肯定找不到真正的最短路径,起点和终点的位置又没什么限制或规则,也没有什么快速有效的进行裁剪。这里需要用到类似于解几何题作辅助线的办法,在每两层之间增加中转结点,所有层间跳转都通过中转结点进行,这样的话,每个结点多2条到中转结点的出边和2条来自中转结点的入边,总共增加的边数是4n,比起 n 2 n^2 n2来少太多了。结点数量虽然也增加了,但增加的结点数不会超过n,算法复杂度仍然和普通Dijkstra是一个等级。

下面是代码,采用的是链式前向星,因为感觉刷题用malloc很容易超时,这一题要用6n次,估计行不通。代码中真正属于Dijkstra算法的部分并不多,大多数代码是初始化、加中间结点、加辅助线去了。

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <limits.h>
#define N 100000 + 1
typedef struct edge{
    int to;
    int cost;
    int next;
}Edge;
typedef struct vertex{
    //int from;
    int depth;
    unsigned long long dist;
    int adj;
}Vetrex;
Vetrex vertexs[3*N];//顶点数最多3*N,1~N用来保存树的节点,N+1~2N用来保存附加的上行中转节点,2N+1~3N用来保存附加的下行中转节点
Edge edges[6*N];//最大边数6*N
int mark[3*N],pq[6*N],len;//pq是顶点索引的最小优先队列,len为队列实际长度
void swim(int a[],int k){
    while(k>1&&vertexs[a[k/2]].dist>vertexs[a[k]].dist){
        int temp=a[k/2];
        a[k]=a[k/2];
        a[k/2]=temp;
        k/=2;
    }
}
void sink(int a[],int len,int k){
    while(2*k<=len){
        int j=2*k;
        if(j<len&&vertexs[a[j]].dist>vertexs[a[j+1]].dist)j++;
        if(vertexs[a[k]].dist<vertexs[a[j]].dist)break;
        int temp=a[k];
        a[k]=a[j];
        a[j]=temp;
        k=j;
    }
}
int dfs(Vetrex a[],int id){
    int temp=a[id].adj;
    int d=a[id].depth,maxd=-1;
    while(temp!=-1){
        int m=-1;
        if(a[edges[temp].to].depth==-1){
            a[edges[temp].to].depth=d+1;
            m=dfs(a,edges[temp].to);
        }
        temp=edges[temp].next;
        maxd=maxd>m?maxd:m;
    }
    return maxd>d?maxd:d;
}
void enqueue(int a[],int *len,int value){
    (*len)++;
    a[*len]=value;
    swim(a,*len);
}
int dequeue(int a[],int *len){
    int temp=a[1];
    a[1]=a[(*len)--];
    if(*len<0)return -1;
    sink(a,*len,1);
    return temp;
}
int main(){
    int T, n, p, s, t, u, v, w;
    int maxdepth;
    scanf("%d", &T);
    for(int c=1;c<=T;c++){
        scanf("%d%d%d%d",&n,&p,&s,&t);
        for(int i=1;i<=n;i++){//初始化树和标志数组
            vertexs[i].adj=-1;
            vertexs[i].depth=-1;
            vertexs[i].dist=__LONG_LONG_MAX__;
            mark[i]=0;
        }
        for (int i = 0; i < n - 1; i++){
            scanf("%d%d%d", &u, &v, &w);
            edges[i].to = v;
            edges[i].cost = w;
            edges[i].next = vertexs[u].adj;
            vertexs[u].adj = i;
            edges[N+i].to = u;
            edges[N+i].cost = w;
            edges[N+i].next = vertexs[v].adj;
            vertexs[v].adj = N+i;

        }
        vertexs[1].depth=0;
        maxdepth=dfs(vertexs,1);//深度优先算法计算每个树节点的深度,返回树的高度
        for(int i=1;i<=n;i++){//初始化附加的点及其标记数组中的值,主要是确保邻接边指向空
            vertexs[N+i].adj=-1;
            vertexs[N+i].dist=__LONG_LONG_MAX__;
            vertexs[2*N+i].adj=-1;
            vertexs[2*N+i].dist=__LONG_LONG_MAX__;
            mark[N+i]=0;
            mark[2*N+i]=0;
        }
        for(int i=1;i<=n;i++){//遍历树节点,根据每个节点的深度,添加到中间节点的上行和下行边;
            if(vertexs[i].depth>0){
                edges[2*N+i].to=2*N+vertexs[i].depth;
                edges[2*N+i].cost=p/2;
                edges[2*N+i].next=vertexs[i].adj;
                vertexs[i].adj=2*N+i;
                edges[3*N+i].to=i;
                edges[3*N+i].cost=p-p/2;
                edges[3*N+i].next=vertexs[N+vertexs[i].depth].adj;
                vertexs[N+vertexs[i].depth].adj=3*N+i;
            }
            if(vertexs[i].depth<maxdepth){
                edges[4*N+i].to=N+vertexs[i].depth+1;
                edges[4*N+i].cost=p/2;
                edges[4*N+i].next=vertexs[i].adj;
                vertexs[i].adj=4*N+i;
                edges[5*N+i].to=i;
                edges[5*N+i].cost=p-p/2;
                edges[5*N+i].next=vertexs[2*N+vertexs[i].depth+1].adj;
                vertexs[2*N+vertexs[i].depth+1].adj=5*N+i;
            }
        }
        len=1;//队列长度
        pq[1]=s;//起始节点是s
        vertexs[s].dist=0;
        //vertexs[s].from=s;
        while(len>0){
            int current=0;
            do{
                current=dequeue(pq,&len);//取出最小节点
            }while(mark[current]==1&&current!=-1);
            if(current==t||current==-1)break;
            mark[current]=1;//标记
            int adj=vertexs[current].adj;
            while(adj!=-1){
                int w=edges[adj].to;
                if(vertexs[w].dist>vertexs[current].dist+edges[adj].cost){
                    vertexs[w].dist=vertexs[current].dist+edges[adj].cost;
                    //vertexs[w].from=current;
                    enqueue(pq,&len,w);
                }
                adj=edges[adj].next;
            }
        }
        printf("Case #%d: %llu\n",c,vertexs[t].dist);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值