并查集-JS实现

450 阅读11分钟

定义

并查集也叫不相交集合(Disjoint Set), 主要用于解决一些元素分组的问题。主要有"合并集合"和"查找集合中的元素"两种操作。并查集非常适合解决"连接"相关的问题。

核心操作

查找(find): 查找元素所在的集合(此处的集合指广义的数据集合)

合并(union): 将两个元素所在的集合合并为一个集合

实现思路

主要有两种实现思路, 实际过程中,推荐使用Quick Union实现。

Quick Find

查找(find)的时间复杂度: O(1)
合并(union)的时间复杂度: O(n)

Quick Union
查找(find)的时间复杂度: O(logn)
合并(union)的时间复杂度: O(logn)

代码实现

初始化时,每个元素各自属于一个单元素集合

Quick Find 的union(v1, v2): 让v1所在集合的所有元素都指向v2的根结点

Quick Find实现

class UnionFind {
    constructor(n) {
        this.parents = new Array(n);
    }
    init() {
        for (let i = 0; i < this.parents.length; i++) {
            this.parents[i] = i;
        }
    }
    //查找v所属的集合(根节点)父节点就是根节点
    find(x) {
        return this.parents[v];
    }
    //合并v1、v2所在的集合,将v1所在集合的所有元素,都嫁接到v2的父节点上
    union(x, y) {
      let p1 = this.find(x);
      let p2 = this.find(y);
      if (p1 == p2) return;

      for (let i = 0; i < this.parents.length; i++) {
        if (this.parents[i] == p1) {
          this.parents[i] = p2;
        }
      }
    }
    //检查v1、v2是否属于同一个集合
    isSame(x, y) {
      return this.find(x) === this.find(y)
    }
}

Quick Union的union(v1, v2): 让v1的根结点指向v2 的根结点

Quick Union实现

class UnionFind {
    constructor() {
        this.parents = new Array(n);
    }
    init(x) {
      for (let i = 0; i < this.parents.length; i++) {
        this.parents[i] = i;
      }
    }
    //查找v所属的集合(根节点),通过parent链条不断地向上找,直到找到根节点
    find(x) {
      while (x != this.parents[x]) {
        x = this.parents[x];
      }
      return x;
    }
    //合并v1、v2所在的集合,将v1的根节点嫁接到v2的根节点上
    union(x, y) {
      let p1 = this.find(x);
      let p2 = this.find(y);
      if (p1 == p2) return;
      this.parents[p1] = p2;
    }
    //检查v1、v2是否属于同一个集合
    isSame(x, y) {
      return this.find(x) === this.find(y)
    }
}

Quick Union优化

在union过程中, 可能会出现树不平衡的情况,甚至退化成链表,需要进行优化。

常见的优化方案有以下两种:

  • 基于size的优化:元素少的树嫁接到元素多的树
  • 基于rank的优化:矮的树嫁接到高的树

Quick Union - 基于size的优化

class UnionFind {
    constructor() {
        this.parents = new Array(n);
        this.sizes = new Array(n);
    }
    init(x) {
      for (let i = 0; i < this.parents.length; i++) {
        this.parents[i] = i;
      }
      for (let i = 0; i < this.sizes.length; i++) {
        this.sizes[i] = 1;
      }
    }
    //查找v所属的集合(根节点),通过parent链条不断地向上找,直到找到根节点
    find(x) {
      while (x != this.parents[x]) {
        x = this.parents[x];
      }
      return x;
    }
    //合并v1、v2所在的集合,将v1的根节点嫁接到v2的根节点上
    union(x, y) {
      let p1 = this.find(x);
      let p2 = this.find(y);
      if (p1 == p2) return;

      if (this.sizes[p1] < this.sizes[p2]) {
  			this.parents[p1] = p2;
  			this.sizes[p2] += this.sizes[p1];
  		} else {
  			this.parents[p2] = p1;
  			this.sizes[p1] += this.sizes[p2];
  		}
    }
    //检查v1、v2是否属于同一个集合
    isSame(x, y) {
      return this.find(x) === this.find(y)
    }
}

Quick Union - 基于rank的优化

基于size的优化,也会出现树不平衡的情况, 如下图所示

基于rank优化,如下图所示

class UnionFind {
    constructor() {
        this.parents = new Array(n);
        this.ranks = new Array(n);
    }
    init(x) {
      for (let i = 0; i < this.parents.length; i++) {
        this.parents[i] = i;
      }
      for (let i = 0; i < this.ranks.length; i++) {
        this.ranks[i] = 1;
      }
    }
    //查找v所属的集合(根节点),通过parent链条不断地向上找,直到找到根节点
    find(x) {
      while (x != this.parents[x]) {
        x = this.parents[x];
      }
      return x;
    }
    //合并v1、v2所在的集合,将v1的根节点嫁接到v2的根节点上
    union(x, y) {
      let p1 = this.find(x);
      let p2 = this.find(y);
      if (p1 == p2) return;

