上一篇 下一篇 回到顶部 目录 分享文章 返回首页
目录

信竞学习笔记:最短路

发表于
更新于
8 6.0~7.7 分钟 2678

1. 最短路

1.1. 概念

给定一个有权图,图中有 n 个点和 m 条边,边的权值可正可负,不一定有向。现在给定起点 s 和终点 t,在 s 到 t 的所有路径中,寻找经过的边的权值之和最小的一条路径,这就是最短路问题。

1.2. 多源最短路和单源最短路

多源最短路,指的是一类可能有多组询问且起点和终点均不固定的最短里问题;单源最短路,指的是一类起点固定但终点不固定的最短路问题。

1.3. 算法

通常求解最短路问题,根据类型和图的特性的不同,会采用不同的算法:

类别

图的特性

算法

时间复杂度

单源最短路

无权图(即边权均为 1)

BFS(广度优先搜索)

O(n+m)O(n+m)(最坏)

单源最短路

有权图,边权非负,稠密图

Dijkstra 朴素算法

O(n2)O(n^2)

单源最短路

有权图,边权非负,稀疏图

Dijkstra 堆优化算法

O((m+n)logn)O((m+n) \log n)

单源最短路

有权图,边权可能有负,一般无负环

SPFA 算法

O(m)O(nm)O(m) \sim O(nm)

多源最短路

有权图,边权可能有负,一般无负环

Floyd-Warshall 算法

O(n3)O(n^3)

2. 多源最短路

2.1. Floyd-Warshall 算法简介

求解多源最短路问题,通常采用 Floyd-Warshall 算法(简称 Floyd 算法),通过基于动态规划的预处理,一次性得到图中任意两个节点的最短路。时间复杂度 O(n3)O(n^3)

2.2. 算法过程

维护一个三维数组 ff,定义 fk,i,jf_{k, i, j} 为只经过点 1k1 \sim k,点 ii 到点 jj 的最短路。第 kk 层的状态从第 k1k-1 层继承而来。若新路径不经过点 kk,则 fk,i,j=fk1,i,jf_{k, i, j} = f_{k-1, i, j};反之,若新路径经过点 kk,则点 ii 到点 jj 的最短路就是点 ii 到点 kk 的最短路加上点 kk 到点 jj 的最短路,随后在两种情况中取较小值,状态转移方程如下:

fk,i,j=min{fk1,i,j,fk1,i,k+fk1,k,j}f_{k, i, j} = \min\{f_{k-1, i, j}, f_{k-1, i, k} + f_{k-1, k, j}\}

若点 ii 和点 jj 有邻边,初始化 f0,i,jf_{0, i, j} 为边长;否则,初始化 f0,i,jf_{0, i, j} 为无穷大。初始化 f0,i,if_{0, i, i}00。为达到初始化目的,可以直接使用邻接矩阵存图。根据上面的状态转移方程求解即可。

2.3. 滚动数组优化

三维数组太费空间了,特别是数据范围较大的时候。我们可以对最外层的 kk 使用滚动数组进行优化,压缩到二维。注意到:

fk,i,k=min{fk1,i,k,fk1,i,k+fk1,k,k}fk,k,j=min{fk1,k,j,fk1,k,k+fk1,k,j}f_{k, i, k} = \min\{f_{k-1, i, k}, f_{k-1, i, k} + f_{k-1, k, k}\}\\ f_{k, k, j} = \min\{f_{k-1, k, j}, f_{k-1, k, k} + f_{k-1, k, j}\}

又:

fk1,k,k=0f_{k-1, k, k} = 0

所以:

fk,i,k=fk1,i,kfk,k,j=fk1,k,jf_{k, i, k} = f_{k-1, i, k}\\ f_{k, k, j} = f_{k-1, k, j}

所以,上述状态转移方程中,每个元素更新所用的值并未发生改变,所以可以省略第一维,此时状态转移方程为:

fi,j=min{fi,j,fi,k+fk,j}f_{i, j} = \min\{f_{i, j}, f_{i, k} + f_{k, j}\}

核心代码:

for (int k=1; k<=n; k++) {
    for (int i=1; i<=n; i++) {
        for (int j=1; j<=n; j++) {
            f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
        }
    }
}

除了多源最短路外,Floyd-Warshall 算法也适适用于传递闭包问题和最小环问题。

2.4. 传递闭包问题

传递闭包问题是离散数学中的概念。给定一个集合 SS 及集合中若干元素之间的传递关系,求所有元素之间的传递关系。在图论中,传递闭包问题被转化为如下的一个问题:给定一个有向图,求所有点对之间的联通性问题。

有了多源最短路问题的铺垫,我们很容易列出如下的状态转移方程:

fi,j=fi,kfk,jf_{i, j} = f_{i, k} \land f_{k, j}

其中 fi,jf_{i,j} 代表从点 ii 出发是否能够到达点 jj\land 是合取(逻辑与)符号。在实际代码中,我们可以先判断 fi,kf_{i, k} 是否为真,若其为假,则无需继续枚举 jj。 核心代码如下:

