邻接矩阵和邻接表_[力扣743] 带权邻接表的单源最短路

896e39d1908647e561ff98087d92044c.png

题目链接

743. 网络延迟时间

题目描述

有 N 个网络节点,标记为 1 到 N。

给定一个列表 times,表示信号经过有向边的传递时间。 times[i] = (u, v, w),其中 u 是源节点,v 是目标节点, w 是一个信号从源节点传递到目标节点的时间。

现在,我们从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1。

样例

输入:times = [[2,1,1],[2,3,1],[3,4,1]], N = 4, K = 2 输出:2

数据范围

N 的范围在 [1, 100] 之间。
K 的范围在 [1, N] 之间。
times 的长度在 [1, 6000] 之间。
所有的边 times[i] = (u, v, w) 都有 1 <= u, v <= N 且 0 <= w <= 100。

算法

图论的题目一般是 3 步,建图 -> 图论算法 -> 后处理。

本题也是一样的思路:先建邻接表,然后求单源最短路,然后根据找到的各个点的最短路,找到从起始点出发可以到达的最远的距离。

一般比较难的题目难点都是在建图这一步。有的是非常隐晦的图论问题,想不到建图处理,例如 127. 单词接龙;有的是建图过程中 corner case 非常多,很容易漏,例如 444. 序列重建。成功完成建图之后(一般是建邻接表比较多),之后的图论算法就比较模板化了。

当边的权都相同时,求最短路直接一趟 BFS就可以,典型题目 1091. 二进制矩阵中的最短路径。当边权不同的时候有以下几种。

dijkstra 数组实现: O(E + V^2),有贪心思想,不能处理负权
dijkstra 堆实现: O(ElogV + VlogV),有贪心思想,不能处理负权
bellman ford: O(VE), 可以处理负权,可以检测负环
spfa: O(VE), bellman ford 的优化, 可以处理负权,可以检测负环
floyd: O(V^3) 可以把所有点之间的最短路径求出,如果求多源最短路,时间复杂度可摊销

如果没有负权的话,一般使用 dijkstra 最好。

代码(c++)

代码框架就是 建立邻接表 -> 单源最短路算法 -> 后处理

单源最短路算法的 5 种算法都是模板,对应的实现在引申部分

class Solution {
public:
    int networkDelayTime(vector<vector<int>>& times, int N, int K) {
        vector<vector<vector<int> > > g(N + 1);
        for(vector<int> &edge: times)
            g[edge[0]].push_back({edge[1], edge[2]});

        // 求带权邻接表的最短路
        vector<int> d = dijkstra_array(g, K, N);
        // vector<int> d = dijkstra_heap(g, K, N);
        // vector<int> d = bellman_ford(g, K, N);
        // vector<int> d = spfa(g, K, N);
        // vector<int> d = floyd(g, K, N);

        int res = 0;
        for(int i = 1; i <= N; i++)
        {
            if(d[i] == -1)
                return -1;
            res = max(res, d[i]);
        }
        return res;
    }
};

引申: 权邻接表的单源最短路模板代码

接口:

// g 是建好的邻接表,start 是起始点,N 是节点个数; 返回各个点到 start 的最短路径
vector<int> shortest_path(vector<vector<vector<int> > >& g, int start, int N);

5个算法的接口都一样,下面是对应的模板代码,原理部分没有展开。

如果能够将问题转化成模板问题,再套模板就轻松愉快了。这个有点类似下棋或者炒菜的背谱,只会背谱肯定是不好的,但是背谱可以快速下出挑不出什么毛病的棋,快速炒出80分的菜。

1. dijkstra 数组实现

vector<int> dijkstra_array(vector<vector<vector<int> > >& g, int start, int N)
{
    // dijkstra 数组实现 O(E + V^2)
    // 不能有负权

    // 存放 start 到各个点的最短路径
    vector<int> d(N + 1, -1);
    d[start] = 0;
    // 记录是否找到 start 到该点的最短路径
    vector<bool> visited(N + 1, false);
    visited[start] = true;

    // 初始化 start 到各个点的距离
    for(vector<int> son: g[start])
        d[son[0]] = son[1];

    for(int cnt = 1; cnt <= N - 1; ++cnt)
    {
        int min_val = INT_MAX / 2, min_idx = 0;
        // 遍历所有节点,找到离 start 最近的节点
        for(int i = 1; i <= N; ++i)
        {
            if(d[i] != -1 && !visited[i] && d[i] < min_val)
            {
                min_idx = i;
                min_val = d[i];
            }
        }

        // 标记离 start 最近距离节点已经找到
        visited[min_idx] = true;

        // 根据刚刚找到的距离 start 最短的节点,
        // 通过该节点更新 start 与其它节点的距离
        for(vector<int> son: g[min_idx])
        {
            if(d[son[0]] != -1) // 之前路径与当前更新路径的最小值
                d[son[0]] = min(d[son[0]], min_val + son[1]);
            else // 该节点第一次访问,直接更新
                d[son[0]] = min_val + son[1];
        }
    }
    return d;
}

