图的最短路径问题

图的最短路径问题

在图中,我们经常要求从某一个点到另一个点的最短距离,这种问题就叫最短路径问题。下面介绍几种常用的最短路径算法。

算法模版:洛谷 P3371 【模板】单源最短路径(弱化版)

Floyed

Floyed 算法是最简单的最短路径算法,可以计算图中任意两边的最短路径。时间复杂度为 \(O(n^3)\),空间复杂度为 \(O(n^2)\),适用于出现负边权的情况。

算法的思想如下:

设 \(dis_{i,j}\) 是从 \(i\) 到 \(j\) 的最短路径。若 \(dis_{i,k}+dis_{k, j}<dis_{i,j}\),则可以将 \(dis_{i,j}\) 更新为 \(dis_{i,k}+dis_{k, j}\)。

算法描述

  1. 初始化:若点 \(u,v\) 有边相连,边权为 \(w\),则 \(dis_{u,v}=w\);否则 \(dis_{u,v}=\infty\)。
  2. 计算过程:按照上述原理进行循环枚举。
  3. 算法结束:\(dis_{i,j}\) 即为从 \(i\) 到 \(j\) 的最短路径。

算法实现

#include <cstdio>
#include <cstring>

int dis[105][105];
int n, m, s;

void floyed() {
    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] + dis[k][j] < dis[i][j])
                    dis[i][j] = dis[i][k] + dis[k][j];
}

int main() {
    memset(dis, 0x3f, sizeof(dis));

    scanf("%d%d%d", &n, &m, &s);
    for (int i = 1; i <= m; i++) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        dis[u][v] = w;
    }

    floyed();

    for (int i = 1; i <= n; i++)
        printf("%d ", dis[s][i]);

    return 0;
}

为什么这里要将 \(k\) 放在外层循环呢?因为每次做状态转移时,\(dis_{i,k}\) 和 \(dis_{k,j}\) 都需要已经算出,以保证算法的正确性。

算法变形

Floyed 算法还有一种广泛的应用:对于没有边权的图,我们可以使用该算法的变形来判断两点是否相连。

判断两点连通,需要满足以下两个条件中的至少一个:

  1. 已经判断出 \(i,j\) 两点连通。
  2. 存在点 \(k\) ,使得 \(i,k\) 连通且 \(k,j\) 连通。

具体代码如下:

#include <cstdio>
#include <cstring>

bool dis[105][105];
int n, m, s;

void floyed() {
    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]);
}

int main() {
    memset(dis, 0, sizeof(dis));

    scanf("%d%d%d", &n, &m, &s);
    for (int i = 1; i <= m; i++) {
        int u, v;
        scanf("%d%d", &u, &v);
        dis[u][v] = 1;
    }

    floyed();

    for (int i = 1; i <= n; i++)
        printf("%d ", dis[s][i]);

    return 0;
}

Dijkstra 算法

Dijkstra 算法是用来计算从一个点到其他所有点的最短路径算法,是一种单源最短路径算法。也就是说,只能计算起点只有一个的情况。

Dijkstra 算法的时间复杂度为 \(O(n^2)\),不能处理存在负边权的情况。

算法描述

设起点为 \(s\),\(dis_v\) 表示从 \(s\) 到 \(v\) 的最短路径。

  1. 初始化:\(dis_v=\infty(v\neq s)\),\(dis_s=0\)。
  2. 枚举所有点
    • 在没有访问过的点中找一个点 \(u\) 使得 \(dis_u\) 最小。
    • 将 \(u\) 标记为已确定最短路径。
    • 枚举与 \(u\) 相连的所有未确定最短路径的顶点 \(v\),并更新它的最短路径。
  3. 算法结束:\(dis_v\) 为 \(s\) 到 \(v\) 的最短路径。

模拟算法过程

Dijkstra

如图,以 \(1\) 为起点,粉色表示未访问,绿色表示已访问。使用 Dijkstra 算法模拟从 \(1\) 到所有点的最短路径。

第一次

