前端算法入门之路(六)(并查集)

166 阅读4分钟

船长表情包镇楼

图片名称

并查集

  • 可以解决连通性问题
  • 考虑平均查找次数,将节点数更少的树合并到节点数更多的树上,平均查找次数更少

连通性问题

quick-find算法

  • 基于染色的思想,一开始所有的点的颜色不同
  • 连接两个点的操作,可以看出将一种颜色的点染成另外一个颜色
  • 如果两个点颜色一样,证明连通,否则不连通
  • 这种方法叫做并查集的【quick-find算法】 quick-find总结:查找操作非常快(O(1)),合并操作比较慢(O(n))

quick-union算法

  • 基于树的数据结构,一开始所有的点的根节点都是自己
  • 连接两个点的操作,可以看出将一个点所在集合挂在另一个点的根节点上
  • 如果两个点根节点一样,证明连通,否则不连通
  • 这种方法叫做并查集的【quick-union算法】 quick-union总结:合并操作比较快,查找操作比较慢,连通操作和查找操作都与树高有关。
    如果无脑把一个集合挂到另一个集合上,效率是否最高?如果改进,是按照节点数量还是树的高度为合并参考?

论证改进方案是按照节点数量还是树的高度

论证指标:平均查找次数 = 所有节点的查找次数/节点总数
设a树的节点数量为sa,所有节点深度相加为la,b树的节点数量为sb,所有节点深度相加为lb
当把b树挂到a树下时:平均查找次数 = la+lb+sb/(sa+sb)
当把a树挂到b树下时:平均查找次数 = la+lb+sa/(sa+sb)
约分比较可得:合并的时候节点数量较小的应该作为子集

按秩优化weighted-quick-union算法

  • 设置一个数组记录每个节点(集合)的节点数量
  • 连通操作判断两个集合的节点数量,将数量较小的集合作为子集,同时改变改集合节点数量 总结:性能有了很大提升,但是集合的树结构并不重要,如果能把所有子节点全挂在根节点上,查找的效率才最高

路径压缩的weighted-quick-union算法

  • 查找操作的时候将所有子节点都挂在根节点上 总结:路径压缩效率提升明显,编码难度低,相比按秩优化性价比更高

手撕一个并查集

class UnionSet {
    constructor(n) {
        this.fa = Array(n+1)
        this.size = Array(n+1).fill(1)
        for(let i = 0; i < n; i++) {
            this.fa[i] = i
        }
    }
    get(x) {
        if (this.fa[x] == x) return x
        const root = this.get(this.fa[x])
        this.fa[x] = root
        return root
    }
    marge(a, b) {
        let sa = this.get(a), sb = this.get(b)
        if (sa == sb) return
        if (this.size[sa] > this.size[sb]){
            this.fa[sb] = sa
            this.size[sa] += this.size[sb]
        } else {
            this.fa[sa] = sb
            this.size[sb] += this.size[sa]
        }
    }
}
// 并查集编码模板(去除按秩优化)
class UnionSet {
    constructor(n) {
        this.fa = Array(n+1)
        for(let i = 0; i < n; i++) {
            this.fa[i] = i
        }
    }
    get(x) {
        return this.fa[x] = (this.fa[x] == x ? x : this.get(this.fa[x]))
    }
    marge(a, b) {
        this.fa[this.get(b)] = this.get(a)
    }
}

LeetCode肝题

    1. 省份数量
// 遍历矩阵将符合条件的城市连接,最后遍历所有根节点的数量(get返回值等于自己)
var findCircleNum = function(isConnected) {
    const n = isConnected.length
    let u = new UnionSet(n), ans = 0
    for(let i = 0; i < n; i++) {
        for(let j = 0; j < i; j++) {
            if(isConnected[i][j]) u.marge(i,j)
        }
    }
    for(let i = 0; i < n; i++) {
        if (u.get(i) == i) ans++
    }
    return ans
};
    1. 岛屿数量
// 通过将相邻的'1'连通,最终查询根节点数量
// 连通的时候将二维数组转化为编号,连通相应位置的编号
// 每个'1'节点只需要连通两个方向的其他'1'
var numIslands = function(grid) {
    let row = grid.length, col = grid[0].length, ans = 0
    let toIndex = function(i, j) {
        return i * col + j
    }
    let u = new UnionSet(row * col)
    for(let i = 0; i < row; i++) {
        for(let j = 0; j < col; j++) {
            if (grid[i][j] == '0') continue
            if (i > 0 && grid[i - 1][j] == '1') u.marge(toIndex(i, j), toIndex(i - 1, j))
            if (j > 0 && grid[i][j - 1] == '1') u.marge(toIndex(i, j), toIndex(i, j - 1))
        }
    }
    for(let i = 0; i < row; i++) {
        for(let j = 0; j < col; j++) {
            if (grid[i][j] == '1' && u.get(toIndex(i, j)) == toIndex(i, j)) ans++
        }
    }
    return ans
};
    1. 等式方程的可满足性
// 将相等的字母连通,如果不等的字母也是连通的则等式方程不可满足
var charToIndex = function(s) {
    return s.charCodeAt() - 97
}
var equationsPossible = function(equations) {
    let u = new UnionSet(26)
    for(item of equations) {
        if (item[1] == '!') continue
        u.marge(charToIndex(item[0]), charToIndex(item[3]))
    }
    for(item of equations) {
        if (item[1] == '!' && u.get(charToIndex(item[0])) == u.get(charToIndex(item[3]))) {
            return false
        }
    }
    return true
};
    1. 冗余连接
