【总结】最短路

Floyd

  • Floyd 是最简单的一种求最短路的算法,用于计算任意两点间的最短路。其时间复杂度为 \(\Theta(n ^ 3)\), 并且它适用于负权图。

算法描述:

  • 初始化
    1. 若两点 \(u\)\(v\) 有一条边,则 \(dis_{u,v} = w_{u,v}\)
    2. 否则将其赋值为极大值,一边用 0x3f3f3f3f
for (int k = 1; k <= n; k++)
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            if (d[i][j] > d[i][k] + d[k][j])
                d[i][j] = d[i][k] + d[k][j];

跑完一遍 Floyd 我们就可以得到的 \(dis_{u,v}\) 就是 \(u\), \(v\) 两点的最短路。

为什么 \(k\) 一定要放在最外层?

由于 Floyd 是采用了动态规划的思想。

由于决策需要枚举中转点,我们可以选择以中转点集为阶段。

\(F_{k, i, j}\) 表示 可以经过标号小于等于 \(k\) 的点中转点\(i\)\(j\) 的最短路。

\(F_{0, i, j}=W_{i, j}\)\(W\) 为邻接矩阵。

\(F_{k, i, j}=\min(F_{k - 1, i, j} , F_{k - 1, i, k} + F_{k - 1, k, j})\)

由于 \(k\)DP 的阶段,所以 \(k\) 所在的循环必须要放在最外层。

输出路径

由于我们定义状态 \(dis_{i, j}\), 所以我们选择定义二维数组 \(pre_{i, j}\) 表示 \(i\)\(j\) 的最短路中 \(j\) 的前驱。

\(pre_{i, j}\) 就可以在 Floyd 和 加边时进行赋值:

//Floyd
for (int k = 1; k <= n; k++)
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            if (dis[i][j] > dis[i][k] + dis[k][j]) {
                dis[i][j] = dis[i][k] + dis[k][j];
                pre[i][j] = pre[k][j];
            }
//加边时的初始化
for (int i = 1; i <= m; i++) {
    scanf("%d %d", &x, &y);
    dis[x][y] = 1;
    pre[x][y] = x;
}

例题

这应该算是 Floyd 的模板了,但是这道题要求我们输出案, 这也是比较简单的。但是这题没有 SPJ, 我们可以发现题目中要求了 请输出字典序最小的一组解

所以我们在记录 \(pre\) 数组时,必须最小值,来保证输出的是字典序最小的一组解。并且 \(pre_{i, j}\) 记录的不是 \(j\) 的前驱,而是 \(i\) 的后继。

代码如下:

void Floyd() {
	for (int k = 1; k <= n; k++)
		for (int i = 1; i <= n; i++)
			for (int j = 1; j <= n; j++) {
				if (dis[i][j] > dis[i][k] + dis[k][j]) {
					dis[i][j] = dis[i][k] + dis[k][j];
					pre[i][j] = pre[i][k];
				}
				else if (dis[i][j] == dis[i][k] + dis[k][j] && i != k && pre[i][k] < pre[i][j])
					pre[i][j] = pre[i][k];
			} 
}
void print(int x) {
	if (x == t)
		return ;
	printf("%d ", x);
	if (!pre[x][t] || pre[x][t] == t)
		return ;
	print(pre[x][t]);
}

题意:输出图的一个最小环

我们可以看下面这个例子:

我们很快可以找出它的最小环 1, 2, 4, 5;

我们去掉其他的边在进行观察:

我们不难得出:图中的一个环由一条链,再由另外的一个点链接链的首尾组成。

所以我们就不难写出主要的代码

void Floyd() {
	for (int k = 1; k <= n; k++) {
		for (int i = 1; i < k; i++)
			for (int j = i + 1; j < k; j++)
				if (ans > map[i][k] + map[k][j] + d[i][j]) {
					ans = map[i][k] + map[k][j] + d[i][j];
					cnt = 0;
					int t = j;
					while (t != i && t) {
						path[++cnt] = t;
						t = pre[i][t];
					}
					path[++cnt] = i, path[++cnt] = k;
				}
        for (int i = 1; i <= n; i++)
			for (int j = 1; j <= n; j++)
				if (d[i][j] > d[i][k] + d[k][j]) {
					d[i][j] = d[i][k] + d[k][j];
					pre[i][j] = pre[k][j];
				}
    }
}

