【史上最全】涵盖所有「存图方式」与「最短路算法」

news2024/10/5 14:23:23

题目描述

这是 LeetCode 上的 「1334. 阈值距离内邻居最少的城市」 ,难度为 「中等」

Tag : 「最短路」、「图」

个城市,按从 编号。

给你一个边数组 edges,其中 代表 两个城市之间的双向加权边,距离阈值是一个整数 distanceThreshold

返回能通过某些路径到达其他城市数目最少、且路径距离最大为 distanceThreshold 的城市。如果有多个这样的城市,则返回编号最大的城市。

注意,连接城市 的路径的距离等于沿该路径的所有边的权重之和。

示例 1: alt

输入:n = 4, edges = [[0,1,3],[1,2,1],[1,3,4],[2,3,1]], distanceThreshold = 4

输出:3

解释:城市分布图如上。
每个城市阈值距离 distanceThreshold = 4 内的邻居城市分别是:
城市 0 -> [城市 1, 城市 2] 
城市 1 -> [城市 0, 城市 2, 城市 3] 
城市 2 -> [城市 0, 城市 1, 城市 3] 
城市 3 -> [城市 1, 城市 2] 
城市 0 和 3 在阈值距离 4 以内都有 2 个邻居城市,但是我们必须返回城市 3,因为它的编号最大。

示例 2: alt

输入:n = 5, edges = [[0,1,2],[0,4,8],[1,2,3],[1,4,2],[2,3,1],[3,4,1]], distanceThreshold = 2

输出:0

解释:城市分布图如上。 
每个城市阈值距离 distanceThreshold = 2 内的邻居城市分别是:
城市 0 -> [城市 1] 
城市 1 -> [城市 0, 城市 4] 
城市 2 -> [城市 3, 城市 4] 
城市 3 -> [城市 2, 城市 4]
城市 4 -> [城市 1, 城市 2, 城市 3] 
城市 0 在阈值距离 2 以内只有 1 个邻居城市。

提示:

  • 所有 都是不同的。

基本分析

若能预处理图中任意两点 的最短距离 dist,那么统计每个点 在图中有多少满足 的点 即为答案。

于是问题转换为:「如何求解给定图中,任意两点的最短距离」

存图

在学习最短路之前,我们先搞懂众多图论问题的前置 🧀 :存图。

为了方便,我们约定 为点数, 为边数。

根据点和边的数量级关系,可以将图分成如下两类:

  • 稠密图:边数较多,边数接近于点数的平方,即
  • 稀疏图:边数较少,边数接近于点数,即

同时,根据「稠密图」还是「稀疏图」,我们有如下几种存图方式:

1. 邻接矩阵(稠密图)

这是一种使用二维矩阵来进行存图的方式。

// w[a][b] = c 代表从 a 到 b 有权重为 c 的边
int[][] g = new int[N][N];

// 加边操作
void add(int a, int b, int c) {
    g[a][b] = c;
}
2. 邻接表(稀疏图)

邻接表又叫「链式前向星」,是另一种常见的存图方式,实现代码与「使用数组存储单链表」一致(头插法)。

int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];

// 加边操作
void add(int a, int b, int c) {
    e[idx] = b;
    ne[idx] = he[a];
    w[idx] = c;
    he[a] = idx++;
}

首先 idx 是用来对边进行编号的,然后对存图用到的几个数组作简单解释:

  • he 数组:存储是某个节点所对应的边的集合(链表)的头结点;
  • e 数组:由于访问某一条边指向的节点;
  • ne 数组:由于是以链表的形式进行存边,该数组就是用于找到下一条边;
  • w 数组:用于记录某条边的权重为多少。

当我们想要遍历所有由 a 点发出的边时,可以使用如下方式:

for (int i = he[a]; i != -1; i = ne[i]) {
    int b = e[i], c = w[i]; // 存在由 a 指向 b 的边,权重为 c
}
3. 类

这是最简单,但使用频率最低的存图方式。

只有当我们需要确保某个操作复杂度为严格 时,才会考虑使用。

具体的,建立一个类来记录有向边信息:

class Edge {
    // 代表从 a 到 b 有一条权重为 c 的边
    int a, b, c;
    Edge(int _a, int _b, int _c) {
        a = _a; b = _b; c = _c;
    }
}

随后,使用诸如 List 的容器,存起所有边对象。在需要遍历所有边时,对容器进行进行遍历:

List<Edge> es = new ArrayList<>();

...

for (Edge e : es) {
    ...
}

综上,第 种方式,往往是 OJ 给我们边信息的方式,我们自己几乎不会用这种方式建图。

实际运用中,熟练掌握「如何根据点和边的数量级关系,来决定使用邻接矩阵(稠密图)还是邻接表(稀疏图)」即可。

Floyd(邻接矩阵)

Floyd 算法作为「多源汇最短路」算法,对于本题尤其适合。

Floyd 算法基于「动态规划」,其原始三维状态定义为 ,表示「所有从点 到点 ,且允许经过点集 的路径」中的最短距离。

状态转移方程:

代表从 但必然不经过点 的路径, 代表必然经过点 的路径,两者中取较小值更新

不难发现任意的 依赖于 ,可采用「滚动数组」的方式进行优化。

dist 声明为二维数组, 代表从点 到点 的最短距离,并采取 [枚举中转点 - 枚举起点 - 枚举终点] 三层循环的方式更新

如此一来,跑一遍 Floyd 算法便可得出任意两点的最短距离。

