NO.78十六届蓝桥杯备战|数据结构-并查集|双亲表示法|初始化|查询|合并|判断|亲戚|Lake Counting|程序自动分析(C++)

双亲表⽰法

接下来要学习到的并查集,本质上就是⽤双亲表⽰法实现的森林。因此,我们先认识⼀下双亲表⽰法。
在学习树这个数据结构的时,讲到树的存储⽅式有很多种:孩⼦表⽰法,双亲表⽰法、孩⼦双亲表⽰法以及孩⼦兄弟表⽰法等。对⼀棵树⽽⾔,除了根节点外,其余每个结点⼀定有且仅有⼀个双亲,双亲表⽰法就是根据这个特点存储树的,也就是把每个结点的双亲存下来。因此,我们可以采⽤数组来存储每个结点的⽗亲结点的编号,这就实现了双亲表⽰法
![[Pasted image 20250408153417.png]]

但是,在实现并查集的时,我们⼀般让根节点⾃⼰指向⾃⼰。因此,上述存储就变成
![[Pasted image 20250408152922.png]]

并查集的概念

在有些问题中,我们需要维护若⼲个集合,并且基于这些集合要频繁执⾏下⾯的操作:

  • 查询操作:查找元素x属于哪⼀个集合。⼀般会在每个集合中选取⼀个元素作为代表,查询的是这个集合中的代表元素;
  • 合并操作:将元素x所在的集合与元素y所在的集合合并成⼀个集合;(注意,合并的是元素所在的集合,不是这两个元素)
  • 判断操作:判断元素x 和y 是否在同⼀个集合

并查集(UnionFind):是⼀种⽤于维护元素所属集合的数据结构,实现为⼀个森林,其中每棵树表⽰⼀个集合,树中的节点表⽰对应集合中的元素,根节点来代表整个集合
![[Pasted image 20250408155700.png]]

并查集的实现

初始化

初始状态下,所有的元素单独成为⼀个集合:

  • 让元素⾃⼰指向⾃⼰即可
const int N = 1e6 + 10;  
int n;  
int fa[N]; // 双亲表⽰法所需的数组  
// 初始化并查集  
void init()  
{  
	for(int i = 1; i <= n; i++) fa[i] = i;  
}
查询操作

查询操作是并查集的核⼼操作,其余所有的操作都是基于查询操作实现的!
找到元素x 所属的集合:

  • ⼀直向上找爸爸
// 查询操作  
int find(int x)  
{  
	if(fa[x] == x) return x;  
	return find(fa[x]);  
	// ⼀⾏实现  
	return fa[x] == x ? x : find(fa[x]);  
}
合并操作

将元素x 所在的集合与元素y 所在的集合合并成⼀个集合:

  • 让元素x 所在树的根节点指向元素y 所在树的根节点。(反过来也是可以的)
// 合并操作  
void un(int x, int y) // 注意,函数名字不能⽤ union,因为它是 C++ 的关键字  
{  
	int fx = find(x);  
	int fy = find(y);  
	fa[fx] = fy;  
}
判断操作

判断元素x 和元素y 是否在同⼀集合:

  • 看看两者所在树的根节点是否相同
// 判断是否在同⼀集合  
bool issame(int x, int y)  
{  
	return find(x) == find(y);  
}

并查集的优化

极端情况:在合并的过程中,整棵树变成⼀个链表。
路径压缩:在查询时,把被查询的节点到根节点的路径上的所有节点的⽗节点设置为根节点,从⽽减⼩树的深度。也就是说,在向上查询的同时,把在路径上的每个节点都直接连接到根上,以后查询时就能直接查询到根节点

// 找根节点 - 路径压缩  
int find(int x)  
{  
	if(fa[x] == x) return x;  
	return fa[x] = find(fa[x]);  
	// ⼀⾏实现  
	return fa[x] == x ? x : fa[x] = find(fa[x]);  
}

还有⼀种优化⽅式是按秩合并,但是基本上不⽤按秩合并,并查集的时间复杂度就很优秀了

P3367 【模板】并查集 - 洛谷
#include <bits/stdc++.h>
using namespace std;

const int N = 2e5 + 10;

