zoukankan      html  css  js  c++  java
  • 图论:最短路

    图论:最短路

    参考资料:
    《算法竞赛进阶指南》
    《算法竞赛从入门到进阶》
    浙江大学《数据结构(第二版)》

    例题:

    1. SDUT 2143
    2. SDUT 2894

    前置知识:使用邻接矩阵,邻接表存储和访问图(特别是有权图)。

    简略复习一下,邻接表可以链表,数组,STL中的vector来实现。用数组的方法又称链式前向星存图。我们使用一个表头数组head记录了从每个节点出发的第一条边在ver和edge数组中的存储位置,使用边集数组ver和edge记录了每条边的终点和边权,使用数组Next模拟了链表中的指针,表示从相同起点出发的下一条边在ver和edge数组中的存储位置。核心代码如下:

    //加入有向边(x, y), 权值为z
    void add(int x, int y, int z) {
        ver[++tot] = y, edge[tot] = z;
        Next[tot] = head[x], head[x] = tot;
    }
    
    //访问从x出发的所有边
    for (int i = head[x]; i != -1; i = next[i]) {
        int y = ver[i], z = edge[i];
        //找到了一条有向边(x, y), 权值为z
    }
    

    单源最短路径

    单源最短路径问题是说,给定一张有向图(无向图可以视为特殊的有向图)G = (V, E), V是点集, E是边集, |V| = n, |E| = m。给定一个起点s,求长度为n的数组dist,其中dist[i]表示从起点s到节点i的最短路径长度。
    数据结构和算法动态可视化

    Dijkstra算法

    Dijkstra算法只适用于所有边的长度(权值)都是非负数的图,用于求解单源最短路径问题。

    • 算法流程:
    1. 初始化dist[s] = 0, 其余结点的dist值为正无穷大。
    2. 找出一个未被标记的、dist[x]最小的节点x,然后标记节点x。
    3. 扫描节点以x为起点的所有边(x, y, z),若dist[y] > dist[x] + z,则使用dist[x] + z更新dist[y]。
    4. 若还有未标记的节点,重复2~3两个步骤。
    • 算法原理:
    1. 将图中的点分为两部分,点集S1 = {起点s, 已经确定最短路径的点(即dist中的值就是要求的最短路径长度)}, S2 = {未确定最短路径的点}。注意在该算法中,dist数组中存储的最短路径是路径上只包含S1中的点的最短路径长度。
    2. 容易发现S1中的点,其dist数组的值已不可能被其他点优化。那么可以在S2中也寻找到这样的一个点,将其放入S1中。用该点优化一遍它的邻接点。重复该步骤直至所有的点都被放入S1中。注释:若存在一条边(x, y, z),有dist[y] > dist[x] + z,则从s到y的最短路径可以由s先到x,再由x到y,称为用点x优化点y,优化后dist[y] = dist[x] + z。
    3. S2中dist值最小的点就是我们要寻找的点。证明:记该点为v1,假设v1的最短路径长度还可以被S2中的其他点优化,记优化后的最短路径中与S1相接的点为v2,v2至v1之间的边权和为z,那么可以得到:dist[v2] + z < dist[v1],显然这与已知条件dist[v1]最小不符,故v1就是满足条件的点。可以将其放入S1中。
    4. 算法开始时,点集S1中只包含起点,起点的最短路径长度显然已经确定,值为0。由上文证明可知,每一次操作将确定一个点的最短路径长度,经过n-1次循环后所有点的最短路径长度都已经确定,算法结束。
    • 代码:
    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 105, M = 20010;
    int tot, ver[M], Next[M], head[N], edge[M];
    int dist[N];
    bool vis[N];
    
    void addedge(int x, int y, int z) {
        ver[++tot] = y, edge[tot] = z;
        Next[tot] = head[x], head[x] = tot;
    }
    
    void dijkstra(int s, int n) {
        memset(dist, 0x3f, sizeof(dist));
        memset(vis, false, sizeof(vis));
        dist[s] = 0;
        int x = 0;
        for (int i = 0; i < n; i++) {
            x = 0;
            for (int j = 1; j <= n; j++) {
                if (!vis[j] && (x == 0 || dist[j] < dist[x])) x = j;
            }
            vis[x] = true;
            for (int j = head[x]; j; j = Next[j]) {
                int y = ver[j], z = edge[j];
                if (dist[y] > dist[x] + z) dist[y] = dist[x] + z;
            }
        }
    }
    
    int main() {
        int n, m, s, x, y, z;
        while (~scanf("%d %d %d", &n, &m, &s)) {
            memset(head, -1, sizeof(head));
            tot = -1;
            for (int i = 0; i < m; i++) {
                scanf("%d %d %d", &x, &y, &z);
                addedge(x, y, z);
            }
            dijkstra(s, n);
            for (int i = 1; i <= n; i++) {
                printf("%d%c", dist[i], i == n ? '
    ' : ' ');
            }
        }
        return 0;
    }
    

    Bellman-Ford算法

    • 算法流程:
    1. 扫描所有边(x, y, z),若dist[y] > dist[x] + z,则用 dist[x] + z 更新 dist[y]。
    2. 重复上述步骤直至没有更新操作发生。
    • Bellman算法只需进行n - 1次即可。下面给出证明。
    1. 对于图中最长的最短路径,最多只会包含n - 1条边(即经过n个节点),否则说明该最短路径重复经过了某些点,形成了回路(而且是一个负圈,如果是正的则最短路径中不会包含),显然这与已知矛盾。
    2. 既然最长的路径只可能包含n - 1条边,那么每扫描所有边一次可以确定其中一条边,循环n - 1次就可以确定全部路径。
    • 代码:
    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 105;
    const int M = 2e4 + 10;
    struct node {
        int x, y, z;
    } e[M];
    int dist[N];
    
    void Bellman(int s, int n, int m) {
        memset(dist, 0x3f, sizeof(dist));
        dist[s] = 0;
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < m; j++) {
                int x = e[j].x, y = e[j].y, z = e[j].z;
                if (dist[y] > dist[x] + z) dist[y] = dist[x] + z;
            }
        }
    }
    
    int main() {
        int n, m, s, x, y, z, cnt;
        while (~scanf("%d %d %d", &n, &m, &s)) {
            cnt = 0;
            for (int i = 0; i < m; i++) {
                scanf("%d %d %d", &x, &y, &z);
                e[cnt++] = {x, y, z};
            }
            Bellman(s, n, cnt);
            for (int i = 1; i <= n; i++) {
                printf("%d%c", dist[i], i == n ? '
    ' : ' ');
            }
        }
        return 0;
    }
    

    SPFA算法

    SPFA算法通称为“队列优化的 Bellman-Ford算法”,只在中国大陆流行“SPFA算法”的称谓。

    • 算法流程:
    1. 建立一个队列,最初队列中只含有起点s。
    2. 取出队头节点x,扫描它的所有出边(x, y, z),若dist[y] > dist[x] + z,则更新dist[y]。同时,若y不在队列中,则把y入队。
    3. 重复上一步骤直至队列为空。
    • 原理: 在Bellman-Ford算法中,若dist[x]没有发生改变,则对于x的所有出边(x, y, z)的扫描都是无效的。为了减少这些冗余扫描,使用一个队列存储dist值发生了改变的节点,对它的出边进行扫描直至队列为空(即已经不存在dist值可以发生改变的点)。

    代码:

    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 105;
    const int M = 2e4 + 10;
    int head[N], ver[M], edge[M], Next[M], tot;
    int dist[N];
    bool vis[N];
    
    void addedge(int x, int y, int z) {
        ver[++tot] = y, edge[tot] = z;
        Next[tot] = head[x], head[x] = tot;
    }
    
    void spfa(int s) {
        memset(dist, 0x3f, sizeof(dist));
        memset(vis, false, sizeof(vis));
        dist[s] = 0, vis[s] = true;
        queue<int> Q;
        Q.push(s);
        while (!Q.empty()) {
            int x = Q.front();
            Q.pop();
            vis[x] = false;
            for (int i = head[x]; i; i = Next[i]) {
                int y = ver[i], z = edge[i];
                if (dist[y] > dist[x] + z) {
                    dist[y] = dist[x] + z;
                    if (!vis[y]) Q.push(y), vis[y] = true;
                }
            }
        }
    }
    
    int main() {
        int n, m, s, x, y, z;
        while (~scanf("%d %d %d", &n, &m, &s)) {
            memset(head, 0, sizeof(head)), tot = 0;
            for (int i = 0; i < m; i++) {
                scanf("%d %d %d", &x, &y, &z);
                addedge(x, y, z);
            }
            spfa(s);
            for (int i = 1; i <= n; i++) {
                printf("%d%c", dist[i], i == n ? '
    ' : ' ');
            }
        }
        return 0;
    }
    

    任意两点间最短路径

    为了求出图中任意两点间的最短路径,当然可以把每一个点当做起点求解N次单源最短路径问题。不过对于该类问题,使用Floyd算法可以用非常简单的程序解决。

    Floyd算法

    设D[k, i, j] 表示 “经过若干个编号不超过 k 的结点” 从 i 到 j 的最短路长度。该问题可以划分为两个子问题,经过编号不超过 k - 1 的节点从 i 到 j,或者从 i 先到 k 再到 j。于是:
    D[k, i, j] = min(D[k - 1, i, j], D[k - 1, i, k] + D[k - 1, k, j]);
    初值为 D[0, i, j] = A[i, j],其中A为邻接矩阵。可以发现,Floyd算法的本质是动态规划。其中k是阶段,我们可以通过D[0, i, j]求出D[1, i, j],再由D[1, i, j]求出D[2, i, j],直至求出D[n, i, j],算法结束。

    • 代码:
    #include <bits/stdc++.h>
    
    using namespace std;
    
    const int N = 105;
    const int INF = 0x3f3f3f3f;
    int dist[N][N];
    
    void Floyd(int n) {
        for (int k = 1; k <= n; k++)
            for (int i = 1; i <= n; i++)
                if (dist[i][k] != INF)
                    for (int j = 1; j <= n; j++)
                        dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
    }
    
    int main() {
        int n, m, x, y, z;
        while (~scanf("%d %d", &n, &m)) {
            memset(dist, 0x3f, sizeof(dist));
            for (int i = 0; i <= n; i++) dist[i][i] = 0;
            for (int i = 0; i < m; i++) {
                scanf("%d %d %d", &x, &y, &z);
                dist[x][y] = min(dist[x][y], z);
            }
            Floyd(n);
            for(int i=1; i<=n; i++) {
                for(int j=1; j<=n; j++) {
                    printf("%d%c", dist[i][j], j==n?'
    ':' ');
                }
            }
        }
        return 0;
    }
    
  • 相关阅读:
    StringTokenizer与indexOf()和substring()
    如何保证session一致性
    用存储过程实现for循环执行sql语句
    python 字符串替换
    python 基础
    执行env.render()渲染环境时报错get_screens raise NotImplementedError('abstract')
    安装TensorFlow的一些问题
    sqoop
    Hadoop序列化与Java序列化
    RPC
  • 原文地址:https://www.cnblogs.com/sdutzxr/p/12986721.html
Copyright © 2011-2022 走看看