学习笔记——并查集(上)模板和种类并查集

简介

并查集是用于处理点集之间关系的一种方法,支持合并和查询两种操作,即将两个集合合并和查询两个点是否位于同一集合内,是一种树形数据结构,一般采用数组实现。
首先定义一个数组 f f f,对于点 i i i f [ i ] f[i] f[i]存储着i的父节点的下标,通过递归查找找到其根节点。而合并点 i , j i,j ij时,首先找到 i , j i,j ij的父节点 p i , p j pi,pj pipj,然后让 f [ p i ] = p j f[pi]=pj f[pi]=pj f [ p j ] = p i f[pj]=pi f[pj]=pi即可,也就是让 i , j i,j ij所在的集合的根节点同一。查询时只要查询两个点的根节点是否一样即可。

int f[100];//存储父节点
void ini()//初始化
{
 for (int i = 1; i <= 100; i++)
  f[i] = i;//一开始节点i根节点当然是i啦
}
int find(int i)
{
 if (i == f[i])//如果i的父节点使它本身,说明到头了,i就是根节点
  return i;
 return find(f[i]);//没到头就继续往下找
}
void merge(int i, int j)
{
 f[find(i)] = find(j);/*合并直接让一个集合的根节点连向另一个根节点就行,毕竟
 根节点就是一个集合的标志*/
}
bool query(int i, int j)//查找其根节点是否一致即可
{
 return find(i) == find(j);
}

路径压缩

在并查集中,我们关心的只是该节点的根节点,在多次合并后会出现一条一条的链在数组中,查询要从叶节点走到根节点。那既然我们不关心路径,只关心根节点,那让所有的非根节点直接指向根节点,那查询时岂不是一步到位!这就是路径压缩。具体实现看代码

int find(int i)
{
 if (i == f[i])
  return i;
 return (fd[i] = find(i));/*这里只多了一步,既然find最终返回的是根节点,那
 寻找时让路上的节点等于find就行了。*/
}

集合数量记录

对于点的编号完全大于0,每个数组保存它的父节点,那根节点呢?其实根节点不一定时指向自己,它可以是一个负数,当 f i n d ( ) find() find()到负数是就返回,所以根节点的数组可以用来记录额外的信息,比如集合数量。我们定义集合数量= − f [ 根 节 点 标 号 ] -f[根节点标号] f[]。具体实现看代码。

int f[100];//存储父节点
void ini()//初始化
{
 for (int i = 1; i <= 100; i++)
  f[i] = -1;//一开始每个集合只有一个点,几位-1
}
int find(int i)
{
 if (f[i] < 0)//如果记录的是负数,那就到头了
   return i;
 return find(f[i]);//没到头就继续往下找
}
void merge(int i, int j)
{
 f[find(j)]+=f[find(i)];//更新规模
 f[find(i)] = find(j);//合并
}
int query(int i)//查询点i所在集合的点数量
{
	return -f[find(i)];
}

Q1并查集【模板】

原题传送门->洛谷P3367
典型模板,给你m个操作,有合并,有查询,输出查询结果即可。

#include<iostream>
#include<vector>
#include<climits>
#include<string>
#include<queue>
#include<functional>
#include<set>
#include<algorithm>
#include<cmath>
#include<iomanip>
using namespace std;
int f[10001];
void iui()
{
    for (int i = 1; i <= 10000; i++)
        f[i] = i;
}
int find(int x)
{
    return f[x] == x ? x : (f[x] = find(f[x]));
}
void merge(int i, int j)
{
    f[find(i)] = find(j);
}
bool query(int i, int j)
{
    return find(i) == find(j);
}
int main()
{
    iui();
    int n, m;
    cin >> n >> m;
    while (m--)
    {
        int op, i, j;
        cin >> op >> i >> j;
        if (op == 1)merge(i, j);
        else
        {
            if (query(i, j))
                cout << 'Y' << endl;
            else
                cout << 'N' << endl;
        }
    }
    return 0;
}

Q2修复公路

