算法学习笔记(25)——Dijikstra算法(单源最短路)

Dijkstra 算法

Dijkstra算法用于在所有边权都非负的图上,求单源点最短路

\(n\) 是图上结点的数量,\(m\) 是边的数量。

  • 朴素Dijkstra算法的时间复杂度是 \(O(n^2)\) ,适合稠密图(点少边多);
  • 堆优化版的Dijkstra算法的时间复杂度是 \(O(m\log n)\) ,适合稀疏图(点多边少)。

如果是稠密图,那么边的数量 \(m\) 和点数的平方 \(n^2\) 基本是一个规模的。如果用堆优化版的Dijkstra,那么复杂度就可以视为 \(O(n^2\log n)\) ,这个时候是不如朴素版Dijkstra的。

如果是稀疏图,那么边的数量 \(m\) 和点的数量 \(n\) 基本是一个规模的,题目数据可能给到 \(10^5\) 。如果用朴素版的Dijkstra,那么规模就是 \(10^{10}\) 了,肯定会超时。

Dijkstra算法属于贪心算法。

朴素Dijkstra

例如求1号点到n号点的最短距离

  1. 初始化每个点到起点的距离dist[1]=0表示1号点到起点(自己)的距离是0,其余的dist[i]=正无穷
  2. 集合S[]存储当前已经确定了最短距离的点,初始化为空集。
  3. 由于第一个我们默认最短距离为0,所以只需要对剩下未确定的n-1个点循环操作
    • 每次找到不在集合中的距离最小的点,并将这个点加入到集合里面
    • 用新加入的点t更新到其他点的最短距离。更新的原则是,如果从起点到某一点j的距离dist[j]大于从起点先到t,再到该点的距离dist[t] + g[t][j],那么就更新该点到起点的最短距离。(新加入的点当作中介,看看经过他
#include <iostream>
#include <cstring>

using namespace std;

const int N = 510;

int n, m;
int g[N][N];    // 邻接矩阵存图
int dist[N];    // 存储每一点到起点的最短距离
bool st[N];     // 标记每一点是否确定了最短距离

void dijkstra()
{
    // 初始化距离数组
    memset(dist, 0x3f, sizeof dist);
    // 初始化起点的距离
    dist[1] = 0;
    
    // 对剩下n-1个点循环操作
    for (int i = 0; i < n - 1; i ++ ) {
        // 找到不在集合中的距离最短的点
        int t = -1;
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[j] < dist[t]))
                t = j;
        
        // 将距离最短的点加入集合,确定了该点到起点的最短距离
        st[t] = true;
        
        // 利用新加入的点更新其他点到起点的距离
        for (int j = 1; j <= n; j ++ )
            dist[j] = min(dist[j], dist[t] + g[t][j]);
    }
}

int main()
{
    memset(g, 0x3f, sizeof g);  // 图里的每条边都初始化为正无穷,表示不可达
    
    cin >> n >> m;
    for (int i = 0; i < m; i ++ ) {
        int x, y, z;
        cin >> x >> y >> z;
        g[x][y] = min(g[x][y], z); //只存储最短的那一条边
    }
    
    dijkstra();
    
    if (dist[n] == 0x3f3f3f3f) puts("-1"); // 如果n号点不可达,输出-1
    else cout << dist[n] << endl;
    
    return 0;
}

堆优化Dijkstra

如果是稀疏图,题目所给的节点数很大,使用朴素Dijkstra会超时(\(O(n^2)\)),所以需要考虑优化。
朴素Dijkstra的算法思想如下:

循环n - 1次
	找一个结点t,是不在S中的距离最近的点
	将t加入到S里
	用t更新其它点的距离

通过分析,第一个操作是每次找出距离最近的点,这一步骤的时间复杂度是 \(O(n^2)\) ,第二个操作是将找出来的点加入确定了最小距离的点的集合,时间复杂度是 \(O(1)\) ,第三个操作是用t更新其他点的距离,本质上就是遍历了所有的边,所以时间复杂度是 \(O(m)\)
综上,主要的时间复杂的的瓶颈在于第一步找出最小值的操作,而在一堆数里面每次找出一个最小值,我们可以通过小根堆以 \(O(1)\) 的时间复杂度来实现,在 \(n-1\) 次循环总共就是 \(O(n)\) 的时间复杂度。
如果用堆存储每个点到起点的距离,则第三个更新到其他点的距离这一操作,实际上就是修改堆中一个元素,时间复杂度为 \(O(\log n)\) ,一共要修改 \(m\) 条边,所以总的时间复杂度是 \(O(m\log n)\)
所以堆优化版的Dijkstra算法的时间复杂度在于第三个步骤,与朴素版的Dijkstra有区别。所以堆优化Dijkstra算法的时间复杂度就是 \(O(m\log n)\)

另外, 通过手写模拟堆来做,才能支持修改堆里的任意一个元素,但是手写堆很麻烦。
我们选择用STL的优先队列来做,但是优先队列不支持修改任意一个元素,这里使用的解决方案就是用冗余。即每次都直接加入新的元素,这样做的话,堆里总共的元素个数要达到 \(m\) 个,这样实现的堆优化Dijkstra算法的时间复杂度就变 \(O(m\log m)\)

由于 \(m < n^2\) ,所以 \(m\log m < m\log(n^2) < 2m\log n\),它和 \(m\log n\) 实际也是一个级别的。另外因为是稀疏图,所以 \(m\) 实际远比 \(n^2\) 小,所以这个常数也比 \(2\) 小很多了,就是一点几,这样写方便而且速度也够。

由于这样做的堆里存在很多冗余,所以找到的最小值可能之前已经确定过了,这里就用st数组来判断,如果是已经确定过的,就把它扔掉再重新找就可以了。

#include <iostream>
#include <cstring>
#include <queue>

using namespace std;

const int N = 150010;

typedef pair<int, int> PII;

int n, m;
int h[N], e[N], ne[N], w[N], idx;   // 带边权的邻接表
int dist[N];    // 距离数组
bool st[N];     // 标记数组

// 邻接表存储图加边
void add(int a, int b, int c)
{
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++;
}

void dijkstra()
{
    // 初始化距离数组
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    
    // 堆中元素按第一关键字由小到大排序,按照{距离,点}的格式存储,即按边递增排序
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, 1});
    
    // 枚举每一条边
    while (heap.size()) {
        // 取出堆顶元素,就是距离最近的点
        auto t = heap.top();
        heap.pop();
        
        // 如果该点已经计算过最短距离,则跳过(冗余)
        int ver = t.second, distance = t.first;
        if (st[ver]) continue;
        // 该点还没有确定最短距离,则将它加入集合S
        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];
                heap.push({dist[j], j});
            }
        }
    }
}

int main()
{
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for (int i = 0; i < m; i ++ ) {
        int x, y, z;
        cin >> x >> y >> z;
        add(x, y, z);
    }
    
    dijkstra();
    
    if (dist[n] == 0x3f3f3f3f) puts("-1");
    else cout << dist[n] << endl;
    
    return 0;
}
posted @ 2022-12-10 09:32  S!no  阅读(265)  评论(0编辑  收藏  举报