并查集
2016-05-19 00:48
771 查看
一、并查集概念
并查集实现不相交集合的元素查询和集合合并。
二、并查集的实现
并查集的实现大概是一个森林的结构,节点属性father为节点的父亲,其中 x.father == x 作为根节点的标志,(也可以指向x.father == -1等等作为标志),各元素用所在树的根节点代表,查询一个元素相当于返回这个元素所在树的根节点,合并两个集合是把一个集合的根节点作为另一个集合的根节点的孩子,即合并setA, setB 的主要操作为 setA.root.father = setB.root;
下面贴代码:(用数组实现)
代码都只要一句话,非常简洁,下面介绍一下代码细节:
void makeset(int n);
创建n个集合,第i个集合只含有一个元素i(i=1,...,n)
int find(int x)
查找元素x所在集合,用返回集合根节点表示。函数用递归形式给出,也可以改成迭代形式。递归的终点是x==father[x] ,即x是根节点,然后返回根节点。这行代码还实现了路径的压缩,当查找一个元素时,走过路径的节点在递归下依次变成根节点的孩子,这样减少下次查询到该节点的时间。
void unionset(int x,int y)
先寻找x和y的所在集合,然后把y所在集合根节点变成x所在集合根节点的孩子。
三,简单例题
1、poj1611 The Suspect
思路:给集合加上一个num属性记录集合中有的元素个数,注意unionset函数要更新num[x]。
代码:
2、poj2524 Ubiquitous Religions
思路:和前面一题几乎一样。
代码:
3.、poj1182 食物链
思路: 给元素增加一个关系域,在同一集合中,两个元素关系域相同代表同类,关系域相差1(在模3的情况下)代表前者吃后者。
每次遇到x,y,如果它们在不同集合,那么它们还没有关系,一定不是谎言,然后把第两个集合并到第一个集合上,并根据操作是1或2更新第二个集合每个元素的关系域。
集合关系域的更新如下:
假设输入为d,x,y, x 所在集合根为 p:
p.relation = (y.relation - x.relation + d + 2 )%3; //(1)
然后依次迭代p的每个棵子树,
a.relation = (a.relation + a.father.relation)%3; //(2)
直到整颗树更新完毕。至于为什么这样更新,稍微画画图就知道了。
现在我们来看它们已经在同一集合的情况。这时,它们必然已经有关系了,那么就要判断语句给出的关系是否正确。还是假设输入为d,x,y,如果
(relation[x]-relation[y]+3)%3 != d-1代表是假话。只要加一个计数器计假话数就好了。这样我们就得到了整个题目的算法。
最后要注意的是,在真正实现的时候,(2)那里并不需要真的把整棵树更新,相反我们可以在查找函数上做功夫,使得每次查找的时候就更新查找那个结点的关系域就行了。
最后的最后,本题数据有点坑爹!只能一次输入,改成多次输入会WA。
代码:
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#define MAX 50005
using namespace std;
int father[MAX], relation[MAX];//仍然是用数组表示
void makeset(int n)
{
for (int i=1;i<=n;i++)
{
father[i] = i;
relation[i] = 0;
}
}
int find(int x)
{
if (x!=father[x])
{
int temp = father[x];
father[x] = find(father[x]);
relation[x] = (relation[x]+relation[temp])%3; //在查找的时候更新所需结点的关系域
}
return father[x];
}
void unionset(int x,int y, int d)
{
int p = find(x), q = find(y);
father[p] = q;
relation[p] = (relation[y]-relation[x]+d+2)%3; //这里只更新p的关系域,当查找到p的子树种节点的时候,查找函数会帮忙更新那个结点
}
int main(void)
{
#ifdef EMO
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
#endif
int n,k,lie=0;
scanf("%d%d",&n,&k);
makeset(n);
while (k--)
{
int d,x,y,p,q;
scanf("%d%d%d",&d,&x,&y);
if (x>n || y>n) {lie++; continue;}
p = find(x);
q = find(y);
if (p!=q) unionset(x,y,d);
else if ((relation[x]-relation[y]+3)%3!=d-1)
{
lie++;
}
}
printf("%d\n",lie);
return 0;
}
四、小结
并查集的代码并不复杂,可以说很简单。并查集的用处很广,可以作为很多算法的辅助算法。之后还会继续更新一些有关并查集的姿势。
五、参考资料及扩展阅读
http://www.cnblogs.com/cherish_yimi/archive/2009/10/11/1580839.html http://blog.csdn.net/lalor/article/details/7388805
并查集实现不相交集合的元素查询和集合合并。
二、并查集的实现
并查集的实现大概是一个森林的结构,节点属性father为节点的父亲,其中 x.father == x 作为根节点的标志,(也可以指向x.father == -1等等作为标志),各元素用所在树的根节点代表,查询一个元素相当于返回这个元素所在树的根节点,合并两个集合是把一个集合的根节点作为另一个集合的根节点的孩子,即合并setA, setB 的主要操作为 setA.root.father = setB.root;
下面贴代码:(用数组实现)
#define MAX 10005 int father[MAX]; void makeset(int n) { for (int i=1;i<=n;i++) father[i] = i; } int find(int x) { return x==father[x]?father[x]:father[x]=find(father[x]); } void unionset(int x,int y) //合并y所在集合到x所在集合 { father[find(y)] = find(x); }
代码都只要一句话,非常简洁,下面介绍一下代码细节:
void makeset(int n);
创建n个集合,第i个集合只含有一个元素i(i=1,...,n)
int find(int x)
查找元素x所在集合,用返回集合根节点表示。函数用递归形式给出,也可以改成迭代形式。递归的终点是x==father[x] ,即x是根节点,然后返回根节点。这行代码还实现了路径的压缩,当查找一个元素时,走过路径的节点在递归下依次变成根节点的孩子,这样减少下次查询到该节点的时间。
void unionset(int x,int y)
先寻找x和y的所在集合,然后把y所在集合根节点变成x所在集合根节点的孩子。
三,简单例题
1、poj1611 The Suspect
思路:给集合加上一个num属性记录集合中有的元素个数,注意unionset函数要更新num[x]。
代码:
#include <cstdio> #define MAX 30005 int father[MAX], num[MAX]; void makeset(int n) { for (int i=0;i<n;i++) { father[i] = i; num[i] = 1; } } int find(int x) { return father[x]==x?father[x]:father[x]=find(father[x]); } void unionset(int x,int y) { x = find(x); y = find(y); if (x==y) return ; father[y] = x; num[x] += num[y]; } int main(void) { int n,m,k,i,a,b; while (~scanf("%d%d",&n,&m),n|m) { makeset(n); while (m--) { scanf("%d",&k); if (k) scanf("%d",&a); for (i=1;i<k;i++) { scanf("%d",&b); unionset(a,b); } } printf("%d\n",num[find(0)]); } return 0; }
2、poj2524 Ubiquitous Religions
思路:和前面一题几乎一样。
代码:
#include <cstdio> #define MAX 50005 int father[MAX],num[MAX],diff; void makeset(int n) { for (int i=1;i<=n;i++) { father[i] = i; num[i] = 1; } } int find(int x) { if (x!=father[x]) father[x] = find(father[x]); return father[x]; } void unionset(int x,int y) { x = find(x); y = find(y); if (x==y) return ; if (num[x] > num[y])//把num小的集合并到num大的集合,貌似可以减少树的高度。 { father[y] = x; num[x] += num[y]; } else { father[x] = y; num[y] += num[y]; } diff--; } int main(void) { int n,m,cas = 1; while (~scanf("%d%d",&n,&m),n|m) { diff = n; makeset(n); for (int i=0;i<m;i++) { int a,b; scanf("%d%d",&a,&b); unionset(a,b); } printf("Case %d: %d\n",cas++,diff); } return 0; }
3.、poj1182 食物链
思路: 给元素增加一个关系域,在同一集合中,两个元素关系域相同代表同类,关系域相差1(在模3的情况下)代表前者吃后者。
每次遇到x,y,如果它们在不同集合,那么它们还没有关系,一定不是谎言,然后把第两个集合并到第一个集合上,并根据操作是1或2更新第二个集合每个元素的关系域。
集合关系域的更新如下:
假设输入为d,x,y, x 所在集合根为 p:
p.relation = (y.relation - x.relation + d + 2 )%3; //(1)
然后依次迭代p的每个棵子树,
a.relation = (a.relation + a.father.relation)%3; //(2)
直到整颗树更新完毕。至于为什么这样更新,稍微画画图就知道了。
现在我们来看它们已经在同一集合的情况。这时,它们必然已经有关系了,那么就要判断语句给出的关系是否正确。还是假设输入为d,x,y,如果
(relation[x]-relation[y]+3)%3 != d-1代表是假话。只要加一个计数器计假话数就好了。这样我们就得到了整个题目的算法。
最后要注意的是,在真正实现的时候,(2)那里并不需要真的把整棵树更新,相反我们可以在查找函数上做功夫,使得每次查找的时候就更新查找那个结点的关系域就行了。
最后的最后,本题数据有点坑爹!只能一次输入,改成多次输入会WA。
代码:
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#define MAX 50005
using namespace std;
int father[MAX], relation[MAX];//仍然是用数组表示
void makeset(int n)
{
for (int i=1;i<=n;i++)
{
father[i] = i;
relation[i] = 0;
}
}
int find(int x)
{
if (x!=father[x])
{
int temp = father[x];
father[x] = find(father[x]);
relation[x] = (relation[x]+relation[temp])%3; //在查找的时候更新所需结点的关系域
}
return father[x];
}
void unionset(int x,int y, int d)
{
int p = find(x), q = find(y);
father[p] = q;
relation[p] = (relation[y]-relation[x]+d+2)%3; //这里只更新p的关系域,当查找到p的子树种节点的时候,查找函数会帮忙更新那个结点
}
int main(void)
{
#ifdef EMO
freopen("in.txt","r",stdin);
freopen("out.txt","w",stdout);
#endif
int n,k,lie=0;
scanf("%d%d",&n,&k);
makeset(n);
while (k--)
{
int d,x,y,p,q;
scanf("%d%d%d",&d,&x,&y);
if (x>n || y>n) {lie++; continue;}
p = find(x);
q = find(y);
if (p!=q) unionset(x,y,d);
else if ((relation[x]-relation[y]+3)%3!=d-1)
{
lie++;
}
}
printf("%d\n",lie);
return 0;
}
四、小结
并查集的代码并不复杂,可以说很简单。并查集的用处很广,可以作为很多算法的辅助算法。之后还会继续更新一些有关并查集的姿势。
五、参考资料及扩展阅读
http://www.cnblogs.com/cherish_yimi/archive/2009/10/11/1580839.html http://blog.csdn.net/lalor/article/details/7388805
相关文章推荐
- C#数据结构之顺序表(SeqList)实例详解
- Lua教程(七):数据结构详解
- 解析从源码分析常见的基于Array的数据结构动态扩容机制的详解
- C#数据结构之队列(Quene)实例详解
- C#数据结构揭秘一
- C#数据结构之单链表(LinkList)实例详解
- 数据结构之Treap详解
- 用C语言举例讲解数据结构中的算法复杂度结与顺序表
- C#数据结构之堆栈(Stack)实例详解
- C#数据结构之双向链表(DbLinkList)实例详解
- JavaScript数据结构和算法之图和图算法
- Java数据结构及算法实例:冒泡排序 Bubble Sort
- Java数据结构及算法实例:插入排序 Insertion Sort
- Java数据结构及算法实例:考拉兹猜想 Collatz Conjecture
- java数据结构之java实现栈
- java数据结构之实现双向链表的示例
- Java数据结构及算法实例:选择排序 Selection Sort
- Java数据结构及算法实例:朴素字符匹配 Brute Force
- Java数据结构及算法实例:汉诺塔问题 Hanoi
- Java数据结构及算法实例:快速计算二进制数中1的个数(Fast Bit Counting)