并查集及应用

1 并查集

1.1 初始化

并查集: 一种数据结构,用于处理一些不相交集合的合并和查询问题。它主要包括两个操作:查找(find)和合并(union)。

为了便于理解,我们让集合里的点构成一些父子关系(事实上,集合里的这些点并没有拓扑关系,是平等存在的),对于每个点,我们只关心它的父亲。

f a ( i ) fa(i) fa(i) 表示点 i i i 的父亲。

初始化: 每个点最开始自己是自己的父亲。

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

1.2 查找(find)

为了判断两个点是否在同一个集合里,我们不能直接判断它们的父亲是否相同,因为可能存在父亲不相同,祖父相同,或者祖父的祖父相同……所以,判断两点是否在同一集合,要判断它们是否祖先相同

查找的时间复杂度为 O ( α ( n ) ) O(\alpha(n)) O(α(n)) α ( n ) \alpha(n) α(n) 为反阿克曼函数,增长极其缓慢,趋近 O ( 1 ) O(1) O(1))。

在集合里,祖先只是一个集合的标志,便于我们进行判断和操作的一个点。

对于需要查找的点 x x x,我们可以找到 x x x 的父亲 f a ( x ) fa(x) fa(x),然否找到 f a ( x ) fa(x) fa(x) 的父亲 f a ( f a ( x ) ) fa(fa(x)) fa(fa(x)),……一直找到点自己是自己的父亲时停止。这个点即为祖先,因为它不再有父亲了。

int find(int x) {
    if (fa[x] == x)
        return x;
    return find(fa[x]);
}

还可以进行优化:与其通过父亲向上逐层地找祖先,不如直接做祖先的儿子。在递归过程中,修改 f a ( x ) fa(x) fa(x) 的值。

int find(int x) {
    if (fa[x] == x)
        return x;
    return fa[x] = find(fa[x]);
}

1.3 合并(union)

现有两个集合(单个的点也看做一个集合),需要合并为一个集合,简单的方法就是让其中一个集合的祖先成为另一个集合祖先的儿子

在合并时,不用关心两个集合是否出现包含的情况,因为重复说明两个点的祖孙关系没有影响。

合并的时间复杂度(不包括find())为 O ( 1 ) O(1) O(1)

由于union是关键字,我们定义函数名为add

void add(int x, int y) {
    x = find(x);
    y = find(y);
    fa[x] = y;
}

2 例题

2.1 亲戚

P1551 亲戚 - 洛谷

题目描述

n n n 个人,给定 m m m 对亲戚关系,先询问 p p p 次,查询 p i p_i pi p j p_j pj 是否具有亲戚关系。

题解

并查集模板。询问 p i p_i pi p j p_j pj 是否有亲戚关系,只要查询是否在同一集合中即可。

代码如下:

#include <bits/stdc++.h>
#define endl '\n'
#define file(FILENAME) \
    freopen(FILENAME ".in", "r", stdin), freopen(FILENAME ".out", "w", stdout)
#define CLOSE                    \
    ios::sync_with_stdio(false); \
    cin.tie(0);                  \
    cout.tie(0)
using namespace std;
typedef long long ll;
const int mod = 1e9 + 7;
const int N = 1e5 + 10;
int n, m, p, fa[N];
int a, b;
int find(int x) {
    if (fa[x] == x)
        return x;
    return fa[x] = find(fa[x]);
}
void add(int x, int y) {
    x = find(x);
    y = find(y);
    fa[x] = y;
}
int main() {
    CLOSE;
    cin >> n >> m >> p;
    for (int i = 1; i <= n; i++)
        fa[i] = i;
    while (m--) {
        cin >> a >> b;
        add(a, b);
    }
    while (p--) {
        cin >> a >> b;
        cout << (find(a) == find(b) ? "Yes\n" : "No\n");
    }
    return 0;
}

2.2 [NOI 2015] 程序自动分析

P1955 [NOI2015] 程序自动分析 - 洛谷

题目描述

T T T 组数据,每组 n n n 个条件表达式, x i = x j ( e = 1 ) x_i=x_j(e = 1) xi=xj(e=1) x i ≠ x j ( e = 0 ) x_i\neq x_j(e = 0) xi=xj(e=0),求所有的条件是否相互矛盾,不矛盾输出YES,否则输出NO

题解

先处理 e = 1 e=1 e=1 的条件:如果 a = b a=b a=b b = c b = c b=c ,我们可以将 a , b , c a,b,c a,b,c 放在一个集合里。

再处理 e = 0 e = 0 e=0 的条件:如果 a ≠ b a\neq b a=b,但 a , b a,b a,b 在同一个即合理,则矛盾。

题目要求的数字的范围是 1 ∼ 10 , 000 , 000 , 000 1\sim 10,000,000,000 110,000,000,000,所以不能开这么大,可以使用离散化,或者用map

为了节约时间,也可以用unordered_map或者pair,我使用了前者。

代码如下:

#include <bits/stdc++.h>
#define endl '\n'
#define file(FILENAME) \
    freopen(FILENAME ".in", "r", stdin), freopen(FILENAME ".out", "w", stdout)
