47. 参加科学大会(第六期模拟笔试)
代码随想录
卡码网无难度标识
思路:(摘录修改自代码随想录)
题目解读:
本题就是求最短路,
最短路是图论中的经典问题即:给出一个有向图,一个起点,一个终点,问起点到终点的最短路径。
接下来,我们来详细讲解最短路算法中的 dijkstra 算法。
dijkstra算法:在有权图(权值非负数)中求从起点到其他节点的最短路径算法。
需要注意两点:
dijkstra 算法 和 prim算法思路非常接近
dijkstra 算法 同样是贪心的思路,不断寻找距离 源点最近的没有访问过的节点。
在dijkstra算法中,同样有一个数组很重要,起名为:minDist。
minDist数组 用来记录 每一个节点距离源点的最小距离。
dijkstra初始化+三部曲:(具体模拟的流程可以见代码随想录模拟过程)
以本题示例中的图为例:
初始化:
minDist数组数值初始化为无穷大,源点(节点1) 到自己的距离为0,所以 minDist[1] = 0
此时所有节点都没有被访问过,所以 visited数组都为False
解释:
再强调一下 minDist数组的含义:记录所有节点到源点的最短路径。
所以minDist初始化的时候就应该初始为最大值,这样才能在后续出现最短路径的时候及时更新。
(示例中节点编号是从1开始,所以为了保持一致,minDist数组下标也从 1 开始计数,下标0 就不使用了,这样 下标和节点标号就可以对应上了)
第一步,找到源点到未访问过的结点中距离最近的结点
(其实就是选出minDist数组中值最小且未访问过的结点,进行访问)
第二步,该最近节点被标记为访问过
visited对应值标为True
第三步,更新非访问节点到源点的距离(即更新minDist数组)
(即更新从cur出发且直接相连的非访问节点的minDist值)
设源点s,前两步新访问的最近结点为cur,当前要计算到s距离的非访问结点为某个点node。
那么相当于,在原来的minDist基础上(作为来源一),
拓展从新访问的cur结点出发且直接相邻的非访问结点,
node就属于其中之一,
现在要去计算拓展到的非访问结点,在cur离源点s距离minDist[cur]
的基础上,增加的距离(也就是minDist[cur] + 边cur->node的权重/距离
)(来源二),
这样计算到的新距离,要与原来对应的minDist[node]对比,取更小的进行更新。
也就是说:令边cur->node的权重为val,
minDist[node] = min(minDist[node], minDist[cur] + val)
注意: 这里与prim不一样的地方在于,求源点s到node的距离,该两点是可以非直接相连的!每次求的都是新访问结点之后,最短的源点s到node的距离。
最后的判断:
要判断是否从s出发抵达了t,只需要检查minDist[t]
是否仍为无穷大即可。
若仍为无穷大,说明不存在路径;
否则,存在最短路径且长度为minDist[t]
代码随想录C++代码实现:
#include
#include
#include
using namespace std;
int main() {
int n, m, p1, p2, val;
cin >> n >> m;
vector<vector<int>> grid(n + 1, vector<int>(n + 1, INT_MAX));
for(int i = 0; i < m; i++){
cin >> p1 >> p2 >> val;
grid[p1][p2] = val;
}
int start = 1;
int end = n;
// 存储从源点到每个节点的最短距离
std::vector<int> minDist(n + 1, INT_MAX);
// 记录顶点是否被访问过
std::vector<bool> visited(n + 1, false);
minDist[start] = 0; // 起始点到自身的距离为0
for (int i = 1; i <= n; i++) { // 遍历所有节点
int minVal = INT_MAX;
int cur = 1;
// 1、选距离源点最近且未访问过的节点
for (int v = 1; v <= n; ++v) {
if (!visited[v] && minDist[v] < minVal) {
minVal = minDist[v];
cur = v;
}
}
visited[cur] = true; // 2、标记该节点已被访问
// 3、第三步,更新非访问节点到源点的距离(即更新minDist数组)
for (int v = 1; v <= n; v++) {
if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) {
minDist[v] = minDist[cur] + grid[cur][v];
}
}
}
if (minDist[end] == INT_MAX) cout << -1 << endl; // 不能到达终点
else cout << minDist[end] << endl; // 到达终点最短路径
}
个人python代码实现:
import sys
def dijkstra(edges, s, t, n):
# edges边集合,源点结点s(source),目标结点t(target)
# 结点总个数n
# 返回s到t的最短路径长度
m = len(edges) # 边数
# 创建图(邻接矩阵)
graph = [[0] * (n + 1) for _ in range(n + 1)] # 结点编号1~n
for v1, v2, val in edges:
graph[v1][v2] = val # v1 -> v2,权重为val
# 初始化
minDist = [float('inf')] * (n + 1) # 结点编号1~n
visited = [False] * (n + 1)
minDist[s] = 0 # 源点s到自己的距离为0
# 迭代求s到t最短路径长度
for i in range(n): #迭代n次,因为需要访问所有n个结点(因为可能不存在路径!必须要先求完整的dijkstra,不能在访问到终点t的时候直接返回,完整更新结束之后,要利用更新后的dijkstra判断是否能够抵达)
# 第一、二步,找到minDist中的最小值对应结点cur(且一定是未访问过),进行访问
min_dist = float('inf')
cur = s # 初始化为起点s
for node in range(1, n + 1):
if not visited[node] and minDist[node] < min_dist:
min_dist = minDist[node]
cur = node
# print(cur)
visited[cur] = True # 添加cur为已访问
# 第三步,更新从cur出发且直接相连的非访问节点的minDist值
for node in range(1, n + 1):
if not visited[node] and graph[cur][node] > 0:
# 计算新距离
new_dist = minDist[cur] + graph[cur][node]
# 比较更新更小距离
if new_dist < minDist[node]:
minDist[node] = new_dist
# print(minDist)
# 判断是否从s抵达了t
if minDist[t] == float('inf'): return -1
else: return minDist[t]
if __name__ == '__main__':
# 求最短路(最小权值路、最短抵达时间)
lines = sys.stdin.readlines()
n, m = map(int, lines[0].strip().split()) # n个车站(结点),m条公路(边)
# 创建边集合edges(这里将dijkstra封装起来,且能实现直接传入边集合求最短距离,所以就不先构建邻接矩阵了
edges = []
for i in range(1, len(lines)):
# s -> e,权重val
s, e, val = map(int, lines[i].strip().split())
edges.append((s, e, val))
minDistance = dijkstra(edges, 1, n, n) # 起点结点1,终点结点n,结点总个数n
print(minDistance)
拓展一:如果图中边的权值为负数,dijkstra 还合适吗?
不合适!
具体示例见代码随想录链接(该示例中,真正最短路径应该是经过了负值的路径,但是dijkstra却没有成功更新minDist)
对于负权值的出现,大家可以针对某一个场景 不断去修改 dijkstra 的代码,但最终会发现只是 拆了东墙补西墙,对dijkstra的补充逻辑只能满足某特定场景最短路求解。
对于求解带有负权值的最短路问题,可以使用 Bellman-Ford 算法(见后序文章)
拓展二:dijkstra与prim算法的区别(摘录修改自代码随想录)
prim算法博客:图论 15. 最小生成树之prim算法-CSDN博客
dijkstra的代码看上去和 prim算法很相似,可以说基本上一致。
唯一区别在 三部曲中的 第三步: 更新minDist数组
因为**prim是求 非访问节点到最小生成树的最小距离,而 dijkstra是求 非访问节点到源点的最小距离**。
prim 更新 minDist数组的写法:
for (int j = 1; j <= v; j++) {
if (!isInTree[j] && grid[cur][j] < minDist[j]) {
minDist[j] = grid[cur][j];
}
}
因为 minDist表示 节点到最小生成树的最小距离,所以 新节点cur的加入,只需要 使用 grid[cur][j] ,grid[cur][j] 就表示 cur 加入生成树后,生成树到 节点j 的距离。
dijkstra 更新 minDist数组的写法:
for (int v = 1; v <= n; v++) {
if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) {
minDist[v] = minDist[cur] + grid[cur][v];
}
}
因为 minDist表示 节点到源点的最小距离,所以 新节点 cur 的加入,需要使用 源点到cur的距离 (minDist[cur]) + cur 到 节点 v 的距离 (grid[cur][v]),才是 源点到节点v的距离。
此时大家可能不禁要想 prim算法 可以有负权值吗?
当然可以!
提示:prim算法只需要将节点以最小权值和链接在一起,不涉及到单一路径。
附代码随想录python题解代码用于比较学习:
import sys
def dijkstra(n, m, edges, start, end):
# 初始化邻接矩阵
grid = [[float('inf')] * (n + 1) for _ in range(n + 1)]
for p1, p2, val in edges:
grid[p1][p2] = val
# 初始化距离数组和访问数组
minDist = [float('inf')] * (n + 1)
visited = [False] * (n + 1)
minDist[start] = 0 # 起始点到自身的距离为0
for _ in range(1, n + 1): # 遍历所有节点
minVal = float('inf')
cur = -1
# 选择距离源点最近且未访问过的节点
for v in range(1, n + 1):
if not visited[v] and minDist[v] < minVal:
minVal = minDist[v]
cur = v
if cur == -1: # 如果找不到未访问过的节点,提前结束
break
visited[cur] = True # 标记该节点已被访问
# 更新未访问节点到源点的距离
for v in range(1, n + 1):
if not visited[v] and grid[cur][v] != float('inf') and minDist[cur] + grid[cur][v] < minDist[v]:
minDist[v] = minDist[cur] + grid[cur][v]
return -1 if minDist[end] == float('inf') else minDist[end]
if __name__ == "__main__":
input = sys.stdin.read
data = input().split()
n, m = int(data[0]), int(data[1])
edges = []
index = 2
for _ in range(m):
p1 = int(data[index])
p2 = int(data[index + 1])
val = int(data[index + 2])
edges.append((p1, p2, val))
index += 3
start = 1 # 起点
end = n # 终点
result = dijkstra(n, m, edges, start, end)
print(result)