原题传送门->洛谷P1111
每修复一条道路实际上就是将道路两端的村庄所在的集合合并,应为两个联通块联通了,典型的并查集操作。题目要求输出最早时间,只要按时间从早到晚将边排序,一条一条合并,判断联通快数量即可。具体实现看代码。

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int n, m;
int f[1001];//并查集数组
struct E{
    int x, y, t;//道路两端节点编号和时间
}edge[100001];
int find(int x)
{
    if (x == f[x])return x;
    return f[x] = find(f[x]);
}
int main()
{
    ios::sync_with_stdio(false);
    cin >> n >> m;
    for (int i = 1; i <= n; i++)//初始化并查集
        f[i] = i;
    for (int i = 1; i <= m; i++)
        cin >> edge[i].x >> edge[i].y >> edge[i].t;
    sort(edge + 1, edge + m + 1, [](E&l, E&r)//排序
    {
        return l.t < r.t;
    });
    int ans = -1;//初始化为-1,答案未更新即联不通,到时直接输出
    for (int i = 1; i <= m; i++)//从早到晚枚举
    {
        E now = edge[i];
        int x = find(now.x), y = find(now.y);
        if (x != y)//若根节点不同
        {
            f[x] = y;//合并
            n--;//联通块数减一(刚开始n个节点独立,联通块数为1)
        }
        if (n == 1)//如果只剩一个联通块(图已联通)
        {
            ans = now.t;//答案更新
            break;
        }
    }
    cout << ans;//直接输出
    return 0;
}

Q3星球大战

原题传送门->洛谷P1197
一看题目,不断摧毁隧道,这是不断分离,而非合并,那咋办嘞?其实分离操作反过来不就是合并吗?所以我们从最后的道路被摧毁开始,往前一步一步修建隧道,再逆序输出答案即可。具体实现看代码

#include<iostream>
#include<vector>
#include<climits>
#include<string>
#include<queue>
#include<functional>
#include<set>
#include<algorithm>
#include<cmath>
#include<iomanip>
using namespace std;
const int MAX = 400001;
int n, m;
int f[MAX];//并查集
vector<int> node[MAX];//图
bool broken[MAX];//标记是否被损坏
int point[MAX];//记录损坏点
int find(int x)
{
    return (f[x] == x) ? x : (f[x] = find(f[x]));
}
int main()
{
    ios::sync_with_stdio(false);
    int n, m;
    cin >> n >> m;
    for (int i = 0; i < n; i++)//初始化
        f[i] = i;
    for (int i = 0; i < m; i++)//建图
    {
        int x, y; cin >> x >> y;
        node[x].push_back(y);
        node[y].push_back(x);
    }
    int k; cin >> k;//k次损坏
    for (int i = 0; i < k; i++)//记录损坏的点
    {
        int x; cin >> x;
        broken[x] = true;
        point[i] = x;
    }
    int ans = n - k;//一开始点独立,反叛军有n-k个占领点(从后往前)
    for (int i = 0; i < n; i++)
    {
        if (!broken[i])
        {
            for(auto e:no/de[i])

                if (!broken[e]&&find(e)!=find(i))/*如果是叛军掌握的两
                个点并且还没合并*/
                {
                    ans--;//联通块数减一
                    f[find(i)] = find(e);//合并
                }
        }
    }
    point[k] = ans;//point数组的损坏点逆序使用,所以用过后可以用来记录答案
    for (int i = k - 1; i >= 0; i--)//从k-1到0共k个点
    {
        int now = point[i];//弹出
        broken[now] = false;//标记为未损坏
        ans++;//联通块加一(有一个点进来了)
        for (auto e : node[now])//枚举该点所有连边
        {
            if (!broken[e]&&find(now)!=find(e))/*如果有点与他相连且没
            坏且还未合并*/
            {
                f[find(now)] = find(e);//合并
                ans--;//联通块数减一
            }
        }
        point[i] = ans;//逆序记录答案

    }
    for (int i = 0; i <= k; i++)//输出
        cout << point[i] << endl;
    return 0;
}

种类并查集

一种特殊的并查集,通过扩大n倍大小来表示n种种类关系间的集合。通过一个例子来讲会比较清晰

Q4关押罪犯