通过上述推导,不难发现,我们并没提及边权的正负问题,因此 Floyd 算法对边权的正负没有限制要求(可处理正负权边的图),且能利用 Floyd 算法可能够对图中负环进行判定。

Java 代码:

class Solution {
    public int findTheCity(int n, int[][] edges, int distanceThreshold) {
        int[][] g = new int[n][n];
        // 初始化邻接矩阵
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                g[i][j] = i == j ? 0 : 0x3f3f3f3f;
            }
        }
        // 存图
        for (int[] e : edges) {
            int a = e[0], b = e[1], c = e[2];
            g[a][b] = g[b][a] = Math.min(g[a][b], c);
        }
        // 最短路
        floyd(g);
        // 统计答案
        int ans = -1, cnt = n + 10;
        for (int i = 0; i < n; i++) {
            int cur = 0;
            for (int j = 0; j < n; j++) {
                if (i != j && g[i][j] <= distanceThreshold) cur++;
            }
            if (cur <= cnt) {
                cnt = cur; ans = i;
            }
        }
        return ans;
    }
    void floyd(int[][] g) {
        int n = g.length;
        // floyd 基本流程为三层循环: [枚举中转点 - 枚举起点 - 枚举终点] => 松弛操作  
        for (int p = 0; p < n; p++) {
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    g[i][j] = Math.min(g[i][j], g[i][p] + g[p][j]);
                }
            }
        }
    }
}

C++ 代码:

class Solution {
public:
    int findTheCity(int n, vector<vector<int>>& edges, int distanceThreshold) {
        vector<vector<int>> g(n, vector<int>(n, 0x3f3f3f3f));
        // 初始化邻接矩阵
        for (int i = 0; i < n; i++) g[i][i] = 0;
        // 存图
        for (const auto& e : edges) {
            int a = e[0], b = e[1], c = e[2];
            g[a][b] = g[b][a] = min(g[a][b], c);
        }
        // 最短路
        floyd(g);
        // 统计答案
        int ans = -1, cnt = n + 10;
        for (int i = 0; i < n; i++) {
            int cur = 0;
            for (int j = 0; j < n; j++) {
                if (i != j && g[i][j] <= distanceThreshold) cur++;
            }
            if (cur <= cnt) {
                cnt = cur; ans = i;
            }
        }
        return ans;
    }
    void floyd(vector<vector<int>>& g) {
        int n = g.size();
        // floyd 基本流程为三层循环: [枚举中转点 - 枚举起点 - 枚举终点] => 松弛操作  
        for (int p = 0; p < n; p++) {
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    g[i][j] = min(g[i][j], g[i][p] + g[p][j]);
                }
            }
        }
    }
};

Python 代码:

class Solution:
    def findTheCity(self, n: int, edges: List[List[int]], distanceThreshold: int) -> int:
        def floyd(g: List[List[int]]) -> None:
            n = len(g)
            # floyd 基本流程为三层循环: [枚举中转点 - 枚举起点 - 枚举终点] => 松弛操作
            for p in range(n):
                for i in range(n):
                    for j in range(n):
                        g[i][j] = min(g[i][j], g[i][p] + g[p][j])

        g = [[float('inf')] * n for _ in range(n)]
        # 初始化邻接矩阵
        for i in range(n):
            g[i][i] = 0
        # 存图
        for a, b, c in edges:
            g[a][b] = g[b][a] = min(g[a][b], c)
        # 最短路
        floyd(g)
        # 统计答案
        ans, cnt = -1, n + 10
        for i in range(n):
            cur = sum(1 for j in range(n) if i != j and g[i][j] <= distanceThreshold)
            if cur <= cnt:
                cnt, ans = cur, i
        return ans

TypeScript 代码:

function findTheCity(n: number, edges: number[][], distanceThreshold: number): number {
    const floyd = function (g: number[][]): void {
        const n = g.length;
        // floyd 基本流程为三层循环: [枚举中转点 - 枚举起点 - 枚举终点] => 松弛操作
        for (let p = 0; p < n; p++) {
            for (let i = 0; i < n; i++) {
                for (let j = 0; j < n; j++) {
                    g[i][j] = Math.min(g[i][j], g[i][p] + g[p][j]);
                }
            }
        }
    }

    const g = Array.from({ length: n }, () => Array(n).fill(0x3f3f3f3f));
    // 初始化邻接矩阵
    for (let i = 0; i < n; i++) g[i][i] = 0;
    // 存图
    for (const [a, b, c] of edges) g[a][b] = g[b][a] = Math.min(g[a][b], c);
    // 最短路
    floyd(g);
    // 统计答案
    let ans = -1, cnt = n + 10;
    for (let i = 0; i < n; i++) {
        let cur = 0;
        for (let j = 0; j < n; j++) {
            if (i !== j && g[i][j] <= distanceThreshold) cur++;
        }
        if (cur <= cnt) {
            cnt = cur; ans = i;
        }
    }
    return ans;
};
  • 时间复杂度:初始化邻接矩阵和建图复杂度为 floyd 算法复杂度为 ;统计答案复杂度为 ;整体复杂度为
  • 空间复杂度:

朴素 Dijkstra(邻接矩阵)

最为经典的「单源最短路」算法,通常搭配「邻接矩阵」使用,应用在边数较多的“稠密图”上。

朴素 Dijkstra 算法基于「贪心」,通过维护一维的距离数组 dist 实现, 表示从源点出发到点 的最短距离。

朴素 Dijkstra 算法在每一次迭代中,都选择 dist 中值最小的点进行松弛操作,逐渐扩展最短路径范围。

Java 代码:

