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 等等。有興趣的小夥伴能夠催更一下小豬快點去寫關於圖的專題內容。麼麼嗒~
最後,關於上面的優化代碼,實際上是還有優化空間的。但是小豬苯苯的,有沒有小夥伴能夠幫幫小豬呢? >.<
加油武漢,天佑中華