      if (this.ranks[p1] < this.ranks[p2]) {
        this.parents[p1] = p2;
      } else if((this.ranks[p1] > this.ranks[p2])) {
        this.parents[p2] = p1;
      } else {
        this.parents[p1] = p2;
        this.ranks[p2] += 1;
      }
    }
    //检查v1、v2是否属于同一个集合
    isSame(x, y) {
      return this.find(x) === this.find(y)
    }
}

路经压缩(Path Compression)

基于rank的优化,树会相对平衡一些,但是随着Union次数的增多,树的高度依然会越来越高,导致find操作变慢,尤其底层节点(因为find是不断向上找到根结点),因此,可以基于路经压缩优化。路经压缩是指在find时该路经上的所有节点都指向根结点,从而降低树的高度。

class UnionFind {
    constructor() {
        this.parents = new Array(n);
        this.ranks = new Array(n);
    }
    init(x) {
      for (let i = 0; i < this.parents.length; i++) {
        this.parents[i] = i;
      }
      for (let i = 0; i < this.ranks.length; i++) {
        this.ranks[i] = 1;
      }
    }
    //查找v所属的集合(根节点),通过parent链条不断地向上找,直到找到根节点
    find(x) {
      if (this.parents[x] != x) {
        this.parents[x] = this.find(this.parents[x]);
      }
      return this.parents[x];
    }
    //合并v1、v2所在的集合,将v1的根节点嫁接到v2的根节点上
    union(x, y) {
      let p1 = this.find(x);
      let p2 = this.find(y);
      if (p1 == p2) return;

      if (this.ranks[p1] < this.ranks[p2]) {
        this.parents[p1] = p2;
      } else if((this.ranks[p1] > this.ranks[p2])) {
        this.parents[p2] = p1;
      } else {
        this.parents[p1] = p2;
        this.ranks[p2] += 1;
      }
    }
    //检查v1、v2是否属于同一个集合
    isSame(x, y) {
      return this.find(x) === this.find(y)
    }
}

路经压缩使路经上的所有节点都指向根结点,实现成本稍高(上面实现采用递归实现),还有2种更优的实现,不但能降低树高,实现成本也比路经压缩低,路经分裂(path Spliting)和路经减半(Path Halving)。

路径分裂(path Spliting)

路径分裂是指在find过程中每个节点都指向其祖父节点(parent 的 parent), 如下图所示。

实现过程基于rank优化,不同的是find实现,代码如下:

find(v) { 
  while (v != this.parents[v]) {
    let p = this.parents[v];
    this.parents[v] = this.parents[this.parents[v]];
    v = p;
  }
  return v;
}

路经减半(Path Halving)

路径减半是指在find过程中每隔一个节点就指向其祖父节点(parent 的 parent), 如下图所示。

实现过程基于rank优化,不同的是find实现,代码如下:

find(v) {
  while (v != this.parents[v]) {
    this.parents[v] = this.parents[this.parents[v]];
    v = this.parents[v];;
  }
  return v;
}

Union-Find应用

990. 等式方程的可满足性

给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:"a==b" 或 "a!=b"。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。 
只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。

示例 1:

输入:["a==b","b!=a"]
输出:false
解释:如果我们指定,a = 1b = 1,那么可以满足第一个方程,但无法满足第二个方程。没有办法分配变量同时满足这两个方程。

示例 2:输入:["b==a","a==b"]输出:true解释:我们可以指定 a = 1b = 1 以满足满足这两个方程。

来源:leetcode-cn.com/problems/sa…

/**
 * @param {string[]} equations
 * @return {boolean}
 */
var equationsPossible = function(equations) {
    const uf = new UnionFind(26)
    // 先让相等的字母形成连通分量
    for (const e of equations) {
        if(e[1] === '=') uf.union(e.charCodeAt(0) - 97, e.charCodeAt(3) - 97)
    }
    // 检查不相等关系是否打破相等关系的连通性
    for (const e of equations) {
        if(e[1] === '!' && uf.find(e.charCodeAt(0) - 97) === uf.find(e.charCodeAt(3) - 97)) {
            return false;
        }
    }
    return true;
};

class UnionFind {
    constructor(num) {
        this.parents = new Array(num);
        this.ranks = new Array(num);
        for (let i = 0; i < num; i++) {
            this.parents[i] = -1;
            this.ranks[i] = 0;
        }
    }
    find(x) {
        while(this.parents[x] !== -1) {
            x = this.parents[x];
        }
        return x;
    }
    union(x, y) {
        let p1 = this.find(x);
        let p2 = this.find(y);
        if (p1 == p2) return;

        if (this.ranks[p1] < this.ranks[p2]) {
            this.parents[p1] = p2;
        } else if((this.ranks[p1] > this.ranks[p2])) {
            this.parents[p2] = p1;
        } else {
            this.parents[p1] = p2;
            this.ranks[p2] += 1;
        }
    }
}

547. 省份数量

有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。 省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。 给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。 返回矩阵中 省份 的数量。 

来源:leetcode-cn.com/problems/nu…

代码实现:采用路径压缩的方式实现

/**
 * @param {number[][]} isConnected
 * @return {number}
 */
