一.并查集及其优化
- 并查集:由若干不相交集合组成,是一种简单但是很好用的数据结构,拥有优越的时空复杂性,一般用于处理一些不相交集合的查询和合并问题。
- 三种操作
1.Make_Set(x) 初始化操作,初始化的时候,每个结点各自为一个集合,这个时候father[i]=i,即此时这个结点就是这个集合的根结点,也就是它本身。
2.Find_Set(x) 查找操作,其具体功能就是找到x这个元素所在集合的根结点。可以用来判断两个结点是否在同一个集合,如果根结点不同自然就不再同一个集合中。
3.Union(x,y) 合并操作,将连个元素合并到同一个集合当中,在合并之前,一般利用Find_Set()来判断是否在同一个集合当中。

如图为合并操作:

  • 并查集的优化方法(改进时间的启发式策略)

    • 1 路径压缩(Path Compression)是一种在每次使用“ 查找”(Find_Set)时压扁树结构的方法。在查找过程中将查找路径上所有结点都指向根结点,把所有访问过的结点直接链接到根,这样可以减少将来的查找时间。可以想象,如果树比较极端,退化成一条链了,那么在最坏的时候每次查询都达到了O(n),而路径压缩就可以提高效率。
      而且,路径压缩并不改变任何节点的秩但是会改变子树的高度.

      具体如下图所示:

    • 2 按秩合并(Union by rank)其思想是把包含较少节点的树的根指向包含较多节点的树的根,将较短的树连接到较高的树的根部,这样可以降低树的高度。
      这里需要使用一个Rank来记录秩的变化,每一个结点都有对应的一个Rank,,最初的集合中只有一个根结点,以及一个为0的Rank(秩)。在这里秩只是表示结点高度的一个上界。秩用来代替了高度或者深度,这是因为路径压缩会改变树的高度,所以不记录子树的高度的准确值,而是采用上界这样一个估计值来表示。
      具体如下图所示:
  • 代码实现:

#define MAX 5000
int n, m, Father[MAX], Rank[MAX];
void Make_Set(int x)
{
Father[x] = x;
Rank[x] = 0;
}
int Find_Set(int x)
{ //递归写法,数据量比较少的时候可以写
return Father[x] == x ? x : Father[x] = Find_Set(Father[x]);
}
int Find_Set(int x)
{
//递归写法,数据极端容易RE,可能栈溢出。默认堆栈的大小为8192KB.
if (x != Father[x])
Father[x] = Find_Set(Father[x]);
return Father[x];
}
int Find_Set(int x) //非递归写法,适合数据量比较大的时候
{
int p = x, temp;
while (Father[p] != p) p = Father[p];
while (x != p)
{
temp = Father[x];
Father[x] = p;
x = temp;
}
return x;
}
void Union(int x, int y) //按秩合并
{
x = Find_Set(x);
y = Find_Set(y);
if (x == y) return;
if (Rank[x] > Rank[y]) /*如果x的Rank比较高,就把x作为y的父节点*/
{
father[y] = x;
Rank[x] += Rank[y];
}
else
{
father[x] = y;
if (Rank[x] == Rank[y])
{
Rank[y]++;
}
}
}
  • 时间复杂度的考虑
    有几种情况,即是否使用路径压缩(Path Compression)和按秩合并(Union by rank)的四种情况,如果不使用优化手段,树可以无限制的增长,这样查找和合并的时间就是O(n).
    只是使用路径压缩(Path Compression)的话,最坏的时间就是

    只是使用按秩合并(Union by rank)的话,时间可以达到O(mlog n),其中n为结点数目。
    当同时使用的时候。可以确保每个操作分摊下来的时间是O(α(n)),这是最优的,
    这个α(n)是 inverse Ackermann function(逆阿克曼函数),当我们现在宇宙中的任何n值,可以得知α(n)<5,所以并查集运算基本恒定。
    详细参考:维基百科
    二.并查集基本应用
    从题目开始:HDU1232 畅通工程
  • 题目分析:建设最少的道路,使得所有城镇都相互交通,但不一定有直接的道路相连,只要互相间接通过道路可达即可。
  • 换句话说,这里就是在求有几个不相交的集合数,更准确就是求这个无向图的连通分量个数
    AC代码如下:
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstdbool>
#include<algorithm>
using namespace std;
#define MAX 1005
int Father[MAX];
void init()
{
for (int i = 1; i < MAX; i++)
Father[i] = i;
}
int Find_Set(int x)
{ //递归写法,数据量比较少的时候可以写
return Father[x] == x ? x : Father[x] = Find_Set(Father[x]);
}
void Union(int x, int y)
{
x = Find_Set(x), y = Find_Set(y);
if (x != y)
Father[x] = y;
}
int main(void)
{
int m, n, t1, t2;
while (scanf("%d", &n) != EOF)
{
if (n == 0)
break;
scanf("%d",&m);
init();
while (m-- > 0)
{
scanf("%d%d",&t1,&t2);
Union(t1, t2);
}
int ans = 0;
for (int i = 1; i <= n; i++)
{
if (Father[i] == i)
ans ++;
}
printf("%d\n",ans - 1);
}
}

