Dijkstra

非常经典的单源最短路算法。仅能用于正权图(边权可为 \(0\))。拥有朴素版 \(O(n^2)\) 和堆优化版\(O((n + m)\log{m})\)。朴素版一般用邻接矩阵存图,而优化版使用邻接表或者链式前向星,我常用链式前向星。

中心思想

每次在没用过的点内找一个距离起点最近的点,用这个点对其他点进行松弛操作。
如果把这个点到起点的最短距离称为边的话,即找最短未使用的边,并用这个边对其他所以边进行松弛。

松弛即利用某个点使得某条边的距离变短,如下图:
![[graph (1) 1.png|161]]
可以看出,从 \(1\)\(2\) 的距离为 \(5\),但从 \(1\)\(3\),再从\(3\)\(2\) 的总距离仅为 \(3\),因此就可以用 \(3\) 这个点,对 \(1\)\(2\) 之间的最短距离缩小,即松弛。有点时候我喜欢叫扩展

注意当一个点为用过,说明这个点当时已经有最小距离了。

算法流程

  1. 枚举每个点的 \(dist\),找到最小的未使用的 \(dist\)
  2. 对这个点进行标记。
  3. 利用这个点对其他点进行松弛也就是,dist[j] = max(dist[j], dist[t] + w[t, j])
  4. 不断进行 \(1 \sim 3\) 操作 \(n - 1\) 次(即进行 \(n - 1\) 轮操作)。

正确性证明

可以看成,这是贪心的思想,正常的思路,每次都找最小边,那么最后得到的最短距离也应为最小。
反证法
因为是边权非负,所以每次选取的最短边距离 dist 是单调不减序列。
设一个点为 \(u\),当前它的 dist 为最短未使用的距离,我们选择它使用,如果再此之后,还有其他点 \(k\) 能把他扩展更小:

  1. 如果 \(k\) 点在 \(u\) 点之后使用,因为每次选的 \(dist\) 单调不减(后面的 dist 是由前面的 dist 加 一个非负数得出的,因此单调不减),那么dist[k] >= dist[u],而两点间距离 w[k, u] 最小为 \(0\),那么dist[k] + w[k, u] >= dist[u],那么这个点 \(k\) 就无法使得dist[u]更小,那么说明这种情况不可能;
  2. 如果 \(k\) 点在 \(u\) 点之前使用则说明 \(dist[k] \le dist[u]\) 那么如果 u 可以被更新,那么它会在使用 \(u\) 之前更新,因此这种情况不可能。
    综上,不可能存在这样的 \(k\) 点,即没有点能再更新 \(u\),在 \(u\) 使用以后,因此算法是正确的。

从这里也可以看出来,如果上面w[k, u] < 0那么k就有可能吧dist[u]变得更小,而我们是不会再去使用dist[u]去扩展其他点的,也就是说,\(u\) 之后的一些点无法利用这个dist,变得更小,从而使得我们的算法错误。
例子:
![[graph (2).png|250]]
可以看出,我们会先用 \(d_2\) 去把 \(d_4\) 更新成 dist 为 \(4\)。然后才会去用 \(3\) 去更新,这时候,dist[2]会变成 \(-95\),但因为 \(2\) 已经使用过了,所以不会再去使用它了,于是dist[4]无法更新,算法错误。

因为每一轮松弛结束之后,都会出现一个新的最小 \(dist\),而最开始有一个源点的最小 \(dist\),所以只需要进行 \(n - 1\) 轮松弛就可以完成最短路扩展。

代码

中心思想就是这样,代码也显而易见。

朴素dijkstra (邻接表)

dijkstra 正确性来自于贪心 也就是 \(st\) 数组内的数(dist) 必须逐渐变大这样才能保证后面的数更新的时候,当前的第三边dist[t]都是最小值。
dist[x]表示xstart的最短距离。

int dijkstra(int S, int T)
{
	memset(st, 0, sizeof 0);
	memset(dist, 0x3f, sizeof dist);
    dist[S] = 0;
    
    int k = n;
    while ( -- k) // 实际上运行n - 1 次就行 n次一样不错就是多算一遍 可改成 --k 但注意 prim 是必定 n 次
    {
        int t = -1; // 最小的未使用dist的点
        for (int i = 1; i <= n; i ++ )
            if (!st[i] && (t == -1 || dist[t] > dist[i]))
                t = i;
                
        st[t] = true; 
        
        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (!st[j]) dist[j] = min(dist[j], dist[t] + w[i]);
        }
    }
    
    return dist[T];
}

朴素 dijkstra 邻接矩阵

看代码或者理论应该就能看出来,是 \(\operatorname O(n^2)\) 的。

int dijkstra()
{
    dist[S] = 0;
    int k = n;
    
    while (k -- )
    {
        int t = -1;
        for (int i = 1; i <= n; i ++ )
            if (!st[i] && (t == -1 || dist[t] > dist[i])) 
	            t = i;
	    
        st[t] = true;
        
        for (int i = 1; i <= n; i ++ )
            if (!st[i]) dist[i] = min(dist[i], dist[t] + g[t][i]); // 可以加上 if (!st[i])
    }
    
    return dist[T];
}

堆优化 dijkstra

关于堆优化 Dijkstra,就是优化了找最小 dist 点的过程,使用了小根堆进行排序,把原来 \(\operatorname O(n)\) 的枚举换成 \(\operatorname O(\log n)\) 的排序,从而优化了时间复杂度,也因此堆优化 Dijkstra 有个\(\operatorname O(\log n)\) 而遍历所有点和边是 \(\operatorname O(n + m)\) 的,加起来就是 \(\operatorname O((n+m)\log n)\)

小根堆排序,一般用 pair 存,因为要用 dist 来排序,还要记录这个点的下标,并且 pair 自带,第一键值优先的排序性质,所以使用。

此时堆里面是存的是可能造成松弛的点,因为一个点只有被松弛过才有可能去造成下一次松弛,因此一个点只要被松弛过,我们就加入这个点。但这样会出现重复入队情况,比如点 \(3\) 先被点 \(2\) 松弛了一次,又被点 \(1\) 松弛了一次,此时 \(3\) 就会两次进入队列,就会重复无效扩展。为了防止无效的松弛,所以需要进行判断,如果这个点已经使用过了,即 \(st[i] = true\),那么我们就跳过这个点,防止无效使用。

#define x first
#define y second

typedef pair<int, int> PII;

int dijkstra()
{
	memset(dist, 0x3f, sizeof dist);
	memset(st, 0, sizeof st);
    priority_queue<PII, vector<PII>, greater<PII>> q;
    q.push({0, S});
    dist[S] = 0;
    while (q.size()) // 如果队列里没有点,则说明无法进行松弛,则算法结束
    {
        auto t = q.top();
        q.pop();
        int ver = t.y;
        if (st[ver]) continue; // 排除重复使用情况
        st[ver] = true;      // 标记已经用这个点更新过了 (此点目前最小)
        
        for (int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[ver] + w[i])
            {
                dist[j] = dist[ver] + w[i];
                q.push({dist[j], j}); // 只有更新了才可能最小
            }
        }
    }
    return dist[T];
}

扩展

求解最长路

Dijkstra 是可以用于求解最长路的,但是必须全非正权。如果是正权可能部分情况是对的,但大部分情况是错误的,如下图:
![[Pasted image 20250805071951.png|299]]
根据算法原理,第一次松弛会松弛出 \(2,3\) 两个点,接着第二次会使用 \(3\) 点去松弛其他店,此时 \(3\) 已经使用,但并不为最长路。可见,Dijkstra 无法求解正权图的最长路。

本质上来说,dijkstra 可以在 \(dist\) 单调递增(即用点 \(v\) 松弛的点 \(u\)\(dist\) 一定大于等于 \(v\)\(dist\))的时候求解最短路,可以在 dist 单调递减(即用点 \(v\) 松弛的点 \(u\)\(dist\) 一定小于等于 \(v\)\(dist\))的时候求解最长路。

updata 2025.9.22

posted @ 2024-05-12 21:01  blind5883  阅读(37)  评论(0)    收藏  举报