并查集
定义
- 设不相交的k个集合 S 1 , S 2 ⋯ S k {S_1,S_2\cdots S_k} S1,S2⋯Sk,每个集合都有一个元素作为代表
- 希望有下面三种操作:
- MAKE_SET(x):建立一个新的集合,唯一成员就是x
- UNION(x,y):将包含x,y的两个动态集合合并成一个新集合
- FIND_SET(x):返回一个指针,指向包含x的唯一集合代表。
无向图的连通分量
有了上面的操作,给定一个无向图 G = ( V , E ) G = (V,E) G=(V,E),可以给出一个找出其所有连通分量的算法
- 令G中的所有顶点都自成一个集合,即对G中每个顶点调用MAKE_SET(x)
- 对于G中的每条边,如果两个顶点调用FIND_SET后结果不相同,则调用UNION(x,y)函数将其合并
- 最终每个集合显然就是一个连通分量
链式实现
- 对于每个集合而言,其具有一个头部,头部包含head和tail;分别指向第一个结点和最后一个结点
- 对每个结点而言,其具有如下的结构:
- set:指向该集合的头部
- data:结点包含的数据
- next: 该集合的下一个结点
- makeset和findset很容易实现
- UNION操作需要将待合并其中一个链表中的所有结点 的set域指向另一个链表的代表集合的头节点,这种操作是线性的。
- 考虑合并的启发式策略:为链表的头节点增加该链表的结点个数,总是将结点数较少的链表合并到结点数较多的链表
森林表示
- 使用有根树表示集合
- 每个结点表示一个成员,每棵树表示一个集合
- 每个结点指向且仅指向它的父节点
- 根节点就是这个集合的代表
启发式策略1-按秩归并
- 对于每个结点,维护一个秩(rank),代表该结点高度的上界,注意并不是代表的结点的确切高度
- 这里的高度意思是:从该节点到后代叶结点最长路径上边的数目
- 于是只有单个结点的树高度为0
- 归并时,使得秩较小的根指向具有较大秩的根
启发式策略2-路径压缩
- 在FIND_SET操作中,将路径上(从该节点到根节点)的结点的指针都设置为根节点
- 由上述对秩的定义可知,路径压缩不改变秩
下面的实现中使用下列结点类型作为并查集的结点:
/**
* 用于实现并查集中每个结点
*/
class UNION_FIND_NODE<E>
{
public E data; //结点的数据
public UNION_FIND_NODE<E> p; //结点的父亲
public int rank; //结点的秩
public UNION_FIND_NODE(E data,UNION_FIND_NODE<E> p,int rank)
{
this.data = data;
this.p = this;
this.rank = 0;
}
}
MAKE_SET的实现
- 让x自成一个集合(即一棵树),根是其自身,秩为0;
public void MAKE_SET(UNION_FIND_NODE<E> x)
{
x.p = x;
x.rank = 0;
}
FIND_SET的实现
- 如果x是根节点(即x==x.p)那么直接返回x,因为x就是这个集合的代表。
- 如果x不是根节点,递归调用此函数,并在回溯过程(弹栈)中(回溯的每个位置都是从该节点到根节点路径上的结点,并且函数返回的都是根节点,用于使得下一层的结点指向根节点)
public UNION_FIND_NODE<E> FIND_SET(UNION_FIND_NODE<E> x)
{
if(x!=x.p)
x.p = FIND_SET(x.p);
return x.p;
}
UNION的实现
- 采用按秩归并的启发式归并策略
- 首先找到x,y结点的集合代表即其根节点
- 对根节点而言,设为a,b;
- 如果a的秩 > b的秩,则将b集合归并到a集合中。只需要b.p = a
- 如果a的秩 < b的秩,则反过来
- 如果a的秩 == b的秩序,可以将a归并到b中,但此时需要将b的秩加1,以维护上述秩中上界的定义
public void UNION(UNION_FIND_NODE<E> x,UNION_FIND_NODE<E> y)
{
LINK(FIND_SET(x),FIND_SET(y));
}
private void LINK(UNION_FIND_NODE<E> x,UNION_FIND_NODE<E> y)
{
if(x.rank > y.rank)
{
y.p = x;
}
else
{
x.p = y;
if(x.rank == y.rank)
y.rank++;
}
}
- 并查集的平摊分析很复杂的。假设总共m个上述三种操作,而其中包含n个makeset。则可以证明,当同时包含上述三种操作时候,最坏的运行时间为 O ( m α ( n ) ) O(m\alpha(n)) O(mα(n)),其中 α ( n ) \alpha(n) α(n)是一个增长十分缓慢的函数。这个运行时间是超线性的
完整代码
/**
* 用于实现并查集中每个结点
*/
class UNION_FIND_NODE<E>
{
public E data;
public UNION_FIND_NODE<E> p;
public int rank;
public UNION_FIND_NODE(E data,UNION_FIND_NODE<E> p,int rank)
{
this.data = data;
this.p = this;
this.rank = 0;
}
}
class UNION_FIND<E>
{
public void MAKE_SET(UNION_FIND_NODE<E> x)
{
x.p = x;
x.rank = 0;
}
public void UNION(UNION_FIND_NODE<E> x,UNION_FIND_NODE<E> y)
{
LINK(FIND_SET(x),FIND_SET(y));
}
public UNION_FIND_NODE<E> FIND_SET(UNION_FIND_NODE<E> x)
{
if(x!=x.p)
x.p = FIND_SET(x.p);
return x.p;
}
private void LINK(UNION_FIND_NODE<E> x,UNION_FIND_NODE<E> y)
{
if(x.rank > y.rank)
{
y.p = x;
}
else
{
x.p = y;
if(x.rank == y.rank)
y.rank++;
}
}
}
269周赛T4-Find All People With Secret
- 首先对所有的meeting按照time从小到大进行排序
- 对于每个相同的时间
- 只要两个人有联系则将其加入到并查集的一个集合中
- 处理完毕后,第二次遍历这个时间内meeting的所有人物:
- 如果当前遍历到的人物与0号人物处于一个集合中,则继续遍历下一个人物
- 否则将其从当前所属的集合中分离开,即将其重新作为一个单结点作为一颗有根树
- 于是在每个时间段处理完毕后,与0号人物处在一个集合中的所有人物就是当前时间段后知道秘密的所有人物
- 注意这里对于并查集增加了一个分离操作。实际上其实现很简单,只需要令当前结点的父亲p为自己即可。
- 这里的二次遍历是必须的,即必须先要找到特定时间内所聚成一类的人(即有接触的)
/**
* 用于实现并查集中每个结点
*/
class UNION_FIND_NODE<E>
{
public E data;
public UNION_FIND_NODE<E> p;
public int rank;
public UNION_FIND_NODE(E data)
{
this.data = data;
this.p = this;
this.rank = 0;
}
}
class UNION_FIND<E>
{
public void MAKE_SET(UNION_FIND_NODE<E> x)
{
x.p = x;
x.rank = 0;
}
public void UNION(UNION_FIND_NODE<E> x,UNION_FIND_NODE<E> y)
{
LINK(FIND_SET(x),FIND_SET(y));
}
public UNION_FIND_NODE<E> FIND_SET(UNION_FIND_NODE<E> x)
{
if(x!=x.p)
x.p = FIND_SET(x.p);
return x.p;
}
private void LINK(UNION_FIND_NODE<E> x,UNION_FIND_NODE<E> y)
{
if(x.rank > y.rank)
{
y.p = x;
}
else
{
x.p = y;
if(x.rank == y.rank)
y.rank++;
}
}
}
class Solution {
public List<Integer> findAllPeople(int n, int[][] meetings, int firstPerson) {
Arrays.sort(meetings, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return Integer.compare(o1[2],o2[2]);
}
});
int i = 0;
UNION_FIND<Integer> union_find = new UNION_FIND<>();
UNION_FIND_NODE<Integer>[] nodes = new UNION_FIND_NODE[n];
for(int j = 0;j<n;j++)
{
nodes[j] = new UNION_FIND_NODE<Integer>(j);
union_find.MAKE_SET(nodes[j]);
}
union_find.UNION(nodes[0],nodes[firstPerson]);
while(i < meetings.length)
{
int j = i + 1;
while(j<meetings.length && meetings[j][2] == meetings[i][2])
j++;
for(int a = i;a < j;a++)
{
union_find.UNION(nodes[meetings[a][0]],nodes[meetings[a][1]]);
}
for(int a = i;a < j;a++)
{
if(union_find.FIND_SET(nodes[meetings[a][0]]) != union_find.FIND_SET(nodes[0]))
nodes[meetings[a][0]].p = nodes[meetings[a][0]];
if(union_find.FIND_SET(nodes[meetings[a][1]]) != union_find.FIND_SET(nodes[0]))
nodes[meetings[a][1]].p = nodes[meetings[a][1]];
}
i = j;
}
List<Integer> ans = new ArrayList<>();
for(i = 0;i<n;i++)
if(union_find.FIND_SET(nodes[0]) == union_find.FIND_SET(nodes[i]))
{
ans.add(i);
}
return ans;
}
}