Week2Day4A:单源最短路【2023 安全创客实践训练|笔记】

内容为武汉大学国家网络安全学院2022级大一第三学期“996”实训课程中所做的笔记,仅供个人复习使用,如有侵权请联系本人,将于15个工作日内将博客设置为仅粉丝可见。

目录

Dijkstra 算法

单源最短路问题

算法流程

算法演示

Dijkstra 算法演示

迷阵突围


Dijkstra 算法

单源最短路问题

单源最短路问题是指:求源点 s 到图中其余各顶点的最短路径。

如果用我们之前学习的 DFS 来解决单源最短路问题,效率上会很慢,能解决的问题的数据规模非常小。

而 BFS 能解决的最短路问题只限制在边权为 1 的图上。对于边权不同的图,利用 BFS 求解最短路是错误的,比如下面这个图,如果用 bfs 解决,点 3 和点 4 的最短路求出来是错误的。

 

解决单源最短路径问题常用 Dijkstra 算法,用于计算一个顶点到其他所有顶点的最短路径。Dijkstra 算法的主要特点是以起点为中心,逐层向外扩展(这一点类似于 bfs,但是不同的是,bfs 每次扩展一个层,但是 Dijkstra 每次只会扩展一个点),每次都会取一个最近点继续扩展,直到取完所有点为止。

注意:Dijkstra 算法要求图中不能出现负权边。

算法流程

我们定义带权图 G 所有顶点的集合为 V,接着我们再定义已确定从源点出发的最短路径的顶点集合为 U,初始集合 U 为空,记从源点 s 出发到每个顶点 v 的距离为 d_v​,初始 d_s​=0。接着执行以下操作:

  1. 从 V−U 中找出一个距离源点最近的顶点 v,将 v 加入集合 U。

  2. 并用 d_v​ 和顶点 v 连出的边来更新和 v 相邻的、不在集合 U 中的顶点的 d,这一步称为松弛操作。

  3. 重复步骤 1 和 2,直到 V=U 或找不出一个从 s 出发有路径到达的顶点,算法结束。

如果最后 V≠U,说明有顶点无法从源点到达;否则每个 d_i​ 表示从 s 出发到顶点 i 的最短距离。

Dijkstra 算法的时间复杂度为 O(V^2),其中 V 表示顶点的数量。

算法演示

接下来,我们用一个例子来说明这个算法。

初始每个顶点的 d 设置为无穷大 ⁡inf,源点 M 的 d_M​ 设置为 0。当前 U=∅,V−U 中 d 最小的顶点是 M。从顶点 M 出发,更新相邻点的 d。

更新完毕,此时 U={M},V−U 中 d 最小的顶点是 W。从 W 出发,更新相邻点的 d。

更新完毕,此时 U={M,W},V−U 中 d 最小的顶点是 E。从 E 出发,更新相邻顶点的 d。

更新完毕,此时 U={M,W,E},V−U 中 d 最小的顶点是 X。从 X 出发,更新相邻顶点的 d。

更新完毕,此时 U={M,W,E,X},V−U 中 d 最小的顶点是 D。从 D 出发,没有其他不在集合 U 中的顶点。

此时 U=V,算法结束,单源最短路计算完毕。

参考程序

#include <stdio.h>
#include <string.h>
#define maxn 1001
#define maxm 10101
#define min(a,b) (((a)<(b))?(a):(b))
const int inf = 0x3f3f3f3f;
struct edge {
    int v, w, next;
} e[maxm * 2];
int p[maxn], eid, n, m, s, d[maxn], vis[maxn];
void insert(int u, int v, int w) {
    struct edge t = {v, w, p[u]};
    e[eid] = t;
    p[u] = eid++;
}
void insert2(int u, int v, int w) {
    insert(u, v, w);
    insert(v, u, w);
}