class Solution {
    public int findTheCity(int n, int[][] edges, int distanceThreshold) {
        int[][] g = new int[n][n];
        // 初始化邻接矩阵
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                g[i][j] = i == j ? 0 : 0x3f3f3f3f;
            }
        }
        // 存图
        for (int[] e : edges) {
            int a = e[0], b = e[1], c = e[2];
            g[a][b] = g[b][a] = Math.min(g[a][b], c);
        }
        int ans = -1, cnt = n + 10;
        for (int i = 0; i < n; i++) {
            // 单源最短路
            int[] dist = dijkstra(g, i);
            int cur = 0;
            for (int j = 0; j < n; j++) {
                if (i != j && dist[j] <= distanceThreshold) cur++;
            }
            if (cur <= cnt) {
                cnt = cur; ans = i;
            }
        }
        return ans;
    }
    int[] dijkstra(int[][] g, int x) {
        int n = g.length;
        // 起始先将所有的点标记为「未更新」和「距离为正无穷」
        boolean[] vis = new boolean[n];
        int[] dist = new int[n];
        Arrays.fill(dist, 0x3f3f3f3f);
        // 只有起点最短距离为 0
        dist[x] = 0;
        // 有多少个点就迭代多少次
        for (int k = 0; k < n; k++) {
            // 每次找到「最短距离最小」且「未被更新」的点 t
            int t = -1;
            for (int i = 0; i < n; i++) {
                if (!vis[i] && (t == -1 || dist[i] < dist[t])) t = i;
            }
            // 标记点 t 为已更新
            vis[t] = true;
            // 用点 t 的「最小距离」更新其他点
            for (int i = 0; i < n; i++) dist[i] = Math.min(dist[i], dist[t] + g[t][i]);
        }
        return dist;
    }
}

C++ 代码:

class Solution {
public:
    int findTheCity(int n, vector<vector<int>>& edges, int distanceThreshold) {
        vector<vector<int>> g(n, vector<int>(n, 0x3f3f3f3f));
        // 初始化邻接矩阵
        for (int i = 0; i < n; i++) g[i][i] = 0;
        // 存图
        for (const auto& e : edges) {
            int a = e[0], b = e[1], c = e[2];
            g[a][b] = g[b][a] = min(g[a][b], c);
        }
        int ans = -1, cnt = n + 10;
        for (int i = 0; i < n; i++) {
            // 单源最短路
            vector<int> dist = dijkstra(g, i);
            int cur = count_if(dist.begin(), dist.end(), [distanceThreshold](int d) { return d <= distanceThreshold; });
            if (cur <= cnt) {
                cnt = cur; ans = i;
            }
        }
        return ans;
    }
    vector<intdijkstra(const vector<vector<int>>& g, int x) {
        int n = g.size();
        vector<boolvis(n, false);
        vector<intdist(n, 0x3f3f3f3f);
        // 只有起点最短距离为 0
        dist[x] = 0;
        // 有多少个点就迭代多少次
        for (int k = 0; k < n; k++) {
            // 每次找到「最短距离最小」且「未被更新」的点 t
            int t = -1;
            for (int i = 0; i < n; i++) {
                if (!vis[i] && (t == -1 || dist[i] < dist[t])) t = i;
            }
            // 标记点 t 为已更新
            vis[t] = true;
            // 用点 t 的「最小距离」更新其他点
            for (int i = 0; i < n; i++) dist[i] = min(dist[i], dist[t] + g[t][i]);
        }
        return dist;
    }
};

Python 代码:

class Solution:
    def findTheCity(self, n: int, edges: List[List[int]], distanceThreshold: int) -> int:
        def dijkstra(g, x):
            n = len(g)
            vis = [False] * n
            dist = [float('inf')] * n
            # 只有起点最短距离为 0
            dist[x] = 0
            # 有多少个点就迭代多少次
            for k in range(n):
                # 每次找到「最短距离最小」且「未被更新」的点 t
                t = min((i for i in range(n) if not vis[i]), key=lambda i: dist[i])
                # 标记点 t 为已更新
                vis[t] = True
                # 用点 t 的「最小距离」更新其他点
                for i in range(n):
                    dist[i] = min(dist[i], dist[t] + g[t][i])
            return dist

        g = [[float('inf')] * n for _ in range(n)]
        # 初始化邻接矩阵
        for i in range(n):
            g[i][i] = 0
        # 存图
        for a, b, c in edges:
            g[a][b] = g[b][a] = min(g[a][b], c)
        ans, cnt = -1, n + 10
        for i in range(n):
            # 单源最短路
            dist = dijkstra(g, i)
            cur = sum(1 for j in range(n) if i != j and dist[j] <= distanceThreshold)
            if cur <= cnt:
                cnt, ans = cur, i
        return ans

TypeScript 代码:

