图最短路径

2018/02/19 Algorithms 共 10089 字,约 29 分钟

最短路

模板

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

应用

最短路径时要求极大化第二标尺

三种附加考法:第一标尺是距离,如果距离相等的时候,新增第二标尺

  1. 新增边权(第二标尺),要求在最短路径有多条时要求路径上的花费之和最小

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];
    }
  }
}
  1. 给定每个点的点权(第二标尺),要求在最短路径上有多条时要求路径上的点权之和最大

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];
    }
  }
}
  1. 直接问有多少条最短路径

增加一个数组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算法的不同

  1. Floyd算法是求任意两点之间的距离,是多源最短路,而Dijkstra(迪杰斯特拉)算法是求一个顶点到其他所有顶点的最短路径,是单源最短路。
  2. Floyd算法属于动态规划,我们在写核心代码时候就是相当于推dp状态方程,Dijkstra(迪杰斯特拉)算法属于贪心算法。
  3. Dijkstra(迪杰斯特拉)算法时间复杂度一般是o(n^2),Floyd算法时间复杂度是o(n^3),Dijkstra(迪杰斯特拉)算法比Floyd算法块。
  4. Floyd算法可以算带负权的,而Dijkstra(迪杰斯特拉)算法是不可以算带负权的。并且Floyd算法不能算负权回路。

Bellman-Ford

求单源最短路,可以判断有无负权回路(若有,则不存在最短路),可以处理边为负的情况。时效性较好,时间复杂度O(VE)。

与Dijkstra算法不同的是,在Bellman-Ford算法中,边的权值可以为负数。

首先介绍一下松弛计算。如下图:

image-20210125182546414

松弛计算之前,点B的值是8,但是点A的值加上边上的权重2,得到5,比点B的值(8)小,所以,点B的值减小为5。这个过程的意义是,找到了一条通向B点更短的路线,且该路线是先经过点A,然后通过权重为2的边,到达点B。

算法思想:

  1. 遍历所有的边并relax,共计V-1次
  2. 最后一次遍历,如果还能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

限制第二标尺

  • 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

裸的floyd warshall

Search

    Table of Contents