省份数量(题号547)
题目
有n
个城市,其中一些彼此相连,另一些没有相连。如果城市a
与城市b
直接相连,且城市b
与城市c
直接相连,那么城市a
与城市c
间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个n x n
的矩阵isConnected
,其中isConnected[i][j] = 1
表示第i
个城市和第j
个城市直接相连,而isConnected[i][j] = 0
表示二者不直接相连。
返回矩阵中省份的数量。
示例 1:
输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2
复制代码
示例 2:
输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]]
输出:3
复制代码
提示:
- 1 <= n <= 200
- n == isConnected.length
- n == isConnected[i].length
- isConnected[i][j] 为 1 或 0
- isConnected[i][i] == 1
- isConnected[i][j] == isConnected[j][i]
链接
解释
这题和岛屿问题其实差不多,就是理解题目意思上有点费劲,笔者反正是花了不少时间才理解了题目的真正含义。
岛屿问题的一个坐标[i, j]
其实都是一个独立的元素,但在这题中,每个坐标代表的不是独立的元素,而是一个关系,真正的元素只有n
个,因为题目说了这是一个n*n
的矩阵。
比方说点[1, 4]
,那就证明城市1和城市4其实是有关系的,它们属于一个省份,所以会被关联起来。
这样一来题目就简单了一些,写法上倒是没有什么太大的问题,问题主要有两个?:
- 并查集的变种(数组形态)
- 性能差异
先看看第一点,由于现在元素的个数只有n
个,所以可以使用数组,利用数组的index
和value
来确定一个数组的自身内容和其parent
,最后只需要获取index
和value
相等的元素即可。
第二点的话需要具体看代码说了,这里单纯说说不清楚。
?看看代码:
自己的答案(泛洪算法)
说实话,泛洪算法在这里其实也是管用的,只是需要注意泛洪的范围并不是周围的,而是i
和j
相同的元素,对应代码如下:
var findCircleNum = function(isConnected) {
var len = isConnected.length - 1
loop = [-1, 1]
count = 0
function findNeighbor(I, J) {
if (!isConnected[I][J]) return
isConnected[I][J] = 0
for (let i = 0; i <= len; i++) {
for (let j = 0; j <= len; j++) {
if (isConnected[i][j] === 1 && (i === I || j === J)) {
findNeighbor(i, j)
}
}
}
}
for (let i = 0; i <= len; i++) {
for (let j = 0; j <= len; j++) {
if (isConnected[i][j]) {
count++
findNeighbor(i, j)
}
}
}
return count
};
复制代码
因为其特殊的关系条件,导致在每次执行findNeighbor
方法时都需要遍历一次整个二维数组,这样显然造成了性能的浪费,结果也是十分自然的,耗时和内存占用都只有最后的5%,并不推荐。
自己的答案(并查集)
在了解了具体逻辑并且看了一眼答案之后自己写出来的代码,写完发现是真的简单?:
class UnionFind {
constructor(n) {
this.parents = new Array(n)
while (n--) this.parents[n] = n
}
findSet(x) {
while (x !== this.parents[x]) x = this.parents[x]
return x
}
unionSet(x, y) {
var xP = this.findSet(x)
var yP = this.findSet(y)
if (xP !== yP) this.parents[xP] = yP
}
getCount() {
return this.parents.filter((v, i) => i === v).length
}
}
function findCircleNum(isConnected) {
var len = isConnected.length
uf = new UnionFind(len)
for (let i = 0; i < len; i++) {
for (let j = 0; j <len; j++) {
if (isConnected[i][j]) uf.unionSet(i, j)
}
}
return uf.getCount()
}
复制代码
确实很简单,但这里涉及到了上面说的第二个问题——性能问题。
笔者真是百思不得其解,这段代码看上去没啥毛病啊,可性能就是上不去,在后20%~30%之间徘徊,但这段代码是笔者参照了一个题解写出来的,整体代码十分相像,但人的题解性能就会达到50%以上,有时甚至会70%,这就有点莫名其妙了,如果有的读者看出来问题所以希望可以给笔者指正一下,感恩的❤️~
更好的方法(递归&&泛洪算法)
讲真,这种想法简直是把题目理解到了极致。
笔者在初次看这个答案的时候竟然看的一头雾水,看了好几遍,跑了几次,对照着题目才把这种方法看懂,简直是强得令人发指,几乎可以说是最强的答案了。
先看看代码,对照着解释会更好理解?:
var findCircleNum = function(isConnected) {
var count = 0
len = isConnected.length
visited = new Uint8Array(len)
function findNeighbor(i) {
visited[i] = 1
for (let j = len; j--;) {
if (isConnected[i][j] && !visited[j]) findNeighbor(j)
}
}
for (let j = len; j--;) {
if (visited[j] === 0) {
count++
findNeighbor(j)
}
}
return count
};
复制代码
这个答案是笔者自己写的,借鉴了小宇的答案后自己写出来的,和原答案有些出入,但原理是一样的,只是实现上有些小的出入,性能其实并不如原答案。
首先想理解这个答案,先要看见一个大前提——城市共有n
个。
是的,也就是说省份最多也会有n
个,此时的数组是酱的?:
[
[1, 0, 0],
[0, 1, 0],
[0, 0, 1]
]
复制代码
结果为1
的元素只有[1, 1]
、[2, 2]
、[3, 3]
。也就是对角线上的元素。那么这里可以先将所有的城市标记为0
,然后从头开始遍历城市,每遇到一个为0
的城市就将其标记为1
,并且在总数上加1。
之后查找与这个城市有关的城市,也就是isConnected[i][j]
为1节点,拿到此时的j
,如果j
城市没有被标记过,那就把j
城市也标记了,其值改为1,再找到与j
城市相关的城市,一直进行递归。
那么最后只要返回最后的count
就好了。
这种方法的好处是不用循环一个二维数组,只需要循环两次一维数组即可,也是某种意义上的降维吧。并且而外新增的内容只有一个等同于城市数量的一维数组,里面的值只有1
或者0
。
简单是很简单,主要是很难想到。
这里还借鉴了一些原答案的细节部分以提升性能,比方说使用Unit8Array
来减少内存占用,使用for
循环的--
模式来进行更快的运算,原答案真的很强啊。
更好的方法(并查集)
在自己的答案并查集解法中,笔者说过有性能问答,达不到原答案的性能。
这里的原答案也是小宇写的,强得一匹,这里放一下代码,如果有读者看出了原答案比我的强在哪里希望可以在评论区回复,笔者是真的尽力了~
class UnionFind {
constructor (n) {
this.parents = new Uint8Array(n)
while (n--) this.parents[n] = n
}
union (x, y) {
const rootX = this.find(x), rootY = this.find(y)
if (rootX !== rootY) this.parents[rootX] = rootY
}
find (x) {
while (x !== this.parents[x]) x = this.parents[x]
return x
}
}
var findCircleNum = function(isConnected) {
let i = -1, unionFind = new UnionFind(isConnected.length)
while (++i < isConnected.length)
for (let j = i + 1; j < isConnected[i].length; j++)
if (isConnected[i][j]) unionFind.union(i, j)
return unionFind.parents.filter((v, i) => v === i).length
};
复制代码
小结
这里其实还有一些别的解法,比方说BFS,这里就不多做赘述了,看上面的小宇同学的答案就行了,答案种类真的巨丰富,就是感觉解释部分比较时,像笔者这种FW就很难看懂了,果然是强者的世界啊~
PS:想查看往期文章和题目可以点击下面的链接:
这里是按照日期分类的?
经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的?
有兴趣的也可以看看我的个人主页?