最短路学习笔记

· · 个人记录

目录

概念

字面意义,最短路径就是连接两点的这些路径中最短的一条。

Floyed-Warshall

Floyed-Warshall(弗洛伊德)算法,是最简单的最短路算法,可以计算图中任意两点的最短路。

复杂度: O(n^{3})

思想:枚举 i, j 再在外层枚举一个中间点 k 来松弛,如果原先 ik 到距离加上 kj 到距离小于原先 ij 的距离,就更新 ij 距离的最小值。

初始化:若点 u,v 间有一条权值为 w 的边,dis[u][v] = w,否则 dis[u][v] = \infty

for (int k = 1; k <= n; k++) //中间点 
{
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= n; j++)
        {
            if (dis[i][k] == INT_MAX || dis[k][j] == INT_MAX)   continue; //i、k和k、j是否有最小值 
            dis[i][j] = min (dis[i][j], dis[i][k] + dis[k][j]); //更新 
        }
    }
}

练习:P1339 [USACO09OCT]Heat Wave G

判断连通性

和求最短路相似

初始化:若点 u, v 间有边, dis[u][v] = true,否则 dis[u][v] = false

for (int k = 1; k <= n; k++)
{
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= n; j++)
        {
            dis[i][j] = (dis[i][j] || (dis[i][k] && dis[k][j]));
        }
    }
}

Floyed适用于负边权

Dijkstra

Dijkstra (迪杰斯特拉算法)算法,是单源最短路算法,可以计算图中任意点到源点的最短路。

复杂度: O(n^{2})

思想:主要是贪心思想,每次寻找一个未被访问的一个点 u 使得 dis[u] 是最小的,将 u 标记被访问,再枚举更新与 u 相连的最短路径。

初始化:设起点为 s,dis[s] = 0

void dijkstra ()
{
    for (int i = 1; i <= n; i++)    dis[i] = INT_MAX; //初始化 
    dis[s] = 0;
    for (int i = 1; i <= n; i++)
    {
        //找最小值、 最小值编号 
        int minn = INT_MAX, minj = -1;
        for (int j = 1; j <= n; j++)
        {
            if (minn > dis[j] && !vis[j])
            {
                minn = dis[j];
                minj = j;
            }
        }
        if (minj == -1) break;
        //标记 
        vis[minj] = true;
        //更新 
        for (int j = 1; j <= n; j++)
        {
            if (vis[j])                 continue;
            if (mp[minj][j] == INT_MAX) continue;
            dis[j] = min (dis[j], dis[minj] + mp[minj][j]);
        }
    }
    return ;
}

练习:P3371 【模板】单源最短路径(弱化版)

你会惊奇地发现这个复杂度太高了,需要优化。

Dijkstra + 堆优化

基本思想是一样的,用优先队列把找找最小值、 最小值编号优化了。

复杂度:O(m + nlogn)

void dijkstra ()
{
    for (int i = 1; i <= n; i++)    dis[i] = INF; //初始化 
    dis[s] = 0;
    q.push (Node {0, s});
    while (!q.empty ())
    {
        Node cur = q.top ();
        q.pop ();
        if (vis[cur.x]) continue;
        //标记 
        vis[cur.x] = true;
        for (int j = head[cur.x]; j != -1; j = edge[j].next)
        {
            int to = edge[j].to;
            if (vis[to])    continue;
            //更新 
            if (dis[to] > dis[cur.x] + edge[j].dis)
            {
                dis[to] = dis[cur.x] + edge[j].dis;
                q.push (Node {dis[to], to});
            }
        }
    }
    return ;
}

练习:P4779 【模板】单源最短路径(标准版)

Dijkstar不适用存在负边权的情况

Bellman-Ford

简称 Ford(福特)算法,也是单源最短路算法。

复杂度: O(NE) , N 是点的数量, E 是边的数量。

每次遍历边, 遍历 (n - 1) 次,每次标记与更新与黑点相连的白点。

建议用边集数组。

初始化:设起点为 s,dis[s] = 0

void ford ()
{
    for (int i = 1; i <= n; i++)    dis[i] = INF; //初始化 
    dis[s] = 0;
    for (int i = 1; i < n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
            int u = edge[j].u, v = edge[j].v; //u、v是这条边连接的两个点 
            //更新 
            if (dis[v] == INF)  continue;
            dis[v] = min (dis[v], dis[u] + edge[j].w);
        }
    } 
    return ;
}

图解:

  1. 初始化了 dis[1] = 0

$dis[1] = dis[2] = dis[3] = dis[4] = \infty$。 2. 通过点 $1$,更新了 $3,4,5$ 三个点。 ![](https://cdn.luogu.com.cn/upload/image_hosting/qlyd1wpl.png) $dis[1] = 0$。 $dis[2] = 2$。 $dis[3] = 1$。 $dis[4] = 3$。 $dis[5] = \infty$。 3. 通过点 $2,3$ 两个点,更新了点 $4,5$ 两个点。 ![](https://cdn.luogu.com.cn/upload/image_hosting/52r5wea8.png) $dis[1] = 0$。 $dis[2] = 2$。 $dis[3] = 1$。 $dis[4] = 1$。 $dis[5] = 4$。 但 **Bellman_Ford 不能处理有负环的情况**,因为: ![](https://cdn.luogu.com.cn/upload/image_hosting/6nrt7zq9.png) 在路径 $2 - 4 - 5 - 3 - 2$ 这条回路的边权和为 $-3$。在有负环的情况下 $1$ 到 $6$ 的最短路为无穷小,所以 Bellman-Ford 在有负环的情况下会输出错误提示。 ### 判断负环 若程序运行完了后还存在边使得 $dis[u] + edge[j].w \le dis[v]$,则存在负环。 ```c bool check () { for (int j = 1; j <= m; j++) { int u = edge[j].u, v = edge[j].v; //u、v是这条边连接的两个点 if (dis[u] != INT_MAX && dis[u] - edge[j].w < dis[v]) return true; } return false; } ``` [练习:P2136 拉近距离](https://www.luogu.com.cn/problem/P2136) ## SPFA ![](https://cdn.luogu.com.cn/upload/image_hosting/kyq1gvc2.png) 思想和 Bellman-Ford 一样,只是它的优化版。用队列存储点的信息,再用一个数组标记点 $i$ 是否在队列中。因为一个点最多入队 $(n - 1)$ 次,所以可以判断是否存在负环。 复杂度: $O(kE)$,$k$ 是常数,平均值为 $2$ ($1 \leq k \leq E$),$E$ 为边的数量。 ```c bool spfa (int s) { memset (dis, 0x3f, sizeof (dis)); //初始化 //起点入队 dis[s] = 0; q.push (s); in_que[s] = true; cnt[s]++; while (!q.empty ()) { int t = q.front (); q.pop (); in_que[t] = false; for (int i = head[t]; i != -1; i = edge[i].next) { int to = edge[i].to; if (dis[to] > dis[t] + edge[i].w) { dis[to] = dis[t] + edge[i].w; //入队 if (in_que[to]) continue; q.push (to); in_que[to] = true; cnt[to]++; if (cnt[to] >= n) return true; //判断负环 } } } return false; } ``` [练习1:P3385 【模板】负环](https://www.luogu.com.cn/problem/P3385) [练习2:P1828 [USACO3.2]香甜的黄油 Sweet Butter](https://www.luogu.com.cn/problem/P1828)