首页 > 其他分享 >并查集

并查集

时间:2024-02-05 14:34:22浏览次数:46  
标签:结点 int sum 查集 合并 fnd

【并查集是什么】

并查集是用来表示一些不相交集合的算法。

它可以很快地处理两个点之间是否在一个连通块中。

【并查集的特点】

  1. 动态合并集合;

  2. 合并之后就不能拆开了。

并查集开始前,先按顺序把初始集合编号。

(初始也不一定每个都是单个元素)

【并查集的实现】

数据结构分类:抽象结构、存储结构。

  1. 抽象结构:画出来的形式(就像一棵树画在纸上,有结点也有边);

  2. 存储结构:实现代码的形式(vector数组)。

并查集的抽象结构:一些集合。

存储结构:树。


把并查集比作一些小组,小组之间会合并。

初始集合 1~n 编号。

\(p_i = x\) 表示:\(i\) 的上级是 \(x\)。

初始:\(p_i = i\)。(每个人自成一组)

并(unn):每次合并 \(x\)、\(y\),令 \(p_x = y\)。

合并时,把 \(x\) 和 \(y\) 所在组合并,\(x\)、\(y\) 未必就是组长

并查集的存储结构很像森林。

如果把 \(i\) 连向 \(p_i\),差不多就是森林。

所以,\(p_i\) 可以近似看作 \(i\) 的父结点

森林中,一棵树的根结点(组长)有一个自环。(初始化)


查(fnd):只要两个元素所在组的组长相同,这两个元素必定在同一组(不可能重复)。

如果一个元素的 \(p\) 就是自己,这个元素就是组长。

(这个元素没有其他父结点)

并,需要通过查来实现:

合并 \(x\) 和 \(y\) 的组,只把 \(x\) 的组长的父结点变成 \(y\) 的组长(如果 \(x\) 和 \(y\) 同组就啥也不干)。

查 \(y\) 组的时候,不会影响,因为不会跑到 \(x\) 去。

查 \(x\) 组的时候,到了原本的根结点也不会停,因为此时的原根结点的父结点不是自己,而是 \(y\) 的组长,于是会返回 \(y\) 组长。

合并两个组,只和两个组的组长有关系,和其他任何无关。


void init() {
    for (int i = 1; i <= n; i++)
        p[i] = i;
}

int fnd(int x) {
    if (p[x] == x)
        return x;
    return fnd(x);
}

void unn(int x, int y) {
    int xx = fnd(x), yy = fnd(y);
    if (xx == yy)
        return ;
    p[xx] = yy;
}

【优化:路径压缩与按秩合并】

注:仅一个优化可能是不够的。

两个优化板子题

【按秩合并:小树连大树】

这里有两棵树,我们要合并它们。

那我们是把规模小的连向规模大的,还是大的连向小的?

是小的连大的,这个优化可以把查询复杂度优化到 \(\log\) 级别。

证明:查询时每往上走一个,当前子树规模至少乘二。

如果是大连小,就有可能构造出一条链,就变成 \(O(size)\) 了。

void unn(int x, int y) {
    int xx = fnd(x), yy = fnd(y);
    if (xx == yy)
        return ;
    if (sz[xx] > sz[yy])
        swap(xx, yy);
    p[xx] = yy;
    sz[yy] += sz[xx];
}

【路径压缩:缩小深度】

因为我们的fnd是一步一步往上走的,而且我们只关心根结点是谁。

所以,我们希望每个点距离根结点都要更近一些。

如果一个结点距离根结点很远,其实我们可以把它的父结点直接改成根,这样不影响查询结果。

而且,我们进行每一次查询时,因为是不断往上递归的。

所以,这条递归上去的路径上的所有点,我们回溯时都可以把它的父结点改成根结点。

等我们下次再查询的时候,只要往上走一步,就直接到根结点了。

int fnd(int x) {
    if (p[x] == x)
        return x;
    return p[x] = fnd(p[x]);
}

【类似树上前缀和的并查集】

Experience

还是需要两个优化。

这里的难点:两个组合并之后,它们的经验依然可能不同。

考虑一个类似树上前缀和的手法:

加经验值,只在根结点上加。

一个点的经验值,就是从它到根结点路径上所有点上的经验值的和。

但是,我们合并的时候,在这之前两个集合的经验是不能共享的,我们这么合并却都算了进去。

所以,我们在合上去的那棵树的根结点,减去另一个根结点上的经验。(类似于补偿回来)

举个栗子:我们合并这两颗树。

合完变成这样:

但是左边黄树的点本来应该是 5 经验,合并之后都变成 12 经验了。

于是我们补偿。

这样左边的经验值就对了。