#define CLOSE                    \
    ios::sync_with_stdio(false); \
    cin.tie(0);                  \
    cout.tie(0)
using namespace std;
typedef long long ll;
const int mod = 1e9 + 7;
const int N = 1e5 + 10;
int T, n;
ll a[N], b[N], e[N];
unordered_map<ll, int> fa;
int find(int x) {
    if (fa[x] == x)
        return x;
    return fa[x] = find(fa[x]);
}
void add(int x, int y) {
    x = find(x);
    y = find(y);
    fa[x] = y;
}
int main() {
    scanf("%d", &T);
    while (T--) {
        fa.clear();
        scanf("%d", &n);
        for (int i = 1; i <= n; ++i) {
            scanf("%lld%lld%lld", &a[i], &b[i], &e[i]);
            fa[a[i]] = a[i];
            fa[b[i]] = b[i];
        }
        for (int i = 1; i <= n; ++i)
            if (e[i])
                add(a[i], b[i]);
        bool flag = true;
        for (int i = 1; i <= n; ++i)
            if (!e[i] && find(a[i]) == find(b[i]))
                flag = false;
        printf((flag ? "YES\n" : "NO\n"));
    }
    return 0;
}

2.3 [NOIP2017 提高组] 奶酪

P3958 [NOIP2017 提高组] 奶酪 - 洛谷

题目描述

NOIP2017 提高组 D2T1

输入共 T T T 组数据。

一个底无限大、高 z = h z=h z=h 的矩形奶酪,其中有 n n n 个半径为 r r r 的球体空洞,给出每个洞球心的坐标 x i x_i xi y i y_i yi z i z_i zi,如果两个洞球面相切或相交,则两个洞连通。现有一老鼠在奶酪底部( z = 0 z = 0 z=0),问是否可以通过空洞到达奶酪顶部 ( z = h z = h z=h)。若能输出Yes,否则输出No

三维空间计算两点 P 1 ( x 1 , y 1 , z i ) , P 2 ( x 2 , y 2 , z 2 ) P_1(x_1,y_1,z_i),P_2(x_2,y_2,z_2) P1(x1,y1,zi),P2(x2,y2,z2) 坐标公式:
d i s t ( P 1 , P 2 ) = ( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 + ( z 1 − z 2 ) 2 dist(P_1,P_2)=\sqrt{(x_1-x_2)^2+(y_1-y_2)^2 + (z_1-z_2)^2} dist(P1,P2)=(x1x2)2+(y1y2)2+(z1z2)2

题解

如果两球心 P 1 , P 2 P_1,P_2 P1,P2 的球能够连通( d i s t ( P 1 , P 2 ) ≤ 2 ⋅ r dist(P_1,P_2)\le 2\cdot r dist(P1,P2)2r),则将其放到一个集合里。

如果存在一个集合,既有能够连通底部的球的球心( z i − r ≤ 0 z_i-r\le0 zir0),又有能够连通顶部的球的球心( z j + r ≥ h ) z_j+r\ge h) zj+rh),那么这个集合内的球一定可以从顶部到达底部(中间不可能断开,因为在同一个集合里)。

is1标记是否连通底部,is2标记是否连通顶部。

代码如下:

#include <bits/stdc++.h>
#define endl '\n'
#define file(FILENAME) \
    freopen(FILENAME ".in", "r", stdin), freopen(FILENAME ".out", "w", stdout)
#define CLOSE                    \
    ios::sync_with_stdio(false); \
    cin.tie(0);                  \
    cout.tie(0)
using namespace std;
typedef long long ll;
const ll mod = 1e9 + 7;
const ll N = 1010;
ll n, T, h, r, fa[N], is1[N], is2[N];
ll find(ll x) {
    if (fa[x] == x)
        return x;
    return fa[x] = find(fa[x]);
}
void add(ll x, ll y) {
    x = find(x);
    y = find(y);
    fa[x] = y;
}
struct node {
    ll x, y, z;
} e[N];
double dist(node a, node b) {
    return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y) + (a.z - b.z) * (a.z - b.z));
}
int main() {
    CLOSE;
    cin >> T;
    while (T--) {
        memset(is1, 0, sizeof is1);
        memset(is2, 0, sizeof is2);
        cin >> n >> h >> r;
        for (ll i = 1; i <= n; i++)
            cin >> e[i].x >> e[i].y >> e[i].z;
        for (ll i = 1; i <= n; i++)
            fa[i] = i;
        for (ll i = 1; i <= n; i++) {
            for (ll j = 1; j < i; j++) {
                if (dist(e[i], e[j]) <= r * 2.0)
                    add(i, j);
            }
        }
        for (ll i = 1; i <= n; i++) {
            if (e[i].z - r <= 0)
                is1[find(i)] = true;
            if (e[i].z + r >= h)
                is2[find(i)] = true;
        }
        bool flag = false;
        for (ll i = 1; i <= n; i++)
            if (is1[i] && is2[i])
                flag = true;
        cout << (flag ? "Yes\n" : "No\n");
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值