首页 > 其他 > 详细

图论:最短路

时间:2020-05-29 12:11:32      阅读:34      评论:0      收藏:0      [点我收藏+]

图论:最短路

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

例题:

  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 ? ‘\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 ? ‘\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 ? ‘\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?‘\n‘:‘ ‘);
            }
        }
    }
    return 0;
}

图论:最短路

原文:https://www.cnblogs.com/sdutzxr/p/12986721.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!