2. dijkstra 堆实现

vector<int> dijkstra_heap(vector<vector<vector<int> > >& g, int start, int N)
{
    // dijkstra 堆实现 O(ElogV + VlogV)
    // 不能有负权

    // 存放 start 到各个点的最短路径
    vector<int> d(N + 1, INT_MAX / 2);
    d[start] = 0;

    priority_queue<vector<int>, vector<vector<int> >, Cmp> pq; // 队列元素 (节点编号,到 start 的距离)
    pq.push({start, 0});
    while(!pq.empty())
    {
        vector<int> cur = pq.top();
        pq.pop();
        if(d[cur[0]] < cur[1]) continue;
        for(vector<int> son: g[cur[0]])
        {
            if(d[son[0]] <= d[cur[0]] + son[1]) continue;
            d[son[0]] = d[cur[0]] + son[1];
            pq.push({son[0], d[son[0]]});
        }
    }
    return d;
}

struct Cmp
{
    bool operator() (const vector<int>& item1, const vector<int>& item2)
    {
        return item1[1] > item2[1]; // 最小堆
    }
};

3. bellman ford

松弛操作,它的原理是著名的定理:“三角形两边之和大于第三边”

vector<int> bellman_ford(vector<vector<vector<int> > >& g, int start, int N)
{
    // bellman ford  O(VE)
    // 可以检测负环

    vector<int> d(N + 1, -1);
    d[start] = 0;

    // 进行 N - 1 轮松弛
    // 因为任意两点之间最短路最多包含 N - 1 条边
    for(int cnt = 1; cnt <= N - 1; ++cnt)
    {
        // u: 源节点,v: 子节点, w: uv 的权
        for(int u = 1; u <= N; ++u)
        {
            if(d[u] == -1) continue;
            for(vector<int> &son: g[u])
            {
                int v = son[0], w = son[1];
                // 判断能否通过 u -> v 缩短 d[v] (松弛)
                if(d[u] + w < d[v] || d[v] == -1)
                    d[v] = d[u] + w;
            }
        }
    }
    /* 可以检测负环
    for(int u = 1; u <= N; ++u)
    {
        for(vector<int> &son: g[u])
        {
            int v = son[0], w = son[1];
            if(d[u] + w < d[v])
                // 有负环
        }
    }*/
    return d;
}

4. spfa

SPFA算法的基本思想:在Bellman-Ford算法中,很多松弛操作其实都是没有必要的,例如对于一条从 x 到 y 的边,如果连 x 都还没被松弛,那 y 肯定也还不能被 x 松弛。用一个队列来存储已经被松弛过的点,然后用队列里的点去松弛其他点,就可以避免用一个还没有被松弛的点去松弛另外的点。

vector<int> spfa(vector<vector<vector<int> > >& g, int start, int N)
{
    // 与 bellman ford 相同, O(VE)
    // 可以检测负环

    vector<int> d(N + 1, -1);
    d[start] = 0;
    queue<int, list<int> > q;
    q.push(start);

    // 记录每个点到 start 的节点个数
    vector<int> cnt(N + 1, 0);
    cnt[start] = 1;
    while(!q.empty())
    {
        int cur = q.front();
        q.pop();
        for(vector<int> &son: g[cur])
        {
            if(d[son[0]] == -1 || d[son[0]] > d[cur] + son[1])
            {
                cnt[son[0]] = cnt[cur] + 1;
                if(cnt[son[0]] > N) return d; // 若 son 到 start 的节点个数大于 N 了说明有负环
                // 当最短距离发生变化且不在队列中时,将该节点加入队列
                d[son[0]] = d[cur] + son[1];
                q.push(son[0]);
            }
        }
    }
    return d;
}

5. floyd

vector<int> floyd(vector<vector<vector<int> > >& g, int start, int N)
{
    // floyd 需要邻接矩阵 O(V^3)
    // 可以做多源最短路
    vector<vector<int> > adj_matrix(N + 1, vector<int>(N + 1, -1));
    for(int i = 1; i <= N; ++i)
        adj_matrix[i][i] = 0;
    for(int u = 1; u <= N; ++u)
        for(vector<int> son: g[u])
            adj_matrix[u][son[0]] = son[1];

    // 遍历所有节点,其中 k 是用于松弛的点
    for(int k = 1; k <= N; ++k)
        for(int i = 1; i <= N; ++i)
            for(int j = 1; j <= N; ++j)
                // 使用 k 松弛 i -> j 的最短路径
                if(adj_matrix[i][k] != -1 && adj_matrix[k][j] != -1)
                {
                    if(adj_matrix[i][j] != -1)
                        adj_matrix[i][j] = min(adj_matrix[i][j], adj_matrix[i][k] + adj_matrix[k][j]);
                    else
                        adj_matrix[i][j] = adj_matrix[i][k] + adj_matrix[k][j];
                }

    vector<int> d(N + 1, -1);
    for(int i = 1; i <= N; ++i)
        d[i] = adj_matrix[start][i];

    return d;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值