There are N
network nodes, labelled 1
to N
.html
Given times
, a list of travel times as directededges times[i] = (u, v, w)
, where u
is the source node, v
is the target node, and w
is the time it takes for a signal to travel from source to target.node
Now, we send a signal from a certain node K
. How long will it take for all nodes to receive the signal? If it is impossible, return -1
.git
Example 1:github
Input: times = [[2,1,1],[2,3,1],[3,4,1]], N = 4, K = 2 Output: 2
Note:算法
N
will be in the range [1, 100]
.K
will be in the range [1, N]
.times
will be in the range [1, 6000]
.times[i] = (u, v, w)
will have 1 <= u, v <= N
and 0 <= w <= 100
.
这道题给了咱们一些有向边,又给了一个结点K,问至少须要多少时间才能从K到达任何一个结点。这其实是一个有向图求最短路径的问题,求出K点到每个点到最短路径,而后取其中最大的一个就是须要的时间了。能够想成从结点K开始有水流向周围扩散,当水流到达最远的一个结点时,那么其余全部的结点必定已经流过水了。最短路径的经常使用解法有迪杰斯特拉算法 Dijkstra Algorithm, 弗洛伊德算法 Floyd-Warshall Algorithm, 和贝尔曼福特算法 Bellman-Ford Algorithm,其中,Floyd 算法是多源最短路径,即求任意点到任意点到最短路径,而 Dijkstra 算法和 Bellman-Ford 算法是单源最短路径,即单个点到任意点到最短路径。这里由于起点只有一个K,因此使用单源最短路径就好了。这三种算法还有一点不一样,就是 Dijkstra 算法处理有向权重图时,权重必须为正,而另外两种能够处理负权重有向图,可是不能出现负环,所谓负环,就是权重均为负的环。为啥呢,这里要先引入松弛操做 Relaxtion,这是这三个算法的核心思想,当有对边 (u, v) 是结点u到结点v,若是 dist(v) > dist(u) + w(u, v),那么 dist(v) 就能够被更新,这是全部这些的算法的核心操做。Dijkstra 算法是以起点为中心,向外层层扩展,直到扩展到终点为止。根据这特性,用 BFS 来实现时再好不过了,注意 while 循环里的第一层 for 循环,这保证了每一层的结点先被处理完,才会进入进入下一层,这种特性在用 BFS 遍历迷宫统计步数的时候很重要。对于每个结点,都跟其周围的结点进行 Relaxtion 操做,从而更新周围结点的距离值。为了防止重复比较,须要使用 visited 数组来记录已访问过的结点,最后在全部的最小路径中选最大的返回,注意,若是结果 res 为 INT_MAX,说明有些结点是没法到达的,返回 -1。普通的实现方法的时间复杂度为 O(V2),基于优先队列的实现方法的时间复杂度为 O(E + VlogV),其中V和E分别为结点和边的个数,这里多说一句,Dijkstra 算法这种类贪心算法的机制,使得其没法处理有负权重的最短距离,还好这道题的权重都是正数,参见代码以下:数组
解法一:优化
class Solution { public: int networkDelayTime(vector<vector<int>>& times, int N, int K) { int res = 0; vector<vector<int>> edges(101, vector<int>(101, -1)); queue<int> q{{K}}; vector<int> dist(N + 1, INT_MAX); dist[K] = 0; for (auto e : times) edges[e[0]][e[1]] = e[2]; while (!q.empty()) { unordered_set<int> visited; for (int i = q.size(); i > 0; --i) { int u = q.front(); q.pop(); for (int v = 1; v <= 100; ++v) { if (edges[u][v] != -1 && dist[u] + edges[u][v] < dist[v]) { if (!visited.count(v)) { visited.insert(v); q.push(v); } dist[v] = dist[u] + edges[u][v]; } } } } for (int i = 1; i <= N; ++i) { res = max(res, dist[i]); } return res == INT_MAX ? -1 : res; } };
下面来看基于 Bellman-Ford 算法的解法,时间复杂度是 O(VE),V和E分别是结点和边的个数。这种算法是基于 DP 来求全局最优解,原理是对图进行 V - 1 次松弛操做,这里的V是全部结点的个数(为啥是 V-1 次呢,由于最短路径最多只有 V-1 条边,因此只需循环 V-1 次),在重复计算中,使得每一个结点的距离被不停的更新,直到得到最小的距离,这种设计方法融合了暴力搜索之美,写法简洁又不失优雅。以前提到了,Bellman-Ford 算法能够处理负权重的状况,可是不能有负环存在,通常形式的写法中最后一部分是检测负环的,若是存在负环则报错。不能有负环缘由是,每转一圈,权重和都在减少,能够无限转,那么最后的最小距离都是负无穷,无心义了。没有负环的话,V-1 次循环后各点的最小距离应该已经收敛了,因此在检测负环时,就再循环一次,若是最小距离还能更新的话,就说明存在负环。这道题因为不存在负权重,因此就不检测了,参见代码以下:spa
解法二:设计
class Solution { public: int networkDelayTime(vector<vector<int>>& times, int N, int K) { int res = 0; vector<int> dist(N + 1, INT_MAX); dist[K] = 0; for (int i = 1; i < N; ++i) { for (auto e : times) { int u = e[0], v = e[1], w = e[2]; if (dist[u] != INT_MAX && dist[v] > dist[u] + w) { dist[v] = dist[u] + w; } } } for (int i = 1; i <= N; ++i) { res = max(res, dist[i]); } return res == INT_MAX ? -1 : res; } };
下面这种解法是 Bellman Ford 解法的优化版本,由热心网友旅叶提供。之因此能提升运行速度,是由于使用了队列 queue,这样对于每一个结点,不用都松弛全部的边,由于大多数的松弛计算都是无用功。优化的方法是,若某个点的 dist 值不变,不去更新它,只有当某个点的 dist 值被更新了,才将其加入 queue,并去更新跟其相连的点,同时还须要加入 HashSet,以避免被反复错误更新,这样的时间复杂度能够优化到 O(E+V)。Java 版的代码在评论区三楼,旅叶声称能够 beat 百分之九十多,但博主改写的这个 C++ 版本的却只能 beat 百分之二十多,hmm,因缺斯汀。不过仍是要比上面的解法二快不少,博主又仔细看了看,发现很像解法一和解法二的混合版本哈,参见代码以下:code
解法三:
class Solution { public: int networkDelayTime(vector<vector<int>>& times, int N, int K) { int res = 0; unordered_map<int, vector<pair<int, int>>> edges; vector<int> dist(N + 1, INT_MAX); queue<int> q{{K}}; dist[K] = 0; for (auto e : times) edges[e[0]].push_back({e[1], e[2]}); while (!q.empty()) { int u = q.front(); q.pop(); unordered_set<int> visited; for (auto e : edges[u]) { int v = e.first, w = e.second; if (dist[u] != INT_MAX && dist[u] + w < dist[v]) { dist[v] = dist[u] + w; if (visited.count(v)) continue; visited.insert(v); q.push(v); } } } for (int i = 1; i <= N; ++i) { res = max(res, dist[i]); } return res == INT_MAX ? -1 : res; } };
讨论:最后再来讲说这个 Floyd 算法,这也是一种经典的动态规划算法,目的是要找结点i到结点j的最短路径。而结点i到结点j的走法就两种可能,一种是直接从结点i到结点j,另外一种是通过若干个结点k到达结点j。因此对于每一个中间结点k,检查 dist(i, k) + dist(k, j) < dist(i, j) 是否成立,成立的话就松弛它,这样遍历完全部的结点k,dist(i, j) 中就是结点i到结点j的最短距离了。时间复杂度是 O(V3),到处透露着暴力美学。除了这三种算法外,还有一些很相似的优化算法,好比 Bellman-Ford 的优化算法- SPFA 算法,还有融合了 Bellman-Ford 和 Dijkstra 算法的高效的多源最短路径算法- Johnson 算法,这里就不过多赘述了,感兴趣的童鞋可尽情的 Google 之~
Github 同步地址:
https://github.com/grandyang/leetcode/issues/743
参考资料:
https://leetcode.com/problems/network-delay-time/description/
https://leetcode.com/problems/network-delay-time/discuss/109982/C++-Bellman-Ford