[路飞] 岛屿数量——并查集

230 阅读3分钟

「这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战」。

记录 1 道算法题

岛屿数量

leetcode-cn.com/problems/nu…


并查集比起DFS和BFS会相对复杂一些,需要用不同的方法来处理处理过的节点。并查集是将其他节点并入到一个节点。一般用数组实现,即存放下标,然后读这个位置的值的时候如果下标不对就根据读到的下标继续找数组的对应位置,直到下标匹配上,这个就是根节点。

[0, 1, 2, 3, 4, 5]

先准备这样一个数组,他们的根节点都是自己,如果我们读 arr[1] 读到的是 1,就是他自己。

然后我们合并一些节点,数组变成了下面这样样子

[0, 0, 0, 2, 3, 2]

当我们读 arr[1] 的时候得到 0, 0 !== 1,所以 1 被并入了,至于并入到哪呢,要去读 arr[0],然后发现得到 0 === 0。所以 1 就是 0 的下属。

再继续看当我们读 arr[3] 的时候得到 2,2 !== 3,所以 3 被并入了。然后读 arr[2],得到 0,然后再读 arr[0]。他们之间的关系就是 3 是 2 的下属, 2 是 0 的下属。

0,1,2 是一个集合,3,4 是一个集合,2,3,5 是一个集合。然后0,2,3 就是集合之间相互连接的点。

用代码来实现上面的过程就是一个递归查找的过程。

    function find(parent, i) {
        if (parent[i] !== i) {
            // 路径压缩
            parent[i] = find(parent, parent[i])
        }
        return parent[i]
    }

路径压缩是通过重新赋值向上合并更多节点,压缩后找子节点就更快了。比如

                a
              b
            c
          d
        e
          
          变成
          
               a
             b   c
                d  e

立体的二维数组我们需要找4个方向,首先我们一开始准备一个一维数组。这里用数学式子来转换二维数组的下标。

    r 行 c 列
    const parent = new Array(c * r)
    for (let i = 0; i < r; i++) {
        for(let j = 0; j < c; j++) {
            parent[i * c + j] = i * c + j
        }
    }

我们用一维数组来保存了父节点的下标,除此之外我们还需要保存不同的集合之间是平级的还是不平级。平级的话随便合并都行,但如果是不平级的话,更高级的那一个要做父节点。

所以我们还要用一个数组来存放他们的等级,最低级 0,合并之后 ++,用对象来存的话,一个数组就可以解决。

    const rank = new Array(c * r).fill(0)

首先自己都指向自己,所以在一开始的时候我们只要合并了第一个,后面的遍历都会出现非指向。

第一次合并前 find 找到的下标肯定是都是自己,各不相同。如果合并过了,两个 find 的结果是一样的。所以如果两个不相等的时候,我们就要做合并操作。合并是根据等级决定谁做父级。

    function union(parent, rank, self, neighbor) [
        self = find(parent, self)
        neighbor = find(parent, neighbor)
        
        if (self !== neighbor) {
            if (rank[self] === rank[neighbor]) {
                // 同级合并后等级++
                parent[neighbor] = self
                rank[self]++
            } else if (rank[self] > rank[neighbor]) {
                parent[neighbor] = self
            } else {
                parent[self] = neighbor
            }
            // count--
        }
    }

接下来要解决怎么计数的问题。我们知道立体的二维数组会往四个方向都去尝试,所以不处理是肯定有重复的。理论上来说,我们只需要每一个第一次合并的时候 +1,其他的都是同一个岛屿,都是他的子节点。但我们知道什么时候是第一次合并是一件很麻烦的事情。但是我们很容易知道他有没有合并。收集陆地 1 的总数量,只要合并了我们就 -1。而第一次合并的时候不会 -1,因为 -1 出现在对隔壁节点的 union,而第一次合并早于这一步。所以剩下的就是岛屿的数量。

计算上右下左4个方向的下标,然后合并。

    function around(grid, parent, rank, i, j, c) {
        const self = i * c + j
        
        if (grid[i + 1]?.[j] === '1') {
            const neighbor = (i + 1) * c + j
            union(parent, rank, self, neighbor)
        }
        if (grid[i]?.[j + 1] === '1') {
            const neighbor = i * c + (j + 1)
            union(parent, rank, self, neighbor)
        }
        if (grid[i - 1]?.[j] === '1') {
            const neighbor = (i - 1) * c + j
            union(parent, rank, self, neighbor)
        }
        if (grid[i]?.[j - 1] === '1') {
            const neighbor = i * c + (j - 1)
            union(parent, rank, self, neighbor)
        }
    }

因为跨了几层,一直传参很麻烦,所以用一个类来处理。

完整代码如下:

    class UnionFind {
        constructor(grid, r, c) {
            this.grid = grid
            this.parent = new Array(r * c)
            this.rank = new Array(r * c).fill(0)
            this.count = 0
            
            for(let i = 0; i < r; i++) {
                for(let j = 0; j < c; j++) {
                    this.parent[i * c + j] = i * c + j
                    grid[r][c] === '1' && this.count++
                }
            }
            
            for(let i = 0; i < r; i++) {
                for(let j = 0; j < c; j++) {
                    if (grid[r][c] === '1') {
                        grid[r][c] = '0'
                        this.around(i, j, c)
                    }
                }
            }
        }
        
        around(i, j , c) {
            let self = i * c + j
            if (grid[i + 1]?.[j] === '1') {
                const neighbor = (i + 1) * c + j
                this.union(current, neighbor)
            }
            if (grid[i]?.[j + 1] === '1') {
                const neighbor = i * c + (j + 1)
                this.union(current, neighbor)
            }
            if (grid[i - 1]?.[j] === '1') {
                const neighbor = (i - 1) * c + j
                this.union(current, neighbor)
            }
            if (grid[i]?.[j - 1] === '1') {
                const neighbor = i * c + (j - 1)
                this.union(current, neighbor)
            }
        }
        
        union(self, neighbor) {
            self = find(self)
            neighbor = find(neighbor)
            
            if (self !== neighbor) {
                const l1 = this.rank[self]
                const l2 = this.rank[neighbor]
                if (l1 === l2) {
                    this.parent[neighbor] = self
                    this.rank[self]++
                } else if (l1 > l2) {
                    this.parent[neighbor] = self
                } else {
                    this.parent[self] = neighbor
                }
                this.count--
            }
        }
        
        find(i) {
            if (this.parent[i] === i) {
                this.parent[i] = this.find(this.parent[i])
            }
            return this.parent[i]
        }
    }

结束