记录 1 道算法题
省份数量
547. 省份数量 - 力扣(LeetCode) (leetcode-cn.com)
n 个省份组成的矩阵,有连接的省份之间串起来算一个,没有连接的独立省份算一个。符合用并查集的解题方法。因为每个省份与他联通的其他省份都是串起来,其他省份被收归到这个省份的下属。
如果立体的理解这个矩阵,就是行和列是对称的,就是顺着45°轴对称,接下来的深度优先广度优先都是以这种视角解题。
a a a a a a a
a
a
a
a
a
a
或者
a a a a a
a
a
a
-
深度优先
首先和二叉树的深度优先一样,先从矩阵中拿出第一行,然后遍历第一行的省份,遇到符合的省份就以这个省份为基点,读取这个省份的关系,即这个省份所在的行,然后遍历这一行。
因为这个矩阵的对称的,也就是说他有一半是重复的,和九九乘法表一样。而深度优先也避免不了要排除掉重复的省份。就比如 2 x 4 和 4 x 2。因为遍历矩阵是一行一行读。也就造成了我们不知道这个深度是不是有的节点已经被读过了。比如
a b c d e a b c d e当我们去读到 c * d 的时候,我们就进入 c 的深度,去读 c 这一行。我们不知道 a * c, b * c 有没有被读过。因为 a 和 c 的关系要看 a 这一行有没有被读跟 b 这一行有没有被读。假如我们之前在 b * ?的时候进入了 b 的深度,我们就知道了 b 和其他省份之间的关系。就是在第一行的时候,如果 a x b 没有进入深度,我们就不知道 b 这一行的情况,如果进入了我们就知道了 b * c。那后面进入 a * c 的深度 c 的时候,我们就已经提前知道了 c * b,如果再计算就会重复。所以要引入一个集合来存放已经登记过的省份。
里面比较抽象的是因为行和列的省份下标是对应的,所以当下标共用的时候就要在脑子里做一次转换。把下标作为省份的id 会好理解一点。
function findCircleNum(isConnected) { const provinces = isConnected.length const set = new Set() let count = 0 for(let i = 0; i < provinces; i++) { if (!set.has(i)) { // 还没有被深度遍历 // 每一个省份都记一次,然后通过set收集深度遍历时的关系, // 过滤掉这个省份的下属,不重复计数 dfs(isConnected, provinces, i, set) count++ } } return count } function dfs(isConnected, provinces, i, set) { for(let j = 0; j < provinces; j++) { // 用于深度遍历时排除已经遍历过的省份行。 if (isConnected[i][j] === 1 && !set.has(j)) { // 要遍历这个省份行了所以标记一下,收集成他的下属, // 不重复计数。 set.add(j) // 深度遍历 dsf(isConnected, provinces, j, set) } } } -
广度优先
广度优先和二叉树的广度优先一样,一行一行遍历,不像深度那样,遇到省份就跳到那个省份所在行进行遍历。
首先广度优先要有一个数组收集每一行的省份,因为是要处理掉连接的省份,不让他们重复计数,所以也是要有一个集合来记录已经被连接的省份。
function findCircleNum(isConnected) { const provinces = isConnected.length const set = new Set() const queue = [] let count = 0 for(let i = 0; i < isConnected.length; i++) { // 避免已经被收集连接的省份 if (!set.has(i)) { // 数组代表每一层的省份,包括他的连通省份 queue.push(i) count++ while(queue.length) { // 拿出省份 const p = queue.shift() // 标记,在这个循环里, // 收集到的和他连通的省份以及连通的连通的连通的*n省份都会被标记。 set.add(p) for(let j = 0; j < provinces; j++) { // 将这一行遍历一遍,收集与他连通的省份,以及他们的连通省份, // 都会收集起来,避免重复计数 if (isConnected[p][j] && !set.has(j)) { queue.push(j) } } } } } return count } -
并查集
并查集就是给节点认一个父亲,形成一个父子的树状结构,而子也是其他节点的父,从而形成了一个庞大的族谱。和将连接的省份串起来,形成一条链是一样的。使用一个一维数组存放省份的id,与下标对应,然后如果读到数组里的值和下标不对应,则说明不是根节点,需要往下继续找,通过遍历来得到最终的父亲。
原本:[0, 1, 2, 3, 4] 变成树状结构之后:[0, 3, 2, 2, 4]这里表明了有两个独立的节点 0 和 4。以及一个父亲 2。
怎么找的呢?
首先在读
arr[1]的时候得到1 !== 3,所以这个 1 的父亲就是 3,然后这时侯在顺着读arr[3]得到3 !== 2,说明这个 3 的父亲是 2,然后继续读arr[2],这时候2 === 2,找到这个根就是 2,其他 1 和 3 都是他的后代,所以这三个节点算一个节点。用这个并查集的时候不需要额外用一个集合标记哪些已经被找到的连接省份。因为下标就是他们的父亲,只要不是自己那就是后代。
function findCircleNum(isConnected) { const provinces = isConnceted.length const parent = Array.from(new Array(provinces), (_, i) => i) for(let i = 0; i < provinces; i++) { for(let j = i + 1; j < provinces; j++) { // 都可以,作用就是将根节点放到被人的后代里面, // 串起来,成立父子关系 // parent[find(parent,j)) = find(parent, i) // or parent[find(parent, i) = find(parent, j) } } let count = 0 for(let i = 0; i < parent.length; i++) { if (parent[i] === i) { count++ } } return count } // find函数的功能是找到根节点。并返回根节点的下标/id,同一个 function find(parent, i) { if (parent[i] !== i) { return find(parent, parent[i]) } else { return parent[i] } }
结束