下面是使用了按秩合并的写法:

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstdbool>
#include<algorithm>
using namespace std;
#define MAX 1005
int Father[MAX], Rank[MAX];
void init()
{
for (int i = 1; i < MAX; i++)
Father[i] = i;
}
int Find_Set(int x)
{ //递归写法,数据量比较少的时候可以写
return Father[x] == x ? x : Father[x] = Find_Set(Father[x]);
}
void Union(int x, int y) //按秩合并
{
x = Find_Set(x);
y = Find_Set(y);
if (x == y) return;
if (Rank[x] > Rank[y]) /*如果x的Rank比较高,就把x作为y的父节点*/
{
Father[y] = x;
Rank[x] += Rank[y];
}
else
{
Father[x] = y;
if (Rank[x] == Rank[y])
{
Rank[y]++;
}
}
}
int main(void)
{
int m, n, t1, t2;
while (scanf("%d", &n) != EOF)
{
if (n == 0)
break;
scanf("%d",&m);
init();
while (m-- > 0)
{
scanf("%d%d",&t1,&t2);
Union(t1, t2);
}
int ans = 0;
for (int i = 1; i <= n; i++)
{
if (Father[i] == i)
ans ++;
}
printf("%d\n",ans - 1);
}
}

三.带权并查集
- 带权并查集,是指结点存有权值信息的并查集。并查集以森林的形式存在, 而结点的权值大多是记录该结点与祖先关系的信息。
比如权值可以记录该结点到根结点的距离,也可以是相对于根节点的状态。

1.食物链(POJ1182)这道题是十分经典的关于带权并查集的题目,就是维护当前结点和根结点关系。
- 题目分析:有三种动物,存在互相吃与被吃的关系,A吃B, B吃C,C吃A。现在给定K句话,这些话中有真有假,现在需要判断其中假话的个数。

  • 一种思路,通过并查集来将所有同时出现的情况都合并到一起,如果1能吃掉2,那么只有三种情况:
  • 1是A,2是B
  • 1是B,2是C
  • 1是C,2是A
    然后把 1是A 和 2是B 的情况合并到一起,同理对于剩下两种情况 ,这样处理之后,如果之后又加入了一句话发现 2是B 和 2是A的情况在一个集合中,那么就一定判断这句话是假的了。

  • 之后发现有更好的方法:这里的难点就在于不知道具体是什么动物。
    虽然不知道每个数字对应的动物,但是可以知道动物之间的关系。那么就可以把已知关系的点合并在一棵树上,然后记录每个点与根结点的关系。这里采用的规则如下:
    如果一个点和根结点的相同那么关系记为 0,
    如果一个点可以吃掉根结点那么关系记为 1,
    如果一个点可以被根结点吃掉那么关系记为 2。
    我们只要一直维护每个点与根结点的关系,这样我们就可以判断某一个描述是否与之前的描述冲突了。
    具体而言,在判断X,Y的关系的时候,找出X,Y的根结点A,B
    如果A!=B,则X,Y还没有建立联系,此时关于这两个元素的判断都可以认为是对的。之后需要合并这两个子树,具体关系如下图所示这里的式子

    要满足(reltion[x] + t) % 3 = (d + relation[y]) % 3,所以 t = (d + relation[y] - relation[x] + 3) % 3,满足这一个条件,既可以判断这句话正确性。
    如果A==B,说明存在关系,这个时候需要判断是否正确,通过求和取模的关系,这个是通过找规律得来, 需要写出一些情况来合理推断。

  • AC代码