function findTheCity(n: number, edges: number[][], distanceThreshold: number): number {
    const dijkstra = function (g: number[][], x: number): number[]  {
        const n = g.length;
        const vis = Array(n).fill(false), dist = Array(n).fill(0x3f3f3f3f);
        // 只有起点最短距离为 0
        dist[x] = 0;
        // 有多少个点就迭代多少次
        for (let k = 0; k < n; k++) {
            // 每次找到「最短距离最小」且「未被更新」的点 t
            let t = -1;
            for (let i = 0; i < n; i++) {
                if (!vis[i] && (t === -1 || dist[i] < dist[t])) t = i;
            }
            // 标记点 t 为已更新
            vis[t] = true;
            // 用点 t 的「最小距离」更新其他点
            for (let i = 0; i < n; i++) dist[i] = Math.min(dist[i], dist[t] + g[t][i]);
        }
        return dist;
    }
    
    const g = Array.from({ length: n }, () => Array(n).fill(0x3f3f3f3f));
    // 初始化邻接矩阵
    for (let i = 0; i < n; i++) g[i][i] = 0;
    // 存图
    for (const [a, b, c] of edges) g[a][b] = g[b][a] = Math.min(g[a][b], c);
    let ans = -1, cnt = n + 10;
    for (let i = 0; i < n; i++) {
        // 单源最短路
        const dist = dijkstra(g, i);
        const cur = dist.filter(d => d <= distanceThreshold).length;
        if (cur <= cnt) {
            cnt = cur; ans = i;
        }
    }
    return ans;
};
  • 时间复杂度:初始化邻接矩阵和建图复杂度为 ;统计答案时,共执行 次朴素 dijkstra 算法,朴素 dijkstra 复杂度为 ,总复杂度为 。整体复杂度为
  • 空间复杂度:

堆优化 Dijkstra(邻接表)

堆优化 Dijkstra 算法与朴素 Dijkstra 算法都是「单源最短路」算法。

堆优化 Dijkstra 算法通过数据结构「优先队列(堆)」来优化朴素 Dijkstra 中的“找 dist 中值最小的点”的过程。

相比于复杂度与边数无关的 朴素 Dijkstra 算法,复杂度与边数相关的 堆优化 Dijkstra 算法更适合边较少的“稀疏图”。

Java 代码:

class Solution {
    int N = 110, M = N * N, INF = 0x3f3f3f3f, idx, n;
    int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
    void add(int a, int b, int c) {
        e[idx] = b;
        ne[idx] = he[a];
        w[idx] = c;
        he[a] = idx++;
    }
    public int findTheCity(int _n, int[][] edges, int distanceThreshold) {
        n = _n;
        // 初始化链表头
        Arrays.fill(he, -1);
        // 存图
        for (int[] e : edges) {
            int a = e[0], b = e[1], c = e[2];
            add(a, b, c); add(b, a, c);
        }
        // 统计答案
        int ans = -1, cnt = n + 10;
        for (int i = 0; i < n; i++) {
            // 单源最短路
            int[] dist = dijkstra(i);
            int cur = 0;
            for (int j = 0; j < n; j++) {
                if (i != j && dist[j] <= distanceThreshold) cur++;
            }
            if (cur <= cnt) {
                cnt = cur; ans = i;
            }
        }
        return ans;
    }
    int[] dijkstra(int x) {
        // 起始先将所有的点标记为「未更新」和「距离为正无穷」
        int[] dist = new int[n];
        Arrays.fill(dist, 0x3f3f3f3f);
        boolean[] vis = new boolean[n];
        dist[x] = 0;
        // 使用「优先队列」存储所有可用于更新的点
        // 以 (点编号, 到起点的距离) 进行存储,优先弹出「最短距离」较小的点
        PriorityQueue<int[]> q = new PriorityQueue<>((a,b)->a[1]-b[1]);
        q.add(new int[]{x, 0});
        while (!q.isEmpty()) {
            // 每次从「优先队列」中弹出
            int[] poll = q.poll();
            int u = poll[0], step = poll[1];
            // 如果弹出的点被标记「已更新」,则跳过
            if (vis[u]) continue;
            // 标记该点「已更新」,并使用该点更新其他点的「最短距离」
            vis[u] = true;
            for (int i = he[u]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] <= dist[u] + w[i]) continue;
                dist[j] = dist[u] + w[i];
                q.add(new int[]{j, dist[j]});
            }
        }
        return dist;
    }
}

C++ 代码:

class Solution {
public:
    static const int N = 110, M = N * N;
    int he[N], e[M], ne[M], w[M], idx, n, INF = 0x3f3f3f3f;
    void add(int a, int b, int c) {
        e[idx] = b;
        ne[idx] = he[a];
        w[idx] = c;
        he[a] = idx++;
    }
    int findTheCity(int _n, vector<vector<int>>& edges, int distanceThreshold) {
        n = _n;
        // 初始化链表头
        fill(he, he + n, -1);
        // 存图
        for (const auto& e : edges) {
            int a = e[0], b = e[1], c = e[2];
            add(a, b, c); add(b, a, c);
        }
        // 统计答案
        int ans = -1, cnt = n + 10;
        for (int i = 0; i < n; i++) {
            // 单源最短路
            vector<int> dist = dijkstra(i);
            int cur = count_if(dist.begin(), dist.end(), [distanceThreshold](int d) { return d <= distanceThreshold; });
            if (cur <= cnt) {
                cnt = cur; ans = i;
            }
        }
        return ans;
    }
    vector<intdijkstra(int x) {
        // 起始先将所有的点标记为「未更新」和「距离为正无穷」
        vector<intdist(n, INF);
        vector<boolvis(n, false);
        dist[x] = 0;
        // 使用「优先队列」存储所有可用于更新的点
        // 以 (点编号, 到起点的距离) 进行存储,优先弹出「最短距离」较小的点
        priority_queue<pair<intint>, vector<pair<intint>>, greater<pair<intint>>> q;
        q.push({0, x});
        while (!q.empty()) {
            // 每次从「优先队列」中弹出
            auto [step, u] = q.top();
            q.pop();
            // 如果弹出的点被标记「已更新」,则跳过
            if (vis[u]) continue;
            // 标记该点「已更新」,并使用该点更新其他点的「最短距离」
            vis[u] = true;
            for (int i = he[u]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] <= dist[u] + w[i]) continue;
                dist[j] = dist[u] + w[i];
                q.push({dist[j], j});
            }
        }
        return dist;
    }
};

