最短路径
一、Bellman-Ford算法
Q:有一张有 n n n 个点、 m m m 条边的有向图,可能存在重边、负边和自环,但不存在负环,求起点 s s s 到每个点的最短路径。
1.1 算法简析
记图为
G
G
G;
G
[
u
]
G[u]
G[u] 表示以
u
u
u 为起点的所有边的集合;
e
(
u
,
v
)
e(u, v)
e(u,v) 表示
u
u
u 到
v
v
v 的某一条有向边(
e
(
u
,
v
)
∈
G
[
u
]
e(u, v)\in G[u]
e(u,v)∈G[u]),
e
(
u
,
v
)
.
w
e(u, v).w
e(u,v).w 为权值;。
设
d
[
u
]
=
d[u] =
d[u]= 从起点
s
s
s 到
u
u
u 的最短路径,则
d [ v ] = m i n ( { d [ u ] + e ( u , v ) . w ∣ e ( u , v ) ∈ G [ u ] } ) d[v] = min(\{d[u] + e(u, v).w | e(u, v)\in G[u]\}) d[v]=min({d[u]+e(u,v).w∣e(u,v)∈G[u]})
注: e ( u , v ) e(u, v) e(u,v) 可能有重边,所以用集合形式表示。
1.2 代码
#define MAX 20 // 最大容纳的点数
#define INF 1e8 // 防止 d[e.from] + e.worth 溢出
// 定义边的数据类型
typedef struct
{
int from, to, worth; // from -- 起点; to -- 终点; worth -- 权值
} edge;
int n, m, s; // n -- 点数; m -- 边数; s -- 起点
vector<edge> E; // 存放边
int d[MAX]; // d[i] -- 从 s 到 i 的最短路径
int pre[MAX]; // 记录前驱点
// 寻找最短路径
void bellman_ford(int s)
{
// 初始化
fill(begin(d), end(d), INF);
d[s] = 0;
// 外层循环至多进行 n - 1 次
for (int i = 0; i < n - 1; i++)
{
bool flag = false; // 初始状态为未松弛
// 遍历每一条边
for (int j = 0; j < m; j++)
{
edge e = E[j];
// 松弛操作
if (d[e.to] > d[e.from] + e.worth) // 松弛条件
{
d[e.to] = d[e.from] + e.worth; // 松弛
pre[e.to] = e.from; // 更新前驱点
flag = true; // 松弛过
}
}
if (!flag) // 若遍历完所有的边都未进行松弛,则已经求出最短路径
break;
}
}
// 打印 s 到 i 的最短路径
void print_path(int i)
{
printf("%d\n", d[i]); // s 到 i 的最短路径
// 记录从 i 到 s 的路径
vector<int> ans;
int p = i;
ans.push_back(p);
while (p != s)
{
p = pre[p];
ans.push_back(p);
}
reverse(ans.begin(), ans.end()); // 逆序路径
printf("%d", ans[0]);
for (int i = 1; i < ans.size(); i++)
printf(" -> %d", ans[i]);
}
1.3 注意点
- 1、定义
INF
时,要防止溢出。
松弛的条件是d[e.to] > d[e.from] + e.worth
,d[e.from]
的值可能仍是INF
。若INF
定义得太大,如#define INF __INT_MAX__
,则d[e.from] + e.worth
可能会溢出(因为int d[MAX]
)。
在数学上,一个有限数加上一个无限数仍为无限大。但在编程中,存在数据类型的制约,溢出后可能变成一个很小的数。
为了防止溢出,可以采取以下三个方法:
// 法一:合理定义 INF
#define INF 1e8
int d[MAX]; // 以 int 为例。若结果很大,则采用 long long
/* ********************************************************************* */
// 法二:将 INF 定义为 __INT_MAX__,修改松弛条件
#define INF __INT_MAX__
int d[MAX]; // 以 int 为例。若结果很大,则采用 long long
...
if (d[e.from] != INF && d[e.to] > d[e.from] + e.worth)
...
/* ********************************************************************* */
// 法三:使 d[] 的类型最大值远大于 INF
#define INF __INT_MAX__
long long d[MAX]; // 适用于结果用 int 就能存储的情况
- 2、外层循环
for (int i = 0; i < n - 1; i++)
,最多进行n - 1
次(n
为点数)。
这是 B e l l m a n − F o r d Bellman-Ford Bellman−Ford 的一个性质,即在不存在负环的条件下,该算法最多进行n - 1
次外循环就可得到最短路径。
利用该性质,可以检测一张图是否存在负环。若存在负环,则不存在最短路径,因为该值可以一直变小。显然,若无限制,会进行第n
次外循环。
// 若图中有负环,则返回 true; 否则,返回 false
bool bellman_ford(int s)
{
// 初始化
fill(begin(d), end(d), 0);
// 若无负环,外层循环至多进行 n - 1 次;若存在负环,万层循环会进行第 n 次
for (int i = 0; i < n; i++)
{
// 遍历每一条边
for (int j = 0; j < m; j++)
{
edge e = E[j];
// 松弛操作
if (d[e.to] > d[e.from] + e.worth) // 松弛条件
{
d[e.to] = d[e.from] + e.worth; // 松弛
// 若 i == n - 1,即进行第 n 次循环,则存在负环
if (i == n - 1)
return true;
}
}
}
return false;
}
- 3、定义
flag
来优化循环。
若不存在负环,可能不需要进行n - 1
次外循环,提前求出最短路径。若在某一次循环中,遍历所有的边,都未进行松弛操作,说明已经求出了最短路径,无需再进行松弛。这时,没有必要再进行循环。 - 4、利用
pre[MAX]
记录最短路径。
我们只需要在每次进行松弛时,更新i
的前驱点pre[i]
。若要打印s
到t
的最短路径,只要从pre[t]
开始,从t
到s
打印路径,最后逆序即可。
二、Dijkstra算法
Q:有一张有 n n n 个点、 m m m 条边的有向图,可能存在重边和自环,但不存在负边和负环,求起点 s s s 到每个点的最短路径。
2.1 算法简析
记图为
G
G
G;
G
[
u
]
G[u]
G[u] 表示以
u
u
u 为起点的所有边的集合;
e
(
u
,
v
)
e(u, v)
e(u,v) 表示
u
u
u 到
v
v
v 的某一条有向边(
e
(
u
,
v
)
∈
G
[
u
]
e(u, v)\in G[u]
e(u,v)∈G[u]),
e
(
u
,
v
)
.
w
e(u, v).w
e(u,v).w 为权值;
在
B
e
l
l
m
a
n
−
F
r
o
d
Bellman-Frod
Bellman−Frod 中,只要满足松弛条件,我们就进行 d[e.to] = d[e.from] + e.worth
。但是,在算法执行的过程中,如果
d
[
e
.
f
r
o
m
]
d[e.from]
d[e.from] 不是 e.from
的最短路径,那么 d[e.to]
肯定也不是正确结果。在这种情况下,进行松弛是没有必要的,除非 d[e.from]
是正确的最短路径。
因此,我们改变策略,只对已经找到最短路径的顶点,以该顶点为起点的边进行松弛。我们的重点是如何确定已经找到最短路径的点。一开始,我们只确定了起点的最短路径
d
[
s
]
=
0
d[s] = 0
d[s]=0。显然,与 s
最接近的点就是我们要找的顶点。
2.2 代码
#define MAX 20 // 点的最大数目
#define INF 1e8 // 比结果大的数
// 定义边
typedef struct
{
int to, worth; // to -- 终点; worth -- 权值
} edge;
// 定义点
typedef struct
{
int id, d; // id -- 编号为 id 的点; d -- s 到 id 的最短路径
} dis;
int n, m, s; // n -- 点的数目; m -- 边的数目; s -- 起点
vector<edge> G[MAX]; // G[i] -- 以 i 为起点的边的集合
int d[MAX]; // d[i] -- s 到 i 的最短路径
int pre[MAX]; // 记录前驱点
// 最小堆的cmp()
struct cmp
{
bool operator()(const dis &a, const dis &b)
{
return a.d > b.d;
}
};
// 求 s 到各点的最短路径
void dijkstra(int s)
{
// 初始化
fill(begin(d), end(d), INF);
d[s] = 0;
priority_queue<dis, vector<dis>, cmp> Q; // Q为最小堆
dis tmp;
tmp = {s, 0};
Q.push(tmp);
while (!Q.empty())
{
dis q = Q.top();
Q.pop();
int u = q.id;
if (d[u] < q.d) // q.d非最短路径,不必进行下一步
continue;
for (int i = 0 ; i < G[u].size(); i++) // 此时d[u]已经为最短路径
{
edge e = G[u][i];
if (d[e.to] > d[u] + e.worth) // 以u为起点进行松弛
{
d[e.to] = d[u] + e.worth;
pre[e.to] = u; // 记录前驱点
tmp = {e.to, d[e.to]};
Q.push(tmp);
}
}
}
}
// 打印 s 到 i 的最短路径
void print_path(int i)
{
printf("%d\n", d[i]); // s 到 i 的最短路径
// 记录从 i 到 s 的路径
vector<int> ans;
int p = i;
ans.push_back(p);
while (p != s)
{
p = pre[p];
ans.push_back(p);
}
reverse(ans.begin(), ans.end()); // 逆序路径
printf("%d", ans[0]);
for (int i = 1; i < ans.size(); i++)
printf(" -> %d", ans[i]);
}
int main()
{
scanf("%d%d%d", &n, &m, &s);
for (int i = 0; i < m; i++)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
edge e = {b, c};
G[a].push_back(e);
}
dijkstra(s);
for (int i = 1; i <= n; i++)
printf("%d ", d[i]);
return 0;
}
三、Floyd-Warshall算法
Q:有一张有 n n n 个点、 m m m 条边的无向图,可能存在重边、负边和自环,但不存在负环,求 i i i 到 j j j 的最短路径。
3.1 算法简析
设
d
[
i
]
[
j
]
=
d[i][j] =
d[i][j]= 从
i
i
i 到
j
j
j 的最短路径,则
d
[
i
]
[
j
]
=
m
i
n
(
d
[
i
]
[
j
]
,
d
[
i
]
[
k
]
+
d
[
k
]
[
j
]
)
d[i][j] = min(d[i][j], d[i][k] + d[k][j])
d[i][j]=min(d[i][j],d[i][k]+d[k][j])
3.2 代码
#define MAX 20
#define INF 1e8
int n, m;
int d[MAX][MAX];
void floyd_warshall(void)
{
// 初始化
// 若i == j, 则d[i][j] = 0; 否则, d[i][j] = INF
for (int i = 0; i < MAX; i++)
for (int j = 0; j < MAX; j++)
if (i == j)
d[i][j] = 0;
else
d[i][j] = INF;
scanf("%d%d", &n, &m);
for (int i = 0; i < m; i++)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
// 处理无向重边,存最短的边
d[a][b] = min(d[a][b], c);
d[b][a] = min(d[b][a], c);
}
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]);
}
注:
- 初始时, d [ i ] [ j ] d[i][j] d[i][j] 为 e ( i , j ) e(i, j) e(i,j) 的权值。若不存在,则 d [ i ] [ j ] = I N F d[i][j] = INF d[i][j]=INF;若 i = = j i == j i==j,则 d [ i ] [ j ] = 0 d[i][j] = 0 d[i][j]=0。
- 外中内三层循环都是
n
次(顶点数)。初始值是0
还是1
取决于顶点编号是从0
还是1
开始。 - 三重循环结束后, d [ i ] [ j ] d[i][j] d[i][j] 即为 i i i 到 j j j 的最短路径;若 d [ i ] [ j ] = I N F d[i][j] = INF d[i][j]=INF,则不存在 i i i 到 j j j 的路径。
完