zoukankan      html  css  js  c++  java
  • 2019.9.17 初级数据结构——并查集及其应用

    一、并查集基础

    (一)引入

    我们先来看一个问题。

    某学校有N个学生,形成M个俱乐部。每个俱乐部里的学生有着一定相似的兴趣爱好,形成一个朋友圈。一个学生可以同时属于若干个不同的俱乐部。根据“我的朋友的朋友也是我的朋友”这个推论可以得出,如果A和B是朋友,且B和C是朋友,则A和C也是朋友。请编写程序计算最大朋友圈中有多少人。

    输入格式:
    输入的第一行包含两个正整数N(≤30000)和M(≤1000),分别代表学校的学生总数和俱乐部的个数。后面的M行每行按以下格式给出1个俱乐部的信息,其中学生从1~N编号:

    第i个俱乐部的人数Mi(空格)学生1(空格)学生2 … 学生Mi

    输出格式:
    输出给出一个整数,表示在最大朋友圈中有多少人。

    输入样例:
    7 4
    3 1 2 3
    2 1 4
    3 5 6 7
    1 6
    输出样例:
    4

    我们把这个题抽象化:

    现在有n个数,它们按照一定规律合并,形成一些集合,求最大集合的元素个数。

    这个合并规律如下:

    (1)同一个俱乐部的所有人属于同一个集合;

    (2)若两个俱乐部包含同一个人,则这两个俱乐部属于同一个集合。

    按照正常做法,我们对于每一个人按照上述规律扫描剩余的人,运行复杂度O(n^很大)。

    这个方法显然过不了。

    我们重新分析这个题目,主要难点有两个:

    第一,对于第一条,同一个俱乐部的所有人如何存储它们所在同一个集合内。

    这个很好解决,我们再开一个book数组,对于每一个人i,假设它在cnt集合内,我们标记book[i]=cnt。

    如果这样标记,对于第二个合并规律,我们扫描另一个俱乐部的所有人,我们把每一个book[i]都置成要合并到的俱乐部,总复杂度O(nm)。

    但这样还是过不了,所以我们考虑把第二个合并步骤的复杂度优化到O(1),也就是说我们只需要修改一个数就可以修改整个集合。

    对于这样的修改方式,我们可以想到一种数据结构——

    利用树存储每个集合有两个好处:

    第一,每棵树只有一个树根(也就是查找树根就可以查找整棵树)

    第二,两棵树合并只需要把一棵树的树根接到另一棵上(所以修改的复杂度是O(1))。

    大概就是上面这么合并

    也就是说我们可以用树优化查找和合并集合的过程,其本质就是对树之间的处理。

    又因为,这一类题的根本步骤就是查找合并,所以我们把这样的数据结构叫做并查集


    (二)并查集的基本操作

    刚才我们已经知道,并查集中合并两个元素实际上是合并他们所在树的祖先。

    为了查询这两个点是否在一棵树上,我们首先要找到他们两个点的祖先分别是谁,并判断他们的祖先是否是同一个点即可。

    但是因为树会退化成一条链,所以我们在查找祖先时要把下图的第一个树变成第二个树,这是后话。

    第二个图的意思就是所有的点都连到根节点上。

    我们暂且不提怎么查找一个点的祖先,在我们查找两个点祖先之后,判断他们的祖先是不是同一个点,如果不是同一个点合并即可。

    所以我们用fa[x]表示x的父亲,则代码如下:

    void u(int a,int b)
    {
    	int c=f(a),d=f(b);
    	if(fa[c]!=fa[d])
    	{
    		fa[min(c,d)]=fa[max(c,d)];
    		return;
    	}
    }

    一定要注意的是,绝对不能让fa[min(a,b]直接等于fa[max(a,b)],因为这样只合并了两个点,而不是其所在的树。

    同时,注意合并时要按照一定的大小顺序,所以这里是按小往大合的。

    所以这是合并两个点的方法。

    关于查找祖先的方法,有一个非常绝妙的方法:

    int f(int o)
    {
    	if(fa[o]==o)return o;
    	return fa[o]=f(fa[o]);
    }

    第一行非常容易理解,如果o的祖先时自己则它自己就是这棵树的祖先。

    第二行我们把它分解成两部分:

    (1)f(fa[o])找到fa[o]的祖先。

    (2)return fa[o]=f(fa[o]) 将o的父亲置为其父亲的祖先

    也就是说 我们成功把一棵近似于一条链的树 弄成了一棵最多只有三层的树。

    这个过程换句话说是这样:

    (1)递归地找点o每一级祖先o',直到找到它真正的祖先o1;

    (2)回溯,对于每一个o',将fa[o]置为o1.

    也就是说 通过这个步骤,这棵树的每一个点都直接连接到了祖先上

    所以回到这章开始的部分,对于那两棵树之间的变化,有一种方法就是上面说的更改每个点与祖先的连接方式,这种方式叫作路径压缩

    其实还有一个稍微好实现一点的方法,对于每个树根,我们记录它所在的树的高度,把这个高度叫作这棵树的

    对于每个合并操作(不是路径压缩时的查找操作),我们把秩小的树合到秩大的树上,这样可以保证合出来的新树的秩不大于原来任何一棵树

    若两棵树的秩一样,则新树的秩是原树的秩+1.

    这种方法也叫按秩合并

    最后记得一开始初始化,令fa[x]=x。

    上个板子:

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    using namespace std;
    int fa[100050];
    int f(int o)
    {
    	if(fa[o]==o)return o;
    	return fa[o]=f(fa[o]);
    }
    void u(int a,int b)
    {
    	int c=f(a),d=f(b);
    	if(c!=d)fa[min(c,d)]=fa[max(c,d)];
    	return;
    }
    int main()
    {
    	int n,m;
    	scanf("%d%d",&n,&m);
    	for(int i=1;i<=n;i++)fa[i]=i;
    	for(int i=1;i<=m;i++)
    	{
    		int x,y,opt;
    		scanf("%d%d%d",&x,&y,&opt);
    		if(!opt)u(x,y);
    		else
    		{
    			if(f(x)!=f(y))puts("NO");
    			else puts("YES");
    		}
    	}
    	return 0;
    }
    

      高级用法待填坑。

    /*====年轻人,瞎搞是出不了省一的,这就是现实====*/
  • 相关阅读:
    Ubuntu14.04+cuda 7.5+cudnn_v4+tensorflow安装
    error C2275: “XXX”: 将此类型用作表达式非法
    DLL调试方法
    OpenCV ——双线性插值(Bilinear interpolation)
    OpenCV ——背景建模之CodeBook(2)
    OpenCV ——背景建模之CodeBook(1)
    OpenCV——GMM混合高斯模型
    OpenCV——运用于pixels war游戏
    【SpringSecurityOAuth2】源码分析@EnableOAuth2Sso在Spring Security OAuth2 SSO单点登录场景下的作用
    【Spring】简述@Configuration配置类注册BeanDefinition到Spring容器的过程
  • 原文地址:https://www.cnblogs.com/qxds/p/11536627.html
Copyright © 2011-2022 走看看