以后再给这组加经验,就直接在新根结点加即可。

(因为从黄到绿根,一定恰好经过黄根一遍,我们在黄根减掉即可)

【怎么进行两个优化】

首先按秩合并一定可以,因为我们都没有关心谁连谁的问题。

但是,路径压缩怎么进行?

我们定义 \(sum_x\) 为 \(x\) 的经验值。

如果不进行路径压缩,\(sum_x\) 就要不停通过 \(p_x\) 递归,每递归一次就把递归到的结点的经验值加到 \(sum_x\) 上,这显然是可行的。

但是,我们发现:一个点的 \(sum\),等于它到根结点路径上的所有点的经验值之和。

我们进行一个这样的操作:递归的时候顺手把上面的经验加到相邻的下面

(如果上面的就是根结点就不加了,因为下面输出的时候会加)。

输出的时候输出这个点的经验加根结点的经验(如果本身就是根结点就不加了)。

例子:

假设我们要算最下面的点的经验。

当他递归到根的时候,返回;

退到了经验 2 的点,因为上面是根结点,所以直接返回;

退到了经验 3 的点,上面不是跟结点,加:

再退,退到经验 4 的点,加:

注意,加的不是3,而是5 。

我们可以发现,这相当于把从父结点到根结点的儿子都加了。

(类似前缀和递推)

于是我们最下面的点经验值变成 9。

而此时我们查询这个路径上任何一个点,按照上面输出的规则,都是正确的。

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

int n, m, x, y, v;
string s; 
//p[i]表示i与谁在一组,sz[i]表示i代表的组的集合个数
//sum[i]为i的标记,i到跟的路径标记之和为i的经验值 
int p[200005], sz[200005], sum[200005]; 
void init() {
	for (int i = 1; i <= n; i++)
		p[i] = i, sz[i] = 1, sum[i] = 0;
} 
int fnd(int x) {//返回x所在组的代表 
	if (p[x] == x)
		return x;
	int rt = fnd(p[x]);//先记录根是谁
	//这说明此次是有效路径压缩 ,且因为已经递归压缩了p[x],故此时只压缩了p[x]这个点
	if (p[x] != rt) 
		sum[x] += sum[p[x]]; 
	return p[x] = rt; //路径压缩,每个点直连根节点 
}
void unn(int x, int y) {//合并x,y所在组
	int rx = fnd(x), ry = fnd(y);//x,y所在组代表 
	if (rx == ry) //是同一组 
		return ;
	if (sz[rx] > sz[ry])//确保小连大 
		swap(rx, ry);
	//rx一方的点,在rx多减一次sum[ry],在ry多加一次 sum[ry],值仍然正确;ry一边则不受影响 
	sz[ry] += sz[rx], sum[rx] -= sum[ry];
	p[rx] = ry;//rx合并进了ry组 
}
int main()
{
	cin >> n >> m; 
	init();
	for (int i = 1; i <= m; i++) {
		cin >> s;
		if (s == "join") {
			cin >> x >> y;
			unn(x, y);
		}
		else if (s == "add") {
			cin >> x >> v;
			sum[fnd(x)] += v;//直接加到根,因为本组所有路径恰好算根一次 
		}
		else {
			cin >> x;
			if (fnd(x) == x) //根到根的路径只有自己 
				cout << sum[x] << endl; 
			else
				cout << sum[x] + sum[fnd(x)] << endl;
		}
	}
    return 0;
}

【带权并查集】

类似于上面的 \(sum\),这里的权应该是一个合并的时候顺手就维护了的属性。(不用写边结构体)

银河英雄传说

这题里面带的权就是到根结点(头部飞船)的距离。

注意:这里还是只有根结点会带权,其他点在合并的时候千万别动!!!

求两个点之间的距离,就是两个点到头部飞船的距离之差。

Parity Game

定义点:一次回答(l 和 r)。

这题的权就是奇偶性(01).

两个点在一个集合,只当它们存在一个共同端点,或者可以通过若干个其他有共同端点的点到达。

两个点之间的距离就是它们之间有奇数个还是偶数个1。

(因为a+b和a-b同奇偶,所以求距离只要到根的距离相加即可,不用考虑同侧)

如果在一个集合里面有矛盾的(和是0,却有一个是1),就有问题。

【改造问题】

【离线倒序】

Closing the Farm

这题是不断删边,但是并查集只能处理合并的关系。

考虑把所有操作存下来,再倒过来,就变成不断加边了。

如果有加有删,就另想他法吧(可持久化?)。

【类拆点】

团伙

定义点是一个人。

朋友的关系是传递的,但是敌人的关系却不是。

