Hi 你们好,我是张小猪。欢迎来到『宝宝也能看懂』系列之 leetcode 周赛题解。git
这里是第 173 期的第 3 题,也是题目列表中的第 1334 题 -- 『阈值距离内邻居最少的城市』github
有 n
个城市,按从 0
到 n-1
编号。给你一个边数组 edges
,其中 edges[i] = [fromi, toi, weighti]
表明 fromi
和 toi
两个城市之间的双向加权边,距离阈值是一个整数 distanceThreshold
。算法
返回能经过某些路径到达其余城市数目最少、且路径距离 最大 为 distanceThreshold
的城市。若是有多个这样的城市,则返回编号最大的城市。shell
注意,链接城市 i
和 j
的路径的距离等于沿该路径的全部边的权重之和。segmentfault
示例 1:数组
输入: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:app
输入: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 在阈值距离 4 之内只有 1 个邻居城市。
提示:优化
2 <= n <= 100
1 <= edges.length <= n * (n - 1) / 2
edges[i].length == 3
0 <= fromi < toi < n
1 <= weighti, distanceThreshold <= 10^4
(fromi, toi)
都是不一样的。MEDIUMspa
看完题目的第一反应,又是套路。哼,看小猪一套带走你!3d
题目的内容很是直白,就不作过多解释啦。咱们若是抽象的看题目提供的数据,把城市想象成一个个点,城市之间的道路想象成点之间的连线,而道路的长度就是线的权重,那么题目提供的数据其实就是一个无向有权的图。
无向的意思是连线是没有方向的。例如假设从 A 到 B 的直接距离是 3,那么从 B 到 A 的直接距离也就是 3。而有权的意思就是,咱们不一样点之间的连线多是不同的。例如假设从 A 到 B 的直接距离是 3,那么从 B 到 C 的直接距离多是 5。这就是它们的权重不同。
上面这里为何要先解释这个无向有权的问题,由于对于图来讲,其实处理的方式能够有不少。而其中有没有方向、有没有权重,会影响到咱们后续处理数据的逻辑。
不过要是继续这样说下去,那也太不是小猪的风格啦!小猪先说这个概念,就是想让还不知道的小伙伴们不要被那些奇奇怪怪的名词吓到。哼!都是纸脑抚!小猪的风格,固然仍是先从一个栗子出发啦。
对应着上面的图,假设咱们拿到的数据是:
5 [ [1, 4, 10], [0, 2, 6], [3, 4, 1], [1, 2, 2], [1, 0, 1], [3, 2, 3], ] 4
那么咱们面临的第一个问题就是,若是储存这些数据,毕竟每一次都去遍历搜索确定是不现实的。这里咱们能够预想到须要频繁访问的数据是每一个线段的长度,那么对应的也就会但愿这个访问是 O(1) 时间消耗的。说到这里,相信小伙伴们已经想到啦,那就是直接用索引去 mapping 便可。因为咱们的数据中点的名字正好都是从 0 开始的连续数字,因此天然的能够基于数组下标来进行标识。因而乎,第一个问题便迎刃而解。咱们能够获得相似下面的代码:
// JS 的多维数组呀,说多了都是泪 T_T const distance = Array.from({ length: n }, () => new Uint16Array(n)); for (const edge of edges) { distance[edge[0]][edge[1]] = distance[edge[1]][edge[0]] = edge[2]; }
接下来就到了核心的问题,那就是如何知道每个点到其余点的最短距离。咱们能够先来看看上面栗子中,从 0 号城市出发到 2 号城市的状况:
0 到 1 的直接距离是 1:
1 到 4 的直接距离是 1:
4 到 3 的直接距离是 1:
咱们这里列举出了全部不包含回头路的状况,能够看到从 0 到 2 的路线实际上是有 3 条的:
这里其实能够获得几个简单的信息:
说到这里,相信小伙伴们已经发现了,咱们没办法经过一些计算的方式去直接求得这个结果。只能基于数据去遍历每一种状况才能知道最终的结果。
既然提到了遍历,又是从起点到终点上的路径的距离和,那么可能会有小伙伴想到咱们是否是能够用以前说过的深度优先遍历呢?咱们能够尝试一下。
假设咱们如今须要找到从 0 到 3 的最短距离。因而咱们开始进行遍历。假设如今的遍历是先访问 2 号点。那么状况能够能是这样:
咱们会前后获取到两条路线,分别是 0 -> 2 -> 3,长度是 7;0 -> 2 -> 1 -> 4 -> 3,长度是 12。
而在深度优先遍历中,为了防止无限循环已经访问过的点,因此咱们会用一个集合记录已经访问过的点。在上面的遍历进行过程当中,若是咱们用紫色来标识已经被访问过的点,那么结果就是当前全部的点都已经被访问过了。
接下来,遍历继续进行。来到了 0 -> 1 这条线路。然而因为已经被访问过了,因此就不会继续走下去了。可是,其实咱们知道,从 0 到 3 的最短路径就是这条线路 0 -> 1 -> 4 -> 3,长度是 3。
这是一个最初解决这个问题很容易犯的错误。包括广度优先遍历也是同样的道理。不过其实咱们也不是不能够用深度优先遍从来实现,只是逻辑会更复杂一些。而且因为效率也不高,因此咱们也许能够尝试换一个思路来解决。
咱们先忽略这个看起来很吓人的名字,把视线回到上面的栗子中。
在前面的分析中,咱们已经列举过了从 0 到 2 的全部状况。虽然有 3 条路径,不过均可以归结成两种,即从 0 直接到 2,或者从 0 借助其余点再到 2。至于借助多个点的路径,能够理解成从 0 借助 3 到 2;而 0 到 3 又没有直接链接,因此即可从 0 借助 4 到 3。以此类推。
咱们能够把上面的分析再换成比较抽象的点,例如从 i 到 j 的最短距离。假设这个最短距离为 d[i][j]
,那么它可能来自于这两个点的直接距离 graph[i][j]
,或者是借助 k 点以完成的链接 d[i][k] + d[k][j]
。至于这里的 d[i][k]
和 d[k][j]
也就同理能够获得了。
当咱们基于上面的思路,计算出每个点到其余点的最短距离了以后。剩下的就很是简单了,根据题目给定的阈值进行计数和判断便可获得结果。具体流程以下:
[1, 10^4]
,因此我填充了 10001
。基于以上流程,能够获得相似下面的代码:
const findTheCity = (n, edges, distanceThreshold) => { const distance = Array.from({ length: n }, () => new Uint16Array(n).fill(10001)); for (const edge of edges) { distance[edge[0]][edge[1]] = distance[edge[1]][edge[0]] = edge[2]; } for (let i = 0; i < n; ++i) { for (let j = 0; j < n; ++j) { for (let k = 0; k < n; ++k) { if (k === j) continue; distance[j][k] = Math.min(distance[j][k], distance[j][i] + distance[i][k]); } } } let city = 0; let minNum = n; for (let i = 0; i < n; ++i) { let curNum = 0; for (let j = 0; j < n; ++j) { distance[i][j] <= distanceThreshold && ++curNum; } if (curNum <= minNum) { minNum = curNum; city = i; } } return city; };
上面的代码中,3 层 for
循环的结构能够理解成是 Floyd Warshall 算法的很是模板的实现。只要须要基于这个算法来解决问题,均可以套这样的模板。不过具体根据状况,咱们也能够作一点小小的优化。下面的代码主要作了两点小改动:
distance
矩阵实际上是沿着对角线对称的。天然的,咱们也就只须要进行一半的计算便可。const findTheCity = (n, edges, distanceThreshold) => { const MAX = 10001; const distance = Array.from({ length: n }, () => new Uint16Array(n).fill(MAX)); for (const edge of edges) { distance[edge[0]][edge[1]] = distance[edge[1]][edge[0]] = edge[2]; } for (let i = 0; i < n; ++i) { for (let j = 0; j < n; ++j) { if (i === j || distance[j][i] === MAX) continue; for (let k = j + 1; k < n; ++k) { distance[k][j] = distance[j][k] = Math.min(distance[j][k], distance[j][i] + distance[i][k]); } } } let city = 0; let minNum = n; for (let i = 0; i < n; ++i) { let curNum = 0; for (let j = 0; j < n; ++j) { distance[i][j] <= distanceThreshold && ++curNum; } if (curNum <= minNum) { minNum = curNum; city = i; } } return city; };
这段代码目前跑出了 64ms 暂时 beats 100%.
其实小猪一直很犹豫,究竟要不要加入一些看似吓人的名词。因此这篇文章小猪拖更了好久。(才不是在为拖更找借口呢,哼
最终小猪仍是决定用直白的栗子和语言来解释思路,可是会提到那些看起来奇奇怪怪的名词。主要是但愿不太了解的小伙伴们之后不会再被这些名词吓到啦。毕竟它们都是纸脑抚,小伙伴们和小猪都是最棒(pang)哒!耶~(过完这个年,真的是最 pang 的了...T_T
回到这道题目,关于图的处理方式真的有不少。这道题其实咱们还能够用挺多其余方式去处理的,例如 bellmanford、dijkstra 等等。有兴趣的小伙伴能够催更一下小猪快点去写关于图的专题内容。么么嗒~
最后,关于上面的优化代码,实际上是还有优化空间的。但是小猪苯苯的,有没有小伙伴能够帮帮小猪呢? >.<
加油武汉,天佑中华