原题传送门->洛谷p1525
这道题题目要求将罪犯关再两个监狱,即两个集合,两种关系,而不是单一的一种关系了。如果罪犯i,j有怨气,要将他们关在两个不同的监狱,这怎么实现呢?
首先有监狱A监狱B两个监狱,我们开两倍的并查集数组,前1~n表示A监狱,n+1
~2n表示监狱B。关押罪犯i,j时,只要 p [ f i n d ( i + n ) ] = f i n d ( j ) p[find(i+n)]=find(j) p[find(i+n)]=find(j), p [ f i n d ( j + n ) ] = f i n d ( i ) p[find(j+n)]=find(i) p[find(j+n)]=find(i),也就是B监狱关押i,A监狱关押j,将这个关系并入一个集合,同时A监狱关押i,B监狱关押j也并入一个集合。查询时只要监狱A同时出现i,j或监狱B同时出现i,j说明关着关着i,j关在了一起,即 f i n d ( i ) = = f i n d ( j ) ∣ ∣ f i n d ( i + n ) = = f i n d ( j + n ) find(i)==find(j)||find(i+n)==find(j+n) find(i)==find(j)find(i+n)==find(j+n)。这就是种类并查集。
而这道题也是个贪心,将矛盾关系以怨气值从小到大排列,一条一条关押,合并,直到关押某一对罪犯时发现一关押他们就在同一监狱出现,这就是最小怨气值了,输出即可。具体看代码

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int MAX = 20001;
int n, m;
int f[MAX * 2];//开二被数组
struct rel {
    int x, y, val;//关系集,罪犯x和y有val的怨气
}edge[100001];
int find(int x)
{
    return f[x] == x ? x : (f[x] = find(f[x]));
}
int main()
{
    ios::sync_with_stdio(false);
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= m; i++)
        cin >> edge[i].x >> edge[i].y >> edge[i].val;
    for (int i = 1; i <= n * 2; i++)//初始化
        f[i] = i;
    sort(edge + 1, edge + 1 + m, [](rel&l, rel&r)//排序
    {
        return l.val > r.val;
    });
    for (int i = 1; i <= m; i++)
    {
        int x = edge[i].x, y = edge[i].y;
        if (find(x)==find(y)||find(x+n)==find(y+n))//如果在同一监狱就直接输出
        {//并推出main
            cout << edge[i].val;
            return 0;
        }
        f[find(x + n)] = find(y);//否则就建立关系
        f[find(y + n)] = find(x);
    }
    cout << 0;//如果和平解决就输出0
    return 0;
}

Q5食物链

原题传送门->洛谷P2024
这到题的物种有三种关系(环形食物链),即同类,猎物,天敌,物种i,j的关系
只有这三种。这里我将1到n表示同类,1+n到2n表示猎物,2n+1到3n表示天敌。对于操作1,只要物种i,j就没有猎物关系也没有天敌关系就是真话(先不管后两个条件),同类关系的建立需要:

  f[find(x)] = find(y);
  f[find(x+n)] = find(y+n);
  f[find(x+2*n)] = find(y+2*n);

也就是同类之间具有相同的同类,相同的猎物,相同的天敌更新三种关系(同类,猎物,天敌)。对于操作2,只要XY既不是同类也没有X是Y的猎物关系,那就是真话。既然X吃Y,关系建立只需要建立X的猎物是Y,X的天敌是Y的猎物,X的同类是Y的天敌,即三种关系都更新一下(同类,猎物,天敌)。

f[find(x + n)] = find(y);//X的猎物是Y
f[find(x + 2 * n)] = find(y + n);//X的天敌是Y的猎物
f[find(x)] = find(y + 2 * n);//X的同类是Y的天敌

这就是这道题的核心,剩下的细节看代码吧

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
const int MAX = 3*(5e4 + 1);
int n, m;
int f[MAX];//并查集
int find(int x)
{
    return (f[x] == x) ? x : (f[x] = find(f[x]));
}
int main()
{
    ios::sync_with_stdio(false);//
    cin >> n >> m;
    int ans = 0;//假话数
    for (int i = 1; i <= 3*n; i++)
        f[i] = i;
    int op, x, y;
    for (int i = 1; i <= m; i++)
    {
        cin >> op >> x >> y;
        if (x > n || y > n) { ans++; continue; }//条件2
        if (op == 2 && x == y) { ans++; continue; }//条件3
        if (op == 1)
        {//如果X的猎物时Y或X的天敌时Y,就是假话
            if (find(x+n) == find(y) || find(x+2*n) == find(y)) { ans++; continue; }
            f[find(x)] = find(y);
            f[find(x+n)] = find(y+n);
            f[find(x+2*n)] = find(y+2*n);
        }
        if (op == 2)
        {//如果X的同类时Y或X的天敌是Y,就是假话
            if (find(x) == find(y) || find(x + 2 * n) == find(y)) { ans++; continue; }
            f[find(x + n)] = find(y);
            f[find(y + n)] = find(x+2*n);
            f[find(x)] = find(y + 2 * n);
        }
    }
    cout << ans;//输出答案
    return 0;
}
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值