Featured image of post 并查集及其进阶

并查集及其进阶

并查集及其进阶

并查集及其进阶

普通并查集

并查集是一种用于管理元素所属集合的数据结构,实现为一个森林,其中每棵树表示一个集合,树中的节点表示对应集合中的元素。

disjoint-set

两种操作:

  • 合并(Union):合并两个元素所属集合(合并对应的树)
  • 查询(Find):查询某个元素所属集合(查询对应的树的根节点),这可以用于判断两个元素是否属于同一集合

并查集在经过修改后可以支持单个元素的删除、移动;使用动态开点线段树还可以实现可持久化并查集。

初始化

初始时,每个元素都位于一个单独的集合,表示为一棵只有根节点的树。方便起见,我们将根节点的父亲设为自己。

1
for(int i=1;i<=n;i++) fa[i] = i;

查询

我们需要沿着树向上移动,直至找到根节点。

disjoint-set-find

1
2
3
4
5
int find(int x)
{
    if(x!=fa[x]) return find(fa[x]);
    return fa[x];
}

路径压缩

查询过程中经过的每个元素都属于该集合,我们可以将其直接连到根节点以加快后续查询。

disjoint-set-compress

1
2
3
4
5
int find(int x)
{
    if(x!=fa[x]) return fa[x] = find(fa[x]);
    return fa[x];
}

合并

要合并两棵树,我们只需要将一棵树的根节点连到另一棵树的根节点。

disjoint-set-merge

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
bool merge(int x,int y)
{
    int fx = find(x),fy = find(y);
    if(fx!=fy)
    {
        fa[fx] = fy;
        return true;
    }
    return false;
}

删除

要删除一个叶子节点,我们可以将其父亲设为自己。为了保证要删除的元素都是叶子,我们可以预先为每个节点制作副本,并将其副本作为父亲。

移动

与删除类似,通过以副本作为父亲,保证要移动的元素都是叶子。

复杂度

时间复杂度

同时使用路径压缩和启发式合并之后,并查集的每个操作平均时间仅为 $O(\alpha(n))$,其中 $\alpha$ 为阿克曼函数的反函数,其增长极其缓慢,也就是说其单次操作的平均运行时间可以认为是一个很小的常数。

Ackermann函数: $A(m, n)$ 的定义是这样的:

$$ A(m, n) = \begin{cases}n+1&\text{if }m=0\\A(m-1,1)&\text{if }m>0\text{ and }n=0\\A(m-1,A(m,n-1))&\text{otherwise}\end{cases} $$

而反 Ackermann 函数 $\alpha(n)$ 的定义是阿克曼函数的反函数,即为最大的整数 $m$ 使得 $A(m, m) \leqslant n$。

空间复杂度

显然为 $O(n)$。

例题

P1536 村村通

STD:

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include "bits/stdc++.h"
using namespace std;
using ll = long long;
const int N = 1010; //100005

int f[N];
int find(int x)
{
    if(x != f[x]) f[x] = find(f[x]);
    return f[x];
}

void ship(int a,int b)
{
    int fa = find(a),fb = find(b);
    if(fa != fb) f[fa] = fb;
}

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    int n,m;
    while(1)
    {
        cin >> n;
        if(!n) break;
        cin >> m;
        for(int i=1;i<=n;i++) f[i] = i;
        while(m--)
        {
            int a,b;
            cin >> a >> b;
            ship(a,b);
        }
        int ans = 0;
        for(int i=1;i<=n;i++) 
            if(f[i]==i) ans++;
        cout << ans - 1 << endl;
    }
    return 0;
}

带权并查集

我们还可以在并查集的边上定义某种权值、以及这种权值在路径压缩时产生的运算,从而解决更多的问题。

相对祖先的距离

通过在节点之间记录距离值$dist[x]$,表示节点 x 到其父节点之间的“差值”。在路径压缩时递归累加这些差值,从而使 find 操作返回节点与根之间的累积差距。

find函数新的实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
int find(int x)
{
    if(fa[x]!=x)
    {
        int t = fa[x];
        dis[x] += dis[fa[x]];
        fa[x] = t;
    }
    return fa[x];
}

例题

「NOI2002」银河英雄传说

思路

对于每个点,分别记录所属链的头结点、该点到头结点的距离以及它所在集合的大小。

