# 并查集

并查集是一种树形的数据结构，用于处理一些不交集（不相交的两个集合）的合并及查询问题。

它支持两种操作：

1. 查找：确定某个元素处于哪个子集。
2. 合并：将两个子集合并成一个集合。

## 思想

新学期学校组织春游，要求每个人必须和同班同学结伴游玩。因为是新生大家都还不熟悉，无法确认对方是否是自己同学，因此只能询问对方班主任姓名，来进行确认。

并查集就是在这种思想下产生的数据结构。

上例只需要查找一层就能获得想要的结果。实际情况下，往往需要往上查找好几层，才能找到想要的结果。

并查集包含了多个树形集合。每个集合的根节点被选取作为代表，用来表示整个集合。就如同班主任一样。

查找操作返回元素所属集合的代表；合并操作是对于两个集合而言的，它将某个代表设置为另一个代表的子节点。

## 基础操作

### 初始化

设置单元素集合。

```go
func makeSet(x int) {
    fa[x] = x
    return
}
```

### 查找

查找某个元素的祖先。

```go
func find(x int) int {
    if fa[x] == x { // 如果是祖先则返回
        return x
    }
    return find(fa[x]) // 如果自己不是祖先，则问上级祖先是谁
}
```

### 合并

只需要将一个集合设置为另一个集合的子节点，就能实现合并。

```go
func union(x, y) {
    xRoot := find(x)
    yRoot := find(y)
    if xRoot == yRoot {
        return
    }
    fa[xRoot] = yRoot
}
```

## 优化方法

### 路径压缩

每次进行查找操作，都需要一层一层往上询问，效率比较低。

所谓“路径压缩”，就是在进行“查找”时，将路径上的每个节点直接连到根节点，使得树更加扁平化。

“路径压缩”可以使以后的”查找“操作更加快速。

```go
func find(x int) int {
    if fa[x] != x {
        fa[x] = find(fa[x])
    }
    return fa[x]
}
```

### 按秩合并（启发式合并）

在进行合并操作时，无论将哪一个集合连接到另一个集合下面，总能得到正确的结果。但是采用不同的连接方法，对于查找操作，将会存在不同的时间复杂度。

将一颗节点数和深度都较小的集合树连接到另一颗集合树下，相比于另一种方案，执行查找操作的用时会更少。

但是不可能总能得到节点数和深度都较小的集合树，因此可以选择两者之一作为衡量指标。

现在讨论使用深度作为衡量指标。节点数的讨论类似。

路径压缩会改变树的高度，如果使用深度作为衡量指标，每次压缩时都要重新计算树的深度，查找算法复杂度会变高。初始化集合时，可以使用”秩“代替”深度“。将秩初始化为0，两个秩同为r的集合树合并时，它们的秩r+1，否则秩不变。如果没有路径压缩，秩和深度则总是相等的。

```go
func makeSet(x int) {
    fa[x] = x
    rank[x] = 0
    return
}

// ... 省略查找算法

func union(x, y) {
    xRoot := find(x)
    yRoot := find(y)
    if xRoot == yRoot {
        return
    }

    if rank[xRoot] < rank[yRoot] {
        fa[xRoot] = yRoot
    } else if rank[xRoot] > rank[yRoot] {
        fa[yRoot] = xRoot
    } else {
        fa[xRoot] = yRoot
        rank[yRoot]++
    }
}
```

## 补充

### 带权并查集

带权并查集的节点之间，具有一定的权重。比如一个部门中，副部长的职级高，他和部长之间联系的紧密程度，要高于普通成员和部长之间的联系，权重就比较高。此时的合并规则，既可以使用秩，又可以自己设定。