Python 代码:

import heapq

class Solution:
    def findTheCity(self, n: int, edges: List[List[int]], distanceThreshold: int) -> int:
        N, M, INF, idx = 110110 * 110, float('inf'), 0
        he, e, ne, w = [-1] * N, [0] * M, [0] * M, [0] * M

        def add(a, b, c):
            nonlocal idx
            e[idx] = b
            ne[idx] = he[a]
            w[idx] = c
            he[a] = idx
            idx += 1

        def dijkstra(x):
            # 起始先将所有的点标记为「未更新」和「距离为正无穷」
            dist = [float('inf')] * n
            vis = [False] * n
            dist[x] = 0
            # 使用「优先队列」存储所有可用于更新的点
            # 以 (点编号, 到起点的距离) 进行存储,优先弹出「最短距离」较小的点
            q = [(0, x)]
            heapq.heapify(q)
            while q:
                # 每次从「优先队列」中弹出
                step, u = heapq.heappop(q)
                # 如果弹出的点被标记「已更新」,则跳过
                if vis[u]: continue
                # 标记该点「已更新」,并使用该点更新其他点的「最短距离」
                vis[u] = True
                i = he[u]
                while i != -1:
                    j, c = e[i], w[i]
                    i = ne[i]
                    if dist[j] <= dist[u] + c: continue
                    dist[j] = dist[u] + c
                    heapq.heappush(q, (dist[j], j))
            return dist

        # 初始化链表头
        he = [-1] * N
        # 存图
        for a, b, c in edges:
            add(a, b, c)
            add(b, a, c)
        # 统计答案
        ans, cnt = -1, n + 10
        for i in range(n):
            # 单源最短路
            dist = dijkstra(i)
            cur = sum(1 for j in range(n) if i != j and dist[j] <= distanceThreshold)
            if cur <= cnt:
                cnt, ans = cur, i
        return ans
  • 时间复杂度:初始化邻接表和建图复杂度为 ;统计答案时,共执行 次堆优化 dijkstra 算法,堆优化 dijkstra 复杂度为 ,总复杂度为 。整体复杂度为
  • 空间复杂度:

Bellman Ford(类)

虽然题目规定了不存在「负权边」,但我们仍然可以使用可以在「负权图中求最短路」的 Bellman Ford 进行求解,该算法也是「单源最短路」算法,复杂度为

通常为了确保 ,可以单独建一个类代表边,将所有边存入集合中,在 次松弛操作中直接对边集合进行遍历。

由于本题边数量级为 ,共对 个点执行 Bellman Ford 算法,因此整体会去到 ,有 TLE 风险。

Java 代码:

class Solution {
    int n;
    public int findTheCity(int _n, int[][] edges, int distanceThreshold) {
        n = _n;
        int ans = -1, cnt = n + 10;
        for (int i = 0; i < n; i++) {
            int[] dist = bf(edges, i);
            int cur = 0;
            for (int j = 0; j < n; j++) {
                if (i != j && dist[j] <= distanceThreshold) cur++;
            }
            if (cur <= cnt) {
                cnt = cur; ans = i;
            }
        }
        return ans;
    }
    int[] bf(int[][] edges, int x) {
        int[] dist = new int[n];
        // 起始先将所有的点标记为「距离为正无穷」, 只有起点最短距离为 0
        Arrays.fill(dist, 0x3f3f3f3f);
        dist[x] = 0;
        // 有多少个点就迭代多少次
        for (int k = 0; k < n; k++) {
            // 每次都使用上一次迭代的结果,执行松弛操作
            int[] prev = dist.clone();
            for (int[] e : edges) {
                int a = e[0], b = e[1], c = e[2];
                dist[b] = Math.min(dist[b], prev[a] + c);
                dist[a] = Math.min(dist[a], prev[b] + c);
            }
        }
        return dist;
    }
}

C++ 代码:

class Solution {
public:
    int findTheCity(int n, vector<vector<int>>& edges, int distanceThreshold) {
        int ans = -1, cnt = n + 10;
        for (int i = 0; i < n; i++) {
            vector<int> dist = bf(edges, i, n);
            int cur = 0;
            for (int j = 0; j < n; j++) {
                if (i != j && dist[j] <= distanceThreshold) cur++;
            }
            if (cur <= cnt) {
                cnt = cur; ans = i;
            }
        }
        return ans;
    }
    vector<intbf(vector<vector<int>>& edges, int x, int n) {
        // 起始先将所有的点标记为「距离为正无穷」, 只有起点最短距离为 0
        vector<intdist(n, 0x3f3f3f3f);
        dist[x] = 0;
        // 有多少个点就迭代多少次
        for (int k = 0; k < n; k++) {
            // 每次都使用上一次迭代的结果,执行松弛操作
            vector<int> prev = dist;
            for (const auto& e : edges) {
                int a = e[0], b = e[1], c = e[2];
                dist[b] = min(dist[b], prev[a] + c);
                dist[a] = min(dist[a], prev[b] + c);
            }
        }
        return dist;
    }
};

Python 代码:

