787. Shortest Path With Constraint

https://leetcode.com/problems/cheapest-flights-within-k-stops/

在Weighted DAG里找到src到dst的最短路,且中间经过的点不能超过K. Solution with Path Tracking

Dijkstra, 使用priority_queue:

跟Dijkstra 最短路的思想类似,从src开始,一直往pq里插入所有可能的path,然后greedy的每轮只拿最小的vertice继续往前走,直到找到dst.

由于必须同时满足:

  • v.k >= -1
  • v.index == dst

而且一直都用priority_queue找最小的,所以不存在early return的情况。

这个解法有很多的element会被repeatedly insert,memory usage大约到达了第二种解法的两倍! (因为priority_queue不支持decreaseKey operation,所以我们没办法in-place update已经有的key)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Solution {
    struct V {
        int dist, index, k;
    };

public:
    int findCheapestPriceDijk(int n,
                          const vector<vector<int>>& flights,
                          int src, int dst, int K) {
        // build graph
        unordered_map<int, unordered_map<int, int>> g;
        for (const auto& e : flights) {
            g[e[0]][e[1]] = e[2]; // from, to, weight
        }

        // tracker
        vector<int> pathTo(n);

        // Dijkstra
        using T = tuple<int, int, int>;
        priority_queue<T, vector<T>, greater<T>> pq;  // dis, k, cur
        pq.push({0, K, src});

        while (!pq.empty()) {
            auto [dis, k, from] = pq.top();
            pq.pop();

            if (k < -1) continue;
            if (from == dst) return dis;

            for (const auto& [to, price] : g[from]) {
                pq.push({price + dis, k - 1, to});
            }
        }

        return -1;
    }
};
SPFA, 使用queue:

SFPA(其实就是BFS!)从src一直往dst找,每轮挪动1,同时一直keep track cheapest,distTo存在的意义就在于cache 已经visited过的vertice (optimize performance)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Solution {
public:
    int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
        int cheapest = -1;

        unordered_map<int, unordered_map<int, int>> adj_list;
        vector<int> distTo(n);
        distTo[src] = -1;

        for (const auto& flight : flights) {
            adj_list[flight[0]][flight[1]] = flight[2];
        }

        // {index, price}
        queue<pair<int, int>> q;
        q.push({src, 0});

        while (!q.empty() && K >= -1 ) {
            int size = q.size();
            while (size-- > 0) {    // concise
                auto cur = q.front();
                int index = cur.first, price = cur.second;
                q.pop();

                if (index == dst) {
                    cheapest = -1 == cheapest ? price : min(cheapest, price);
                }
                else
                {
                    for (const auto& e : adj_list[index]) {
                        if (distTo[e.first] == 0 || distTo[e.first] > e.second + price) {
                            q.push({e.first, price + e.second});
                            distTo[e.first] = price + e.second;
                        }
                    }
                }
            }

            K--;
        }

        return cheapest;
    }
};
SPFA Rev2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Solution {
public:
    int findCheapestPrice(int n,
                          const vector<vector<int>>& flights,
                          int src, int dst, int K) {
        // build graph
        unordered_map<int, unordered_map<int, int>> g;
        for (const auto& e : flights) {
            g[e[0]][e[1]] = e[2]; // from, to, weight
        }

        // SPFA
        // setup
        vector<int> distTo(n, INT_MAX);
        // vector<char> inQueue(n, false);  -> optional optimization
        queue<tuple<int, int, int>> q;   // cur, K, acc

        // init
        q.push({src, K, 0});
        // inQueue[src] = true;

        while (!q.empty()) {
            auto [cur, k, acc] = q.front();
            q.pop();
            // inQueue[src] = false;

            if (k < 0) {  // k stop, k+1 edges
                continue;
            }

            for (const auto& [to, weight] : g[cur]) {
                if (distTo[to] > acc + weight) {
                    distTo[to] = acc + weight;
                    // if (!inQueue[to]) {
                        q.push({to, k - 1, acc + weight});
                    // }
                }
            }
        }

        return distTo[dst] == INT_MAX ? -1 : distTo[dst];
    }
};

Floyd (复杂度高 O(n3))

基于动态规划的做法。

Dijkstra(不能处理负环和负边)

复杂度为O(E*logV)

适合dense 和 sparse graph

从这里让我想到了implement 一下Dijkstra。其实跟解法1确实非常相似,但是也有同样的问题就是priority_queue没办法in-place update key。

还有一点不一样的是,Dijkstra没有K的限制,所以可以keep track两个东西:

  • distTo
  • edgeTo

然后在插入的时候只有最优解的时候才插入,其他时候直接跳过。(priority_queue的解法则不行,必须得插入所有的元素因为不到找到dst不知道哪个会是最短的)

Code

Bellman-Ford (都能处理)

复杂度为O(E*V)

不适合dense graph

SPFA(都能处理,但复杂度不稳定)

复杂度为O(E*V) in worst case,但是average running time是 O(E), 可惜并没有办法证明。

不适合dense graph

Code