不管后面写了啥,一定要把最重要的写在前面:(总记不住)

  • 使用并查集前 一定要初始化、一定要初始化、一定初始化
  • findFather(i) 找根结点,不是 father[i] 数组

并查集的基本操作

使用数组表示并查集:

1
int father[N];

father[i] 表示元素 i 的父亲结点

初始化

初始情况下,每个元素都是单独的集合,因此其父结点是自己,这在并查集中被称为 Reflexive

1
2
3
for (int i = 1; i <= N; i++) {
father[i] = i;
}

查找与路径压缩

一个集合中只有一个根结点,因此反复地查找父结点,直到 father[x] == x ,则 x 为该集合的根节点。

  • 递推与递归方式查找根(无路径压缩)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 递推方式
    int findFather(int x) {
    while (x != father[x]) {
    x = father[x];
    }
    return x;
    }
    // 递归方式
    int findFather(int x) {
    if (x == father[x]) return x;
    return findFather(father[x]);
    }

    简单的查找方式非常简单,但是对每个结点而言,要查找根总需要反复地向上找,比较费时,所以更好的办法是在查找根结点的同时,在路上把所有 x 的爸爸爷爷结点直接指向根。这样后面再找的时候时间就是 O(1) 了。

  • 递归方式查找根并压缩路径

    简单分析:

    1. 若当前结点的父亲结点等于自己则表示找到了集合的根
    2. 若不相等,则递归地查找其父结点的根。同时,将当前结点的父结点直接指向根结点
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int findFather(int x) {
    if (x == father[x]) return x;
    int fa = findFather(father[x]);
    father[x] = fa;
    return fa;
    }

    int findFather(int x) {
    return x == father[x] ? x : father[x] = findFather(father[x]);
    }
  • 迭代的方式查找根结点与压缩路径

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int findFather(int x) {
    int a = x;
    while (x != father[x]) x = father[x];
    while (z != father[z]) {
    int z = a;
    a = father[a];
    father[z] = x;
    }
    return x;
    }

合并集合

将两个集合合并在一起,只需要找到两个集合的根,将其中一个根的父结点设为另一个根即可。因为和 C/C++ 中的 union 关键字冲突,所以函数写成了 uni

1
2
3
4
5
void uni(int a, int b) {
int faA = findFather(a);
int faB = findFather(b);
if (faA != faB) father[faA] = faB;
}

并查集解题中的习惯操作

并查集有一些基础的常规操作,为了缩短在编程考试中减少对“常规”操作的思考时间(虽然也很简单),故记录下来,形成自己习惯性的模板代码。

对象散列编号

题目中常常给定的元素编号是连续的,通常是 1~N ,因此可以直接将对象 id 作为下标,表示其在并查集中的位置,但是有时候给定的是字符串,或是给定的范围很大,这个时候需要对每个元素重新编号,最好的方式是 从序号1开始,用两个map分别做 id->num 和 num->id 的映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int main() {
...
....
int pos = 1; // 初始编号从 1 开始
map<string, int> idToNum; // 不管给定的是什么类型和数据,都管它叫 id
map<int, string> numToId; // num 即编号
string a, b;
while (T--) { // T 为给定要循环输入的次数
cin >> a >> b;
int ta, tb;
if (idToNum[a] != 0) ta = idToNum[a];
else {
numToId[pos] = a;
idToNum[a] = pos;
ta = pos++;
}
if (idToNum[b] != 0) tb = idToNum[b];
else {
numToId[pos] = b;
idToNum[b] = pos;
tb = pos++;
}
uni(ta, tb); // 合并 ta、tb
}
....
...
}

用 unordered_set 记录序号非连续元素

有时候题目给定了元素的编号,并且这个编号在合理的范围内,但是编号不连续,因为并查集建立完成后需要遍历所有元素以找出不同集合,所以,如果是不连续的元素编号则可以使用 unordered_set 记录输入的元素,之前想过用 vector 但是可能出现重复输入一样的编号,所以不适用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unordered_set<int> s;
string a, b;
while (T--) {
scanf("%d%d", &a, &b);
...
// 有些时候需要判断a、b是否不为-1
s.insert(a);
s.insert(b);
...
uni(a, b);
}
for (auto it : s) {
int fa = findFather(it);
...
}

遍历元素查找集合

在并查集建立好之后,需要找出各个集合对之做操作。

1
2
3
4
for (int i = 1; i < pos; i++) {	// 如果无需做散列,则是 1~N
int fa = findFather(numToId[i]);
...
}

有时候需要将一个集合作为一个结构去看,其中需要保存一些题目要求的数据,并做排序。则要用到 map 和 vector

1
2
3
4
5
6
7
8
9
10
11
12
struct node { ...properties... };
......
map<int, node> mp;
for (int i = 1; i < pos; i++) { // 如果无需做散列,则是 1~N
int fa = findFather(numToId[i]);
mp[fa].property1 = ... ;
mp[fa].property2 = ... ;
...
}
vector<node> ans;
for (auto it : mp) ans.push_back(it.second);
sort(ans.begin(), ans.end(), cmp);