class Solution:
    def findTheCity(self, n: int, edges: List[List[int]], distanceThreshold: int) -> int:
        def bf(edges: List[List[int]], x: int, n: int) -> List[int]:
            # 起始先将所有的点标记为「距离为正无穷」, 只有起点最短距离为 0
            dist = [float('inf')] * n
            dist[x] = 0
            # 有多少个点就迭代多少次
            for k in range(n):
                # 每次都使用上一次迭代的结果,执行松弛操作
                prev = dist.copy()
                for a, b, c in edges:
                    dist[b] = min(dist[b], prev[a] + c)
                    dist[a] = min(dist[a], prev[b] + c)
            return dist

        ans, cnt = -1, n + 10
        for i in range(n):
            dist = bf(edges, i, n)
            cur = sum(1 for j in range(n) if i != j and dist[j] <= distanceThreshold)
            if cur <= cnt:
                cnt, ans = cur, i
        return ans

TypeScript 代码:

function findTheCity(n: number, edges: number[][], distanceThreshold: number): number {
    const bf = function(x: number): number[] {
        // 起始先将所有的点标记为「距离为正无穷」, 只有起点最短距离为 0
        const dist = new Array(n).fill(0x3f3f3f3f);
        dist[x] = 0;
        // 有多少个点就迭代多少次
        for (let k = 0; k < n; k++) {
            // 每次都使用上一次迭代的结果,执行松弛操作
            const prev = dist.slice();
            for (const e of edges) {
                const a = e[0], b = e[1], c = e[2];
                dist[b] = Math.min(dist[b], prev[a] + c);
                dist[a] = Math.min(dist[a], prev[b] + c);
            }
        }
        return dist;
    }

    let ans = -1, cnt = n + 10;
    for (let i = 0; i < n; i++) {
        const dist = bf(i);
        let cur = 0;
        for (let j = 0; j < n; j++) {
            if (i !== j && dist[j] <= distanceThreshold) cur++;
        }
        if (cur <= cnt) {
            cnt = cur; ans = i;
        }
    }
    return ans;
};
  • 时间复杂度:统计答案时,共执行 Bellman Ford 算法, Bellman Ford 复杂度为 ,总复杂度为 。整体复杂度为
  • 空间复杂度:

SPFA(邻接表)

SPFA 也是一类能够处理「负权边」的单源最短路算法。

最坏情况下,复杂度为 ,在特定情况下,其效率优于 Dijkstra 算法,近似

基本执行流程如下:

  1. 用双端队列来维护待更新节点,初始将源点放入队列

  2. 每次从队列头中取出一个节点,对其所有相邻节点执行松弛操作

    1. 若某个相邻节点的最短距离发生了更新,且该节点不在队列中,将它加入队列中
  3. 重复以上步骤,直到队列为空

Java 代码:

class Solution {
    int N = 110, M = N * N, INF = 0x3f3f3f3f, idx, n;
    int[] he = new int[N], e = new int[M], ne = new int[M], w = new int[M];
    void add(int a, int b, int c) {
        e[idx] = b;
        ne[idx] = he[a];
        w[idx] = c;
        he[a] = idx++;
    }
    public int findTheCity(int _n, int[][] edges, int distanceThreshold) {
        n = _n;
        // 初始化链表头
        Arrays.fill(he, -1);
        // 存图
        for (int[] e : edges) {
            int a = e[0], b = e[1], c = e[2];
            add(a, b, c); add(b, a, c);
        }
        // 统计答案
        int ans = -1, cnt = n + 10;
        for (int i = 0; i < n; i++) {
            // 单源最短路
            int[] dist = spfa(i);
            int cur = 0;
            for (int j = 0; j < n; j++) {
                if (i != j && dist[j] <= distanceThreshold) cur++;
            }
            if (cur <= cnt) {
                cnt = cur; ans = i;
            }
        }
        return ans;
    }
    int[] spfa(int x) {
        int[] dist = new int[n];
        boolean[] vis = new boolean[n];
        // 起始先将所有的点标记为「未入队」和「距离为正无穷」
        Arrays.fill(dist, INF);
        // 只有起点最短距离为 0
        dist[x] = 0;
        // 使用「双端队列」存储,存储的是点编号
        Deque<Integer> d = new ArrayDeque<>();
        // 将「源点/起点」进行入队,并标记「已入队」
        d.addLast(x);
        vis[x] = true;
        while (!d.isEmpty()) {
            // 每次从「双端队列」中取出,并标记「未入队」
            int u = d.pollFirst();
            vis[u] = false;
            // 尝试使用该点,更新其他点的最短距离
            // 如果更新的点,本身「未入队」则加入队列中,并标记「已入队」
            for (int i = he[u]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] <= dist[u] + w[i]) continue;
                dist[j] = dist[u] + w[i];
                if (vis[j]) continue;
                d.addLast(j);
                vis[j] = true;
            }
        }
        return dist;
    }
}

C++ 代码:

class Solution {
public:
    static const int N = 110, M = N * N;
    int he[N], e[M], ne[M], w[M], idx, n, INF = 0x3f3f3f3f;
    void add(int a, int b, int c) {
        e[idx] = b;
        ne[idx] = he[a];
        w[idx] = c;
        he[a] = idx++;
    }
    int findTheCity(int _n, vector<vector<int>>& edges, int distanceThreshold) {
        n = _n;
        fill(he, he + N, -1);
        for (const auto& e : edges) {
            int a = e[0], b = e[1], c = e[2];
            add(a, b, c); add(b, a, c);
        }
        int ans = -1, cnt = n + 10;
        for (int i = 0; i < n; i++) {
            vector<int> dist = spfa(i);
            int cur = count_if(dist.begin(), dist.end(), [&](int d) { return d != INF && d <= distanceThreshold; });
            if (cur <= cnt) {
                cnt = cur; ans = i;
            }
        }
        return ans;
    }
    vector<intspfa(int x) {
        // 起始先将所有的点标记为「未入队」和「距离为正无穷」
        vector<intdist(n, INF);
        vector<boolvis(n, false);
        // 只有起点最短距离为 0
        dist[x] = 0;
        // 使用「双端队列」存储,存储的是点编号
        deque<int> d;
        // 将「源点/起点」进行入队,并标记「已入队」
        d.push_back(x);
        vis[x] = true;
        while (!d.empty()) {
            // 每次从「双端队列」中取出,并标记「未入队」
            int u = d.front();
            d.pop_front();
            vis[u] = false;
            // 尝试使用该点,更新其他点的最短距离
            // 如果更新的点,本身「未入队」则加入队列中,并标记「已入队」
            for (int i = he[u]; i != -1; i = ne[i]) {
                int j = e[i];
                if (dist[j] <= dist[u] + w[i]) continue;
                dist[j] = dist[u] + w[i];
                if (vis[j]) continue;
                d.push_back(j);
                vis[j] = true;
            }
        }
        return dist;
    }
};