找到 \(dis_1=0\) 最短,于是将 \(1\) 标记为已访问,并修改与其相连的其他点的值:\(dis_2=2,dis_3=4,dis_4=7\)。如图所示。

第一次

第二次

在未访问的点中找到 \(dis_2=2\) 最短,于是将 \(2\) 标记为已访问,并修改其他点的值:\(dis_3=3,dis_5=4\)。如图所示。

第二次

第三次

在未访问的点中找到 \(dis_3=3\) 最短,于是将 \(3\) 标记为已访问,并修改其他点的值:\(dis_4=4\)。注意:此时 \(dis_5=9>4\),所以不更新 \(dis_5\)。如图所示。

第三次

第四次

在未访问的点中找到 \(dis_4=4\) 最短,于是将 \(4\) 标记为已访问。此时,没有任何结点与其连通,所以不用更新。如图所示。

第四次

第五次

现在只剩下一个未访问结点,即点 \(5\)。将其标记为已访问,但无法更新结点。此时,所有结点都已被访问,所以算法结束。如图所示。

第五次

下面是该算法的具体执行过程,其中,加粗的数字表示该结点已被访问。

\(dis\) 初始化 第一次 第二次 第三次 第四次 第五次(最终结果)
\(1\) \(0\) \(\bm 0\) \(\bm 0\) \(\bm 0\) \(\bm 0\) \(\bm 0\)
\(2\) \(\infty\) \(2\) \(\bm 2\) \(\bm 2\) \(\bm 2\) \(\bm 2\)
\(3\) \(\infty\) \(4\) \(3\) \(\bm 3\) \(\bm 3\) \(\bm 3\)
\(4\) \(\infty\) \(7\) \(7\) \(4\) \(\bm 4\) \(\bm 4\)
\(5\) \(\infty\) \(\infty\) \(4\) \(4\) \(4\) \(\bm 4\)

算法原理

如图,以 \(1\) 为起点,\(dis_3<dis_2\)。若距离全部为正数,不管 \(2,3\) 之间距离是多少,由 \(2\) 中转再到 \(3\) 的距离肯定大于从 \(1\) 直接到 \(3\) 的距离。

Dijkstra 算法原理

至此,Dijkstra 不能处理负边权的原因也真相大白了。在上图中,若 \(2,3\) 间距离为 \(-2\),则该算法会认为从 \(1\) 直接到 \(3\) 的距离最短,但实际上由 \(2\) 中转的距离是更短的。

代码实现

这里使用邻接表来存储图。C++ 代码如下。

#include <cstdio>
#include <cstring>
#include <vector>
using namespace std;

struct edge {
    int to, cost;
};

vector <edge> g[10005];
int dist[10005];
bool vis[10005];
int n, m, s;

void dijkstra() {
    memset(dist, 0x3f, sizeof(dist));
    memset(vis, 0, sizeof(vis));
    dist[s] = 0;

    for (int i = 1; i <= n; i++) {
        int u = -1;
        int mind = 0x3f3f3f3f;
        for (int v = 1; v <= n; v++)
            if (!vis[v] && dist[v] < mind) {
                mind = dist[v];
                u = v;
            }
        if (u == -1) break;

        vis[u] = 1;

        for (edge e : g[u]) {
            int v = e.to, w = e.cost;
            if (!vis[v] && dist[u] + w < dist[v])
                dist[v] = dist[u] + w;
        }
    }
}

int main() {
    scanf("%d%d%d", &n, &m, &s);
    for (int i = 1; i <= m; i++) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        g[u].push_back((edge){v, w});
    }

    dijkstra();

    for (int i = 1; i <= n; i++)
        printf("%d ", dist[i]);

    return 0;
}
上面的代码无法通过本题,思考一下应该怎么修改。

(提示:可以使用 vector 来初始化数组,并使用 climits 头文件中的 INT_MAX 宏计算 int 类型最大值)

posted @ 2026-10-16 13:40  zzx321  阅读(6)  评论(0)    收藏  举报