『学习笔记』并查集

并查集是一种用于解决元素分组问题的数据结构。它有两种操作:

  • 合并:把两个不相交的集合合并为一个集合。
  • 查询:查找两个元素是否在同一集合里。

我们可以把它理解为朋友关系:朋友的朋友是朋友。

朋友关系

比如,一共有 \(5\) 个人,分别为 \(a,b,c,d,e\)。每个人都有一个代表,即为自己的朋友之一,一个或多个人的代表需要是同一个人,遇到什么事就把锅推给代表。若一个人没有朋友,那么他的代表就是自己。

图中每个人的箭头即指向每个人的代表。

现在我们得知,\(b\)\(a\) 的朋友,我们使用并查集来合并他们,并使得 \(a\) 的代表(也就是 \(a\))是他们这对朋友的代表。这里的代表可以选择 \(a\),也可以选择 \(b\)

\(c\) 和 \(b\) 交朋友,我们可以使 \(c\) 的代表,也就是 \(c\),他的代表变为 \(b\)

但是你会发现,它们 \(a,b,c\) 的代表不相同,\(c\) 的代表是 \(b\)

没关系,我们可以通过 \(c\) 找到代表 \(b\),又能通过 \(b\) 找到代表 \(a\)\(a\) 的代表为自己,所以还是能够找到自己的代表的。

现在 \(d\)\(e\) 又交了朋友,我们将这两个好朋友的代表选为 \(d\)

\(e\)\(b\) 交了朋友,我们要让 \(e\) 的代表 \(d\),修改代表为 \(b\)

目前,所有人都有朋友关系了。

那么如何判断两个人是否有朋友关系呢?

当然是看看两个人的代表的代表的代表的代表的代表......是不是同一个人啦!

例如,我们判断 \(c\)\(e\) 是否拥有朋友关系,那么就需要分别找到他们的代表的代表的代表的代表......然后判断一下是不是同一个人。

从上图可以得知, \(c\) 的代表的代表的代表的代表......是 \(a\)

接下来找找 \(e\) 的代表的代表的代表的代表......是谁吧。

可见,\(e\) 的代表的代表的代表的代表......也是 \(a\)

那么 \(c\)\(e\) 就是好朋友啦!

代码实现

我们使用一个数组 \(f\) 来存储每个人的代表。

int f[N];

初始化

刚开始的时候,所有人的代表都要为自身。

void init(int n){ // 元素总个数 n
    for(int i=1; i<=n; i++)
        f[i]=i;
}

查询

这个操作,就是找到自己的代表的代表的代表的代表......了。

什么时候才停下来不再找代表,返回代表编号?

当目前找到的代表,他的代表为自身的时候,就没有继续找的必要了,那也是死循环。所以直接返回。

int find(int x){return f[x]==x ? x : find(f[x]);}

合并

在上面的演示中,我们是直接让 \(x\) 的代表的代表的代表的代表......的代表变成 \(y\) 的。

而这样做实在慢了些,如果这样合并后,要查询 \(x\) 的代表的代表的代表的代表......会怎么样?我们会先找到原来 \(x\) 的代表的代表的代表的代表......再找一遍 \(y\) 的代表的代表的代表的代表,才能找到最终的代表。

怎么样可以让他更快些呢?当然是让 \(x\) 的代表的代表的代表的代表......的代表直接变成 \(y\) 的代表的代表的代表的代表......了!

void merge(int x,int y){f[find(x)]=find(y);}

演示中的方法是这样的:

void merge(int x,int y){f[find(x)]=y;}

路径压缩优化

相信你已经注意到,我们如果多次找 \(x\) 的代表的代表的代表的代表......,那么每次最坏情况下要找 \(n\) 次代表,所以时间复杂度为 \(\mathcal{O}(n)\),显然很慢。

要是我们沿着一条路找了一遍代表,那为什么不让找过的人的代表都变成最终找到的答案呢?

比如,有这样一个并查集:

我们要查找 \(9\) 的代表的代表的代表的代表......可见,要把所有节点都找一遍,才能找到 \(1\)

找第一遍的时候,我们没办法,只能这样 \(\mathcal{O}(n)\) 找。可再找好几遍呢?还这么找,岂不浪费时间?

于是,我们第一遍找到答案就直接将答案放进 \(f_x\),这样也不影响整个并查集,还能加速下次查找,下次查找 \(x\) 节点,\(\mathcal{O}(1)\) 就能得出答案。

要做到这样,只需要在 find 函数中加入一点点东西:

int find(int x){return f[x]==x ? x : f[x]=find(f[x]);}

加入的就是后面的 f[x]=find(f[x]) 中的 f[x]=,这样能将路过的每个节点的答案都存下来,下次再找就只需要 \(\mathcal{O}(1)\) 了。