var findCircleNum = function(isConnected) {
    const provines = isConnected.length;
    const uf = new UnionFind(provines);
    uf.init(provines);
    for (let i = 0; i < provines; i++) {
        for (let j = i + 1; j < provines; j++) {
            if(isConnected[i][j] === 1) {
                uf.union(i, j)
            }
        }
    }
    let circles = 0;
    uf.parents.forEach((ele, index) => {
        if(ele === index) {
            circles++;
        }
    });
    return circles;
};

class UnionFind {
    constructor(n) {
        this.parents = new Array(n);
        this.ranks = new Array(n);
    }
    init(x) {
      for (let i = 0; i < this.parents.length; i++) {
        this.parents[i] = i;
      }
      for (let i = 0; i < this.ranks.length; i++) {
        this.ranks[i] = 1;
      }
    }
    //查找v所属的集合(根节点),通过parent链条不断地向上找,直到找到根节点
    find(x) {
      if (this.parents[x] != x) {
        this.parents[x] = this.find(this.parents[x]);
      }
      return this.parents[x];
    }
    //合并v1、v2所在的集合,将v1的根节点嫁接到v2的根节点上
    union(x, y) {
      let p1 = this.find(x);
      let p2 = this.find(y);
      if (p1 == p2) return;

      if (this.ranks[p1] < this.ranks[p2]) {
        this.parents[p1] = p2;
      } else if((this.ranks[p1] > this.ranks[p2])) {
        this.parents[p2] = p1;
      } else {
        this.parents[p1] = p2;
        this.ranks[p2] += 1;
      }
    }
}

130. 被围绕的区域

给定一个二维的矩阵,包含 'X' 和 'O'(字母 O)。 找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。 

 示例: 

 X X X X
X O O X
X X O X
X O X X 

 运行你的函数后,矩阵变为: 

 X X X X
X X X X
X X X X
X O X X 

来源:leetcode-cn.com/problems/su…

/**
 * @param {character[][]} board
 * @return {void} Do not return anything, modify board in-place instead.
 */
var solve = function (board) {
    class unionFind {
        constructor(n) {
            this.count = n;
            this.parents = new Array(n);
            for (let i = 0; i < n; i++) {
                this.parents[i] = i;
            }
        }

        find(p) {
            if (this.parents[p] != p) {
                this.parents[p] = this.find(this.parents[p]);
            }
            return this.parents[p];
        }

        union(p, q) {
            let rootP = this.find(p);
            let rootQ = this.find(q);
            if (rootP === rootQ) return;
            this.parents[rootP] = rootQ;
            this.count--;
        }
        isConnected(p, q) {
            return this.find(p) === this.find(q)
        }
    }
    let row = board.length;
    if (row == 0) return;
    let col = board[0].length;
    let dummy = row * col;
    let uf = new unionFind(dummy);
    let arr = [[1, 0], [0, 1], [-1, 0], [0, -1]];
    // 构建联通区域
    for (let i = 0; i < row; i++) {
        for (let j = 0; j < col; j++) {
            if (board[i][j] == 'O') {
                // 四个边界都联通
                if (i == 0 || j == 0 || i == row - 1 || j == col - 1) {
                    uf.union(i * col + j, dummy)
                } else {
                    //考察四个方向
                    for (let k = 0; k < 4; k++) {
                        let x = i + arr[k][0];
                        let y = j + arr[k][1];
                        if (board[x][y] == 'O') uf.union(x * col + y, i * col + j);
                    }
                }
            }
        }
    }
    // 判定联通区域
    for (let i = 1; i < row - 1; i++) {
        for (let j = 1; j < col - 1; j++) {
            if (!uf.isConnected(i * col + j, dummy)) board[i][j] = 'X';
        }
    }

};

200. 岛屿数量

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。 岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 此外,你可以假设该网格的四条边均被水包围。

示例 1:

输入:grid = [  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
]
输出:1

示例 2:

输入:grid = [  ["1","1","0","0","0"],
  ["1","1","0","0","0"],
  ["0","0","1","0","0"],
  ["0","0","0","1","1"]
]
输出:3

來源:leetcode-cn.com/problems/nu…

/**
 * @param {character[][]} grid
 * @return {number}
 */
var numIslands = function(grid) {
    const Y = grid.length;
    const X = grid[0].length;
    const uf = new UnionFind();

    for (let i = 0; i < Y; i++) {
        for (let j = 0; j < X; j++) {
            if(grid[i][j] == 1) {
                uf.makeSet([i, j])
            }
        }
    }
    for (let i = 0; i < Y; i++) {
        for (let j = 0; j < X; j++) {
            if(grid[i][j] == 1) {
                if((i + 1 < Y) && (grid[i + 1][j] == 1)) uf.union([i, j], [i + 1, j]);
                if((j + 1 < X) && (grid[i][j + 1] == 1)) uf.union([i, j], [i, j+ 1]);
            }
        }
    }
    return uf.getCount();
};

