最短路径问题
一、Bellman-Ford算法
Q:有一张有 \(n\) 个点、\(m\) 条边的有向图,可能存在重边、负边和自环,但不存在负环,求起点 \(s\) 到每个点的最短路径。
1.1 算法简析
记图为 \(G\);\(G[u]\) 表示以 \(u\) 为起点的所有边的集合;\(e(u, v)\) 表示 \(u\) 到 \(v\) 的某一条有向边(\(e(u, v)\in G[u]\)),\(e(u, v).w\) 为权值;。
设 \(d[u] =\) 从起点 \(s\) 到 \(u\) 的最短路径,则
注:\(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
为点数)。
这是 \(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\) 个点、\(m\) 条边的有向图,可能存在重边和自环,但不存在负边和负环,求起点 \(s\) 到每个点的最短路径。
2.1 算法简析
记图为 \(G\);\(G[u]\) 表示以 \(u\) 为起点的所有边的集合;\(e(u, v)\) 表示 \(u\) 到 \(v\) 的某一条有向边(\(e(u, v)\in G[u]\)),\(e(u, v).w\) 为权值;
在 \(Bellman-Frod\) 中,只要满足松弛条件,我们就进行 d[e.to] = d[e.from] + e.worth
。但是,在算法执行的过程中,如果 \(d[e.from]\) 不是 e.from
的最短路径,那么 d[e.to]
肯定也不是正确结果。在这种情况下,进行松弛是没有必要的,除非 d[e.from]
是正确的最短路径。
因此,我们改变策略,只对已经找到最短路径的顶点,以该顶点为起点的边进行松弛。我们的重点是如何确定已经找到最短路径的点。一开始,我们只确定了起点的最短路径 \(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\) 个点、\(m\) 条边的无向图,可能存在重边、负边和自环,但不存在负环,求 \(i\) 到 \(j\) 的最短路径。
3.1 算法简析
设 \(d[i][j] =\) 从 \(i\) 到 \(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]\) 为 \(e(i, j)\) 的权值。若不存在,则 \(d[i][j] = INF\);若 \(i == j\),则 \(d[i][j] = 0\)。
- 外中内三层循环都是
n
次(顶点数)。初始值是0
还是1
取决于顶点编号是从0
还是1
开始。 - 三重循环结束后,\(d[i][j]\) 即为 \(i\) 到 \(j\) 的最短路径;若 \(d[i][j] = INF\),则不存在 \(i\) 到 \(j\) 的路径。
完
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)