例如上面那个并查集,调用 find(9) 后,它长这样:

P1551 亲戚

题目大意

给你 \(n\) 个人,\(m\) 个亲戚关系,\(p\) 个询问。

如果 \(x\)\(y\) 是亲戚,\(y\)\(z\) 是亲戚,那么 \(x\)\(z\) 也是亲戚。

对于每个询问,给定两个人的编号 \(Pi,Pj\),输出他们是否具有亲戚关系。

思路

这题的亲戚关系和上面的朋友关系很像,所以我们使用一个并查集来维护。

就拿这题来试试路径压缩优化能快多少吧!

不带路径压缩:\(133\text{ms}\)

路径压缩优化:\(42\text{ms}\)

可见,效果还是很显著的。

代码

#include <iostream>
using namespace std;
template<typename T=int>
inline T read(){
    T X=0; bool flag=1; char ch=getchar();
    while(ch<'0' || ch>'9'){if(ch=='-') flag=0; ch=getchar();}
    while(ch>='0' && ch<='9') X=(X<<1)+(X<<3)+ch-'0',ch=getchar();
    if(flag) return X;
    return ~(X-1);
}

const int N=5e3+5;
int n,m,p,a,b;

template<class T=int>
class set{
    public:
        set(int n=1e5){f=new T[l=n+5]; clear();}
        ~set(){delete[] f;}
        T find(T x){return f[x]==x ? x : f[x]=find(f[x]);}
        void merge(T x,T y){f[find(x)]=find(y);}
        void clear(){for(int i=1; i<l; i++) f[i]=i;}
    private:
        T *f;
        int l;
};
set s;

int main(){
    n=read(),m=read(),p=read();
    while(m--){
        a=read(),b=read();
        s.merge(a,b);
    }
    while(p--){
        a=read(),b=read();
        if(s.find(a)==s.find(b)) puts("Yes");
        else puts("No");
    }
    return 0;
}

P2078 朋友

题目大意

有两个公司 A 和 B,小明在 A 公司工作,小红在 B 公司工作。

A 公司只有男员工,编号为正数,而 B 公司只有女员工,编号为负数。

给定 A 公司的人数 \(n\)\(p\) 对朋友关系,B 公司的人数 \(m\)\(q\) 对朋友关系。

请你求出,小明和小红认识的人里(包括他们自己),最多能组成多少对情侣?

小明的编号为 \(1\),小红的编号为 \(-1\)

思路

好家伙刚来一个亲戚又来一个朋友,还组情侣......

题目说了,A 和 B 公司各自内部才有朋友关系,不会有 A 公司的人与 B 公司的人有朋友关系,那么我们直接用两个并查集维护呗!

对于 B 公司的负数编号,直接转为正数即可。

然后我们统计小明和小红各自的朋友数,把公司里每个人的代表都找一遍,只要跟 \(1\) 号(也就是小明小红)的代表一样,就说明这个人是小明或小红的好朋友。

问最多能组成的情侣对数,直接输出小明和小红朋友数的最小值就可以了。

代码

#include <iostream>
using namespace std;
template<typename T=int>
inline T read(){
    T X=0; bool flag=1; char ch=getchar();
    while(ch<'0' || ch>'9'){if(ch=='-') flag=0; ch=getchar();}
    while(ch>='0' && ch<='9') X=(X<<1)+(X<<3)+ch-'0',ch=getchar();
    if(flag) return X;
    return ~(X-1);
}

template<typename T=int>
inline void write(T X){
    if(X<0) putchar('-'),X=~(X-1);
    T s[20],top=0;
    while(X) s[++top]=X%10,X/=10;
    if(!top) s[++top]=0;
    while(top) putchar(s[top--]+'0');
    putchar('\n');
}

int n,m,p,q,x,y,ac,bc;

template<class T=int>
class set{
    public:
        set(int n=1e5){f=new T[l=n+5]; clear();}
        ~set(){delete[] f;}
        T find(T x){return f[x]==x ? x : f[x]=find(f[x]);}
        void merge(T x,T y){f[find(x)]=find(y);}
        void clear(){for(int i=1; i<l; i++) f[i]=i;}
    private:
        T *f;
        int l;
};
set a,b; // a 维护 A 公司,b 维护 B 公司

int main(){
    n=read(),m=read(),p=read(),q=read();
    while(p--) // 输入朋友关系
        a.merge(read(),read());
    while(q--)
        b.merge(-read(),-read());
    for(int i=1; i<=n; i++) // 统计朋友
        ac+=a.find(i)==a.find(1);
    for(int i=1; i<=m; i++)
        bc+=b.find(i)==b.find(1);
    write(min(ac,bc));
    return 0;
}

推荐习题

posted @ 2022-06-25 15:23  仙山有茗  阅读(36)  评论(1编辑  收藏  举报