Dijkstra

介绍

Dijkstra 算法是基于贪心思想的单源最路算法,可以计算起点到其他点的最短路,但是无法处理负边权;

松弛操作

我们首先来了解一个新的概念:松弛

当前 \(1\)\(2\) 的最短路是 \(1 -> 2\)

假设这条边是一条橡皮筋,我们将它放在点 \(3\), 变成下面这个样子。

我们可以发现 \(1 -> 3 -> 2\) 的路径更短,也就是说橡皮筋更加的 松弛(虽然不满足三角形三边关系)

算法实现

定义 \(dis_i\) 表示起点 \(s\) 到点 \(i\) 的最短路长度。

Dijkstra 算法的核心便在于松弛。将点分为蓝白两大阵营,蓝阵营中是已经经过松弛操作确定最短路的点,而白阵营则是没有的。

  • 步骤如下
  1. 在没有被访问过的点中找一个相邻顶点 \(k\),使得 \(dis_k\) 是最小的。
  2. \(k\) 放进蓝阵营中,即 \(vis_k = 1\)
  3. 更新与 \(k\) 相连的每个未确定最短路径(白)的顶点 \(v\),进行松弛操作。
  • 具体实现
struct Node {
    	int e, d;
	bool operator < (const Node o) const{
		return d > o.d;
	}
};
std::priority_queue<Node> q;
void add (int x, int y, int z) {
	next[++tot] = head[x], head[x] = tot, ver[tot] = y, edge[tot] = z;
} 
void Dijkstra() {
	for (int i = 1; i <= n; i++)
		dis[i] = 1e9;
	q.push({x, 0});
	dis[x] = 0;
	while (q.size()) {
		x = q.top().e;
		q.pop();
		if (vis[x])
			continue;
		vis[x] = 1;
		for (int i = head[x]; i; i = next[i]) {
			int y = ver[i], z = edge[i];
			if (dis[y] > dis[x] + z) {
				dis[y] = dis[x] + z;
				q.push({y, dis[y]});
			}
		}
	}
}

例子

题意:求 \(1\)\(n\) 的最短路。

我们学了 Dijkstra 之后这题就非常简单了,就是一道模板题。

#include <cstdio>
#include <queue>
const int MAXN = 2005, MAXM = 20005;
int n, m;
int head[MAXN], next[MAXM], ver[MAXM], edge[MAXM], tot;
int dis[MAXN];
bool vis[MAXN];
struct Node {
	int e, d;
	bool operator < (const Node o) const{
		return d > o.d;
	}
};
std::priority_queue<Node> q;
void add (int x, int y, int z) {
	next[++tot] = head[x], head[x] = tot, ver[tot] = y, edge[tot] = z;
} 
void Dijkstra() {
	for (int i = 1; i <= n; i++)
		dis[i] = 1e9;
	q.push({1, 0});
	dis[1] = 0;
	while (q.size()) {
		int x = q.top().e;
		q.pop();
		if (vis[x])
			continue;
		vis[x] = 1;
		for (int i = head[x]; i; i = next[i]) {
			int y = ver[i], z = edge[i];
			if (dis[y] > dis[x] + z) {
				dis[y] = dis[x] + z;
				q.push({y, dis[y]});
			}
		}
	}
}
int main () {
	scanf("%d %d", &n, &m);
	for (int i = 1; i <= m; i++) {
		int x, y, z;
		scanf("%d %d %d", &x, &y, &z);
		add(x, y, z), add(y, x, z);
	}
	Dijkstra();
	printf("%d", dis[n]);
}

Bellman-Ford