考虑对每一个点 \(i\),\(i+n\) 是他的 “敌人窝点”。

他的每一个敌人,和他的敌人窝点成为朋友。

这样就把敌人关系也改成传递的了。

答案:

一开始 ans 是 n。

只要是朋友关系合并了,ans 就减一(不是敌人窝点的朋友)。

如果是敌人窝点,优先让实际的人作为根

食物链

每个点拆成三个:同类,吃它的,被它吃的


【洛谷题单:并查集基础】

  1. 并查集模板 模板,没什么好说的。

  2. 村村通 顺手统计一下集合个数。

  3. 家谱 不知道为啥用 getchar() 莫名其妙T了几回。

  4. Milk Vists S WA了一次,因为忘记特判两个点在一起。

  5. Closing the Farm 离线倒序。

  6. MooTube 一开始没想出来倒序(完全倒过来)有啥用,但是后来想到离线可以随便排序,然后降序一排就A了。(边写成升序了卡了三分钟)

  7. 团伙 拆成俩。

  8. 食物链 一开始没有想好拆出来的三种点之间的关系。

  9. Parity Game 离散化写错一下。

  10. 银河英雄传说

int fnd(int x) {
    if (p[x] == x)
        return x;
    p[x] = fnd(p[x]);
    dis[x] = dis[p[x]] + 1;
    return p[x];
}

这段代码有两个错误。

第一个:

定义的 \(dis_x\):x到根结点的距离,是可能 “跳跃”的

所以,不能写+1,因为你根本 \(p_x\) 和父结点不是一回事。

第二个:

在更新 \(dis\) 之前,\(p_x\) 已经被更新过了,导致 \(dis\) 一直加的是现在根结点的距离。(也就是不会变)

所以,用旧 \(p_x\) 更新 \(dis\),就不能先更新 \(p_x\) 。

正确写法:

int fnd(int x) {
    if (p[x] == x)
          return x;
    int fa = fnd(p[x]);
    dis[x] += dis[p[x]];
    return p[x] = fa;
}

【CF 的 EDU】

step1

  1. Disjoint Sets Union 按秩合并+路径压缩板子。

  2. Disjoint Sets Union2 额外记录 sz, mx, mn。

  3. Experience 经典 路径压缩时记录属性,类似前缀和。

  4. Cutting a graph 离线倒序。

step2

  1. People are leaving 注意不能按秩合并,而且这题卡输入输出!!!

  2. Parking 上一题的环形版

  3. Restructuring Company 加一个nxt数组,但是这题居然也卡输入!!!

  4. Bosses 普普通通。

  5. Bipartite Graph 小小的拆点。

  6. First Non-Bipartite Edge 和 I 大同小异,一开始没看到是二分图就要输出 -1 挂了一次。

【选做题】

修复公路 板子。

扩散 注意要除以二。

星球大战 开了一堆数组记录有没有被炸掉,改了 5 分钟。

Monkeys 有意思!

首先,并查集不能处理分开(掉下来),所以离线倒序。

其次,考虑带权并查集,类似 Experience 的方式。

\(d_i\) 是猴子 \(i\) 掉落的时间,路径压缩的时候顺便更新。

让 1 号猴子作为永远的根结点。

(这也说明不能按秩合并)

每只猴子的掉落时间,就是从它到根结点的和。

每次合并时,如果哪边有 1,另一边的根结点的 \(t\) 就更新为当前时间。

因为 1 是永远的根结点,所以 1 上边不会有其他点,所以不会有时间会被重复计算。

最后还有一个小细节,一定要先压缩一遍路径才输出,因为路径压缩相当于更新一遍。

#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 2e5 + 5;

int n, m;
int p[N], sz[N], d[N];

void init() {
	for (int i = 1; i <= n; i++) {
		p[i] = i;
		sz[i] = 1;
	}
}

int fnd(int x) {
	if (p[x] == x)
		return x;
	int rt = fnd(p[x]);
	d[x] += d[p[x]];
	return p[x] = rt;
}

void unn(int x, int y, int t) {
	x = fnd(x);
	y = fnd(y);
	if (x == y)
		return ;
	if (x == 1) {
		p[y] = 1;
		d[y] = t;
	}
	else if (y == 1) {
		p[x] = 1;
		d[x] = t;
	}
	else {
		p[x] = y;
		d[x] = 0;
	}
}

int l[N] = {0}, r[N] = {0};

struct Query {
    int u, v;
} q[N * 2];

bool ql[N] = {};
bool qr[N] = {};