class UnionFind {
    constructor() {
        this.parents = {};
        this.count = 0;
    }
    makeSet(x) {
      this.parents[x] = x + '';
      this.count++;
    }
    //查找v所属的集合(根节点),通过parent链条不断地向上找,直到找到根节点
    find(x) {
      while ((x + '') != this.parents[x]) {
        x = this.parents[x];
      }
      return x + '';
    }
    //合并v1、v2所在的集合,将v1的根节点嫁接到v2的根节点上
    union(x, y) {
      let p1 = this.find(x);
      let p2 = this.find(y);
      if (p1 == p2) return;

      this.parents[p1] = p2;
      this.count--;
    }
    //检查v1、v2是否属于同一个集合
    getCount(x, y) {
      return this.count;
    }
}

684. 冗余连接

在本问题中, 树指的是一个连通且无环的无向图。 输入一个图,该图由一个有着N个节点 (节点值不重复1, 2, ..., N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。 结果图是一个以边组成的二维数组。每一个边的元素是一对[u, v] ,满足 u < v,表示连接顶点u 和v的无向图的边。 返回一条可以删去的边,使得结果图是一个有着N个节点的树。如果有多个答案,则返回二维数组中最后出现的边。答案边 [u, v] 应满足相同的格式 u < v。 

示例 1:

输入: [[1,2], [1,3], [2,3]]
输出: [2,3]
解释: 给定的无向图为:
  1
 / \
2 - 3


示例 2:

输入: [[1,2], [2,3], [3,4], [1,4], [1,5]]
输出: [1,4]
解释: 给定的无向图为:
5 - 1 - 2
    |   |
    4 - 3

來源:leetcode-cn.com/problems/re…

/**
 * @param {number[][]} edges
 * @return {number[]}
 */
var findRedundantConnection = function(edges) {
    const count = edges.length;
    const uf = new UnionFind(count + 1);
    uf.init(count + 1);
    for (let i = 0; i < count; i++) {
        const edge = edges[i];
        const node1 = edge[0], node2 = edge[1];
        if(uf.find(node1) !== uf.find(node2)) {
            uf.union(node1, node2)
        } else {
            return edge;
        }
    }
    return [0]
};

class UnionFind {
    constructor(n) {
        this.parents = new Array(n);
        this.ranks = new Array(n);
    }
    init(x) {
      for (let i = 0; i < this.parents.length; i++) {
        this.parents[i] = i;
      }
      for (let i = 0; i < this.ranks.length; i++) {
        this.ranks[i] = 1;
      }
    }
    //查找v所属的集合(根节点),通过parent链条不断地向上找,直到找到根节点
    find(x) {
      if (this.parents[x] != x) {
        this.parents[x] = this.find(this.parents[x]);
      }
      return this.parents[x];
    }
    //合并v1、v2所在的集合,将v1的根节点嫁接到v2的根节点上
    union(x, y) {
      let p1 = this.find(x);
      let p2 = this.find(y);
      if (p1 == p2) return;

      if (this.ranks[p1] < this.ranks[p2]) {
        this.parents[p1] = p2;
      } else if((this.ranks[p1] > this.ranks[p2])) {
        this.parents[p2] = p1;
      } else {
        this.parents[p1] = p2;
        this.ranks[p2] += 1;
      }
    }
}

1319. 连通网络的操作次数

用以太网线缆将 n 台计算机连接成一个网络,计算机的编号从 0 到 n-1。线缆用 connections 表示,其中 connections[i] = [a, b] 连接了计算机 a 和 b。 网络中的任何一台计算机都可以通过网络直接或者间接访问同一个网络中其他任意一台计算机。 给你这个计算机网络的初始布线 connections,你可以拔开任意两台直连计算机之间的线缆,并用它连接一对未直连的计算机。请你计算并返回使所有计算机都连通所需的最少操作次数。如果不可能,则返回 -1 。

示例 1:

输入:n = 4, connections = [[0,1],[0,2],[1,2]]
输出:1
解释:拔下计算机 12 之间的线缆,并将它插到计算机 13 上。

來源:leetcode-cn.com/problems/nu…

可以使用并查集来得到图中的连通分量数。如果其包含 n 个节点,那么初始时连通分量数为 n,每成功进行一次合并操作,连通分量数就会减少 1。 

var makeConnected = function(n, connections) {
    if(connections.length < n - 1) {
        return -1;
    }
    const uf = new UnionFind(n);
    uf.init(n);
    for (const conn of connections) {
        uf.union(conn[0], conn[1])
    }
    return uf.count - 1;
};

class UnionFind {
    constructor(n) {
        this.parents = new Array(n);
        this.ranks = new Array(n);
        this.count = n;
    }
    init(x) {
      for (let i = 0; i < this.parents.length; i++) {
        this.parents[i] = i;
      }
      for (let i = 0; i < this.ranks.length; i++) {
        this.ranks[i] = 1;
      }
    }
    //查找v所属的集合(根节点),通过parent链条不断地向上找,直到找到根节点
    find(x) {
      if (this.parents[x] != x) {
        this.parents[x] = this.find(this.parents[x]);
      }
      return this.parents[x];
    }
    //合并v1、v2所在的集合,将v1的根节点嫁接到v2的根节点上
    union(x, y) {
      let p1 = this.find(x);
      let p2 = this.find(y);
      if (p1 == p2) return;

      if (this.ranks[p1] < this.ranks[p2]) {
        this.parents[p1] = p2;
      } else if((this.ranks[p1] > this.ranks[p2])) {
        this.parents[p2] = p1;
      } else {
        this.parents[p1] = p2;
        this.ranks[p2] += 1;
      }
      this.count--;
    }
    //检查v1、v2是否属于同一个集合
    isSame(x, y) {
      return this.find(x) === this.find(y)
    }
}

765. 情侣牵手

N 对情侣坐在连续排列的 2N 个座位上,想要牵到对方的手。 计算最少交换座位的次数,以便每对情侣可以并肩坐在一起。 一次交换可选择任意两人,让他们站起来交换座位。 人和座位用 0 到 2N-1 的整数表示,情侣们按顺序编号,第一对是 (0, 1),第二对是 (2, 3),以此类推,最后一对是 (2N-2, 2N-1)。 这些情侣的初始座位 row[i] 是由最初始坐在第 i 个座位上的人决定的。 

示例 1:输入: row = [0, 2, 1, 3]
输出: 1
解释: 我们只需要交换row[1]和row[2]的位置即可。


示例 2:输入: row = [3, 2, 0, 1]
输出: 0
解释: 无需交换座位,所有的情侣都已经可以手牵手了。

來源:leetcode-cn.com/problems/co…

利用并查集求出图中的每个连通分量;对于每个连通分量而言,其大小减1就是需要交换的次数。

var minSwapsCouples = function(row) {
    let len = row.length;
    let N = len / 2;
    const uf = new UnionFind(N);
    for (let i = 0; i < len; i += 2) {
        uf.union(Math.floor(row[i] / 2), Math.floor(row[i + 1] / 2))
    }
    return N - uf.getCount();
};

class UnionFind {
    constructor(n) {
        this.parents = new Array(n).fill(0).map((ele, index) => index);
        this.count = n;
    }
    find(v) {
        while (v != this.parents[v]) {
            this.parents[v] = this.parents[this.parents[v]];
            v = this.parents[v];;
        }
        return v;
    }

    //合并v1、v2所在的集合,将v1的根节点嫁接到v2的根节点上
    union(x, y) {
      let p1 = this.find(x);
      let p2 = this.find(y);
      if (p1 == p2) return;

      this.parents[p1] = p2;
      this.count--;
    }
    //检查v1、v2是否属于同一个集合
    getCount() {
      return this.count;
    }
}

839. 相似字符串组

如果交换字符串 X 中的两个不同位置的字母,使得它和字符串 Y 相等,那么称 X 和 Y 两个字符串相似。如果这两个字符串本身是相等的,那它们也是相似的。
例如,"tars" 和 "rats" 是相似的 (交换 0 与 2 的位置); "rats" 和 "arts" 也是相似的,但是 "star" 不与 "tars","rats",或 "arts" 相似。
总之,它们通过相似性形成了两个关联组:{"tars", "rats", "arts"} 和 {"star"}。注意,"tars" 和 "arts" 是在同一组中,即使它们并不相似。形式上,对每个组而言,要确定一个单词在组中,只需要这个词和该组中至少一个单词相似。
给你一个字符串列表 strs。列表中的每个字符串都是 strs 中其它所有字符串的一个字母异位词。请问 strs 中有多少个相似字符串组? 

示例 1:输入:strs = ["tars","rats","arts","star"]
输出:2


示例 2:输入:strs = ["omv","ovm"]
输出:1

来源:leetcode-cn.com/problems/si…

var numSimilarGroups = function(strs) {
    const n = strs.length;
    const m = strs[0].length;
    const uf = new UnionFind(n);
    for (let i = 0; i < n; i++) {
        for (let j = i + 1;  j < n; j++) {
            if(check(strs[i], strs[j], m)) {
                uf.union(i, j)
            }
        }
    }
    return uf.count;
};

function check(a, b, length) {
    let count = 0;
    for (let i = 0; i < length; i++) {
        if(a[i] !== b[i]) {
            count++;
            if(count > 2) {
                return false;
            }
        }
    }
    return true;
}

class UnionFind {
    constructor(n) {
        this.parents = new Array(n).fill(0).map((element, index) => index);
        this.sizes = new Array(n).fill(1);
        this.count = n;
    }
    //查找v所属的集合(根节点),通过parent链条不断地向上找,直到找到根节点
    find(x) {
      while (x != this.parents[x]) {
        x = this.parents[x];
      }
      return x;
    }
    //合并v1、v2所在的集合,将v1的根节点嫁接到v2的根节点上
    union(x, y) {
      let p1 = this.find(x);
      let p2 = this.find(y);
      if (p1 == p2) return;

      if (this.sizes[p1] < this.sizes[p2]) {
  			this.parents[p1] = p2;
  			this.sizes[p2] += this.sizes[p1];
  		} else {
  			this.parents[p2] = p1;
  			this.sizes[p1] += this.sizes[p2];
  		}
          this.count--;
    }
}

947. 移除最多的同行或同列石头

n 块石头放置在二维平面中的一些整数坐标点上。每个坐标点上最多只能有一块石头。
如果一块石头的 同行或者同列 上有其他石头存在,那么就可以移除这块石头。
给你一个长度为 n 的数组 stones ,其中 stones[i] = [xi, yi] 表示第 i 块石头的位置,返回 可以移除的石子 的最大数量。 

示例 1:

输入:stones = [[0,0],[0,1],[1,0],[1,2],[2,1],[2,2]]
输出:5
解释:一种移除 5 块石头的方法如下所示:
1. 移除石头 [2,2] ,因为它和 [2,1] 同行。
2. 移除石头 [2,1] ,因为它和 [0,1] 同列。
3. 移除石头 [1,2] ,因为它和 [1,0] 同行。
4. 移除石头 [1,0] ,因为它和 [0,0] 同列。
5. 移除石头 [0,1] ,因为它和 [0,0] 同行。
石头 [0,0] 不能移除,因为它没有与另一块石头同行/列。

来源:leetcode-cn.com/problems/mo…

var removeStones = function(stones) {
    const unionFindSet = new UnionFind();
    for(let stone of stones) {
        unionFindSet.union(stone[0], stone[1] + 10000);
    }
    return stones.length - unionFindSet.getCount();
};

class UnionFind {
    constructor() {
        this.parents = [];
        this.count = 0;
        this.ranks = []
    }
    init(x) {
        if(this.parents[x] === undefined) {
            this.parents[x] = x;
            this.ranks[x] = 0;
            this.count++;
        }
    }
    getCount() {
        return this.count;
    }
    find(x) {
        if(this.parents[x] !== x) {
            this.parents[x] = this.find(this.parents[x]);
        }
        return this.parents[x]
    }
    union(x, y) {
        this.init(x);
        this.init(y);
        let rootX = this.find(x);
        let rootY = this.find(y);
        if(rootX === rootY) return;
        if(this.ranks[rootX] > this.ranks[rootY]) {
            this.parents[rootY] = rootX;
        } else if (this.ranks[rootX] < this.ranks[rootY]) {
            this.parents[rootX] = rootY;
        } else {
            this.parents[rootY] = rootX;
            this.ranks[rootX]++;
        }
        this.count--;
    }
}

959. 由斜杠划分区域

在由 1 x 1 方格组成的 N x N 网格 grid 中,每个 1 x 1 方块由 /、\ 或空格构成。这些字符会将方块划分为一些共边的区域。 (请注意,反斜杠字符是转义的,因此 \ 用 "\\" 表示。)。 返回区域的数目。 

示例 1:
输入:
[  " /",  "/ "]
输出:2
解释:2x2 网格如下:

示例 2:
输入:
[  " /",  "  "]
输出:1
解释:2x2 网格如下:

示例 3:

输入:
[
  "\\/",
  "/\\"
]
输出:4
解释:(回想一下,因为 \ 字符是转义的,所以 "\\/" 表示 \/,而 "/\\" 表示 /\)
2x2 网格如下:

示例 4:

输入:
[
  "/\\",
  "\\/"
]
输出:5
解释:(回想一下,因为 \ 字符是转义的,所以 "/\\" 表示 /\,而 "\\/" 表示 \/。)
2x2 网格如下:

示例 5:输入:
[  "//",  "/ "]
输出:3
解释:2x2 网格如下:

来源:leetcode-cn.com/problems/re…

var regionsBySlashes = function(grid) {
     let N = grid.length;
     let size = 4 * N * N;
     const UF = new UnionFind(size);
     UF.init(size);
     let i, j, c, start;
     for (i = 0; i < N; i++) {
         for (j = 0; j < N; j++) {
             c = grid[i].charAt(j);
             start = (i * N + j) * 4;
             // 單元內合併
             if(c === ' ') {
                 UF.union(start + 0, start + 1);
                 UF.union(start + 1, start + 2);
                 UF.union(start + 2, start + 3);
             } else if(c === '/') {
                 UF.union(start + 0, start + 3);
                 UF.union(start + 1, start + 2);
             } else {
                 UF.union(start + 0, start + 1);
                 UF.union(start + 2, start + 3);
             }
             // 單元間合併
             if(j + 1 < N) {
                 UF.union(start + 1, 4 * (i * N + j + 1) + 3);
             }
             if(i + 1 < N) {
                 UF.union(start + 2, ((i + 1) * N + j) * 4);
             }
         }
     }
     return UF.getCount();
 };

 class UnionFind {
     constructor(n) {
         this.parents = new Array(n);
         this.ranks = new Array(n);
         let count;
     }
     init(n) {
       for (let i = 0; i < n; i++) {
         this.parents[i] = i;
       }
       for (let i = 0; i < n; i++) {
         this.ranks[i] = 1;
       }
       this.count = n;
     }

     find(v) {
     while (v != this.parents[v]) {
         this.parents[v] = this.parents[this.parents[v]];
         v = this.parents[v];;
     }
     return v;
     }
     //合并v1、v2所在的集合,将v1的根节点嫁接到v2的根节点上
     union(x, y) {
       let p1 = this.find(x);
       let p2 = this.find(y);
       if (p1 == p2) return;

       if (this.ranks[p1] < this.ranks[p2]) {
         this.parents[p1] = p2;
       } else if((this.ranks[p1] > this.ranks[p2])) {
         this.parents[p2] = p1;
       } else {
         this.parents[p1] = p2;
         this.ranks[p2] += 1;
       }
       this.count--;
     }

     getCount() {
       return this.count;
     }
 }

1631. 最小体力消耗路径

你准备参加一场远足活动。给你一个二维 rows x columns 的地图 heights ,其中 heights[row][col] 表示格子 (row, col) 的高度。一开始你在最左上角的格子 (0, 0) ,且你希望去最右下角的格子 (rows-1, columns-1) (注意下标从 0 开始编号)。你每次可以往 上,下,左,右 四个方向之一移动,你想要找到耗费 体力 最小的一条路径。
一条路径耗费的 体力值 是路径上相邻格子之间 高度差绝对值 的 最大值 决定的。
请你返回从左上角走到右下角的最小 体力消耗值 。 

示例 1: 

输入:heights = [[1,2,2],[3,8,2],[5,3,5]]
输出:2
解释:路径 [1,3,5,3,5] 连续格子的差值绝对值最大为 2 。
这条路径比路径 [1,2,2,2,5] 更优,因为另一条路径差值最大值为 3

示例 2: 

 输入:heights = [[1,2,3],[3,8,4],[5,3,5]]
输出:1
解释:路径 [1,2,3,4,5] 的相邻格子差值绝对值最大为 1 ,比路径 [1,3,5,3,5] 更优。 

示例 3: 

 输入:heights = [[1,2,1,1,1],[1,2,1,2,1],[1,2,1,2,1],[1,2,1,2,1],[1,1,1,2,1]]
输出:0
解释:上图所示路径不需要消耗任何体力。 

来源:leetcode-cn.com/problems/pa…

我们将这 mn 个节点放入并查集中,实时维护它们的连通性。 由于我们需要找到从左上角到右下角的最短路径,因此我们可以将图中的所有边按照权值从小到大进行排序,并依次加入并查集中。当我们加入一条权值为 x 的边之后,如果左上角和右下角从非连通状态变为连通状态,那么 x 即为答案。 

var minimumEffortPath = function(heights) {
     const m = heights.length;
     const n = heights[0].length;
     const edges = [];
     for (let i = 0; i < m; i++) {
         for (let j = 0; j < n; j++) {
             const id = i * n + j;
             if(i > 0) {
                 edges.push([id - n, id, Math.abs(heights[i][j] - heights[i - 1][j])]);
             }
             if(j > 0) {
                 edges.push([id - 1, id, Math.abs(heights[i][j] - heights[i][j - 1])]);
             }
         }
     }

     edges.sort((a, b) => a[2] - b[2]);
     const uf = new UnionFind(m * n);
     let ans = 0;
     for (const edge of edges) {
         const x = edge[0], y = edge[1], v = edge[2];
         uf.union(x, y);
         if(uf.isSame(0, m * n - 1)) {
             ans = v;
             break;
         }
     }
     return ans;
 };

 class UnionFind {
     constructor(n) {
         this.parents = new Array(n).fill(0).map((element, index) => index);
         this.sizes = new Array(n).fill(1);
         // 当前连通分量数目
         this.setCount = n;
     }

     find(x) {
       if (this.parents[x] != x) {
         this.parents[x] = this.find(this.parents[x]);
       }
       return this.parents[x];
     }
     //合并v1、v2所在的集合,将v1的根节点嫁接到v2的根节点上
     union(x, y) {
       let p1 = this.find(x);
       let p2 = this.find(y);
       if (p1 == p2) return;

       if (this.sizes[p1] < this.sizes[p2]) {
   			this.parents[p1] = p2;
   			this.sizes[p2] += this.sizes[p1];
   		} else {
   			this.parents[p2] = p1;
   			this.sizes[p1] += this.sizes[p2];
   		}
       this.setCount -= 1;
       return true;
     }
     //检查v1、v2是否属于同一个集合
     isSame(x, y) {
       return this.find(x) === this.find(y)
     }
 }

面试题 17.07. 婴儿名字

每年,政府都会公布一万个最常见的婴儿名字和它们出现的频率,也就是同名婴儿的数量。有些名字有多种拼法,例如,John 和 Jon 本质上是相同的名字,但被当成了两个名字公布出来。给定两个列表,一个是名字及对应的频率,另一个是本质相同的名字对。设计一个算法打印出每个真实名字的实际频率。注意,如果 John 和 Jon 是相同的,并且 Jon 和 Johnny 相同,则 John 与 Johnny 也相同,即它们有传递和对称性。
在结果列表中,选择 字典序最小 的名字作为真实名字。 

示例:

输入:names = ["John(15)","Jon(12)","Chris(13)","Kris(4)","Christopher(19)"], synonyms = ["(Jon,John)","(John,Johnny)","(Chris,Kris)","(Chris,Christopher)"]
输出:["John(27)","Chris(36)"]

来源:leetcode-cn.com/problems/ba…

var trulyMostPopular = function(names, synonyms) {
     const res = [];
     const hash = {};
     const uf = new UnionFind();
     for (let n of names) {
         let name = n.split('(')[0];
         uf.makeSet(name);
     }
     // console.log(uf)
     for (let s of synonyms) {
         const first = s.split(',')[0].slice(1);
         const second = s.split(',')[1].slice(0, -1);
         uf.makeSet(first);
         uf.makeSet(second);
     }
     // console.log(uf)
     for (let s of synonyms) {
         const first = s.split(',')[0].slice(1);
         const second = s.split(',')[1].slice(0, -1);
         uf.union(first, second);
     }
     // console.log(uf)
     for (let n of names){
         let name = n.split('(')[0];
         let rootName = uf.findSet(name);
         let frequency = +(n.split('(')[1].slice(0, -1));
         hash[rootName] = (hash[rootName] || 0) + Number(frequency);
     }
     for (let key in hash) {
         res.push(`${key}(${hash[key]})`);
     }
     return res;
 };

 class UnionFind {
     constructor() {
         this.parents = {};
     }
     makeSet(x) {
         this.parents[x] = x;
     }
     findSet(x) {
         while(this.parents[x] !== x) {
             x = this.parents[x]
         }
         return x;
     }
     union(x, y) {
         this.link(this.findSet(x), this.findSet(y));
     }
     link(x, y) {
         if(x === y) return;
         if(x > y) {
             this.parents[x]  = y;
         } else {
             this.parents[y] = x;
         }
     }
 }

1202. 交换字符串中的元素

给你一个字符串 s,以及该字符串中的一些「索引对」数组 pairs,其中 pairs[i] = [a, b] 表示字符串中的两个索引(编号从 0 开始)。
你可以 任意多次交换 在 pairs 中任意一对索引处的字符。
返回在经过若干次交换后,s 可以变成的按字典序最小的字符串。 

示例 1:

输入:s = "dcab", pairs = [[0,3],[1,2]]
输出:"bacd"
解释: 
交换 s[0] 和 s[3], s = "bcad"
交换 s[1] 和 s[2], s = "bacd"

示例 2:

输入:s = "dcab", pairs = [[0,3],[1,2],[0,2]]
输出:"abcd"
解释:
交换 s[0] 和 s[3], s = "bcad"
交换 s[0] 和 s[2], s = "acbd"
交换 s[1] 和 s[2], s = "abcd"

示例 3:

输入:s = "cba", pairs = [[0,1],[1,2]]
输出:"abc"
解释:
交换 s[0] 和 s[1], s = "bca"
交换 s[1] 和 s[2], s = "bac"
交换 s[0] 和 s[1], s = "abc"

来源:leetcode-cn.com/problems/sm…

通过并查集将待排序字符串分组并进行组内排序后根据对应的下标填回结果值

 var smallestStringWithSwaps = function(s, pairs) {
     const uf = new UnionFind(s.length);
     uf.init();
     for (const [a, b] of pairs) {
         uf.union(a, b)
     }
     const map = {};
     for (let i = 0; i < uf.parents.length; i++) {
         const root = uf.find(i);
         map[root] ? map[root].push(i) : (map[root] = [i])
     }
     const result = [];
     Object.values(map).forEach((value) => {
         const arr = [];
         let idx = 0;
         const tmp = value.map((v) => {
             arr.push(v)
             return s[v]
         })
         tmp.sort((a, b) => a.charCodeAt() - b.charCodeAt());
         tmp.forEach((c) => {
             const index = arr[idx++];
             result[index] = c;
         })
     })
     return result.join('')
     
 };

 class UnionFind {
     constructor(n) {
         this.parents = new Array(n);
         this.ranks = new Array(n);
     }
     init(x) {
       for (let i = 0; i < this.parents.length; i++) {
         this.parents[i] = i;
       }
       for (let i = 0; i < this.ranks.length; i++) {
         this.ranks[i] = 1;
       }
     }
     //查找v所属的集合(根节点),通过parent链条不断地向上找,直到找到根节点
     find(x) {
       while (x != this.parents[x]) {
         x = this.parents[x];
       }
       return x;
     }
     //合并v1、v2所在的集合,将v1的根节点嫁接到v2的根节点上
     union(x, y) {
       let p1 = this.find(x);
       let p2 = this.find(y);
       if (p1 == p2) return;

       if (this.ranks[p1] < this.ranks[p2]) {
         this.parents[p1] = p2;
       } else if((this.ranks[p1] > this.ranks[p2])) {
         this.parents[p2] = p1;
       } else {
         this.parents[p1] = p2;
         this.ranks[p2] += 1;
       }
     }
 }