#include <map>
#include <iostream>
#include<cstdio>
using namespace std;
int father[500999];
int relation[500099];
void Make_Set(int n)
{
for (int i = 1; i <= n; i++)
{
father[i] = i;
relation[i] = 0;
}
}
int Find_Set(int x)
{
if (father[x] == x)
return x;
else
{
int t = father[x];
father[x] = Find_Set(father[x]);
relation[x] = (relation[t] + relation[x]) % 3;
return father[x];
}
} int main()
{
int i, flag = 0, N, K, D, X, Y, A, B;
int res;
scanf("%d", &N);
Make_Set(N);
scanf("%d",&K);
for (i = 0; i < K; i++)
{
scanf("%d%d%d",&D,&X,&Y);
if ((X > N) || (Y > N) || X == Y && D == 2)
{
flag++;
}
else
{
A = Find_Set(X);
B = Find_Set(Y);
if (A != B)
{
father[B] = A;
relation[B] = (D - 1 + relation[X] - relation[Y] + 3) % 3;
}
else
{
if ((relation[Y] - relation[X] + 3) % 3 != D - 1)
flag++;
}
}
}
printf("%d\n",flag);
}

2.再比如这道题:POJ1703 Find them, Catch them

  • 题目分析:有两个帮派,给若干条信息,根据所给信息来判断所询问的两个人是否是同一个帮派。信息包括D [a] [b], 其中[a]和[b]是两个罪犯的标号,他们属于不同的帮派。 A[a][b],这是询问是否ab在同一个集合。
  • 思路:这里明显可以使用并查集,但是给的信息是不同集合的,而并查集是保存同一个集合,所以单纯的并查集是不行了,故增加一个flag,来标记当前结点和根结点的关系,用一个并查集来维护所有的结点。在这里,我使用的规则是:如果当前结点和根结点是一个集合则标记为0,否则为1。
    这样只要一直维护每个点与根结点的关系,就可以判断某一个询问的两个人是否在同一个帮派。
  • 注意:这里有一个特例,就是一共只有两个人的时候,他们是在不同帮派的。
  • 关系转移为在Find_Set()的时候和上一层父亲节点的flag相加,需要查找规律得出。
  • AC代码如下:
#include<iostream>
#include<cstdio>
#define MAX 100005
using namespace std;
int father[MAX], flag[MAX];
void Make_Set(int n)
{
for (int i = 1; i <= n; i++)
{
father[i] = i;
flag[i] = 0;
}
} int Find_Set(int x)
{
if (father[x] == x)
return x;
else
{
int t = father[x];
father[x] = Find_Set(father[x]);
flag[x] = (flag[t] + flag[x]) % 2; //状态转移,0和1分别代表是一个集合,不是一个集合的状态
return father[x]; //当前结点的状态等于前面两个结点的状态的叠加态
}
} void Union(int a, int b)
{
int x = Find_Set(a);
int y = Find_Set(b);
father[x] = y;
flag[x] = (flag[b] + flag[a] + 1 ) % 2; //状态转移,0和1分别代表是一个集合,不是一个集合的状态
} int main(void)
{
char check;
int T, M, N, mem1, mem2, t1, t2;
scanf("%d",&T);
while (T-- > 0)
{
scanf("%d %d",&N,&M);
Make_Set(N);
while(M-->0)
{
getchar();//之前有个换行需要接受,否则WA
scanf("%c %d %d",&check, &mem1, &mem2);
if (N == 2 && check=='A') //特殊样例
{
printf("In different gangs.\n");
}
else if (check == 'D')
{
Union(mem1, mem2);
}
else
{
t1 = Find_Set(mem1);
t2 = Find_Set(mem2);
if (t1 != t2)
{
printf("Not sure yet.\n");
}
if (t1 == t2)
{
if(flag[mem1] != flag[mem2])
printf("In different gangs.\n");
else
printf("In the same gang.\n");
}
}
}
}
return 0;
}

