并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。集就是让每个元素构成一个单元素的集合,也就是按一定顺序将属于同一组的元素所在的集合合并。
- Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集合。
- Union:将两个子集合并成同一个集合。
其实判断一个图是否存在环已经有相应的算法,此文用并查集来判断一个图是否有环。
我们可以用一个一维数组parent[] 来记录子集合。
看下面这个图:
0
| | 1-----2
对每一条边的两个顶点加入集合,发现两个相同的顶点在一个子集合中,就说明存在环。
初始化:parent[n] 的每个元素都为-1,共有n个子集合,表示集合只有当前顶点一个元素。
0 1 2
-1 -1 -1
然后逐个处理每条边。
边0-1:我们找到两个子集合 0 和1,因为他们在不同的子集合,现在需要合并他们(Union). 把其中一个子集合作为对方的父集合.
0 1 2 <----- 1 成为 0 的 父集合 (1 现在代表集合 {0, 1})
1 -1 -1
边0-2:1属于属于子集合1,2属于子集合2,因此合并他们。
0 1 2 <----- 2 作为 1的父集合 (2 现在代表集合 {0, 1, 2})
1 2 -1
边0-2: 0是在子集和2和2也是在子集合2, 因为 0->1->2 // 1 是0 父集合 并且 2 是1的父集合 。因此,找到了环。
代码:
// 用并查集判断是否存在环
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 图中的边
struct Edge
{
int src, dest;
};
// 图结构体
struct Graph
{
// V-> 顶点个数, E-> 边的个数
int V, E;
// 每个图就是 边的集合
struct Edge* edge;
};
// 创建一个图
struct Graph* createGraph(int V, int E)
{
struct Graph* graph = (struct Graph*) malloc( sizeof(struct Graph) );
graph->V = V;
graph->E = E;
graph->edge = (struct Edge*) malloc( graph->E * sizeof( struct Edge ) );
return graph;
}
// 查找元素i 所在的集合( 根 )
int find(int parent[], int i)
{
if (parent[i] == -1)
return i;
return find(parent, parent[i]);
}
// 合并两个集合
void Union(int parent[], int x, int y)
{
int xset = find(parent, x);
int yset = find(parent, y);
parent[xset] = yset;
}
// 检测环
int isCycle( struct Graph* graph )
{
int *parent = (int*) malloc( graph->V * sizeof(int) );
// 初始化所有集合
memset(parent, -1, sizeof(int) * graph->V);
// 遍历所有边
for(int i = 0; i < graph->E; ++i)
{
int x = find(parent, graph->edge[i].src);
int y = find(parent, graph->edge[i].dest);
if (x == y) //如果在一个集合,就找到了环
return 1;
Union(parent, x, y);
}
return 0;
}
// 测试
int main()
{
/* 创建一些的图
0
| | 1-----2 */
struct Graph* graph = createGraph(3, 3);
// 添加边 0-1
graph->edge[0].src = 0;
graph->edge[0].dest = 1;
// 添加边 1-2
graph->edge[1].src = 1;
graph->edge[1].dest = 2;
// 添加边 0-2
graph->edge[2].src = 0;
graph->edge[2].dest = 2;
if (isCycle(graph))
printf( "Graph contains cycle" );
else
printf( "Graph doesn‘t contain cycle" );
return 0;
}
并查集(2)-按秩合并和路径压缩
在上面一讲是并查集(1)-判断无向图是否存在环. 我们使用了并查集的两个操作: union() 和 find()
// find 的原始实现
int find(int parent[], int i)
{
if (parent[i] == -1)
return i;
return find(parent, parent[i]);
}
// union()的原始实现
void Union(int parent[], int x, int y)
{
int xset = find(parent, x);
int yset = find(parent, y);
parent[xset] = yset;
}
上述union()和find()是直接的,最坏的情况下的时间复杂度是线性的。表示子集的树可能会倾斜,像一个链表。下面是一个例子最坏情况的场景。
假设有4个元素 0, 1, 2, 3
初始化
0 1 2 3
Do Union(0, 1)
1 2 3
/
0
Do Union(1, 2)
2 3
/
1
/
0
Do Union(2, 3)
3
/
2
/
1
/
0
以上操作可以优化到O(Log n)在最糟糕的情况下。方法就是在每次合并都把深度较小的集合合并在深度较大的集合下面 。这种技术被称为按秩合并。这样可以防止树的退化,最坏情况不会出现。
继续用上面的例子
0 1 2 3
Do Union(0, 1)
1 2 3
/
0
Do Union(1, 2)
1 3
/ 0 2
Do Union(2, 3)
1
/ | 0 2 3
第二个要优化的就是find(). 路径压缩实际上是在找完根结点之后,在递归回来的时候顺便把路径上元素的父亲指针都指向根结点。
假设集合{0, 1, .. 9} 的树表示如下所示: 当调用 find(3)时
9
/ | \
4 5 6
/ \ / 0 3 7 8
/ 1 2
我们向上查找,找到是这个集合的根节点. 路径压缩就是:直接把 3的 父节点 设置为 9
当下次再查找 1, 2 或 3 时,查找的路径就会变短。
9
/ / \ 4 5 6 3
/ / \ / 0 7 8 1 2
这两个优化方法互为补充。每个操作的时间复杂度比O(Logn)要小。事实上,摊销时间复杂度实际上变成了小的常数。
// 用并查集判断是否存在环
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//图中的边
struct Edge
{
int dest,src;
};
//图结构体
struct Graph
{
// V-> 顶点个数, E-> 边的个数
int V,E;
// 每个图就是 边的集合
struct Edge *edge;
};
struct subset
{
int parent;
int rank;
};
//创建一个图
struct Graph * createGraph(int V, int E)
{
struct Graph *graph=(struct Graph *)malloc(sizeof(struct Graph));
graph->V=V;
graph->E=E;
graph->edge=(struct Edge *)malloc(graph->E*sizeof(struct Edge));
return graph;
}
//使用路径压缩
int find(struct subset subsets[],int i)
{
// 找到 root并使 root 作为 i 的父节点
if(subsets[i].parent==i)
return i;
else
return (subsets[i].parent=find(subsets,subsets[i].parent));
}
//使用按秩合并
void Union(struct subset subsets[], int x, int y)
{
int xroot=find(subsets,x);
int yroot=find(subsets,y);
// 将深度较小的集合 合并到深度大的集合下面
if(subsets[xroot].rank<subsets[yroot].rank)
subsets[xroot].parent=yroot;
else if(subsets[xroot].rank>subsets[yroot].rank)
subsets[yroot].parent=xroot;
else//深度一样,任选一个,并增加另一个
{
subsets[yroot].parent=xroot;
subsets[xroot].rank++;
}
}
int isCycle(struct Graph* graph)
{
int V=graph->V;
int E=graph->E;
struct subset *subsets=(struct subset *)malloc(V*sizeof(struct subset));
for(int i=0;i<V;i++)
{
subsets[i].parent=i;
subsets[i].rank=0;
}
for(int e=0;e<E;e++)
{
int x=find(subsets,graph->edge[e].dest);
int y=find(subsets,graph->edge[e].src);
if(x==y)
return 1;
Union(subsets,x,y);
}
return 0;
}
int main()
{
int V=3,E=2;
struct Graph *graph=createGraph(V,E);
/* 构造以下的图
0
|
|
1-----2 */
graph->edge[0].src=0;
graph->edge[0].dest=1;
graph->edge[1].src=1;
graph->edge[1].dest=2;
// graph->edge[2].src=0;
//graph->edge[2].dest=3;
if(isCycle(graph))
printf("Graph contains cycle\n");
else
printf("Graph doesn‘t contains cycle\n");
return 0;
}
运行结果如下: