最短路问题前提:图中一定不存在负环
Floyd原理
集合
d
(
k
,
i
,
j
)
d(k,i,j)
d(k,i,j):所有从
i
i
i出发,最终走到
j
j
j,且中间只经过节点编号不超过
k
k
k的所有路径长度的最小值
状态计算
根据第
k
k
k个结点是否在路径中
- 所有不包含结点 k k k的路径: d ( k − 1 , i , j ) d(k-1,i,j) d(k−1,i,j)
- 所有包含结点 k k k的路径: d ( k − 1 , i , k ) + d ( k − 1 , k , j ) d(k-1,i,k)+d(k-1,k,j) d(k−1,i,k)+d(k−1,k,j)
故有: d ( k , i , j ) = m i n { d ( k − 1 , i , k ) + d ( k − 1 , k , j ) , d ( k − 1 , i , j ) } d(k,i,j)=min \{d(k-1,i,k)+d(k-1,k,j), d(k-1,i,j) \} d(k,i,j)=min{d(k−1,i,k)+d(k−1,k,j),d(k−1,i,j)}
优化后有: d ( i , j ) = m i n { d ( i , k ) + d ( k , j ) , d ( i , j ) } d(i,j)=min \{d(i,k)+d(k,j), d(i,j) \} d(i,j)=min{d(i,k)+d(k,j),d(i,j)}
Floyd应用
- 最短路
- 传递闭包
- 找最小环
- 恰好经过
K
条边的最短路(倍增)
0、FLoyd算法基础模型
使用前提:图中一定不能包含负环!最短距离会变成-∞
!
时间复杂度: O(n^3)
算法思路:
d[i][j]
:存储所有的边
d[k, i, j]
表示从i
只经过1
到k
这些中间结点到达j
的最短距离,基于动态规划思想,状态转移方程为:d[k, i, j] = d[k-1, i, k] + d[k-1, k, j]
,表示加上第k
个结点之后,i
到j
的距离就表示min(i到j的距离, i->k + k->j 的距离)
。第一维优化后便是:d[i][j] = min(d[i][j], d[i][k] + d[k][j])
。
for (k = 1; k <= n; k ++ )
for (i = 1; i <= n; i ++ )
for (j = 1; j <= n; j ++ )
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
循环之后,d[i][j]
存储的是从i
到j
的最短路径长度。
1、Floyd求最短路
注: 由于本题存在重边和自环,处理方法是
- 对于重边,初始化的时候保留最小边即可;
- 对于自环直接删除即可,即
d[i][i] = 0
。
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 210, INF = 1e9;
int n, m, Q;
int d[N][N];
void floyd() {
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
int main() {
scanf("%d%d%d", &n, &m, &Q);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
if (i == j) d[i][j] = 0; // 对角线上为0(自环)
else d[i][j] = INF; // 其余是正无穷
while (m--) {
int a, b, c; scanf("%d%d%d", &a, &b, &c);
d[a][b] = min(d[a][b], c); // 多条边保留最小边
}
floyd();
while (Q--) {
int a, b; scanf("%d%d", &a, &b);
if (d[a][b] > INF / 2) puts("impossible");
else printf("%d\n", d[a][b]);
}
return 0;
}
2、牛的旅行 (最短路)
- 牧区:节点
- 牧场:节点的连通块
- 牧场直径:连通块中最远的两个节点的距离
分析
假设牧场1的直径为
d
1
d_1
d1,牧场2的直径为
d
2
d_2
d2,将两个牧场连接起来后整个牧场的直径为
d
d
d:
- 若 d ≥ m a x ( d 1 , d 2 ) d \ge max(d_1, d_2) d≥max(d1,d2),即将两个牧场连接之后对其每个牧场内部的直径没有影响,所以整个牧场的最小值一定 ≥ \ge ≥ 所有连通块直径的最大值;
- 如何出 1 1 1中的直径:应该在牧场1、牧场2中取距离连线端点最远的点,加上这根连线所构成的直径;
求解
- 用
Floyd
算法求出任意两点之间的最短距离; - 求 m a x d [ i ] maxd[i] maxd[i],表示和 i i i联通的且距离 i i i最远的点的距离;
-
- 情况1:连通块内部最大值 m a x d [ i ] maxd[i] maxd[i];
- 情况2:连通块之间,枚举在与 i i i不连通的点 j j j,并求出 i 、 j i、j i、j之间连边长度。 i , j i, j i,j需要满足 d [ i , j ] = I N F d[i,j]=INF d[i,j]=INF;直径 = m a x d [ i ] + d i s t [ i , j ] + m a x d [ j ] =maxd[i] + dist[i, j] + maxd[j] =maxd[i]+dist[i,j]+maxd[j];
- 对上面两种情况取 m a x max max;
- 对所有方案的直径取 m i n min min。
#include <iostream>
#include <algorithm>
#include <cmath>
#define x first
#define y second
using namespace std;
typedef pair<double, double> PDD;
const int N = 155;
const double INF = 1e20;
int n;
PDD q[N]; // 坐标
double d[N][N]; // 距离
double maxd[N];
char g[N][N]; // 矩阵
double get_dist(PDD a, PDD b) {
double dx = a.x - b.x;
double dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
int main() {
cin >> n;
for (int i = 0; i < n; i++) cin >> q[i].x >> q[i].y;
for (int i = 0; i < n; i++) cin >> g[i];
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
if (i == j) d[i][j] = 0; // 初始化
else if (g[i][j] == '1') d[i][j] = get_dist(q[i], q[j]); // 有边
else d[i][j] = INF; // 无边
for (int k = 0; k < n; k++)
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
double r1 = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++)
if (d[i][j] < INF / 2) // 连通
maxd[i] = max(maxd[i], d[i][j]);
r1 = max(r1, maxd[i]);
}
double r2 = INF;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
if (d[i][j] > INF / 2) // 不连通
r2 = min(r2, maxd[i] + maxd[j] + get_dist(q[i], q[j]));
printf("%.6lf\n", max(r1, r2));
return 0;
}
3、排序 (传递闭包)
传递闭包
将图中所有能够间接到达的点都连上一条边,这样的图称为传递闭包。比如图中存在三个点a、b、c
,且a → b、b → c
,那么可以添加连线a → c
。
Floyd
算法可以时间复杂度为
O
(
n
3
)
O(n^3)
O(n3)内,将一个图变成传递闭包。比如:假设存在无权图
g
(
i
,
j
)
g(i,j)
g(i,j),规定图中
{
g
(
i
,
j
)
=
1
,
i
→
j
存在边
g
(
i
,
j
)
=
0
,
i
→
j
不存在边
\begin{cases} g(i,j)=1, \quad i → j存在边\\ g(i,j)=0, \quad i → j不存在边\\ \end{cases}
{g(i,j)=1,i→j存在边g(i,j)=0,i→j不存在边将图
g
(
i
,
j
)
g(i,j)
g(i,j)变成传递闭包,计算步骤:
- 初始化: d ( i , j ) = g ( i , j ) d(i,j) = g(i,j) d(i,j)=g(i,j)
- 遍历图
d
(
i
,
j
)
d(i,j)
d(i,j)
for k for i for j if (d(i,k) && d(k,j)) d(i,j) = 1
本题思路
先求闭包,然后判断
t
y
p
e
type
type:
- 矛盾 t y p e = 2 type = 2 type=2: d ( i , i ) = 1 d(i,i) = 1 d(i,i)=1,结束当前循环
- 唯一确定 t y p e = 1 type = 1 type=1:当 i ≠ j i \ne j i=j时, d ( i , j ) 、 d ( j , i ) d(i,j)、d(j,i) d(i,j)、d(j,i)当中必有一个为 1 1 1,结束当前循环
- 没有矛盾,也不能唯一确定,即顺序不唯一, t y p e = 0 type = 0 type=0 继续执行
若唯一确定,如何排序?找到当前没有被标记的数(即为最小数),标记并输出。
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 26;
int n, m;
bool g[N][N], d[N][N]; // 边g,传递闭包d
bool st[N];
void floyd() {
memcpy(d, g, sizeof d);
for (int k = 0; k < n; k++)
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
d[i][j] |= d[i][k] && d[k][j];
}
int check() {
for (int i = 0; i < n; i++) // 判断是否存在矛盾
if (d[i][i]) return 2;
for (int i = 0; i < n; i++) // 能否唯一确定
for (int j = 0; j < i; j++)
if (!d[i][j] && !d[j][i]) return 0;
return 1;
}
char get_min() {
for (int i = 0; i < n; i++)
if (!st[i]) {
bool flag = true;
for (int j = 0; j < n; j++) // 是否没有任何一个元素小于它
if (!st[j] && d[j][i]) {
flag = false; break;
}
if (flag) {
st[i] = true; return 'A' + i;
}
}
}
int main() {
while (cin >> n >> m, n || m) {
memset(g, 0, sizeof g);
int type = 0, t;
for (int i = 1; i <= m; i++) {
char str[5];
cin >> str;
int a = str[0] - 'A', b = str[2] - 'A';
if (!type) { // 结果不确定
g[a][b] = 1;
floyd();
type = check();
if (type) t = i;
}
}
if (!type) puts("Sorted sequence cannot be determined.");
else if (type == 2) printf("Inconsistency found after %d relations.\n", t);
else {
memset(st, 0, sizeof st);
printf("Sorted sequence determined after %d relations: ", t);
for (int i = 0; i < n; i++) // 从小到大输出每个字符
printf("%c", get_min());
printf(".\n");
}
}
return 0;
}
4、观光之旅 (最小环问题)
FLoyd很容易找到包含两个点的环。
将所环按环上编号最大的点来分类,可以分成n
类,我们只需要求出每一类,记为第
k
k
k类的最小值,最后取一个min
即可。
对于每一类,我们可以枚举所有的
(
i
,
j
)
(i,j)
(i,j),每一个环可以表示成如下图。又因为
(
i
,
k
)
、
(
k
,
j
)
(i,k)、(k,j)
(i,k)、(k,j)两条边权重是已知的,分别为
w
(
i
,
k
)
、
w
(
k
,
j
)
w(i,k)、w(k,j)
w(i,k)、w(k,j)。而
(
i
,
j
)
(i,j)
(i,j)只能经过编号为
[
1
,
k
−
1
]
[1,k-1]
[1,k−1]的结点,即为
d
(
i
,
j
)
d(i,j)
d(i,j),第
k
k
k类的最小值为
d
(
i
,
j
)
+
w
(
i
,
k
)
+
w
(
k
,
j
)
d(i,j)+w(i,k)+w(k,j)
d(i,j)+w(i,k)+w(k,j)。
最后将每一类
k
∈
[
1
,
n
]
k \in[1,n]
k∈[1,n]取一个
m
i
n
min
min即可。
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110, INF = 0x3f3f3f3f;
int n, m; // 点数和边数
int d[N][N], g[N][N]; // d:floyd数组 g:每条边的长度
int pos[N][N]; // 记录每个点从哪一个状态转移过来
int path[N], cnt; // 当前最小环的方案,cnt方案数量
// 从i走到j的路径
void get_path(int i, int j) {
int k = pos[i][j];
if (k == 0) return; //如果是0,说明i,j之间不经过除i,j之外的其他点
get_path(i, k); //i->newk
path[cnt++] = k;
get_path(k, j); //newk->j
}
int main() {
cin >> n >> m;
memset(g, 0x3f, sizeof g);
for (int i = 1; i <= n; i++) g[i][i] = 0;
while (m--) {
int a, b, c; cin >> a >> b >> c;
g[a][b] = g[b][a] = min(g[a][b], c);
}
int res = INF;
memcpy(d, g, sizeof d);
for (int k = 1; k <= n; k++) {
// 求最小环
for (int i = 1; i < k; i++) // k是环上最大的点,只需要枚举到k即可
for (int j = i + 1; j < k; j++) // 图是无向图,i、j互换对图没影响,只需枚举一半
if ((long long) d[i][j] + g[j][k] + g[k][i] < res) {
res = d[i][j] + g[j][k] + g[k][i];
cnt = 0;
path[cnt++] = k; path[cnt++] = i;
get_path(i, j); // k→i→j→k
path[cnt++] = j;
}
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];
pos[i][j] = k;
}
}
if (res == INF) puts("No solution.");
else {
for (int i = 0; i < cnt; i++) cout << path[i] << ' ';
cout << endl;
}
return 0;
}
5、牛站 (恰好经过N条边的最短路)
基于倍增思想
d ( k , i , j ) d(k,i,j) d(k,i,j):从 i i i到 j j j,恰好经过 k k k条边的最短路径
d ( a + b , i , j ) d(a+b,i,j) d(a+b,i,j):从 i i i到 j j j,恰好经过 a + b a+b a+b条边的最短路径,枚举中间点 k ∈ [ 1 , n ] k \in [1, n] k∈[1,n],表示从 i i i出发恰好经过 a a a条边之后的点k,然后在恰好经过 b b b条边到 j j j,则有 d ( a + b , i , j ) = m i n ( d ( a , i , k ) , d ( b , k , j ) ) d(a+b,i,j)=min(d(a,i,k),d(b,k,j)) d(a+b,i,j)=min(d(a,i,k),d(b,k,j))显然前面 a a a条边和后面 b b b条边无关联,所以要去取最短路径就需要在前后分别取最小值,最后对 k k k求取遍 k ∈ [ 1 , n ] k \in [1,n] k∈[1,n]的最小值即可。
#include <cstring>
#include <iostream>
#include <algorithm>
#include <map>
using namespace std;
const int N = 210;
int k, n, m, S, E;
int g[N][N]; // 每两个点之间的距离
int res[N][N];
// 变种FLoyd算法
void mul(int c[][N], int a[][N], int b[][N]) {
static int temp[N][N]; // 防止c与a、b冲突
memset(temp, 0x3f, sizeof temp);
for (int k = 1; k <= n; k++)
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
temp[i][j] = min(temp[i][j], a[i][k] + b[k][j]);
memcpy(c, temp, sizeof temp);
}
void qmi() {
memset(res, 0x3f, sizeof res);
for (int i = 1; i <= n; i++) res[i][i] = 0; // 初始化
while (k) {
if (k & 1) mul(res, res, g); // res = res * g
mul(g, g, g); // g = g * g
k >>= 1;
}
}
int main() {
cin >> k >> m >> S >> E;
memset(g, 0x3f, sizeof g);
map<int, int> ids; // 离散化:存储当前已经存在的所有点
if (!ids.count(S)) ids[S] = ++n;
if (!ids.count(E)) ids[E] = ++n;
S = ids[S], E = ids[E];
while (m--) {
int a, b, c;
cin >> c >> a >> b;
if (!ids.count(a)) ids[a] = ++n; // 如果a不存在,给予一个唯一编号
if (!ids.count(b)) ids[b] = ++n; // 如果b不存在,给予一个唯一编号
a = ids[a], b = ids[b]; // 将a、b变成离散化之后的结果
g[a][b] = g[b][a] = min(g[a][b], c);
}
// 快速幂
qmi();
cout << res[S][E] << endl;
return 0;
}