3.POJ2492 A Bug’s Life

  • 题目分析:假设所有bug只能异性之间交配,出现同性则错误
  • 思路:同样使用带权并查集,用flag表示当前结点和根结点的相对关系,两个虫子如果是异性,则一个为0,一个为1,用第一个关系为依据,后面的虫子逐渐更新和根结点的关系,如果发现某两个虫子的flag相同,则为错误信息。
  • 合并x,y的关系式为:
    flag[b] = (flag[x] - flag[y] + 1) % 2,
    flag[b] = (flag[x] - flag[y] + 1) % 2,
    其中,a是x的根结点,b是y的根结点。
  • AC代码如下:
#include<iostream>
#include<cstdio>
#define MAX 100005
using namespace std;
int father[MAX], flag[MAX], Rank[MAX];
void Make_Set(int n)
{
for (int i = 1; i <= n; i++)
{
father[i] = i;
flag[i] = 0;
Rank[i] = 0;
}
} int Find_Set(int x)
{
if (father[x] == x)
return x;
else
{
int t = father[x];
father[x] = Find_Set(father[x]);
flag[x] = (flag[t] + flag[x]) % 2; //状态转移,0和1分别代表是一个集合,不是一个集合的状态
return father[x]; //当前结点的状态等于前面两个结点的状态的叠加态
}
} void Union(int x, int y)
{
int a = Find_Set(x);
int b = Find_Set(y);
if (a == b) return;
if (Rank[a] > Rank[b]) //按秩合并
{
father[b] = a;
Rank[a] += Rank[b];
flag[b] = (flag[x] - flag[y] + 1) % 2;
}
else
{
if (Rank[a] == Rank[b])
{
Rank[b]++;
}
father[a] = b;
flag[a] = (flag[y] - flag[x] + 1) % 2;
}
}
int main(void)
{
int n, m, T, gg, mm, ans;
scanf("%d",&T);
for(int i=1;i<=T;i++)
{
ans = 0;
scanf("%d %d",&n,&m);
Make_Set(n);
while (m-- > 0)
{
scanf("%d %d",&gg,&mm);
Union(gg, mm);
if (flag[gg] == flag[mm] && father[gg] == father[mm])
ans = 1;
}
printf("Scenario #%d:\n",i);
if (ans == 1)
printf("Suspicious bugs found!\n");
else
printf("No suspicious bugs found!\n");
putchar(10);
}
return 0;
}