// 如果是冗余连接,那么当前两个点一定是连通的
var findRedundantConnection = function(edges) {
    let u = new UnionSet(edges.length)
    for(let item of edges) {
        if (u.get(item[0]) == u.get(item[1])) return item
        u.marge(item[0], item[1])
    }
    return []
};
    1. 连通网络的操作次数
// 如果线缆数量小于电脑数量-1,说明线缆不够返回-1
// 将连接的电脑连通,最后计算模块数量
var makeConnected = function(n, connections) {
    if (connections.length < n - 1) return -1
    let u = new UnionSet(n), ans = 0
    for(let item of connections) {
        u.marge(item[0], item[1])
    }
    for(let i = 0; i < n; i++) {
        if (u.get(i) == i) ans++
    }
    return ans - 1
};
    1. 最长连续序列
// 改写一下并查集的实现,增加当前集合的元素数量
// 定义一个map当前值和下标,如果存在当前值-1和当前值+1,则连通当前值的下标和-1、+1的下标
// 最后返回集合元素数量最多的大长度
class UnionSet {
    constructor(n) {
        this.fa = Array(n + 1)
        this.cut = Array(n + 1).fill(1)
        for(let i = 0; i < n; i++) {
            this.fa[i] = i
        }
    }
    get(x) {
        return this.fa[x] = (this.fa[x] == x ? x : this.get(this.fa[x]))
    }
    marge(a, b) {
        let sa = this.get(a), sb = this.get(b)
        if (sa == sb) return
        this.cut[sa] += this.cut[sb]
        this.fa[sb] = sa
    }
}
var longestConsecutive = function(nums) {
    let u = new UnionSet(nums.length), map = {}, ans = 0
    for(let i = 0; i < nums.length; i++) {
        if (map[nums[i]] >= 0) continue
        if (map[nums[i] - 1] >= 0) {
            u.marge(i, map[nums[i] - 1])
        }
        if (map[nums[i] + 1] >= 0) {
            u.marge(i, map[nums[i] + 1])
        }
        map[nums[i]] = i
    }
    for(let i = 0; i < nums.length; i++) {
        if (u.get(i) == i && u.cut[i] > ans) ans = u.cut[i]
    }
    return ans
};
    1. 移除最多的同行或同列石头
// 将同行或者同列的石头连通,最终剩下的石头的数量等于集合的数量
// 可以移除的石子的最大数量就是石头总数量减去集合数量
var removeStones = function(stones) {
    let u = new UnionSet(stones.length), num = 0, map_x = {}, map_y = {}
    for(let i = 0; i < stones.length; i++) {
        let x = stones[i][0]
        let y = stones[i][1]
        if (map_x[x] >= 0) {
            u.marge(i, map_x[x])
        }
        if (map_y[y] >= 0) {
            u.marge(i, map_y[y])
        }
        map_x[x] = i
        map_y[y] = i
    }
    for(let i = 0; i <= stones.length; i++) {
        if (u.get(i) == i) num++
    }

    return stones.length - num
};
    1. 交换字符串中的元素
// 题意是将可连通的字符按从小到大的顺序排列
// 先将所有字母对应的下标连通
// 定义一个map,连通的每一个集合根节点作为key,建立小顶堆
// 遍历的时候从当前元素的根节点的小顶堆中取堆顶元素
var smallestStringWithSwaps = function(s, pairs) {
    let u = new UnionSet(s.length), queueMap = {}, ans = ''
    const h = new MinPriorityQueue();
    for(item of pairs) {
        u.marge(item[0], item[1])
    }
    for(let i = 0; i < s.length; i++) {
        let root = u.get(i)
        if(queueMap[root]) queueMap[root].enqueue(s[i])
        else {
            queueMap[root] = new MinPriorityQueue({
                priority: (item) => item.charCodeAt()
            })
            queueMap[root].enqueue(s[i])
        }
    }
    for(let i = 0; i < s.length; i++) {
        ans += queueMap[u.get(i)].dequeue().element
    }
    return ans
};
    1. 账户合并
var accountsMerge = function(accounts) {
    // 建立邮箱:坐标,邮箱:名称两个map
    let emailToIndex = new Map(), emailToName = new Map(), emailCut = 0
    for(let item of accounts) {
        const name = item[0]
        for(let i = 1; i < item.length; i++) {
            const email = item[i]
            if (!emailToIndex.has(email)) {
                emailToIndex.set(email, emailCut++)
                emailToName.set(email, name)
            }
        }
    }
    // 将每一个不重复的邮箱看作一个集合,进行连通
    let u = new UnionSet(emailCut)
    for(let item of accounts) {
        const emailName = item[1]
        const emailIndex = emailToIndex.get(emailName)
        for(let i = 1; i < item.length; i++) {
            u.marge(emailIndex, emailToIndex.get(item[i]))
        }
    }
    // 建立一个邮箱根节点:邮箱集合的map
    const indexToEmail = new Map()
    for(item of emailToIndex.keys()) {
        const index = u.get(emailToIndex.get(item))
        const account = indexToEmail.get(index) ? indexToEmail.get(index) : []
        account.push(item)
        indexToEmail.set(index, account)
    }
    // 合并账户
    const merged = []
    for(let item of indexToEmail.values()) {
        item.sort()
        const name = emailToName.get(item[0])
        const account = []
        account.push(name)
        account.push(...item)
        merged.push(account)
    }
    return merged
};