介绍

  • Bellman-Ford 算法:对每条边执行更新,迭代 \(n - 1\) 次。
  • 具体操作是对图进行最多 \(n - 1\) 次松弛操作,每次操作对所有的边进行松弛,为什么是 \(n - 1\) 次操作呢?这是因为我们输入的边不一定是按源点由近至远,万一是由远至近最坏情况就得 \(n - 1\)
  • 可以应用于负权有向图,但对于负权无向图,它也没有办法。

具体实现

void Bellman_Ford() {
	for (int i = 1; i <= n; i++)
		dis[i] = INF;
	dis[s] = 0;
	for (int i = 1; i < n; i++)
		for (int j = 1; j <= m; j++)
			if (dis[x[j]] + w[j] < dis[y[j]])
				dis[y[j]] = dis[x[j]] + w[j];
}

如何判断负环

如果进行了 \(n - 1\) 轮迭代后,应该所有点都是最优,但是如果有两点依旧可以松弛,那么就有负环。

for (int i = 1; i <= m; i++)
		if (dis[x[i]] + w[i] < dis[y[i]])
			flag = 0;

这个算法其实在 OI 中也没有什么用。

SPFA

在 Bellman-ford 算法中,有许多松弛是无效的。SPFA 算法先将源点加入队列。然后从队列中取出一个点,对该点的邻接点进行松弛,如果该邻接点松弛成功且不在队列中,则把该点加入队列。如此循环往复,直到队列为空,则求出了最短路径。

void spfa() {
	for (int i = 1; i <= n; i++)
		dis[i] = 1e9;
	q.push(x);
	dis[x] = 0;
	while (q.size()) {
		int x = q.front();
		q.pop();
		vis[x] = 0;
		for (int i = head[x]; i; i = nxt[i]) {
			int y = ver[i], z = edge[i];
			if (dis[y] > dis[x] + z) {
				dis[y] = dis[x] + z;
				pre[y] = x;
				if (!vis[y]) {
					vis[y] = 1;
					cnt[y]++;
					if (cnt[y] >= n) {
						printf("-1");
						exit(0);
					}
					q.push(y);
				}
			} else if (dis[y] == dis[x] + z)
				pre[y] = min(pre[y], x);
		}
	}
}

应用

这里我只选择几道有 价值 的题。只是应为我懒

H 城是一个旅游胜地,每年都有成千上万的人前来观光。为方便游客,巴士公司在各个旅游景点及宾馆,饭店等地都设置了巴士站并开通了一些单程巴士线路。每条单程巴士线路从某个巴士站出发,依次途经若干个巴士站,最终到达终点巴士站。
一名旅客最近到 H 城旅游,他很想去S公园游玩,但如果从他所在的饭店没有一路巴士可以直接到达 S 公园,则他可能要先乘某一路巴士坐几站,再下来换乘同一站台的另一路巴士, 这样换乘几次后到达 S 公园。
现在用整数 1 , 2 , … N 1,2,…N1,2,…N 给 H 城的所有的巴士站编号,约定这名旅客所在饭店的巴士站编号为 1 , S 公园巴士站的编号为 N 。
写一个程序,帮助这名旅客寻找一个最优乘车方案,使他在从饭店乘车到 S 公园的过程中换车的次数最少。

很明显在同一线路上的任意两点之间的边权为 \(0\),即设有线路 \(A\) 外一点 \(P\),且 \(P\) 所在线路与线路 \(A\) 有交点,则该点到线路 \(A\) 上交点的边权为 \(1\),进而到达线路 \(A\) 上任意一点边权皆为 \(1\)

while (m--) {
	int x;
	char ch;
	v.clear();
	do {
		scanf("%d", &x);
		v.push_back(x);
	} while(getchar() == ' ');
	for (int i = 0; i < v.size(); i++)
		for (int j = i + 1; j < v.size(); j++)
			h[v[i]][v[j]] = 1;
}
posted @ 2022-07-23 16:18  zhou_ziyi  阅读(36)  评论(0)    收藏  举报