Dijkstra算法

2022年 10月 14日 43点热度 0人点赞

核心思想

  1. 所有的点分类两类 $\text{A}$ (已经根据此集合中的点去更新其他点的距离), $\text{B}$ (未根据此集合中的点去更新其他点的距离)
  2. $\text{A}$ 和 $\text{B}$ 虽然是两个集合, 代码中可以用一个 vector<bool> 数组实现
  3. 每次从 $\text{B}$ 中取一个最小的, 取更新其他点的距离
  4. $\text{B}$ 中的点为空, 遍历结束

局限性

在 $\text{Dijkstra}$ 算法的应用过程中, 某些有权图的边可能为负, 也就是说, 即使有权图中并不包含可以从节点到达的负权回路, $\text{Dijkstra}$ 算法依然是可以继续应用的, 但是假如存在一个可以直接从节点到达的负回路, 那么算法将无法进行操作, 最短路径的权也无法成立, 使得最短路径无法找到. 总而言之, 当有权图中出现了负权的话, $\text{Dijkstra}$ 算法就不成立了, 这也是该算法的最大缺陷.

优化

值得注意的是, 由于本题边数远大于点数, 是一张稠密图, 因此在运行时间上, 枚举写法要略快于的写法.

代码实现

枚举法

#include <iostream>
#include <vector>
#include <climits>
#include <algorithm>

using namespace std;
#define INF 0x3F3F3F3F

/**
 * Dijkstra 算法的实现
 * @param edges 图中的所有边
 * @param n     途中所有的点, 编号从 1~n
 * @param k     起始点
 * @return      到各个点的最小距离
 */
vector<int> Dijkstra(vector<vector<int>> &edges, int n, int k) {
    vector<vector<int>> g(n, vector<int>(n, INF));  // 邻接表 nxn 的矩阵, 其中的值是权重
    for (auto &e: edges) {                          // 构建邻接表
        int x = e[0] - 1;
        int y = e[1] - 1;
        g[x][y] = e[2];
    }
    vector<int> dist(n, INF);                       // k 到每个点的距离值
    dist[k - 1] = 0;                                // k 到自己的值为 0
    vector<int> used(n);                            // 节点是否已经访问了
    for (int i = 0; i < n; ++i) {                   // 遍历 n 个点
        int x = -1;                                 // 没有使用的最小的顶点
        for (int y = 0; y < n; ++y) {               // 在 dist 中找没有使用的最小的顶点
            if (! used[y] && (x == -1 || dist[y] < dist[x])) {
                x = y;
            }
        }
        used[x] = true;                             // 加入到 S 中
        for (int y = 0; y < n; ++y) {               // 遍历并更新其他点的距离
            dist[y] = min(dist[y], dist[x] + g[x][y]);
        }
    }
    return dist;
}

使用堆优化

#include <iostream>
#include <vector>
#include <climits>
#include <algorithm>
#include <queue>

using namespace std;
#define INF 0x3F3F3F3F

vector<int> Dijkstra(vector<vector<int>> &edges, int n, int k) {
    vector<vector<pair<int, int>>> g(n);
    for (auto &e: edges) {
        int x = e[0] - 1;
        int y = e[1] - 1;
        g[x].emplace_back(y, e[2]);
    }

    vector<int> dist(n, INF);
    dist[k - 1] = 0;
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> q;
    q.emplace(0, k - 1);        // 将距离和点序号入列
    while (! q.empty()) {
        auto p = q.top();
        q.pop();
        int distance = p.first, x = p.second;
        // 准备用来遍历的点比现在的点的距离还大, 就不适合用它来更新了
        if (dist[x] < distance) {
            continue;
        }
        // 用当前点 x 来更新邻接点的距离
        for (auto &e: g[x]) {
            int y = e.first, d = dist[x] + e.second;
            if (d < dist[y]) {
                dist[y] = d;
                q.emplace(d, y);
            }
        }
    }

    return dist;
}

882. 细分图中的可到达节点

class Solution {
public:
    int reachableNodes(vector<vector<int>> &edges, int maxMoves, int n) {
        const int INF = 0x3F3F3F3F;
        vector<vector<int>> g(n, vector<int>(n, INF));
        for (auto &e: edges) {
            g[e[0]][e[1]] = e[2] + 1;
            g[e[1]][e[0]] = e[2] + 1;
        }
        int res = 0;
        vector<int> visited(n, 0);
        queue<int> q;
        vector<int> dist(n, 0);
        q.push(0);
        visited[0] = 1;

        while (!q.empty()) {
            int len = q.size();
            for (int i = 0; i < len; ++i) {
                int v = q.front();
                if (visited[v] == 2) {
                    continue;
                }
                for (int j = 0; j < n; ++j) {

                    if (g[v][j] == INF) {
                        continue;
                    }

                    if (v == 0) {
                        res += min(maxMoves, g[v][j]) + 1;      // 0 到 j 可以走多少步
                        g[v][j] = maxMoves - g[v][j];           // 还剩下多少步
                    } else {
                        if (g[0][v] > 0 && g[0][j] < 0) {                      // v 还有可用步数
                            int relax = max(g[0][j], g[0][v] - g[v][j]);
                            if (relax > g[0][j]) {              // 如果 relax 大于 g[0][j], 说明通过 v 到 j 比 0 到 j 更近
                                res += relax - g[0][j];
                                g[0][j] = relax;
                            }
                        }
                    }

                    if (visited[j] == 0) {
                        visited[j] = 1;
                        q.push(j);
                    }
                }

                visited[v] = 2;
                q.pop();
            }
        }
        return res;
    }
};

官方题解中提到需要在 Dijkstra 过程中记录边的覆盖进度, 但这其实是不必要的. 我们完全可以把这两件事情分成两步来做.

  1. 由边集表示处理得到邻接表表示 (注意初始化边权重的时候要在 n 的基础上再加 1, 因为距离等于增加的节点数目加 1)
  2. 使用 Dijkstra 算法计算从 0 到所有节点的最短距离.
    • 使用 C++ STL 的 priority_queue 记录需要拓展的节点, 注意 priority_queue 默认是大根堆, 这里需要将比较函数设置为 greater<>, 从而将其变成小根堆.
    • 使用 vis[N] 数组记录每一节点是否已经被拓展过. 一个节点最多只能拓展一次.
  3. 利用计算得到的最短距离 dist[N], 遍历边集 $edges$ 中的边. 对其中的任意一条边, 我们可以从其左端点 $u$ 覆盖, 也可以从其右端点 $v$ 覆盖, 最后总的覆盖数目
    $$
    cover=\min(n,\max(0,\min(dist[u]-M, n))+\max(0,\min(dist[v]-M, n)))
    $$
  4. 上一步累加的是新增点的覆盖情况, 我们还需要进行一次对 $N$ 个顶点的遍历, 来确定原始的 $N$ 个顶点的覆盖情况.

作者:lucifer1004
链接:https://leetcode.cn/problems/reachable-nodes-in-subdivided-graph/solution/dijkstradan-yuan-zui-duan-lu-jing-bu-xu-yao-ji-lu-/
来源: 力扣 (LeetCode)
著作权归作者所有. 商业转载请联系作者获得授权, 非商业转载请注明出处.

rainbow

这个人很懒,什么都没留下

文章评论