每次合并将$y$接在$x$的尾部,改变y头的权值和所属链的头结点,同时改变$x$的尾节点。

注意:每次查找的时候也要维护每个节点的权值。

每次查询时计算两点的权值差。

STD

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include "bits/stdc++.h"
using namespace std;
using ll = long long;
const int N = 5e4+10; //100005

int f[N],front[N],num[N];

int find(int x)
{
    if(f[x]!=x)
    {
        int t = find(f[x]);
        front[x] += front[f[x]];
        f[x] = t;
    }
    return f[x];
}

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    int t,x,y;
    char op;
    for(int i=1;i<=30000;i++) f[i] = i,num[i]=1;
    cin >> t;
    while(t--)
    {
        cin >> op >> x >> y;
        int fx = find(x),fy = find(y);
        if(op=='M')
        {
            front[fx] += num[fy];
            num[fy] += num[fx];
            num[fx] = 0;
            f[fx] = fy;
        }
        else
        {
            if(fx != fy) cout << -1 << endl;
            else cout << abs(front[x]-front[y]) - 1 << endl;
        }
    }
    return 0;
}

扩展域并查集

扩展域并查集是在经典并查集中,增加多个“关系域”的支持(例如“吃、被吃”的关系),通过对元素映射成多个身份来维护复杂关系。

域的扩展

通常使用 $kn$ 大小的结构,为每个原始元素构造$k$个代表身份,再根据操作进行合并判断。

例题

「NOI2001」食物链

思路

建立三倍节点标号:每个动物 $i$ 映射为$i$、$i+n$、$i+2n$ 分别代表自身、捕食者、天敌;根据陈述操作进行合并并判断是否矛盾。

注意:这里认为能量能循环流动(不符合生物学),不然无法得出推论三。

给 $x$ 在其他集合内安插两个“虚拟代表”,从而实现关系传递。

  • 推论一:$x$ 吃 $y$,则 x 与 y 的天敌代表 $y + 2n$ 是同类,合并$(x, y + 2n)$;

  • 推论二:$x$ 吃 $y$,则 x 的捕食代表 $x + n$ 与 y 是同类,合并 $(x + n, y)$;

  • 推论三:$x$ 吃 $y$,则 x 的天敌代表 $x + 2n$ 与 y 的捕食代表 $y + n$ 是同类,合并 $(x + 2n, y + n)$。

foodchain

STD

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
//#pragma GCC optimize(2)
#include <bits/stdc++.h>
#define endl "\n"
#define int long long
using namespace std;
const int N = 1e6+10;
const int inf = 1e18+10;

int fa[N];
int ans;
int find(int x)
{
    return fa[x] == x ? x : fa[x] = find(fa[x]);
}

void solve()
{
    int n,k;cin >> n >> k;
    for(int i=1;i<=3*n;i++) fa[i] = i;
    auto merge = [](int x,int y)
    {
        fa[find(y)] = find(x);
    };
    for(int i=1;i<=k;i++)
    {
        int op,x,y;cin >> op >> x >> y;
        if(x>n or y>n) ans++;
        else if(op==1)
        {
            if(find(x)==find(y+n) or find(x)==find(y+n+n)) ans++;
            else
            {
                merge(x,y);
                merge(x+n,y+n);
                merge(x+n+n,y+n+n);
            }
        }
        else
        {
            if(find(x)==find(y) or find(x)==find(y+n)) ans++;
            else
            {
                merge(x,y+n+n);
                merge(x+n,y);
                merge(x+n+n,y+n);
            }
        }
    }
    cout << ans << endl;
}

signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    int _ = 1;
    //cin >> _;
    while(_--)
    {
        solve();
    }
    return 0;
}

本题也可以通过带权并查集实现

习题

「NOI2015」程序自动分析

「JSOI2008」星球大战

1850H - Codeforces

本作品采用知识共享署名-非商业性使用-相同方式共享4.0国际许可协议进行许可(CC BY-NC-SA 4.0)
文章浏览量:Loading
Powered By MC ZBD Studio
发表了43篇文章 · 总计69.19k字
载入天数...载入时分秒...
总浏览量Loading | 访客总数Loading

主题 StackJimmy 设计
由ZephyrBD修改