Hi 你們好,我是張小豬。歡迎來到『寶寶也能看懂』系列之 leetcode 周賽題解。git
這裏是第 171 期的第 3 題,也是題目列表中的第 1319 題 -- 『連通網絡的操做次數』github
用以太網線纜將 n
臺計算機鏈接成一個網絡,計算機的編號從 0
到 n-1
。線纜用 connections
表示,其中 connections[i] = [a, b]
鏈接了計算機 a
和 b
。shell
網絡中的任何一臺計算機均可以經過網絡直接或者間接訪問同一個網絡中其餘任意一臺計算機。segmentfault
給你這個計算機網絡的初始佈線 connections
,你能夠拔開任意兩臺直連計算機之間的線纜,並用它鏈接一對未直連的計算機。請你計算並返回使全部計算機都連通所需的最少操做次數。若是不可能,則返回 -1
。數組
示例 1:網絡
輸入:n = 4, connections = [[0,1],[0,2],[1,2]] 輸出:1 解釋:拔下計算機 1 和 2 之間的線纜,並將它插到計算機 1 和 3 上。
示例 2:數據結構
輸入:n = 6, connections = [[0,1],[0,2],[0,3],[1,2],[1,3]] 輸出:2
示例 3:學習
輸入:n = 6, connections = [[0,1],[0,2],[0,3],[1,2]] 輸出:-1 解釋:線纜數量不足。
示例 4:優化
輸入:n = 5, connections = [[0,1],[0,2],[3,4],[2,3]] 輸出:0
提示:spa
1 <= n <= 10^5
1 <= connections.length <= min(n*(n-1)/2, 10^5)
connections[i].length == 2
0 <= connections[i][0], connections[i][1] < n
connections[i][0] != connections[i][1]
MEDIUM
題目內容並不複雜,就是給定了點的總數,以及一些目前點和點之間的鏈接關係。咱們能夠把任意一個鏈接放到任意的兩點之間,每次這麼作會令操做數加 1。最終須要返回聯通所有點所須要最小的操做數。若是沒法連通所有的點,則返回 -1
。
讀完以後個人第一反應是,這裏給定的數據其實就是一個圖,而且是無向且沒有權重的圖。那麼咱們先把沒法連通所有點的特殊狀況剔除掉吧。因爲鏈接能夠隨便移動,也沒有距離這種概念,因此對於 n
個點,咱們若是有 n-1
個鏈接,則必定能夠經過移動來連通所有的點。因此咱們能夠獲得以下的一個先行判斷:
if (connections.length < n - 1) return -1;
接下來就是題目主體了,即如何獲取到最小的操做數。咱們能夠想象一下,在最初題目給定了點和點的鏈接關係後,對於全部的點,可能會遇到 3 種不一樣的狀況:
而鏈接在一塊兒的點,無論內部鏈接的方式如何,咱們均可以認爲它們組成了一個網絡。而且單獨的一個點咱們也能夠認爲它是一個孤立的網絡。那麼在下圖中,咱們能夠發現,存在着 3 個互不相連的網絡。
若是咱們想知足題目的需求,那麼其實只須要把全部互不相連的網絡打通便可。而且因爲點之間的鏈接沒有距離,能夠隨便移動。因此咱們其實並不須要關心具體如何移動鏈接,咱們只須要找到互不相連的網絡的數量便可。由於咱們須要的最小移動操做數即是互不相連的網絡的數量減 1。
那麼到這裏,咱們的問題便轉換爲了如何獲得互不相連的網絡的數量。對於這個問題,咱們首先看看怎麼找到一個網絡中的全部的點。咱們能夠從任意一個點出發,假設它爲 A,咱們能夠根據題目給定的關係,找到全部和它鏈接起來的點。而後再對後面這些點繼續進行一樣的操做,須要注意的是要過濾掉已經被訪問過的點,避免無限循環。最終,咱們會沒法再獲得未訪問的點。那麼包含着點 A 的這個網絡的全部點就被找到了。
如今咱們再回頭看看須要解決的問題 -- 如何獲得互不相連的網絡的數量。咱們能夠嘗試從每個點出發,找到它所在的那個網絡並標記。這樣遍歷完全部的出發點以後,咱們即可以獲得互不相連的網絡的數量了。
基於上面的思路,咱們能夠設想到,咱們須要一個 Map
去記錄鏈接到當前點的其餘點。而且爲了標記已訪問過,咱們須要一個 visited
數組。
具體流程以下:
遍歷全部點:
基於以上流程,能夠實現相似下面的代碼:
const makeConnected = (n, connections) => { if (connections.length < n - 1) return -1; const graph = new Map(); const visited = new Uint8Array(n); for (const [a, b] of connections) { !graph.has(a) && graph.set(a, []); !graph.has(b) && graph.set(b, []); graph.get(a).push(b); graph.get(b).push(a); } let count = 0; for (let i = 0; i < n; ++i) count += helper(i); return count - 1; function helper(cur) { if (visited[cur]) return 0; visited[cur] = 1; if (graph.has(cur)) { for (const val of graph.get(cur)) helper(val); } return 1; } };
相信有很多小夥伴會發現,這裏的 helper
方法去取同網絡中全部的點不就是深度優先遍歷麼。恭喜你,已經成爲學習委員啦!
那麼,是否能夠用廣度優先遍歷實現呢?固然是能夠的啦。小夥伴們能夠本身嘗試一下,這裏我給一個例子:
function helper(cur) { if (visited[cur]) return 0; const queue = [cur]; for (let idx = 0; idx < queue.length; ++idx) { const val = queue[idx]; visited[val] = 1; if (graph.has(val)) { for (const next of graph.get(val)) { visited[next] === 0 && queue.push(next); } } } return 1; }
從新回到題目給定的點和點的鏈接關係,咱們換一種思路來考慮這個關係。
假設第一個鏈接關係是 a、b 兩個點相連,那麼咱們能夠認爲它們是屬於同一個集合。而且這裏咱們能夠人爲的添加一個方向,即咱們能夠假設最開始是有 a 點,那麼 a 本身造成了一個集合,這個集合的名字就叫作 a。而發生了這個鏈接關係後,b 點加入了 a 這個集合。
假設這時候又產生了鏈接關係 a、c 和 b、d,那麼很顯然 c 和 d 也將加入 a 這個集合。目前的集合狀態以下圖所示:
那麼咱們該如何儲存這個集合呢?一種很直接的方式就是基於 Map
來記錄。例如:
const connected = { a: 'a', b: 'a', c: 'a', d: 'b' };
這樣一來,對於任何一個點,若是咱們想知道它屬於哪個集合,咱們只須要不斷的向下查找,直到找到一個點的值是本身,便表示到了末端,也就獲得了集合的名字。這個查找的方法能夠相似以下來實現:
const find = (target, union) => { while (target !== union[target]) target = union[target]; return target; };
看到這個查找過程,相信小夥伴們會發現,鏈條越長,查找的越慢。因此在創建關係的時候,其實能夠作一個小優化,即對於上面的 d 這個點,因爲咱們知道 b 不是端點,因此咱們能夠把 d 直接連到 b 所連的那個點。從而縮短鏈條的長度。這時候的集合狀態以下圖:
到了這裏,隨着咱們不斷的遍歷鏈接關係,咱們可能會遇到須要把兩個互不相連的集合鏈接起來的狀況。以下圖所示,其中的虛線就是那個把左右兩個集合鏈接起來的新鏈接:
看起來很嚇人的樣子,不過其實仍是紙腦撫。由於咱們仍是隻須要把一個集合的末端指向另一個集合的末端便可。
綜上,咱們不斷的根據給定的鏈接關係創建並豐富咱們的集合。最終,沒有被包含進這個大集合的點便是咱們須要額外移動鏈接來連通的點。具體流程以下:
遍歷給定的鏈接關係:
基於以上流程,能夠實現相似下面的代碼:
const find = (target, union) => { while (target !== union[target]) target = union[target]; return target; }; const makeConnected = (n, connections) => { if (connections.length < n - 1) return -1; const connected = new Uint16Array(n); let count = 1; for (let i = 0; i < n; ++i) connected[i] = i; for (const [a, b] of connections) { const oa = find(a, connected); const ob = find(b, connected); if (oa !== ob) { connected[ob] = oa; ++count; } } return n - count; };
上述代碼其實算是這種並查集思路的模板代碼,基於這種思路的問題均可以用相似的代碼來實現。不過相信小夥伴們會發現,爲了通用,其中對於每一個鏈接關係中的兩個點都是順着鏈條找到了末端,而後再進行處理。而且爲了達到這個目的,咱們對每一個點初始化了一個只包含它的孤立集合。那麼這裏有沒有特定的更優化的方法呢?
在不初始化全部點爲孤立集合的前提下,咱們能夠看看咱們會遇到哪些具體的狀況:
對應的,咱們能夠用 3 種方式對它們進行處理:
最終咱們的結果就是點的總數,減去被歸入集合的點的數量,在加上集合的數量減 1。
基於特定狀況優化後的代碼以下:
const makeConnected = (n, connections) => { if (connections.length < n - 1) return -1; const DEFAULT = n; const connected = new Uint16Array(n).fill(DEFAULT); let point = 0; let set = 0; for (let i = 0; i < connections.length; ++i) { const a = connections[i][0]; const b = connections[i][1]; const va = connected[a]; const vb = connected[b]; if (va === DEFAULT && vb === DEFAULT) { connected[a] = a; connected[b] = a; point += 2; ++set; continue; } if (va !== DEFAULT && vb !== DEFAULT) { let na = va, nb = vb; while (na !== connected[na]) na = connected[na]; while (nb !== connected[nb]) nb = connected[nb]; if (na !== nb) { connected[nb] = na; --set; } continue; } va === DEFAULT ? (connected[a] = vb) : (connected[b] = va); ++point; } return n - point + set - 1; };
這段代碼目前以 68ms 暫時 beats 100%。
這道題給定的數據結構是一個圖,不過咱們暫時沒有對於這個結構作過多展開介紹。而咱們的處理方法中也用到了深度優先遍歷、廣度優先遍歷以及並查集。這幾種其實都是比較常見的處理方式,不知道小夥伴們有沒有從中找到一點套路呢?
歡迎學習委員告訴一下小豬這幾種思路的套路是什麼鴨!小豬...小豬能夠給你摸摸豬鼻子呢 >.< 哼唧