Python 代码:

class Solution:
    def findTheCity(self, n: int, edges: List[List[int]], distanceThreshold: int) -> int:
        m, INF, idx = n * n, 0x3f3f3f3f0
        he, e, ne, w = [-1] * n, [0] * m, [0] * m, [0] * m

        def add(a: int, b: int, c: int):
            nonlocal idx
            e[idx] = b
            ne[idx] = he[a]
            w[idx] = c
            he[a] = idx
            idx += 1

        def spfa(x: int) -> List[int]:
            # 起始先将所有的点标记为「未入队」和「距离为正无穷」
            dist = [INF] * n
            vis = [False] * n
            # 只有起点最短距离为 0
            dist[x] = 0
            # 使用「双端队列」存储,存储的是点编号
            d = deque()
            # 将「源点/起点」进行入队,并标记「已入队」
            d.append(x)
            vis[x] = True
            while d:
                # 每次从「双端队列」中取出,并标记「未入队」
                u = d.popleft()
                vis[u] = False
                i = he[u]
                # 尝试使用该点,更新其他点的最短距离
                # 如果更新的点,本身「未入队」则加入队列中,并标记「已入队」
                while i != -1:
                    j, c = e[i], w[i]
                    i = ne[i]
                    if dist[j] <= dist[u] + c: continue
                    dist[j] = dist[u] + c
                    if vis[j]: continue
                    d.append(j)
                    vis[j] = True
            return dist

        for a, b, c in edges:
            add(a, b, c)
            add(b, a, c)

        ans, cnt = -1, n + 10
        for i in range(n):
            dist = spfa(i)
            cur = sum(1 for d in dist if d != INF and d <= distanceThreshold)
            if cur <= cnt:
                cnt, ans = cur, i
        return ans
  • 时间复杂度:统计答案时,共执行 spfa 算法, spfa 复杂度为 ,总复杂度为 。整体复杂度为
  • 空间复杂度:

最后

这是我们「刷穿 LeetCode」系列文章的第 No.1334 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

更多更全更热门的「笔试/面试」相关资料可访问排版精美的 合集新基地 🎉🎉

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1206904.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

优化千万级数据表的实用指南

文章目录 优化千万级数据表的一次实践一、查询耗时太长二、优化解决方案三、优化后查询速度 优化千万级数据表的一次实践 一、查询耗时太长 随着数据量的增长&#xff0c;数据库性能往往成为关注的焦点。特别是在处理千万级数据表时&#xff0c;查询性能往往成为一个挑战。在这…

服务器数据恢复—服务器发生故障导致数据丢失如何恢复服务器数据?

服务器常见故障&#xff1a; 硬件故障&#xff1a;磁盘、板卡、电源故障等。 软件故障&#xff1a;操作系统崩溃、程序运行错误等。 入侵破坏&#xff1a;加密、删除服务数据等。 不可控力&#xff1a;浸水、火烧、倒塌等。 误操作&#xff1a;格式化、删除、覆盖等。 如何减少…

Cesium和Three.js的初步认识

一、Threejs和Cesium的对比 相同点&#xff1a; 都是基于WebGL技术开发的Javascript库&#xff0c;用于在浏览器中创建和显示动画3D计算机图形。 不同点&#xff1a; Threejs&#xff1a;受众面比较广&#xff0c;是封装了webgl的一些底层用法&#xff0c;让初学者更容易上…

[linux网络实验] 多网卡绑定

聚合链路技术 什么是bonding 提供了一种将多个网络接口设备绑定到一个网络接口的方法。这可用于网络负载平衡和网络冗余&#xff1b; 实现将两个网卡虚拟成一个网卡。这种聚合设备看起来就像一个以太网接口设备。通俗地说&#xff0c;这意味着两个网卡拥有相同的 IP 地址&am…

代码随想录算法训练营第五十三天 | LeetCode 1143. 最长公共子序列、1035. 不相交的线、53. 最大子数组和

代码随想录算法训练营第五十三天 | LeetCode 1143. 最长公共子序列、1035. 不相交的线、53. 最大子数组和 文章链接&#xff1a;最长公共子序列、不相交的线、最大子数组和 视频链接&#xff1a;最长公共子序列、不相交的线、最大子数组和 1. LeetCode 1143. 最长公共子序列 1…

WMS系统出库管理:功能、流程与优势

一、WMS系统出库管理概述 WMS系统是一种用于仓库管理的信息系统&#xff0c;它涵盖了从货物入库、在库管理到出库的全过程。在WMS系统中&#xff0c;出库管理是非常重要的一环&#xff0c;它涉及到的功能包括订单处理、拣货、打包、发货等。通过WMS系统的出库管理&#xff0c;…