int n;
int fa[N];

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

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);

    int T;
    cin >> n >> T;
    
    for (int i = 1; i <= n; i++) fa[i] = i;
    
    while (T--)
    {
        int z, x, y; cin >> z >> x >> y;
        if (z == 1)//合并
        {
            int fx = find(x);
            int fy = find(y);
            fa[fx] = fy;
        }
        else //判断
        {
            if (find(x) == find(y)) cout << "Y" << endl;
            else cout << "N" << endl;
        }
    }
    
    return 0;
}
P1551 亲戚 - 洛谷

具有亲戚关系的两个集合就合并在⼀个集合中。因此,可以⽤并查集解决

#include <bits/stdc++.h>
using namespace std;

const int N = 5010;

int n, m, p;
int fa[N];

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

void un(int x, int y)
{
    int fx = find(x);
    int fy = find(y);
    fa[fy] = fx;
}

bool issame(int x, int y)
{
    return find(x) == find(y);
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> m >> p;

    //初始化
    for (int i = 1; i <= n; i++) fa[i] = i;

    while (m--)
    {
        int x, y; cin >> x >> y;
        un(x, y);
    }

    while (p--)
    {
        int x, y; cin >> x >> y;
        if(issame(x, y)) cout << "Yes\n";
        else cout << "No\n";
    }
    
    return 0;
}
P1596 [USACO10OCT] Lake Counting S - 洛谷

遍历整个矩阵,每次遇到⼀个⽔坑时,就把这个⽔坑右、下,左下以及右下的⽔坑合并在⼀起。最终判断⼀下⼀共有多少个集合

#include <bits/stdc++.h>
using namespace std;

const int N = 110;

int n, m;
char a[N][N];
int fa[N * N];

int dx[] = {0, 1, 1, 1};
int dy[] = {1, 1, 0, -1};

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

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

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);

    cin >> n >> m;

    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < m; j++)
        {
            cin >> a[i][j];        
        }
    }

    for (int i = 0; i < n * m; i++) fa[i] = i;

    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j < m; j++)
        {
            if (a[i][j] == '.') continue;

            for (int k = 0; k < 4; k++)
            {
                int x = i + dx[k], y = j + dy[k];
                if (y >= 0 && a[x][y] == 'W')
                {
                    un(i*m+j, x*m+y);
                }
            }
        }
    }
    int ret = 0;
    for (int i = 0; i < n*m; i++)
    {
        //判断是不是W
        int x = i / m, y = i % m;
        if (a[x][y] == 'W' && fa[i] == i) ret++;
    }
    cout << ret << endl;
    
    return 0;
}
P1955 [NOI2015] 程序自动分析 - 洛谷

先利⽤并查集维护所有相等的信息,然后遍历所有的不相等信息,判断⼀下是否合法。
因为数据范围的问题,需要先对所有的数离散化处理
![[Pasted image 20250408171740.png]]

#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 10;

int n;
struct node
{
    int x, y, e;
}a[N];

//离散化
int pos;
int disc[N * 2];
unordered_map<int, int> mp;

//并查集
int fa[N * 2];

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

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

bool issame(int x, int y)
{
    return find(x) == find(y);
}

bool solve()
{
    cin >> n;
    //清空数据
    pos = 0;
    mp.clear();
    
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i].x >> a[i].y >> a[i].e;
        disc[++pos] = a[i].x; disc[++pos] = a[i].y;
    }
    
    //离散化
    sort(disc+1, disc+1+pos);

    int cnt = 0;
    for (int i = 1; i <= pos; i++)
    {
        int x = disc[i];
        if (mp.count(x)) continue;

        cnt++;
        mp[x] = cnt;
    }

    //初始化
    for (int i = 1; i <= cnt; i++) fa[i] = i;

    //用并查集维护相等的信息
    for (int i = 1; i <= n; i++)
    {
        int x = a[i].x, y = a[i].y, e = a[i].e;
        if (e == 1) un(mp[x], mp[y]);
    }

    //判断不等的信息是否合法
    for (int i = 1; i <= n; i++)
    {
        int x = a[i].x, y = a[i].y, e = a[i].e;
        if (e == 0)
        {
            if (issame(mp[x], mp[y])) return false;
        }
    }
    return true;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);

    int T; cin >> T;
    while (T--)
    {
        if (solve()) cout << "YES" << endl;
        else cout << "NO" << endl;
    }
    
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值