ACM - 最短路 - CodeForces 295B Greg and Graph
CodeForces 295B Greg and Graph
题解
算法是一种基于动态规划的算法,以此题为例介绍最短路算法中的 算法。
我们考虑给定一个图,要找出 号点到 号点的最短路径。则该最短路径只有两种可能:
-
号点直接到达 号点的路径中产生最短路径
-
号点经过一些中间点到达 号点的路径中产生最短路径
我们添加一个点 ,使得 号点到 号点再添加后产生的最短路径比添加前的最短路径更短,则称该操作为松弛。
下面我们以从一个示例开始介绍算法的步骤。
示例
给定一个赋权有向图,如下图:
我们声明一个集合 ,并称该集合为中转集合。
我们声明一个 数组,其中 表示从源点 号点只经过中转集合中的点,到达目标点 号点的最短路径。
每一轮更新都更新 ,直到集合 为图的顶点集 。
初始化更新
初始化为:
数组根据我们的定义,此时 表示不经过图中的任何顶点,直接从源点 号点到达 号点的最短距离,为了方便书写,我们此处采用图的邻接矩阵表示。
故 被初始化为:
第一轮更新
更新为:
数组根据我们的定义,此时 表示由图中 号点中转,直接从源点 号点到达 号点的最短距离。该最短路径有两种可能:
- 最短路径经过 号点;
- 最短路径不经过 号点;
对于第 种情况说明 号点可以松弛之前的最短距离。这两种情况可以统一表示为下式:
此处我们应该注意一下更新的顺序问题,上式表达的含义应该是指只由上一轮的 、 和 来更新出这一轮的 。更准确地说应该可以扩展下数组 的维度。 如下图所示:
其中 表示这是第 轮更新。按我们的意思,应该写成:
即只由第 轮的 、 和 来更新出第 轮的 。那我们为什么可以写成式 的样子?原因在于 和 在此轮更新循环中,始终都没有被更新。后面会更详细地说明这个问题。
故 被更新为:
第二轮更新
更新为:
数组根据我们的定义,此时 表示从源点 号点出发,只经过中转集合 中的点,到达 号点的最短距离。该最短路径有两种可能:
- 最短路径经过 号点;
- 最短路径不经过 号点;
对于第 种情况说明 号点可以松弛之前的最短距离。这两种情况可以统一表示为下式:
我们接着就要问了,为什么经过 号点的最短路径距离等于 。在第一轮更新的时候容易想到,但这轮更新(第二轮更新)中,该问题需要仔细思考一下。
经过 号点的路径这样表示:(源点) (目标点)。而经过 号点的最短路径一定是由 到 的最短路径和 到 的最短路径构成(反证法即可得)。
我们应用这个性质,重新表述为:经过 号点的从源点 出发,由 集合中转到达目标点 的最短路径一定是由从源点 出发,由 集合中转到达目标点 的最短路径和从源点 出发,由 集合中转到达目标点 的最短路径构成(正确性显然,只是由原来的范围全路径集合缩小为 集合中转的那些路径构成的集合)。
而此轮更新仍然不会改变 和 的值,因此不用关心更新顺序的问题。
综上,上式完全正确。故 应该被更新为:
第三轮更新
更新为:
状态转移方程为:
状态转移方程的正确性证明完全同上,而由归纳法容易知道对之后的更新该状态转移方程仍然正确。而该轮仍然不会改变 和 的值,因此不用关心更新顺序的问题。
故 应该被更新为:
第四轮更新
被更新为:
被更新为:
第五轮更新
被更新为:
被更新为:
此时 集合等于图的顶点集 。停止迭代。
最后得到的 就表示源点 到达目标点 的最短路径距离。
程序设计
为了能清晰严谨的说明,我们可以将 设成 。每次循环用 来更新 (即用上一层的 来更新这一层的 )。但实际上,我们发现可以通过减少一个维度来减小空间复杂度,虽然这时我们要注意更新顺序的问题,但可以进一步发现每次迭代 和 都没有发生被更新为新值,即对应的 的第 行和第 列都没有发生变化,这可以使得更新顺序可以任意。
如下图展示了在某一次迭代中更新 的过程,对应的公式描述为:
上图表示那次迭代过程中,第 行和第 列的值都没有被更新为新值,正是由此,明显可以看出 的更新顺序确实可以任意。
程序的主要部分可以写出来:
// n : 表示图的顶点数
for (int k = 0; k < n; ++k) {
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}
}
}
最短路算法对比
算法 | - | ||
---|---|---|---|
空间复杂度 | |||
时间复杂度 | 看具体实现 | ||
负权边时是否可以处理 | 可以 | 不能 | 可以 |
判断是否存在负权回路 | 不能 | 不能 | 可以 |
其中 表示图的顶点数, 表示图的边数。
运用 算法解决该题
题目大意:每次删去一个点,求当前删去该点之前,所有点对最短路径之和。
在每删除一个点前,题目明显要求求出的最短路径不是单源,而是多源的。具体地说,第一,针对所有当前存在的点对;第二,对每一对点对求最短路径。
每次删去一个点,直到把所有点都删尽。倒过来就相当于每次加入一个点,而恰好 算法的建立过程中 数组便记录了这些信息,所以逆序使用 算法即可。
程序:
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
typedef pair<int, int> PII;
#define LL long long
int n, m;
int graph[505][505]; // 邻接矩阵存图
int del[505]; // 删除点的序号
LL sum[505]; // 删除点对前,点对的最短距离和
int dist[505][505];
int main()
{
// 输入图的邻接矩阵
cin >> n;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
cin >> m;
graph[i][j] = m;
}
}
// 输入图的顶点删除顺序
for (int i = 1; i <= n; ++i) cin >> del[i];
// 初始化 dist 数组
memset(dist, 0x3f, sizeof(dist));
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
dist[i][j] = graph[i][j];
}
}
// 根据删除的点的序号,我们逆向添加点,运行Floyd算法
int add[505];
memset(add, 0, sizeof(add));
for (int k = n; k >= 1; --k) {
add[del[k]] = 1;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
dist[i][j] = min(dist[i][j], dist[i][del[k]] + dist[del[k]][j]);
}
}
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
if (add[i] && add[j]) sum[k] += dist[i][j];
}
}
}
for (int k = 1; k <= n; ++k) printf("%I64d ", sum[k]);
printf("\n");
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现