3. 最短路径

3.1. Bellman-Ford 算法

时间复杂度 \(\mathcal{O}(VE)\) ,其中顶点数 \(V\) ,边数 \(E\) 。如果不存在负圈(一条回路的代价和为负),那么每一条最短路径都不会经过同一个顶点两次,因此 while 循环最多执行 \(V-1\) 次。

 1struct edge {int from, to, cost;};
 2
 3edge es[MAX_E];
 4
 5int d[MAX_V]; // 最短距离
 6int V, E; // 顶点数,边数
 7
 8// 从顶点 s 出发的最短距离(假设不存在负圈)
 9void shortestPath(int s)
10{
11  fill(d, d+V, INF);
12  d[s] = 0;
13  while(true)
14  {
15    bool update = false;
16    for(int i = 0; i < E; ++i)
17    {
18      edge e = es[i];
19      if(d[e.from] != INF && d[e.to] > d[e.from] + e.cost)
20      {
21        d[e.to] = d[e.from] + e.cost;
22        update = true;
23      }
24    }
25    if(!update) break;
26  }
27}

检查负圈:如果第 \(V\) 次循环还有更新,则表明存在负圈,返回 true。

 1bool findNegativeLoop()
 2{
 3  fill(d, d+V, 0); // 初始化为 0,防止因为是 d[e.from] == INF 而停止更新
 4  for(int i = 0; i < V; ++i)
 5  {
 6    for(int j = 0; j < E; ++j)
 7    {
 8      edge e = es[j];
 9      if(d[e.to] > d[e.from] + e.cost)
10      {
11        d[e.to] = d[e.from] + e.cost;
12        if(i == V-1) return true;
13      }
14    }
15  }
16  return false;
17}

3.2. Dijkstra 算法

适合处理没有负边的情形。每一次循环,在尚未确定最短距离的顶点中,d[i] 最小的顶点就是下一个确定的顶点。但是如果存在负边,d[i] 在之后的更新中还会变小,因此算法失效。

  • 方法一

    直接使用邻接矩阵,时间复杂度 \(\mathcal{O}(V^2)\)

 1int cost[MAX_V][MAX_V];
 2int d[MAX_V];
 3bool used[MAX_V];
 4int V;
 5
 6void dijkstra(int s)
 7{
 8  fill(d, d+V, INF);
 9  d[s] = 0;
10  fill(used, used+V, false);
11
12  while(true)
13  {
14    int v = -1;
15    for(int u = 0; u < V; ++u)
16    {
17      if(!used[u] && (v == -1 || d[u] < d[v])) v = u;
18    }
19
20    if(v == -1 || d[v] == INF) break;
21    // v == -1 表示所有顶点都找到了最短距离
22    // d[v] == INF 表示后面所有的顶点都已经不可达,直接结束循环
23
24    used[v] = true;
25    for(int u = 0; u < V; ++u)
26    {
27      d[u] = min(d[u], d[v] + cost[v][u]);
28    }
29  }
30}
  • 方法二

    使用最小堆(优先队列),堆中元素个数为 \(\mathcal{O}(V)\),出队(弹出最小值)的次数为 \(\mathcal{O}(E)\),时间复杂度 \(\mathcal{O}(E \log V)\)

 1struct edge {int to, cost;};
 2typedef pair<int, int> P; // first:最短距离,second:顶点
 3
 4int V;
 5vector<edge> G[MAX_V]; // 边
 6int d[MAX_V];
 7
 8void dijkstra(int s)
 9{
10  priority_queue<P, vector<P>, greater<P>> que;
11
12  fill(d, d+V, INF);
13  d[s] = 0;
14
15  que.push(P(0, s));
16  while(!que.empty())
17  {
18    P p = que.top();
19    que.pop();
20
21    int v = p.second;
22    if(d[v] < p.first) continue;
23
24    for(int i = 0; i < G[v].size(); ++i)
25    {
26      edge e = G[v][i];
27      if(d[e.to] > d[v] + e.cost)
28      {
29        d[e.to] = d[v] + e.cost;
30        que.push(P(d[e.to], e.to));
31      }
32    }
33  }
34}