当然还有其他的应用,比如LCA(最近公共祖先问题,以及实现Kruskar算法求最小生成树,还在学习ing.
参考文章来源http://www.ahathinking.com/archives/10.html

并查集(union-find sets)的更多相关文章

  1. 笔试算法题(38):并查集(Union-Find Sets)

    议题:并查集(Union-Find Sets) 分析: 一种树型数据结构,用于处理不相交集合(Disjoint Sets)的合并以及查询:一开始让所有元素独立成树,也就是只有根节点的树:然后根据需要将 ...

  2. 并查集(Union/Find)模板及详解

    概念: 并查集是一种非常精巧而实用的数据结构,它主要用于处理一些不相交集合的合并问题.一些常见的用途有求连通子图.求最小生成树的Kruskal 算法和求最近公共祖先等. 操作: 并查集的基本操作有两个 ...

  3. POJ 1611 The Suspects 并查集 Union Find

    本题也是个标准的并查集题解. 操作完并查集之后,就是要找和0节点在同一个集合的元素有多少. 注意这个操作,须要先找到0的父母节点.然后查找有多少个节点的额父母节点和0的父母节点同样. 这个时候须要对每 ...

  4. Java 并查集Union Find

    对于一组数据,主要支持两种动作: union isConnected public interface UF { int getSize(); boolean isConnected(int p,in ...

  5. 并查集 (Union-Find Sets)及其应用

    定义 并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题.常常在使用中以森林来表示. 集就是让每个元素构成一个单元素的集合,也就是按一定顺序将属于同一组的 ...

  6. 并查集——HDOJ-1232-畅通工程

    并查集 并查集(Union-Find Sets)是一种非常精巧而实用的数据结构,它主要用于处理一些不相交集合的合并问题,在合并之前,需要先判断两个元素是否属于同一集合,这就需要用查找操作来实现.一些常 ...

  7. poj 1611 The Suspects(第一道并查集)

    题意: 有N个学生,编号为0-n-1,现在0号学生感染了非典,凡是和0在一个社团的人就会感染, 并且这些人如果还参加了别的社团,他所在的社团照样全部感染,社团个数为m,求感染的人数. 输入: n代表人 ...

  8. 并查集(UVA 1106)

    POINT: 把每个元素看成顶点,则一个简单化合物就是一条无向边,若存在环(即k对组合中有k种元素),则危险,不应该装箱,反之,装箱: 用一个并查集维护连通分量集合,每次得到一种化合物(x, y)时检 ...

  9. 最小生成树(Minimum Spanning Tree)——Prim算法与Kruskal算法+并查集

    最小生成树——Minimum Spanning Tree,是图论中比较重要的模型,通常用于解决实际生活中的路径代价最小一类的问题.我们首先用通俗的语言解释它的定义: 对于有n个节点的有权无向连通图,寻 ...

  10. bzoj1854 [Scoi2010]游戏【构图 并查集】

    传送门:http://www.lydsy.com/JudgeOnline/problem.php?id=1854 没想到怎么做真是不应该,看到每个武器都有两个属性,应该要想到连边构图的!太不应该了! ...

随机推荐

  1. (二)HTML中的部分标签

    HTML作为一种超文本标记语言,其中用到了大量的标签,昨天主要看了HTML中的图像标签和表格标签. (一)图像标签 <img> <img src="url"/&g ...

  2. hibernate的查询 (比较get 与load)

    hibernate的查询的比较hibernate的查询有很多,Query,find,Criteria,get,load query使用hsql语句,可以设置参数是常用的一种方式 criteria的方式 ...

  3. myeclipse更改类或者是配置文件不用重启tomcat的方法

    一.修改java代码(如action)无需重启与部署方法 方法1:在WebRoot下的META-INF文件夹中新建一个名为context.xml文件,里面添加如下内容(要区分大小写): <Con ...

  4. sql and csharp: Split Function

    T-SQL: declare @int int,@prov int,@city int,@str nvarchar(500) set @str='天河麗特青春:中國廣東省廣州市天河區天河路623號天河 ...

  5. jQuery基础——选择器、效果

    一.使用JS的痛处 在学习和使用js的过程中发现了js的一些痛处: 1.书写繁琐,代码量大. 2.代码复杂. 3.动画效果很难实现.使用定时器,要小心各种定时器的清除.各种操作和处理事件不好实现. 4 ...

  6. js实现图片延时加载的原理

    实现原理: 附:(http://www.cnblogs.com/fishtreeyu/archive/2011/03/12/1982067.html) 把所有需要延时加载的图片改成如下的格式: < ...

  7. <Android 应用 之路> 简易手电筒

    前言 快一个月没有写自己的博客了,由于最近换了工作,换了居住地,所以有一些杂事需要处理,从今天开始恢复正常,不赘述了.进入今天的主题 -– 简易的手电筒. 这个Demo中使用的是比较新的API,M版本 ...

  8. 在 Eclipse Juno 上安装 Marketplace

    Select Help/Install new software... from the menu, select the Juno update site (http://download.ecli ...

  9. Git 基本知识与常用指令

    一.Git代码状态转换图 其中: 未被Git跟踪的状态为unstage状态: 已被Git跟踪的状态为stage状态(stage:阶段),因此包括staging状态和staged状态. untrack ...

  10. Zabbix监控 windows agent安装配置

    下载Windows的zabbix客户端 载地址:http://www.zabbix.com/download.php 选择windows版本的agent下载 从官方下载Zabbix Agent后,压缩 ...