for (int k=1; k<=n; k++) {
    for (int i=1; i<=n; i++) {
        if (f[i][k])
            for (int j=1; j<=n; j++) {
                if (f[k][j]) f[i][j] = 1;
            }
    }
}

或者也可用位运算进行操作,这里不再叙述。

2.5. 最小环问题

给定一个图,求其中由 n(n>3)n (n > 3)个节点构成的边权最小的环的大小。这便是最小环问题。图的最小环也叫作图的围长。

我们分析:选定最小环上两点 iijj,则该环是由 iijj 之间不经过点 kk 的最短路和经过点 kk 的最短路构成的。记 fk,i,jf_{k, i,j} 为只经过点 1k1 \sim k,点 ii 到点 jj 的最短路,wi,jw_{i, j}iijj 的直接距离,可以得出以下的状态转移方程:

ans=min1kn,1i<k,i+1j<k{fk1,i,j+wi,k+wk,j}ans = \min\limits_{1 \leq k \leq n, 1 \leq i < k, i+1 \leq j < k}\{f_{k-1, i, j} + w_{i, k} + w_{k, j}\}

核心代码请读者自行编写,该问题也可使用滚动数组优化。

3. 单源最短路

3.1. Dijkstra 朴素算法

Dijkstra 算法是求解单源最短路问题最常用的算法,基本思想是贪心。Dijkstra 朴素算法通常用于稠密图,堆优化算法通常用于稀疏图。算法的基本过程是,每次确定一个到源点最近的点 tt,用 tt 更新最短路,重复 nn 次即可。使用邻接矩阵存图。时间复杂度为 O(n2)O(n^2)。核心代码(其中 disidis_i 代表起始点 ss 到点 ii 的最短路长度):

memset(dis, 0x3f, sizeof dis);
dis[s] = 0;

for (int i=1; i<=n; i++) {
    int t = -1;
    for (int j=1; j<=n; j++) {
        if (!vis[j] && (t == -1 || dis[t] > dis[j])) {
            t = j;
        }
    }

    vis[t] = 1;
    for (int j=1; j<=n; j++) {
        dis[j] = min(dis[j], dis[t] + g[t][j]);
    }
}

3.2. Dijkstra 堆优化算法

我们借助堆对上述过程进行优化。首先将起始点压入堆中,随后循环执行下面操作,直到堆空:取出并弹出堆顶 uu,则该点就是上面要找的 tt 点,用点 uu 更新临界点的距离,若更新了就将被更新的点压入到堆中。数据结构定义如下:

struct edge {
    int v, w;
};
struct node {
    int dis, u;
    bool operator<(const node &b) const {
        return dis > b.dis;
    }
};

const int N = 5e5 + 100;
int n, m, s, dis[N];
bool vis[N];
vector<edge> g[N];
priority_queue<node> pq;

核心代码如下:

memset(dis, 0x3f, sizeof dis);
dis[s] = 0;
pq.push({0, s});

while (!pq.empty()) {
    int u = pq.top().u;
    pq.pop();
    if (vis[u]) continue;
    vis[u] = 1;

    for (const auto &p : g[u]) {
        int v = p.v, w = p.w;
        if (dis[v] > dis[u] + w) {
            dis[v] = dis[u] + w;
            pq.push({dis[v], v});
        }
    }
}

时间复杂度 O((m+n)logn)O((m+n) \log n)

3.3. Bellman-Ford 算法

Dijkstra 算法不能处理负权边。为此,我们可以使用 Bellman-Ford 算法。Bellman-Ford 算法是一种基于动态规划和松弛算法的最短路算法。我们对图上的每一条边进行松弛。每进行一轮循环,就对所有边进行一次松弛操作:对于一条边 {u,v,w}\{u, v, w\}disv=min{disv,disu+wu,v}dis_v = \min\{dis_v, dis_u + w_{u, v}\}。而当某一次操作没有进行任何松弛操作时,代表已经求解完成,即可直接停止(类似于冒泡排序的优化)。最坏时间复杂度 O(nm)O(nm)。如果存在负环,松弛操作会一直进行,所以该算法可以用来判断负环。请读者自行完成代码。

3.4. SPFA 算法

SPFA 算法(Shortest Path Faster Algorithm)是使用队列优化的 Bellman-Ford 算法,其优化步骤在于:只有上一次被松弛操作所连接而影响到的边,才有可能引起松弛操作,于是我们可以用队列来维护节点。核心代码如下:

memset(dis, 0x3f, sizeof dis);
vis[1] = 1;
dis[1] = 0;
q.push(1);

while (!q.empty()) {
    int u = q.front();
    q.pop();
    vis[u] = 0;
    for (const edge &i : g[u]) {
        int v = i.v, l = i.l;
        if (dis[v] > dis[u] + l) {
            dis[v] = dis[u] + l;
            vis[v] = 1;
            q.push(v);
        }
    }
}

4. 总结

最短路算法是图论中一类常用的算法,在 GESP 七 ~ 八级以及 NOI 系列赛事中也有涉及。学习最短路算法,能够更好地帮助我们完成图论相关题目。