3.3. 实例

  • [LeetCode] Shortest Path to Get All Keys 获取所有钥匙的最短路径。Hint:需要一个状态量来表示到达当前位置已经获得的钥匙(BitMap);当且仅当钥匙状态不相同时,才可以重复经过某一个坐标点。

    https://leetcode.com/problems/shortest-path-to-get-all-keys/

    \(\color{darkgreen}{Code}\)

     1class Solution
     2{
     3public:
     4    int shortestPathAllKeys(vector<string>& grid)
     5    {
     6        if(grid.empty() || grid[0].empty()) return 0;
     7        int row = grid.size();
     8        int col = grid[0].size();
     9        vector<vector<vector<bool>>> visited(row, vector<vector<bool>>(col, vector<bool>(64, false))); // bitmap, row x col x 2^6
    10        queue<pair<int,int>> que; // 坐标和key
    11        int nkey = 0;
    12        for(int i = 0; i < row; ++i)
    13        {
    14            for(int j = 0; j < col; ++j)
    15            {
    16                if(grid[i][j] == '@')
    17                {
    18                    que.push({i*col+j,0});
    19                    visited[i][j][0] = true;
    20                }
    21                if('a' <= grid[i][j] && grid[i][j] <= 'f') nkey |= 1 << (grid[i][j] - 'a');
    22            }
    23        }
    24        int step = 0;
    25        while(!que.empty())
    26        {
    27            int qsize = que.size();
    28            for(int i = 0; i < qsize; ++i) // 从各个出发点出发,同步向前走一步
    29            {
    30                auto p = que.front();
    31                que.pop();
    32                int x = p.first/col, y = p.first%col;
    33                int carry = p.second;
    34                if(carry == nkey) return step;
    35                for(int j = 0; j < 4; ++j)
    36                {
    37                    int nx = x + mv[j][0];
    38                    int ny = y + mv[j][1];
    39                    int nk = carry;
    40                    if(nx < 0 || nx >= row || ny < 0 || ny >= col) continue;
    41                    if(grid[nx][ny] == '#') continue;
    42                    if('A' <= grid[nx][ny] && grid[nx][ny] <= 'F')
    43                    {
    44                        if(!(nk & (1 << (grid[nx][ny] - 'A')))) continue;
    45                        // nk &= ~ (1 << (grid[nx][ny] - 'A')); // 开门不会消耗钥匙
    46                    }
    47                    if('a' <= grid[nx][ny] && grid[nx][ny] <= 'f') nk |= 1 << (grid[nx][ny] - 'a');
    48                    if(!visited[nx][ny][nk]) // 当前钥匙状态为 nk , 未访问过 (nx, ny)
    49                    {
    50                        visited[nx][ny][nk] = true;
    51                        que.push({nx*col+ny, nk});
    52                    }
    53                }
    54            }
    55            ++step; // 此时队列中保存的是从各个出发点出发,走完 step 步的结果
    56        }
    57        return -1;
    58    }
    59private:
    60    static const int mv[4][2];
    61};
    62const int Solution::mv[4][2] = {{-1,0},{0,-1},{0,1},{1,0}};
    
  • [LeetCode] 到达目的地的方案数。

    https://leetcode.cn/problems/number-of-ways-to-arrive-at-destination

    \(\color{darkgreen}{Code}\)

     1from queue import PriorityQueue
     2
     3INF = float('inf')
     4MOD = 10 ** 9 + 7
     5
     6class Solution:
     7    def countPaths(self, n: int, roads: List[List[int]]) -> int:
     8        adj = [[] for _ in range(n)] ## 邻接表
     9        for u, v, time in roads:
    10            adj[u].append((v, time))
    11            adj[v].append((u, time))
    12        d = [0] + [INF] * (n - 1) ## 最短距离
    13        pn = [1] + [0] * (n - 1) ## 最短路径数量
    14        pq = PriorityQueue()
    15        pq.put((0, 0)) ## (从起点到某顶点的距离,顶点)
    16        while not pq.empty():
    17            t, u = pq.get()
    18            if d[u] < t:
    19                continue
    20            for v, time in adj[u]:
    21                if d[v] == d[u] + time:
    22                    pn[v] += pn[u]
    23                elif d[v] > d[u] + time:
    24                    d[v] = d[u] + time
    25                    pn[v] = pn[u]
    26                    pq.put((d[u] + time, v)) ## 发现更短的路径才放入队列
    27                pn[v] = pn[v] % MOD
    28        return pn[-1]
    
  • [LeetCode] 到达目的地的最短时间。

    https://leetcode.com/problems/minimum-time-to-visit-a-cell-in-a-grid

    \(\color{darkgreen}{Code}\)

     1from queue import PriorityQueue
     2class Solution:
     3    def minimumTime(self, grid: List[List[int]]) -> int:
     4        m, n = len(grid), len(grid[0])
     5        assert m >= 2 and n >= 2
     6        ## 先排除一步也走不出去的情形
     7        if grid[0][1] > 1 and grid[1][0] > 1:
     8            return -1
     9        visited = [[False] * n for _ in range(m)]
    10        pq = PriorityQueue()
    11        pq.put((0, 0, 0)) ## (time, row, col)
    12        visited[0][0] = True
    13        while not pq.empty():
    14            t, r, c = pq.get()
    15            if r == m - 1 and c == n - 1:
    16                return t
    17            for dr, dc in [(-1,0), (0,-1), (1,0), (0,1)]:
    18                _r, _c = r + dr, c + dc
    19                if 0 <= _r < m and 0 <= _c < n and not visited[_r][_c]:
    20                    visited[_r][_c] = True
    21                    if grid[_r][_c] <= t + 1:
    22                        ## 下一步直接迈进 (_r, _c)
    23                        pq.put((t + 1, _r, _c))
    24                    else:
    25                        ## 在第 t 和 t-1 步之间来回踱步,以达到时间要求
    26                        ## 这里导致先入队的不一定是先到达的节点,所以使用的是优先队列
    27                        diff = grid[_r][_c] - t
    28                        if diff & 1:
    29                            pq.put((grid[_r][_c], _r, _c))
    30                        else:
    31                            pq.put((grid[_r][_c] + 1, _r, _c))
    32        return -1