【dijkstra求单源最短路 + 堆优化】知识点讲解和代码模板

重温了迪杰斯特拉求单源最短路的知识点,其实发现因为每次循环首先确定离源点为最短距离的那个点的最短路长度 然后以此更新嘛(术语叫“松弛”),其实有一点“动态规划”的路子,因为最短路才能更新出来下一个最短路嘛。而且,到每个点的距离之中,我只能确定离源点最短距离的那个点的最短路长度就是当前的估计值,而其他点当前的估计值不一定是最短路,因为可能会由我这次确定的这个点更新出来它们最终的最短路,所以不能这次不能确定其他点的估计值为最短路长度!

注意使用条件:图中不出现负权边! (因为什么呢?看下面讲解粗体字!via http://blog.51cto.com/ahalei/1387799)

介绍指定一个点(源点)到其余各个顶点的最短路径,也叫做“单源最短路径”。例如求下图中的1号顶点到2、3、4、5、6号顶点的最短路径。
090644t797fce7n20of7j9.png

       与Floyd-Warshall算法一样这里仍然使用二维数组e来存储顶点之间边的关系,初始值如下。
090651l6pt4666tptut66u.png

       我们还需要用一个一维数组dis来存储1号顶点到其余各个顶点的初始路程,如下。
090657ofidcactthcig33i.png

       我们将此时dis数组中的值称为最短路的“估计值”。

       既然是求1号顶点到其余各个顶点的最短路程,那就先找一个离1号顶点最近的顶点。通过数组dis可知当前离1号顶点最近是2号顶点。当选择了2号顶点后,dis[2]的值就已经从“估计值”变为了“确定值”,即1号顶点到2号顶点的最短路程就是当前dis[2]值。为什么呢? 你想啊,目前离1号顶点最近的是2号顶点,并且这个图所有的边都是正数,那么肯定不可能通过第三个顶点中转,使得1号顶点到2号顶点的路程进一步缩短了。因为1号顶点到其它顶点的路程肯定没有1号到2号顶点短,对吧O(∩_∩)O~

       既然选了2号顶点,接下来再来看2号顶点有哪些出边呢。有2->3和2->4这两条边。先讨论通过2->3这条边能否让1号顶点到3号顶点的路程变短。也就是说现在来比较dis[3]和dis[2]+e[2][3]的大小。其中dis[3]表示1号顶点到3号顶点的路程。dis[2]+e[2][3]中dis[2]表示1号顶点到2号顶点的路程,e[2][3]表示2->3这条边。所以dis[2]+e[2][3]就表示从1号顶点先到2号顶点,再通过2->3这条边,到达3号顶点的路程。

       我们发现dis[3]=12,dis[2]+e[2][3]=1+9=10,dis[3]>dis[2]+e[2][3],因此dis[3]要更新为10。这个过程有个专业术语叫做“松弛”。即1号顶点到3号顶点的路程即dis[3],通过2->3这条边松弛成功。这便是Dijkstra算法的主要思想:通过“边”来松弛1号顶点到其余各个顶点的路程。

       同理通过2->4(e[2][4]),可以将dis[4]的值从∞松弛为4(dis[4]初始为∞,dis[2]+e[2][4]=1+3=4,dis[4]>dis[2]+e[2][4],因此dis[4]要更新为4)。

       刚才我们对2号顶点所有的出边进行了松弛。松弛完毕之后dis数组为:
090706vmjy7l2ee2lyalia.png

       接下来,继续在剩下的3、4、5和6号顶点中,选出离1号顶点最近的顶点。 通过上面更新过dis数组,当前离1号顶点最近是4号顶点。此时,dis[4]的值已经从“估计值”变为了“确定值” 下面继续对4号顶点的所有出边(4->3,4->5和4->6)用刚才的方法进行松弛。松弛完毕之后dis数组为:
090714f2p1wppynngj2pep.png

       继续在剩下的3、5和6号顶点中,选出离1号顶点最近的顶点,这次选择3号顶点。此时,dis[3]的值已经从“估计值”变为了“确定值”。对3号顶点的所有出边(3->5)进行松弛。松弛完毕之后dis数组为:
090722ywunackk35i8cni5.png

       继续在剩下的5和6号顶点中,选出离1号顶点最近的顶点,这次选择5号顶点。此时,dis[5]的值已经从“估计值”变为了“确定值”。对5号顶点的所有出边(5->4)进行松弛。松弛完毕之后dis数组为:
090730eq6oqzyq7laqha9y.png

       最后对6号顶点所有点出边进行松弛。因为这个例子中6号顶点没有出边,因此不用处理。到此,dis数组中所有的值都已经从“估计值”变为了“确定值”。

       最终dis数组如下,这便是1号顶点到其余各个顶点的最短路径。
090738azt5clcozl899ekt.png

       OK,现在来总结一下刚才的算法。算法的基本思想是:每次找到离源点(上面例子的源点就是1号顶点)最近的一个顶点,然后以该顶点为中心进行扩展,最终得到源点到其余所有点的最短路径。基本步骤如下:

  • 将所有的顶点分为两部分:已知最短路程的顶点集合P和未知最短路径的顶点集合Q。最开始,已知最短路径的顶点集合P中只有源点一个顶点。我们这里用一个book[ i ]数组来记录哪些点在集合P中。例如对于某个顶点i,如果book[ i ]为1则表示这个顶点在集合P中,如果book[ i ]为0则表示这个顶点在集合Q中。

  • 设置源点s到自己的最短路径为0即dis=0。若存在源点有能直接到达的顶点i,则把dis[ i ]设为e[s][ i ]。同时把所有其它(源点不能直接到达的)顶点的最短路径为设为∞。

  • 在集合Q的所有顶点中选择一个离源点s最近的顶点u(即dis[u]最小)加入到集合P。并考察所有以点u为起点的边,对每一条边进行松弛操作。例如存在一条从u到v的边,那么可以通过将边u->v添加到尾部来拓展一条从s到v的路径,这条路径的长度是dis[u]+e[u][v]。如果这个值比目前已知的dis[v]的值要小,我们可以用新值来替代当前dis[v]中的值。

  • 重复第3步,如果集合Q为空,算法结束。最终dis数组中的值就是源点到所有顶点的最短路径。


接下来是算法模板的部分。由于用到了“链式前向星”变相存储邻接矩阵,记忆有点模糊了的话戳讲解连接:https://blog.csdn.net/acdreamers/article/details/16902023

dijkstra算法模板(省略主函数)这里的注释有我自己的补充,对自己的理解很关键,看一看!

const int MAX_N = 10000;        //边的max
const int MAX_M = 100000;       //点的max
const int inf = 0x3f3f3f3f;     //最大距离
struct edge {                   //v为边起点,u为边终点,w为边的权重
    int v, w, next;
} e[MAX_M];
int p[MAX_N], eid , n;           //p数组就是讲解中的head数组,eid就是讲解中的cnt计数用的。n是图的结点个数。
void mapinit() {                //初始化图,和原来一样
    memset(p, -1, sizeof(p));   //注意这里初始化-1,关系到dijkstra算法中的更新(松弛)if语句条件 
    eid = 0;
}
void insert(int u, int v, int w) {  // 插入带权有向边,多了个权 (链式前向星存储)
    e[eid].v = v;
    e[eid].w = w;
    e[eid].next = p[u];
    p[u] = eid++;
}
void insert2(int u, int v, int w) {  // 插入带权双向边
    insert(u, v, w);
    insert(v, u, w);
}

int dist[MAX_N];  // 存储单源最短路的结果
bool vst[MAX_N];  // 标记每个顶点是否在集合 U 中
bool dijkstra(int s) {
    memset(vst, 0, sizeof(vst));
    memset(dist, 0x3f, sizeof(dist));
    dist[s] = 0;
    for (int i = 0; i < n; ++i) {
        int v, min_w = inf;  // 记录 dist 最小的顶点编号和 dist 值
        for (int j = 0; j < n; ++j) {
                if (!vst[j] && dist[j] < min_w) {//没有访问并且满足要求
                min_w = dist[j];
                v = j;//更新
            }
        }
        if (min_w == inf) {  // 所有点都满足vst[v]==true了,都确定最短路长度了,而循环未结束,说明有顶点是源点无法到达的,anyway,算法该结束了
            return false; //flase的意思是源点不能到达所有顶点
        }
        vst[v] = true;  // 将顶点 v 加入集合 U 中,也就是说,源点到该顶点的最短路长度由估计值变为了确定值,存在dist[v]中。
        for (int j = p[v]; j != -1; j = e[j].next) {
            // 如果和 v 相邻的顶点 x 满足 dist[v] + w(v, x) < dist[x] 则更新 dist[x],这一般被称作“松弛”操作
            int x = e[j].v;
            if (!vst[x] && dist[v] + e[j].w < dist[x]) {//没有访问,满足提议
                dist[x] = dist[v] + e[j].w;//更新
            }
        }
    }
    return true;  // 源点可以到达所有顶点,算法正常结束
}


然而,其实你会发现这个算法复杂度挺高的,O(n^2),仔细一看,第一层循环肯定不能简化了,因为要求源点到每一个点的最短路长度;而第二层循环是可以简化的!因为其实遍历是为了求估计值中的最小值,把它确定下来,而如果用小根堆或者优先队列来存储dist的话,就可以直接取到最小值而无需遍历比较了


【小根堆优化模板

这里的小根堆是用set来伪实现的(谁去手写堆啊我c!),注意其中的元素是pair<int,int>类型的,注意用到less<pair<int,int> >来作为set的第二个参数让C++去自动根据pair的first数值从小到大排序!

然后还要注意对set进行插入时用到了make_pair:

  1.     std::pair主要的作用是将两个数据组合成一个数据,两个数据可以是同一类型或者不同类型。 
  2. 例如std::pair<int,float> 或者 std::pair<double,double>等。 
  3. pair实质上是一个结构体,其主要的两个成员变量是first和second,这两个变量可以直接使用。 
  4. 初始化一个pair可以使用构造函数,也可以使用std::make_pair函数,make_pair函数的定义如下: 
  5. template pair make_pair(T1 a, T2 b) { return pair(a, b); } 

然后还要注意用到了auto的“指针”去指向set.begin() (其实就是set<PII,less<PII> >::iterator)

其他就是迪杰斯特拉算法的常规步骤了。

const int MAX_N = 10000;
const int MAX_M = 100000;
const int inf = 0x3f3f3f3f;
struct edge {
    int v, w, next;
} e[MAX_M];
int p[MAX_N], eid, n;
void mapinit() {
    memset(p, -1, sizeof(p));
    eid = 0;
}
void insert(int u, int v, int w) {  // 插入带权有向边
    e[eid].v = v;
    e[eid].w = w;
    e[eid].next = p[u];
    p[u] = eid++;
}
void insert2(int u, int v, int w) {  // 插入带权双向边
    insert(u, v, w);
    insert(v, u, w);
}

typedef pair<int, int> PII;

set<PII, less<PII> > min_heap;
/*用 set 来伪实现一个小根堆,并具有映射二叉堆的功能。堆中 pair<int, int> 的 second 表示顶点下标,first 表示该顶点的 dist 值,注意,只要写上less<PII>就会一first为标准排序,C++,内部实现,不需要管*/
int dist[MAX_N];  // 存储单源最短路的结果
bool vst[MAX_N];  // 标记每个顶点是否在集合 U 中
bool dijkstra(int s) {
    // 初始化 dist、小根堆和集合 U
    memset(vst, 0, sizeof(vst));
    memset(dist, 0x3f, sizeof(dist));
    min_heap.insert(make_pair(0, s));
    dist[s] = 0;
    for (int i = 0; i < n; ++i) {
        if (min_heap.size() == 0) {  // 如果小根堆中没有可用顶点,说明有顶点无法从源点到达,算法结束
            return false;
        }
        // 获取堆顶元素,并将堆顶元素从堆中删除
        auto iter = min_heap.begin();   //auto的“指针!”数据类型都用上了,为C++11打call!
        int v = iter->second;
        min_heap.erase(*iter);
        vst[v] = true;
        // 进行和普通 dijkstra 算法类似的松弛操作
        for (int j = p[v]; j != -1; j = e[j].next) {
            int x = e[j].v;
            if (!vst[x] && dist[v] + e[j].w < dist[x]) {  //对未确定的估计值进行更新。
                // 先将对应的 pair 从堆中删除,再将更新后的 pair 插入堆
                min_heap.erase(make_pair(dist[x], x));
                dist[x] = dist[v] + e[j].w;
                min_heap.insert(make_pair(dist[x], x));
            }
        }
    }
    return true;  // 存储单源最短路的结果
}

优先队列优化模板的代码可见:https://blog.csdn.net/ergedathouder/article/details/52439438 (会一个就行了,原理都一样,最多代码填空可能考到这个)


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值