题目:
有 n
个网络节点,标记为 1
到 n
。
给你一个列表 times
,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi)
,其中 ui
是源节点,vi
是目标节点, wi
是一个信号从源节点传递到目标节点的时间。
现在,从某个节点 K
发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1
。
前言:
本题需要用到单源最短路径算法Dijkstra,其主要思想是贪心。将所有节点分成两类:已确定从起点到当前点的最短路径长度的节点,以及未确定从起点到当前点的最短路径长度的节点(下面简称【未确定的节点】和【已确定的节点】)。
每次从【未确定节点】中取一个与起点距离最短的点,将它归类为【已确定节点】,并用它【更新】从起点到其他所有【未确定节点】的距离。直到所有点都被归类为【已确定节点】。用节点A【更新】节点B的意思是,用起点到节点A的最短路长度加上节点A到节点B的边的长度,去比较起点到节点B的最短路长度,如果前者小于后者,就用前者更新后者。
每次选择【未确定节点】时,起点到它的最短路径的长度可以被确定。可以这样理解,我们已经用了每一个【已确定节点】更新过了当前节点,无需再次更新(因为一个点不能多次到达)。而当前节点已经是所有【未确定节点】中与起点距离最短的点,不可能被其他【未确定节点】更新。所以当前节点可以被归类为【已确定节点】。
解法一(鲁棒遍历):
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
int index = 0;
//number为节点到k最近的距离,初始值均为10000000
vector<int> number(100,10000000);
//定义hashTable1统计每个节点传输到另一个节点以及距离
unordered_map<int, vector<vector<int>>> hashTable1;
for(int i=0; i<times.size();i++){
vector<int> a;
a.push_back(times[i][1]);
a.push_back(times[i][2]);
hashTable1[times[i][0]].push_back(a);
}
//初始化初始节点的值k
number[k]=0;
auto it = hashTable1.find(k);
//定义未访问更新的节点hashTable2的索引值
vector<int> hashTable2;
//定义hashTable3统计多少个节点到此节点的数量
unordered_map<int, int> hashTable3;
unordered_map<int, int> hashTable4;
for(int i=0;i<times.size();i++){
hashTable3[times[i][1]]++;
}
if(it==hashTable1.end()){
return -1;
}
for(int i=0;i<hashTable1[k].size();i++){
hashTable2.push_back(hashTable1[k][i][0]);
number[hashTable1[k][i][0]] = hashTable1[k][i][1];
}
//遍历所有的传输节点,并记录其到起始节点k的路径,并不断更新所有节点路径直到遍历完成后达到最短路径
//为了防止无限循环,记录每个节点最大的输入节点数量,当达到此最大输入节点数量时,应直接删除
while(!hashTable2.empty()){
int x = hashTable2[0];
hashTable2.erase(hashTable2.begin());
auto it = hashTable1.find(x);
if(hashTable4[x]>hashTable3[x]){
continue;
}
hashTable4[x]++;
for(int i=0;i<hashTable1[x].size();i++){
auto itt = find(hashTable2.begin(), hashTable2.end(), hashTable1[x][i][0]);
if(itt == hashTable2.end()){
hashTable2.push_back(hashTable1[x][i][0]);
}
number[hashTable1[x][i][0]] = min(number[x] + hashTable1[x][i][1], number[hashTable1[x][i][0]]);
}
}
for(int i=1; i<n+1; i++){
if(number[i]==10000000){
return -1;
}
else{
index=max(number[i], index);
}
}
return index;
}
};
存在很多重复访问节点数量的情况,时间复杂度相比Dijkstra贪心思想的算法要高很多。
解法二(Dijkstra 算法-枚举写法):
根据题意,从节点k发出信号,到达节点x的时间就是节点k到节点x的最短路的长度。因此需求出节点k到其余所有点的最短路径,其中的最大值就是答案。若存在从k出发无法到达的点,则返回-1。下面的代码将节点编号减小了 1,从而使节点编号位于 [0, n−1] 范围。实现如下:
class Solution {
public:
int networkDelayTime(vector<vector<int>> ×, int n, int k) {
const int inf = INT_MAX / 2;
//创建g数据结构(单源路径代价距离图),包含一个节点x到另一个节点y,以及其单源路径的代价距离inf
vector<vector<int>> g(n, vector<int>(n, inf));
for (auto &t : times) {
int x = t[0] - 1, y = t[1] - 1;
g[x][y] = t[2];
}
//dist为从节点k出发到其他各个节点的最短路径距离,初始化时出发点dist[k-1]=0。
vector<int> dist(n, inf);
dist[k - 1] = 0;
vector<int> used(n);
for (int i = 0; i < n; ++i) {
int x = -1;
//for循环找到dist中最近的路径距离作为确定的点,其中确定的点的距离即为到起始点k的最短距离
for (int y = 0; y < n; ++y) {
if (!used[y] && (x == -1 || dist[y] < dist[x])) {
x = y;
}
}
//并利用确定的点去更新其余未确定点最短距离(贪心最短距离并不是全局最短距离)
used[x] = true;
for (int y = 0; y < n; ++y) {
dist[y] = min(dist[y], dist[x] + g[x][y]);
}
}
int ans = *max_element(dist.begin(), dist.end());
return ans == inf ? -1 : ans;
}
};
枚举写法的复杂度如下:时间复杂度:O(n^2+m),其中 m 是数组 times 的长度。空间复杂度:O(n^2)。邻接矩阵需占用 O(n2) 的空间。除了枚举,我们还可以使用一个最小根堆来寻找【未确定节点】中与起点距离最近的点。
解法三(DijKstra算法-堆的写法)
class Solution {
public:
int networkDelayTime(vector<vector<int>> ×, int n, int k) {
const int inf = INT_MAX / 2;
//定义pair<int,int>对,通过.emplace_back()函数添加,其中y为目的节点,t[2]为代价成本
vector<vector<pair<int, int>>> g(n);
for (auto &t : times) {
int x = t[0] - 1, y = t[1] - 1;
g[x].emplace_back(y, t[2]);
}
//dist为节点n到其实节点k的最小路径(过程中不断更新dist中元素值)
vector<int> dist(n, inf);
dist[k - 1] = 0;
//使用greater<>将堆定义为最小堆,初始化堆q.emplace(0,k-1)
//堆中存储的是所有节点至初始节点的距离
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> q;
q.emplace(0, k - 1);
while (!q.empty()) {
//从堆中选取q.top()最小的路径值所对应的节点作为确定节点,并进行遍历
auto p = q.top();
q.pop();
//time表示节点p.second到初始节点k的距离
int time = p.first, x = p.second;
//剔除已经确定的节点,当dist[x]<time时,直接跳过(与标记确定节点和未确定节点效果类似)
if (dist[x] < time) {
continue;
}
//根据最小k至已确定节点x的距离,更新dist中其他未确定节点的距离
//并将更新的最短路径添加到堆中(有重复的节点y,不同的距离d存在)
for (auto &e : g[x]) {
//其中e.second是y为以x为起点到y节点的距离,距离d为以k为起点(中间经过x)到节点y的距离
//dist[y]为以k为起点到节点y的最小距离
int y = e.first, d = dist[x] + e.second;
if (d < dist[y]) {
dist[y] = d;
q.emplace(d, y);
}
}
}
int ans = *max_element(dist.begin(), dist.end());
return ans == inf ? -1 : ans;
}
};
堆的写法复杂度如下:时间复杂度:O(mlogm),其中 m 是数组 times 的长度。空间复杂度:O(n+m)。值得注意的是,由于本题边数远大于点数,是一张稠密图,因此在运行时间上,枚举写法要略快于堆的写法。
笔者小记:
1、Dijkstra算法原理:Dijkstra 算法是用于解决 单源最短路径问题 的经典算法。通过不断扩展已知的最短路径集合,逐步寻找从起点到所有其他节点的最短路径。1、初始化设置起点距离为0;2、选择当前未处理节点中最小距离的节点;3、更新邻接节点的最短路径;4、标记当前节点为已处理;5、重复步骤 2 和 3,直到所有节点都被处理完。6、最终,每个节点的距离即为从起点到该节点的最短路径。
2、堆数据结构:在 C++ 中,堆(Heap)是一种常见的数据结构,它通常被用来实现优先队列(priority queue)等功能。堆的特点是它满足 完全二叉树 的结构,并且有两种主要的类型:最大堆(Max-Heap) 和 最小堆(Min-Heap)。默认情况下std::priority_queue
是一个最大堆,堆顶元素是最大值。要创建最小堆,可以使用 greater<>
作为自定义比较器。greater<>
是一个标准的比较器,用于升序排序,使得堆顶是最小值。
- 最大堆(Max-Heap):在最大堆中,父节点的值总是大于或等于其子节点的值,堆顶元素是最大值。
- 最小堆(Min-Heap):在最小堆中,父节点的值总是小于或等于其子节点的值,堆顶元素是最小值。
- 插入(emplace):
O(log n)
,因为在插入新元素时,可能需要上浮(bubble-up)来恢复堆的性质 - 删除堆顶元素(pop):
O(log n)
,因为删除堆顶元素后,可能需要下沉(sink-down)来恢复堆的性质。 - 访问堆顶元素(top):
O(1)
,堆顶元素总是最大(最大堆)或最小(最小堆)。 - 堆化(make_heap):
O(n)
,通过从后往前的方式调整堆。
3、max_exement()函数:std::max_element
是 C++ 标准库中的一个算法函数,用于在容器中查找最大元素的 迭代器。*max_exement(dist.begin(), dist.end())表示取出最大元素的值。
4、pair<int, int>
是一个非常常用的数据结构,它用于存储两个值,其中每个值的类型都可以是任意的。在 pair<int, int>
中,int
表示两个存储的元素的类型是 int
,并且可以通过访问它的成员 first
和 second
来分别访问这两个整数。pair
支持比较操作符(如 <
, >
, ==
等),并按元素逐一进行比较(pair可结合sort()自定义排序函数一起使用)。
-
pair<>
:适用于存储 两种相关的数据,通常在需要表达一对相关数据时使用。比如,在算法中表示一对坐标,或者在关联容器(如std::map
)中表示键值对。其主要特性是它能存储不同类型的两个元素。 -
vector<>
:适用于存储 多个同类型的元素,并且你可能需要动态增加或删除元素。vector
提供了高效的随机访问、添加和删除元素的功能,是最常用的动态数组容器。其主要特性是存储同类型的元素,并且可以动态调整大小。