目录
题目描述
有 n 个网络节点,标记为 1 到 n。
给你一个列表 times,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。
现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。
示例:
方法一(朴素dijkstra算法):
class Solution {
public:
const int inf = INT_MAX / 2;
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
//初始化邻接矩阵,节点到自身的权值为0,到其他节点的权值为无穷大
vector<vector<int>> w(n + 1, vector<int>(n + 1, inf)); //记录节点间的权值
//节点到自身的距离为0
for (int i = 1; i <= n; i++)
{
w[i][i] = 0;
}
//初始化距离为无穷大
vector<int> minlen(n + 1, inf); //minlen[x]表示起点到达节点x的最短距离
//初始化各个节点的是否确定了最短路径为false
vector<int> visit(n + 1, false); //visit[x]表示节点是否已经确定了最短路径
//根据times存图
for (int i = 0; i < times.size(); i++)
{
int a = times[i][0];
int b = times[i][1];
w[a][b] = times[i][2];
}
//开始dijkstra算法
//首先初始化起点k到自身的最短距离为0
minlen[k] = 0;
//i并无实际用途,这里只是循环n次,每次确定一个最短路径
for (int i = 1; i <= n; i++)
{
int t = -1;
//遍历minlen,找到那些还未确定最短路径的节点里面,路径最短的那个节点
for (int j = 1; j <= n; j++)
{
//n次循环刚好能够取到所有节点,而且每个节点肯定能够被取到一次,
//所以不用担心出现未能进入该判断语句而导致t为-1的情况
if (!visit[j] && (t == -1 || minlen[t] > minlen[j]))
{
t = j;
}
}
visit[t] = true; //选出的节点t可以确定它现在的路径值,就是最短路径
//遍历minlen,通过以t作为中间点,更新k到各个节点的路径值
for (int j = 1; j <= n; j++)
{
minlen[j] = min(minlen[j], minlen[t] + w[t][j]);
}
}
int maxtime = INT_MIN;
//这里不能写成for(int len : minlen),因为minlen[0]没有被用到,是没有意义的
for (int i = 1; i <= n; i++)
{
maxtime = max(maxtime, minlen[i]);
}
//初始化时就已经将minlen[k]赋为0了,所以肯定不是判断maxtime是否为INT_MIN
//有可能出现k无法到达的节点,这种情况下k和其的距离为无穷大,所以是和inf比较
return maxtime == inf ? -1 : maxtime;
}
};
djikstra算法,用于求某个起点到其它节点的最短路径,不能用于负权图,参考这篇:
数据结构--Dijkstra算法最清楚的讲解_heroacool的专栏-CSDN博客_数据结构dijkstra算法
忘了自己大二上数据结构时有没有亲手写过dijkstra算法了,写起来异常费劲。
注意最大值不能直接使用INT_MAX,否则下面的这条语句,某个正数加上INT_MAX之后,将会变为一个负数,导致出错。
for (int j = 1; j <= n; j++)
{
minlen[j] = min(minlen[j], minlen[t] + w[t][j]);
}
- 时间复杂度:O(n^2)
- 空间复杂度:O(n^2)
方法二(堆优化的dijkstra算法):
class Solution {
public:
const int inf = INT_MAX / 2;
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
vector<vector<int>> w(n + 1, vector<int>(n + 1, inf));
//节点到自身的距离为0
for (int i = 1; i <= n; i++)
{
w[i][i] = 0;
}
vector<int> minlen(n + 1, inf);
vector<int> visit(n + 1, false);
for (int i = 0; i < times.size(); i++)
{
int a = times[i][0];
int b = times[i][1];
w[a][b] = times[i][2];
}
//优先队列中的元素(d, x),表示起点到节点x的路径为d
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> que;
//可以不用初始化minlen[k] = 0,因为在下一步循环中将会赋值
que.emplace(0, k);
while (!que.empty())
{
int t = que.top().second;
//某个节点可能多次更新了最短路径后,才被访问,
//而在被访问之前优先队列中将会存入多次的到这个节点的路径值,
//所以在后续需要将这些无用的值去掉
if (visit[t])
{
que.pop();
continue;
}
visit[t] = true;
minlen[t] = que.top().first;
//注意这里需要及时将头结点弹出,因为后续的循环中将会向优先队列中加入新的结点,
//可能会出现某个路径值更小的元素排到头结点去,
//等循环过后再弹出的就不一定是原来的头结点了
que.pop();
for (int i = 1; i <= n; i++)
{
if (!visit[i] && minlen[i] > minlen[t] + w[t][i])
{
minlen[i] = minlen[t] + w[t][i];
que.emplace(minlen[i], i);
}
}
}
int maxtime = INT_MIN;
for (int i = 1; i <= n; i++)
{
maxtime = max(maxtime, minlen[i]);
}
return maxtime == inf ? -1 : maxtime;
}
};
与朴素的dijkstra算法不同的是,这里使用了优先队列来求更新最短路径,所以会时间复杂度会降低一些。
时间复杂度:O(mlogm),其中 mm 是数组 times 的长度。
空间复杂度:O(n+m)。
方法三(Floyd算法):
class Solution {
public:
const int inf = INT_MAX / 2;
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
vector<vector<int>> w(n + 1, vector<int>(n + 1, inf));
//节点到自身的距离为0
for (int i = 1; i <= n; i++)
{
w[i][i] = 0;
}
//存图
for (int i = 0; i < times.size(); i++)
{
int a = times[i][0];
int b = times[i][1];
w[a][b] = times[i][2];
}
//开始floyd算法
for (int t = 1; t <= n; t++) //依次在能通过节点t的情况下,求最短路径
{
//更新节点i和节点j之间的最短路径
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= n; j++)
{
w[i][j] = min(w[i][j], w[i][t] + w[t][j]);
}
}
}
int maxtime = INT_MIN;
for (int i = 1; i <= n; i++)
{
maxtime = max(maxtime, w[k][i]);
}
return maxtime == inf ? -1 : maxtime;
}
};
Floyd算法,能够求出任意两点之间的最短路径,同样不能用于负全图,参考这篇:
Floyd-傻子也能看懂的弗洛伊德算法(转) - Yuliang.wang - 博客园
这题用Floyd算法不是优先解,不过可以用来复习一下,毕竟我都忘了Floyd算法是干嘛的了hh
- 时间复杂度:O(n³)
- 空间复杂度:O(n²)
方法四(Bellman Ford算法):
class Solution {
public:
const int inf = INT_MAX / 2;
//表示由节点a到节点b的一条边,其权值为w
struct edge
{
int a;
int b;
int w;
};
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
//这里是易错点,Bellman-ford算法通过存储边来存储图,所以数组edges的大小应该设置为m,而不是n
int m = times.size();
vector<edge> edges(m);
vector<int> minlen(n + 1, inf);
//存图
for (int i = 0; i < m; i++)
{
edges[i].a = times[i][0];
edges[i].b = times[i][1];
edges[i].w = times[i][2];
}
minlen[k] = 0;
//开始 Bellman-Ford算法
//每一轮确定一个到起点的最短路径,除了节点k外共有n - 1个节点,所以循环n - 1轮
for (int i = 0; i < n - 1; i++) //这里i无实际作用,只用于循环
{
for (int index = 0; index < m; index++)
{
int cura = edges[index].a;
int curb = edges[index].b;
int curw = edges[index].w;
//如果当前起点先到节点cura,再到节点curb的路径更短,就更新最短路径
minlen[curb] = min(minlen[curb], minlen[cura] + curw);
}
}
int maxtime = INT_MIN;
for (int i = 1; i <= n ; i++)
{
maxtime = max(maxtime, minlen[i]);
}
return maxtime == inf ? -1 : maxtime;
}
};
Bellman Ford算法用于解决带有负权边的单源最短路径问题,参考这篇:
Bellman-ford(解决负权边)_yuewenyao的博客-CSDN博客
这里使用了邻接表来存储图,Bell Ford算法的思想并不难,跟dijkstra算法一样,找最短路径的关键在于松弛。
Bell Ford还可以用于判断图中是否含有负权环,当循环n - 1次之后,若还能够更新最短路径,就说明图中含有负权环。
- 时间复杂度:O(n*m)O(n∗m)
- 空间复杂度:O(m)O(m)
方法五(SPFA算法,数组实现邻接表):
class Solution {
public:
const int inf = INT_MAX / 2;
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
int m = times.size(); //共有m条边
vector<int> u(m), v(m), w(m); //u[i],v[i],w[i]分别表示第i条边的起点、终点和权值
//firstedge数组的大小由节点的个数决定,nextedge数组的大小由边的个数决定
vector<int> firstedge(n + 1, -1); //firstedge[a]表示从a这个节点出发的某一条边,初始化为-1
vector<int> nextedge(m); //nextedge[i]表示与i这条边相连的下一条边,当已经没有下一条边时,nextedge[i] = -1
//存图
for (int i = 0; i < m; i++)
{
int a = times[i][0];
int b = times[i][1];
u[i] = a;
v[i] = b;
w[i] = times[i][2];
nextedge[i] = firstedge[a];
firstedge[a] = i;
}
queue<int> que;
vector<int> inque(n + 1, false); //用来判断某个节点是否在队列中
vector<int> minlen(n + 1, inf); //起点到某个节点的最短路径
minlen[k] = 0;
que.push(k);
inque[k] = true;
//开始SPFA算法
while (!que.empty())
{
//从队中取出一个节点
int t = que.front();
inque[t] = false;
que.pop();
//遍历从这个节点出发的所有边
for (int i = firstedge[t]; i != -1; i = nextedge[i])
{
int a = u[i];
int b = v[i];
//如果先到a,再到b的路径比较短,就更新
if (minlen[b] > minlen[a] + w[i])
{
minlen[b] = minlen[a] + w[i];
//如果点b已经在队列里了,就继续
if (inque[b])
{
continue;
}
//否则将点b加入队列
que.push(b);
inque[b] = true;
}
}
}
int maxtime = INT_MIN;
for (int i = 1; i <= n; i++)
{
maxtime = max(maxtime, minlen[i]);
}
return maxtime == inf ? -1 : maxtime;
}
};
最后的SPFA算法看到要吐了,主要是一直理解不了用数组实现的邻接表,看了老半天,最终还是专门研究了一下,参考这篇:
作为bellman ford算法的优化,SPFA算法在计算含负权边的图中的单源最短路径问题时,更加高效。同样它也能用于判断图中是否存在负权环,只要某个节点在队列中的出现次数大于n次(n为节点个数)就说明含有负权环。参考这篇文章:
SPFA 算法详解( 强大图解,不会都难!)_muxi@Achilles的专栏-CSDN博客
用数组来实现邻接表确实是个巧妙的方法,相比于利用链表来实现,用数组虽然较为抽象,但是只要理解了之后就会发现其方便又简洁。
SPFA算法和前面的dijkstra算法、floyd算法、bellman-ford算法类似,都是要一个点一个点地进行松弛操作,思想是一样的。
时间复杂度:通常情况下复杂度为 O(k∗m),kk 一般为 4 到 5,最坏情况下仍为 O(n∗m),当数据为网格图时,复杂度会从 O(k∗m) 退化为 O(n∗m)。
方法五(SPFA算法,结构体实现邻接表):
class Solution {
public:
const int inf = INT_MAX / 2;
struct node
{
//存储与当前节点相邻的节点,第一个数字表示节点,第二个数字表示权值
list<pair<int, int>> adjnode;
};
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
vector<node> nodes(n + 1);
vector<int> minlen(n + 1, inf); //存储各个节点的最短路径
int m = times.size();
//存图
for (int i = 0; i < m; i++)
{
int a = times[i][0];
int b = times[i][1];
int w = times[i][2];
nodes[a].adjnode.push_back({ b, w });
}
//开始SPFA算法
queue<int> que; //存储节点的队列
vector<int> inque(n + 1, false); //判断某个节点是否在队列中
minlen[k] = 0;
que.push(k);
inque[k] = true;
while (!que.empty())
{
int t = que.front();
que.pop();
inque[t] = false;
list<pair<int, int>>::iterator it;
for (it = nodes[t].adjnode.begin(); it != nodes[t].adjnode.end(); it++)
{
int b = it->first;
int w = it->second;
//将刷新了的节点入队
if (minlen[b] > minlen[t] + w)
{
minlen[b] = minlen[t] + w;
//已经在队列中的节点不用重复入队
if (inque[b])
{
continue;
}
que.push(b);
inque[b] = true;
}
}
}
int maxtime = INT_MIN;
for (int i = 1; i <= n; i++)
{
maxtime = max(maxtime, minlen[i]);
}
return maxtime == inf ? -1 : maxtime;
}
};
改用了结构体来构造邻接表,其余SPFA算法步骤类似。
第一次使用list这个容器,配合结构体一起使用的话,能够很方便地构造图的邻接表,相比于使用数组实现要更加轻松不少,而且也更形象易懂。