【笔记】并查集

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

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

并查集的基本操作

使用数组表示并查集:

int father[N];

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

初始化

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

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

查找与路径压缩

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

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

    // 递推方式
    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. 若不相等,则递归地查找其父结点的根。同时,将当前结点的父结点直接指向根结点
    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]);
    }
    
  • 迭代的方式查找根结点与压缩路径

    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

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 的映射

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 但是可能出现重复输入一样的编号,所以不适用。

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);
    ...
}

遍历元素查找集合

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

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

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

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);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值