int main() {
    scanf("%d%d%d", &n, &m, &s);  // s 是源点
    memset(p, -1, sizeof(p));
    for (int i = 0; i < m; i++) {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        insert2(u, v, w);
    }
    memset(d, 0x3f, sizeof(d));
    d[s] = 0;
    for (int i = 1; i <= n; i++) {
        int mind = inf;
        int u = 0;
        for (int j = 1; j <= n; j++) {
            if (!vis[j] && d[j] < mind) {
                mind = d[j];
                u = j;
            }
        }
        if (mind == inf) {
            break;
        }
        vis[u] = 1;
        for (int j = p[u]; j != -1; j = e[j].next) {
            int v = e[j].v;
            d[v] = min(d[v], d[u] + e[j].w);
        }
    }
    for (int i = 1; i <= n; i++) {
        printf("%d ", d[i]);
    }
    return 0;
}

Dijkstra 算法演示

 略


迷阵突围

 蒜头君陷入了坐标系上的一个迷阵,迷阵上有 n 个点,编号从 1 到 n。蒜头君在编号为 1 的位置,他想到编号为 n 的位置上。蒜头君当然想尽快到达目的地,但是他觉得最短的路径可能有风险,所以他会选择第二短的路径。现在蒜头君知道了 n 个点的坐标,以及哪些点之间是相连的,他想知道第二短的路径长度是多少。

注意,每条路径上不能重复经过同一个点

1≤n≤200

这道题我们需要去求次短路的长度。我们令图 G=(V,E)。

首先我们肯定是要去求最短路长度的。那么在求得 1 号点到其他点的最短路的时候,我们肯定也能够同时得到一条从 1 号点到 n 号点的最短路径。

那么,我们在更新最短距离的时候需要进行额外的标记,内容是到某个点的最短路径,最后一条边是从哪里出发过去的,我们可以开一个 pre 数组进行记录:

for (int j = p[u]; j != -1; j = e[j].next) { // 更新最短距离
    int v = e[j].v;
    if (d[v] > d[u] + e[j].w) {
        d[v] = d[u] + e[j].w;
        pre[v] = u; // 代表当前到 v 的最短路的最后一条边是从 u 出发
    }
}

然后我们可以去把从 1 到 n 上的最短路的所有结点都取出来:

void getPath(int x) {
    if (x == 1) {
        return;
    }
    /*
    做点什么
    */
    getPath(pre[x]);
    /*
    你也可以在这里选择做点什么
    */
}

而在函数执行的途中,从 prex​ 到 x 的那条边便是 1 到 n 上的最短路径上的一条边。那么在主函数内,你只需要执行:

getPath(n);

就可以把 1 到 n 上的最短路的所有结点都取出来。

然后我们可以得到一个很显然的结论,在图 G 上 1 号点到 n 号点的次短路肯定是 G 的某个子图上 1 号点到 n 号点的最短路。于是我们现在就转变成了考虑在哪些子图上 1 号点到 n 号点的最短路可能会变成图 G 上 1 号点到 n 号点的次短路。

而构造这些子图,我们很明显需要切断 G 上 1 号点到 n 号点的最短路。而因为我们需要求的是次短路,所以删掉一条边就好。所以我们在图 G 上得到一条从 1 号点到 n 号点的最短路径之后,枚举这条路径上的每一条边断掉,这样就可以形成一些子图,再对这些子图去做 1 号点到 n 号点的最短路,然后取最小值,便可以得到答案了。

而删除一条边,并不是真的需要去执行这个操作,你只需要在进行最短路算法的时候进行特判,当遇到这条被“删除”的边的时候不去处理就行了。

struct edge {
  int v, w, next, f;  // f 表示是否有效,1 有效,0 无效
};

// delete an edge
e[i].f = e[i ^ 1].f = 0;  // 删除无向边
e[i].f = 0;  // 删除有向边

// iteration
for (int i = p[u]; ~i; i = e[i].next) {
  if (e[i].f) {
    // ......
  }
}

当然你需要注意的是,如果你删除的是一条 无向边,那么相当于删除了两条有向边,判断的时候需要千万小心。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值