最短路
模板
class Graph:
def dijkstra(self, graph, start):
'''
重要的是定义成本,可能是路径,可能是花费, 如果有限制次数,也可以将次数也算进成本里
如果是次数
:param graph: 存了每个点的next node
'''
import heapq
heap = [(0, start)] # cost from start node,end node
dist = {}
# self.path = [-1 for _ in range(len(graph.keys()))]
while heap:
(cost, u) = heapq.heappop(heap)
if u in dist:
continue
dist[u] = cost
for v, c in graph[u]:
if v in dist:
continue
next = cost + c
# self.path[v] = u
heapq.heappush(heap, (next, v))
return dist
def get_dijkstra_path(self, j):
if self.path[j] == -1:
return [j]
else:
i = self.path[j]
return self.get_dijkstra_path(i) + [j]
def floyd_warshall(self, grid):
if not grid or not grid[0]:
return
n = len(grid)
# self.path = [[-1] * n for _ in range(n)]
# grid: n*n
for k in range(n):
for i in range(n):
for j in range(n):
if grid[i][k] + grid[k][j] < grid[i][j]:
grid[i][j] = grid[i][k] + grid[k][j]
# self.path[i][j] = k
return grid
def get_floyd_warshall_path(self, i, j):
if self.path[i][j] == -1:
return [i, j]
else:
k = self.path[i][j]
return self.get_floyd_warshall_path(i, k) + self.get_floyd_warshall_path(k, j)
def bellman_ford(self, edges, n, start):
"""
Bellman-Ford 算法计算单源最短路径
:param edges: 所有边,形如 [(u, v, w)]
:param n: 节点数
:param start: 起点
:return: 距离数组和路径数组
"""
dist = [float('inf')] * n
prev = [-1] * n
dist[start] = 0
for _ in range(n - 1):
for u, v, w in edges:
if dist[u] != float('inf') and dist[u] + w < dist[v]:
dist[v] = dist[u] + w
prev[v] = u
# 检测负权环
for u, v, w in edges:
if dist[u] != float('inf') and dist[u] + w < dist[v]:
raise ValueError("图中包含负权环")
self.bf_path = prev
return dist
def get_bellman_ford_path(self, j):
if self.bf_path[j] == -1:
return [j]
else:
return self.get_bellman_ford_path(self.bf_path[j]) + [j]
Dijkstra
求的是单源到其他节点的最短路径,需要权值非负
为什么不能处理负权边?
可能出现这样一种情况:因为dijktra算法每次都先寻找前往节点的最小值(正数),并将节点加入已访问集合之中,之后不再对其进行更新。举个例子,目标:寻找A-C的最短路径。使用Dijkstra算法时,比较从A->B和A->C的开销,显然A->C的更小,于是选择到C的路径,并将C处理成处理过的节点。到这里发现了什么问题呢,A->B->C不是更短吗?就是负权边的情况。
时间复杂度
Complexity is
O( (|E|+|V|)log|V|) 一般可以理解为O(VlogV)
外层循环V次, 每次queue pop log(V),因此VlogV, 然后每个边都遍历了一次,因此+E.
算法思想
Dijkstra() {
初始化;
for(循环n次) {
u = 使dis[u]最小的还未被访问的顶点的编号;
记u为确定值;
for(从u出发能到达的所有顶点v){
for(v未被访问 && 以u为中介点使s到顶点v的最短距离更优)
优化dis[v];
}
}
}
朴素算法 o(n2)
//邻接表
struct node {
int v, dis;
}
vector<node> e[maxv];
int n;
int dis[maxv], pre[maxv];// pre用来标注当前结点的前一个结点
bool vis[maxv] = {false};
for(int i = 0; i < n; i++) pre[i] = i; //初始状态设每个点的前驱为自身
void Dijkstra(int s) {
fill(dis, dis + maxv, inf);
dis[s] = 0;
for(int i = 0; i < n; i++) {
int u = -1, minn = inf;
for(int j = 0; j < n; j++) {
if(visit[j] == false && dis[j] < minn) {
u = j;
minn = dis[j];
}
}
if(u == -1) return ;
visit[u] = true;
for(int j = 0; j < e[u].size(); j++) {
int v = e[u][j].v;
if(visit[v] == false && dis[u] + e[u][j].dis < dis[v]) {
dis[v] = dis[u] + e[u][j].dis;
pre[v] = u;
}
}
}
}
可以根据pre 倒着dfs找所有路径
void dfs(int s, int v) {
if(v == s) {
printf("%d\n", s);
return ;
}
dfs(s, pre[v]);
printf("%d\n", v);
}
优先队列 o(elogv)
模板类代码,也可以用visit数组记录访问记录,然后continue,这里直接比较dist和队列里dist是否一致来跳过的。
dis也可以是map, 优先队列弹出的是最短的,dis相当于visit了。
def dijkstra(self, graph, start, end):
heap = [(0, start)] # cost from start node,end node
dist = {}
while heap:
(cost, u) = heapq.heappop(heap)
if u in dist:
continue
dist[u] = cost
for v, c in graph[u]:
if v in dist:
continue
next = cost + c
heapq.heappush(heap, (next, v))
return dist
应用
最短路径时要求极大化第二标尺
三种附加考法:第一标尺是距离,如果距离相等的时候,新增第二标尺
- 新增边权(第二标尺),要求在最短路径有多条时要求路径上的花费之和最小
for(int v = 0; v < n; v++) { //重写v的for循环
if(visit[v] == false && e[u][v] != inf) {
if(dis[u] + e[u][v] < dis[v]) {
dis[v] = dis[u] + e[u][v];
c[v] = c[u] + cost[u][v];
}else if(dis[u] + e[u][v] == dis[v] && c[u] + cost[u][v] < c[v]) {
c[v] = c[u] + cost[u][v];
}
}
}
- 给定每个点的点权(第二标尺),要求在最短路径上有多条时要求路径上的点权之和最大
for(int v = 0; v < n; v++) {
if(visit[v] == false && e[u][v] != inf) {
if(dis[u] + e[u][v] < dis[v]) {
dis[v] = dis[u] + e[u][v];
w[v] = w[u] + weight[v];
}else if(dis[u] + e[u][v] == dis[v] && w[u] + weight[v] > w[v]) {
w[v] = w[u] + weight[v];
}
}
}
- 直接问有多少条最短路径
增加一个数组num[],num[s] = 1,其余num[u] = 0,表示从起点s到达顶点u的最短路径的条数为num[u]
for(int v = 0; v < n; v++) {
if(visit[v] == false && e[u][v] != inf) {
if(dis[u] + e[u][v] < dis[v]) {
dis[v] = dis[u] + e[u][v];
num[v] = num[u];
}else if(dis[u] + e[u][v] == dis[v]) {
num[v] = num[v] + num[u];
}
}
}
求路径,路径反向追溯
可以不用这么麻烦,用Dijkstra求最短路径和pre数组,然后用深度优先遍历来获取想知道的一切,包括点权最大,边权最大,路径个数
因为可能有多条路径,所以Dijkstra部分的pre数组使用vector pre[maxv];
//Dijkstra部分
if(dis[u] + e[u][v] < dis[v]) {
dis[v] = dis[u] + e[u][v];
pre[v].clear();
pre[v].push_back(u);
} else if(dis[i] + e[u] == dis[v]) {
pre[v].push_back(u);
}
既然已经求得pre数组,就知道了所有的最短路径,然后要做的就是用dfs遍历所有最短路径,找出一条使第二标尺最优的路径
注意路径path因为是从末端一直压入push_back到path里面的,所以要输出路径的时候倒着输出
int optvalue;
vector<int> pre[maxv];
vector<int> path, temppath;
void dfs(int v) { // v为当前访问结点
temppath.push_back(v);
if(v == start) {
int value = 路径temppath上的value值;
if(value 优于 optvalue) {
optvalue = value;
path = temppath;
}
temppath.pop_back();
return ;
}
for(int i = 0; i < pre[v].size(); i++)
dfs(pre[v][i]);
temppath.pop_back();
}
// 边权之和
int value = 0;
for(int i = tempptah.size() - 1; i > 0; i--) {
int id = temppath[i], idnext = temppath[i - 1];
value += v[id][idnext];
}
// 点权之和
int value = 0;
for(int i = temppath.size(); i >= 0; i--) {
int id = temppath[i];
value += w[id];
}
Floyd-Warshall
求多源、无负权边的最短路,注意不能有负权回路。用矩阵记录图。时效性较差,时间复杂度O(V^3)。
Floyd-Warshall算法(Floyd-Warshall algorithm)是解决任意两点间的最短路径的一种算法,可以正确处理有向图或负权的最短路径问题,可以正确处理负权并求出结果,但不可存在负权回路。
Floyd-Warshall算法的时间复杂度为O(N^3),空间复杂度为O(N^2)。
算法思想
template <class T>
void ShortestPathFloyd(Graph<T> G, PathMatrix &Path, DistanceMatrix& A)
{
int n = G.NumberOfVertices();
int i, j, k, t;
T u, v, w;
for (i = 0; i < n; i++)
for (j = 0; j < n; j++) {
A[i][j] = Edge[i][j];
Path[i][j] = 0;
}
for (k = 0; k < n; k++)
for (i = 0; i < n; i++)
for (j = 0; j < n; j++)
if (A[i][k] + A[k][j] < A[i][j]) {
A[i][j] = A[i][k] + A[k][j];
Path[i][j] = k;
}
}
求具体路径
假设我们用path[i][j]记录从i到j松弛的节点k,那么从i到j,肯定是先从i到k,然后再从k到j, 那么我们在找出path[i][k] , path[k][j]即可。即 i到k的最短路是 `i -> path[i][k] -> k -> path[k][j] -> k` q` 然后求path[i][k]和path[k][j] ,一直到某两个节点没有中间节点为止public String getPath(int[][] path, int i, int j) { if (path[i][j] == -1) { return " "+i+" "+j; } else { int k = path[i][j]; return getPath(path, i, k)+" "+getPath(path, k, j)+" "; } }
Floyd优缺点分析:
优点:比较容易容易理解,可以算出任意两个节点之间的最短距离,代码编写简单。
缺点:时间复杂度比较高(n3),不适合计算大量数据,当数据稍微大点儿的时候就可以选择其他的算法来解决问题了,不然也会是超时。
Floyd算法与Dijkstra算法的不同
- Floyd算法是求任意两点之间的距离,是多源最短路,而Dijkstra(迪杰斯特拉)算法是求一个顶点到其他所有顶点的最短路径,是单源最短路。
- Floyd算法属于动态规划,我们在写核心代码时候就是相当于推dp状态方程,Dijkstra(迪杰斯特拉)算法属于贪心算法。
- Dijkstra(迪杰斯特拉)算法时间复杂度一般是o(n^2),Floyd算法时间复杂度是o(n^3),Dijkstra(迪杰斯特拉)算法比Floyd算法块。
- Floyd算法可以算带负权的,而Dijkstra(迪杰斯特拉)算法是不可以算带负权的。并且Floyd算法不能算负权回路。
Bellman-Ford
求单源最短路,可以判断有无负权回路(若有,则不存在最短路),可以处理边为负的情况。时效性较好,时间复杂度O(VE)。
与Dijkstra算法不同的是,在Bellman-Ford算法中,边的权值可以为负数。
首先介绍一下松弛计算。如下图:

松弛计算之前,点B的值是8,但是点A的值加上边上的权重2,得到5,比点B的值(8)小,所以,点B的值减小为5。这个过程的意义是,找到了一条通向B点更短的路线,且该路线是先经过点A,然后通过权重为2的边,到达点B。
算法思想:
- 遍历所有的边并relax,共计V-1次
- 最后一次遍历,如果还能relax 则有负权回路
# Bellman-Ford implementation from MIT 6006 course lesson #17
import math
import networkx as nx
# utility: Graph
class Graph:
def __init__(self, vertices):
self.V = vertices
self.edges = []
def add_edge(self, edge):
self.edges.append(edge)
def bellmanFord(graph, s):
d = dict.fromkeys(graph.V, math.inf) # distance pair
# will have default value of Infinity
pi = dict.fromkeys(graph.V, None) # map of parent vertex
# initialize
d[s] = 0
def relax(u, v, w):
if d[v] > d[u] + w:
d[v] = d[u] + w
pi[v] = u
# The length of the longest simple path(path w/o cycle) would be |V| - 1.
# For example, you need 2 edges to connect 3 vertices.
# Otherwise, there exists a negative cycle.
for i in graph.V[:-1]:
for u, v, w in graph.edges:
relax(u, v, w)
for u, v, w in graph.edges:
# even after relaxing all the edges for |V| - 1 times,
# we still have the posibillity to improve the existing path
# this means there are negative cycles
if d[v] > d[u] + w:
return f'there exists a negetive cycle!'
return d, pi
def shortest_path(s, t):
try:
d, pi = bellmanFord(g, s)
except ValueError:
return 'you can\'t find shortest path if the graph has negative cycle!'
path = [t]
current = t
# if parent pointer is None,
# then it's the source vertex
while pi[current]:
path.insert(0, pi[current])
# set current to parent
current = pi[current]
if s not in path:
return f'unable to find shortest path staring from "{s}" to "{t}"'
return f'{" > ".join(path)}'
g = Graph(['A', 'B', 'C', 'D', 'E'])
# graph with negative cycle
nc_edges = [('A', 'B', 5), ('B', 'C', -1), ('C', 'D', 2), ('D', 'B', -2), ('C', 'E', 4)]
# w/o negative cycles
edges = [\
('A', 'B', 10), ('A', 'C', 3), ('B', 'C', 1), ('C', 'B', 4), \
('B', 'D', 2), ('C', 'D', 8), ('D', 'E', 7), ('E', 'D', 9), ('C', 'E', 2)]
# used for both finding shortest path and drawing graph
current_edge_group = edges
for edge in current_edge_group:
g.add_edge(edge)
print( shortest_path('A', 'E') )
G = nx.DiGraph()
G.add_weighted_edges_from(current_edge_group)
nx.draw(G, with_labels = True, node_color='b', font_color='w')
补充 : 考虑:为什么要循环V-1次? 答:因为最短路径肯定是个简单路径,不可能包含回路的, 如果包含回路,且回路的权值和为正的,那么去掉这个回路,可以得到更短的路径 如果回路的权值是负的,那么肯定没有解了
图有n个点,又不能有回路 所以最短路径最多n-1边
又因为每次循环,至少relax一边 所以最多n-1次就行了
总结
floyd-warshall for k, for i , for j. 更新a[i][j]
bellman-ford n-1次relax, 每次relax遍历所有边, 对<u, v>, d[v] = min(d[v], d[u] + weight), 最后判断是否有个边存在d[v] > d[u] + weight即有负边O(V*E)
相关题目
最短路径可动态规划的:
- 需要无后效性, 这个状态有确定的值,后面只依赖这个状态
- 类似bellman-ford dp。
- 可定义f(times, i) 表示t次中转的到i的最小花费总和
- 可定义f(cost, i) 表示成本为cost的到i的最小花费总和
- 枚举times, cost 从1 -> X, 再枚举每个边
可利用dijkstra的
- idea就是拿到一个最短路径的点后,可选择是否再继续拓扑更新下个点。
- 求距离第2的路径长度,那就保存每个点的2个最短路径长度,然后有更新就继续拓扑,没更新就不需要加到堆里来了。
- 可以append时更新dis,也可以pop时更新,append时更新需要if判断下,pop更新第一个就是最优的
- 如果需要求第二标尺,记得选择append时更新。
- 限制第二标尺的。每个点可能会append多次的,就新建一个新的图,在这个新的图上进行dijkstra,这时每个点dist 的key都是(c1, i) 是新的大图了。c1是限制的第二标尺
01bfs
- 如果距离要不是1,要不就是0,那可以01bfs
简单dijkstra
- 743. 网络延迟时间 纯dijkstra单源最短路径,套用模板即可。
限制第二标尺
K次中转的限制下求最短路径 你的任务是找到出一条最多经过
k站中转的路线,使得从src到dst的 价格最便宜 ,并返回该价格解法1: 优先队列,直接存中转次数,队列pop时剔除不要的结果,顺便visit标记也要改成(cost, times), 时间复杂度KE x O(Klogn)
class Solution:
def findCheapestPrice(self, n: int, flights: List[List[int]], src: int, dst: int, K: int) -> int:
graph = collections.defaultdict(list)
for u, v, w in flights:
graph[u].append((v, w))
visit = {}
hq = [(0, src, 0)]
while hq:
cost, u, steps = heapq.heappop(hq)
if (u, steps) in visit:
continue
visit[(u, steps)] = 1
if steps > K + 1:
continue
if u == dst:
return cost
for v, w in graph[u]:
if (v, steps+1) not in visit:
heapq.heappush(hq, (cost + w, v, steps + 1))
return -1
- 解法2: 动态规划滚动数组, f(t,i)表示t次中转的到i的最小花费总和, 时间复杂度o(K*E)
class Solution:
def findCheapestPrice(self, n: int, flights: List[List[int]], src: int, dst: int, k: int) -> int:
inf = float("inf")
g = [inf for _ in range(n)]
g[src] = 0
ans = inf
# 正是belman-ford思想, 1次relax = 1次中转.由于我们最多只能中转 k 次,也就是最多搭乘 k+1 次航班
for i in range(k+1):
f = [inf for _ in range(n)]
for u, v, w in flights:
f[v] = min(f[v], g[u] + w)
g = f
ans = min(ans, f[dst])
return ans if ans < inf else -1
- 1928. 规定时间内到达终点的最小花费 和K次中转相似,这里递推转移是
f[t][i] = min(f[t][i], f[t - cost][j] + passingFees[i])因此不能用滚动数组,老老实实二维数组f = [[float("inf")] * n for _ in range(maxTime + 1)]
限制第二标尺,不能dp只能dijkstra的
可用01bfs替代的裸dijkstra
- 6081. 到达角落需要移除障碍物的最小数目 现在你需要从左上角
(0, 0)移动到右下角(m - 1, n - 1),返回需要移除的障碍物的 最小 数目。- 解法1:优先队列,花费低优先,障碍物花费是1,否则是0
- 解法2: 01bfs 花费0时appendleft,01bfs只对0/固定花费有效。
- 类似题目(优先队列/01bfs):1368. 使网格图至少有一条有效路径的最小代价
裸的floyd warshall