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%。
这道题给定的数据结构是一个图,不过咱们暂时没有对于这个结构作过多展开介绍。而咱们的处理方法中也用到了深度优先遍历、广度优先遍历以及并查集。这几种其实都是比较常见的处理方式,不知道小伙伴们有没有从中找到一点套路呢?
欢迎学习委员告诉一下小猪这几种思路的套路是什么鸭!小猪...小猪能够给你摸摸猪鼻子呢 >.< 哼唧