世界互联网大会|美创科技新一代 数据安全管理平台,携五大新特性发布亮相

11月7日&#xff0c;2023年世界互联网大会“互联网之光”博览会在浙江乌镇开幕。今年是博览会办展第十年&#xff0c;重点突出国际性、创新性、引领性。在新产品新技术发布活动中&#xff0c;美创科技全新升级的新一代 数据安全管理平台&#xff08;简称&#xff1a;DSM Cloud&…

SaaS 电商设计 (四) 如何做大促压测

一.背景&目标 1.1 常见的压测场景 电商大促:一众各大厂的促销活动场景,如:淘宝率先推出的天猫双11,而后京东拉出的京东 618 .还是后续陆陆续续的一些年货节, 3.8 女神节等等.都属于一些常规的电商大促 票务抢购:常见的如承载咱们 80,90 青春回忆的 Jay 的演唱会,还有普罗…

基于Rabbitmq和Redis的延迟消息实现

1 基于Rabbitmq延迟消息实现 支付时间设置为30&#xff0c;未支付的消息会积压在mq中&#xff0c;给mq带来巨大压力。我们可以利用Rabbitmq的延迟队列插件实现消息前一分钟尽快处理 1.1定义延迟消息实体 由于我们要多次发送延迟消息&#xff0c;因此需要先定义一个记录消息…

小程序与公众号下发统一消息接口返回45109

根据微信官方通告&#xff0c;自 2023 年 9 月 20 日起&#xff0c;下发统一消息接口将被收回&#xff0c;返回45109。链接见 小程序与公众号下发统一消息接口调整通知 | 微信开放社区各位开发者&#xff1a;下发统一消息 接口曾支持小程序与公众号统一的模板消息下发。由于小程…

虹科示波器 | 汽车免拆检修 | 2014款保时捷卡宴车行驶中发动机偶尔自动熄火

一、故障现象 一辆2014款保时捷卡宴车&#xff0c;搭载4.8L自然吸气发动机&#xff0c;累计行驶里程约为10.3万km。车主反映&#xff0c;行驶中发动机偶尔自动熄火&#xff0c;尤其在减速至停车的过程中故障容易出现。 二、故障诊断 接车后路试&#xff0c;确认故障现象与车主所…

DTC Network旗下代币DSTC大蒜头即将上线,市场热度飙升

全球数字资产领导者DTC Network宣布其代币DSTC&#xff08;大蒜头&#xff09;即将于近期上线&#xff0c;引发市场广泛关注。DTC Network以其创新性的区块链技术和多维度的网络构建&#xff0c;致力于打造一个融合Web3.0、元宇宙和DAPP应用的去中心化聚合公共平台&#xff0c;…

Shiro快速入门之二

一、前言 Shiro快速入门之一 介绍了Shiro三大核心组件&#xff0c;四大核心功能&#xff0c;以及一个简单的Test Demo&#xff0c;接下来两篇我会用一个比较完整的例子来讲述Shiro的认证及授权是怎么做的&#xff0c;本篇侧重于介绍认证的过程 二、Shiro认证例子 1、例子概述…

立体库堆垛机控制程序手动功能实现

手动操作功能模块 手动前后保护锁 *************提升手动程序段 手动上升&#xff0c;下降保护锁 **********货叉手动程序段

【第2章 Node.js基础】2.4 Node.js 全局对象(二) process 对象

process对象是一个全局对象&#xff0c;提供当前Node.js 进程信息并对其进行控制。通常用于编写本地命令行程序。 1.进程事件 process对象是EventEmitter类的实例&#xff0c;因此可以使用事件的方式来处理和监听process对象的各种事件。以下是一些常用的process对象事件&…

Webpack 性能优化 二次编译速度提升3倍!

本文作者为 360 奇舞团前端开发工程师 Rien. 本篇文章主要记录 webpack 的一次性能优化。 现状 随着业务复杂度的不断增加&#xff0c;项目也开始变得庞大&#xff0c;工程模块的体积也不断增加&#xff0c;webpack 编译的时间也会越来越久&#xff0c;我们现在的项目二次编译的…

医院安全(不良)事件管理系统源码 不良事件报告全套源码

不良事件管理系统是一种专为企业或组织设计的软件工具&#xff0c;用于跟踪、记录和管理不良事件。该系统可以有效地整合不良事件的收集、分类、分析和报告&#xff0c;帮助企业及时识别和处理不良事件&#xff0c;从而降低风险和损失。通过实时监控和自动化报告&#xff0c;该…

JavaScript数据类型和存储区别

目录 一、原始数据类型 二、引用数据类型 三、存储区别 四、常见错误 JavaScript是一种动态类型语言&#xff0c;这意味着变量可以在程序执行过程中改变其数据类型。了解JavaScript中的数据类型和它们的存储方式对于编写高效和可维护的代码至关重要。 在JavaScript中&…

世界互联网大会|云轴科技ZStack受邀分享云原生超融合

11月8日至10日“世界互联网大会乌镇峰会”在浙江嘉兴的乌镇开幕&#xff0c;大会的主题为“建设包容、普惠、有韧性的数字世界——携手构建网络空间命运共同体”&#xff0c;全球各界代表就热点焦点问题展开讨论&#xff0c;反映产业各界对互联网发展的前瞻思考&#xff0c;引领…

knife4j集成Swagger

knife4j集成 配置knife4j 第一步: 导入knife4j对应的maven坐标,knife4j是为MVC框架集成Swagger生成API文档的增强解决方案 <dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><ver…