编程题-网络延迟时间(中等-最优化算法)

题目:

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>> &times, 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>> &times, 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,并且可以通过访问它的成员 firstsecond 来分别访问这两个整数。pair 支持比较操作符(如 <, >, == 等),并按元素逐一进行比较(pair可结合sort()自定义排序函数一起使用)。

  • pair<>:适用于存储 两种相关的数据,通常在需要表达一对相关数据时使用。比如,在算法中表示一对坐标,或者在关联容器(如 std::map)中表示键值对。其主要特性是它能存储不同类型的两个元素。

  • vector<>:适用于存储 多个同类型的元素,并且你可能需要动态增加或删除元素vector 提供了高效的随机访问、添加和删除元素的功能,是最常用的动态数组容器。其主要特性是存储同类型的元素,并且可以动态调整大小。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值