int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        cin >> l[i] >> r[i];
    for (int i = 1, p, h; i <= m; i++) {
        cin >> p >> h;
        q[i].u = p;
        if (h == 1)
        	q[i].v = l[p];
        else
        	q[i].v = r[p];
        
        if (h == 1)
        	ql[p] = true;
        else
			qr[p] = true;
    }
    
    init();
    for (int i = 1; i <= n; i++) {
        if (!ql[i] && l[i] != -1)
            unn(i, l[i], 0);
        if (!qr[i] && r[i] != -1)
            unn(i, r[i], 0);
    }

    for (int i = m; i >= 1; i--) 
        unn(q[i].u, q[i].v, i);
    
    for (int i = 1; i <= n; i++) {
        fnd(i);
        cout << d[i] - 1 << endl;
    }
    return 0;
}

标签:结点,int,sum,查集,合并,fnd
From: https://www.cnblogs.com/FLY-lai/p/18007919

相关文章

  • 并查集
    记录21:042024-2-3目录1.并查集题目记录1.并查集用来快速元素是否属于同一组的数据结构利用路径压缩和按秩合并防止结构退化点击查看代码#defineMAX_N10000intpar[MAX_N];intrank[MAX_N];voidinit(intn){for(inti=0;i<n;i++){par[i]=......
  • 并查集入门1【L2】
    讲义 引例1•有N个计算机中心,开始时都是独立的。后来不断的架设了M条网线,每条网线把其中的2个计算机中心连接起来。•直接或间接连接的计算机中心都可以相互通信,组成一个网络。 •问有多少个连通网络?  引例2•围棋的棋子有黑色和白色两种,相同颜色的棋子如果是上下......
  • POJ1182 食物链 (并查集的应用)
    POJ1182食物链(并查集的应用)Description动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B,B吃C,C吃A。现有N个动物,以1-N编号。每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。有人用两种说法对这N个动物所构成的食物链关系进行描述:第一种说法......
  • POJ2492 (并查集)
    POJ2492(并查集)题目:假设昆虫之间是异性互动,给出昆虫的互动情况,判断假设是否成立;输入:第一行t表示n个测试用例,每个测试用例第一行n,m表示n只昆虫,从1连续编号,m组互动情况;输出:假设不成立:Suspiciousbugsfound!假设成立:Nosuspiciousbugsfound!题解:参考POJ1611#include<cstdio......
  • POJ1611 (简单并查集)
    POJ1611(简单并查集)描述严重急性呼吸系统综合症(SARS)是一种病因不明的非典型肺炎,于2003年3月中旬被确认为全球性威胁。为了尽量减少传染给他人,最好的策略是将嫌疑人与其他人分开。在不传播你的疾病大学(NSYSU),有很多学生团体。同一小组中的学生相互交流频繁,一个学生可能会......
  • POJ 2524 (简单并查集)
    POJ2524(简单并查集)描述当今世界上有如此多不同的宗教,很难将它们全部记录下来。您有兴趣了解您所在大学的学生信仰多少种不同的宗教。您知道您的大学有n名学生(0<n<=50000)。你不可能询问每个学生的宗教信仰。此外,许多学生不愿意表达自己的信仰。避免这些问题的一种方......
  • 并查集
    简介并查集是一种维护集合的数据结构。支持合并和查询元素所在集合。我们将所有元素用点表示,从属关系用边表示,那么每个集合构成了一棵有向树。每个点有且仅有一条出边,指向其父亲。其中根节点的出边指向自己。集合用根节点的编号代表。实现初始化一开始所有的元素指向自己。......
  • 【模板】并查集
    并查集是解决两元素是否属于同一集合,将一个集合合并另一集合的数据结构。具体来说,我们使用数字代替集合,比如集合1,集合2.使用数组f[i]维护元素i属于的集合,比如f[2]=4表示元素2属于集合4。具体我们有以下实现功能的代码1初始化表示集合的数组。cin>>n>>m;for(int......
  • Codeforces Round 170 (Div. 1)A. Learning Languages并查集
    如果两个人会的语言中有共同语言那么他们之间就可以交流,并且如果a和b可以交流,b和c可以交流,那么a和c也可以交流,具有传递性,就容易联想到并查集,我们将人和语言看成元素,一个人会几种语言的话,就将这些语言和这个人所在的集合合并,最后求一下人一共在几个连通块中,连通块的个数-1就是答案,......
  • P1536 村村通(并查集)
    村村通题目描述某市调查城镇交通状况,得到现有城镇道路统计表。表中列出了每条道路直接连通的城镇。市政府"村村通工程"的目标是使全市任何两个城镇间都可以实现交通(但不一定有直接的道路相连,只要相互之间可达即可)。请你计算出最少还需要建设多少条道路?输入格式输入包含若干......