0x41 并查集

0x41 并查集

并查集(Disjoint-set)是一种可以动态维护若干个不重叠的集合,并支持查询和合并的数据结构。详细来说,并查集包括一下两种基本操作:

1.Get,查询元素属于哪个集合。

2.Merge,把两个集合合并成一个大集合。

首先定义集合的表示方法,在并查集中,我们采用“代表元”法,即为每个集合选择一个固定的元素,作为整个集合的“代表”。

其次我们需要定义归属关系的表示方法。第一种思路用数组 f [ x ] f[x] f[x]保存元素 x x x所在集合的“代表”。这种方式可以快速查询元素的归属集合,但在合并时需要修改大量元素的 f f f值,效率很低。第二种思路是使用一个树形结构储存每个集合,树上的每个节点是一个元素,树根是集合的代表元素。整个并查集本质上是一个森林(若干棵树)。我们可以维护一个数组 f a fa fa来记录这个森林,用 f a [ x ] fa[x] fa[x]来保存 x x x的根节点。特别地,令树根的 f a fa fa为自己。这样一来合并两个集合时,只需要连接两个树根(令其中一个树根为另一个树根的子节点,即 f a [ r o o t 1 ] = r o o t 2 fa[root_1]=root_2 fa[root1]=root2)。不过在查询元素归属时,需要从该元素开始通过 f a fa fa存储的值不断递归访问父节点,直至到达树根。为了提高查询效率,我们提出路径压缩和按秩合并两种思想。

1.路径压缩和按秩合并

我们在每次执行Get操作的同时,把访问过的每个节点(也就是所查询的元素的全部祖先)全都指向树根。这种优化方法被称为路径压缩。采用路径压缩的并查集,每次Get操作的均摊时间复杂度是 O ( l o g N ) O(logN) O(logN)

还有一种优化方法是按秩合并。“秩”可以被定义为数的深度(未路径压缩时)。也可以被定义成集合的大小。无论哪种定义,我们都可以把集合的“秩“记录在代表元素,也就是树根上。合并时把秩较小的树根合并到秩较大的树根上。采用按秩合并的并查集,每次Get操作的均摊时间复杂度也是 O ( l o g N ) O(logN) O(logN)

一般来说,我们只使用路径压缩就够了。

int fa[SIZE];
void initial()  //初始化
{
    for(int i=1;i<=SIZE;++i)
        fa[i]=i;
}
int Get(int x)
{
    if(fa[x]==x)
        return x;
   	return fa[x]=Get(fa[x]); //路径压缩,fa直接赋值为代表元素
}
void Merge(int x,int y) //合并x和y所在的两个集合
{
    fa[Get(x)]=Get(y);
}

并查集能够在一张无向图中维持节点的连通性。实际上,并查集擅长动态维护许多具有传递性的关系。所谓传递性,顾名思义,就是A和B有某种关系,B和C也具有这种关系,那么A和C也具有这种关系,比如等于关系。

2.“扩展域”与“边带权”的并查集

种类并查集(扩展域)

种类并查集在普通并查集“亲戚的亲戚也是亲戚”的基础上再进行一些“分类”,但是这个分类呢并不是根据物品的种类来进行分类,而是类似“敌人的敌人是朋友”的分类(并没有说明“朋友的敌人是我的敌人”!要根据具体题目分析)。

种类并查集常规套路:不是开多个或多维并查集数组,而是扩大并查集规模

举个例子:我们要维护朋友和敌人这两个关系,则将普通并查集的规模扩大两倍,原来的 1 ∼ n 1\sim n 1n还是存放朋友关系,但是 n + 1 ∼ 2 n n+1\sim 2n n+12n则是存放敌人关系,然后每次操作都分别维护

朋友的朋友还是朋友,朋友的敌人也是敌人,敌人的敌人也是朋友,具体实现时,有 a a a b b b,我们一般用 a + N a+N a+N b + N b+N b+N,分别表示 a a a b b b的对立,这里就是敌人关系。如果 a a a b b b是朋友 m e r g e ( a , b ) merge(a,b) merge(a,b)表示它们是朋友,放在朋友关系里,再用 a + N a+N a+N b + N b+N b+N表示都是 a a a b b b的敌人,则 a + N a+N a+N b + N b+N b+N是朋友,因为敌人的敌人是朋友, m e r g e ( a + N , b + N ) merge(a+N,b+N) merge(a+N,b+N)如果 a a a b b b是敌人,则 b + N b+N b+N a a a的朋友, a + N a+N a+N b b b的朋友,则 m e r g e ( a , b + N ) merge(a,b+N) merge(a,b+N) m e r g e ( a + N , b ) merge(a+N,b) merge(a+N,b)。这样就完成了关系的维护。

带权并查集(边带权)

并查集实际上是由若干棵树构成的森林,我们可以在数中的每条边上记录一个权值,即维护一个数组 d d d,用 d [ x ] d[x] d[x]保存节点 x x x到父节点 f a [ x ] fa[x] fa[x]之间的边权。在每次路径压缩后,每个访问过的节点都会直接指向树根,如果我们同时更新这些节点的 d d d值,就可以利用路径压缩过程来统计每个节点到树根之间的路径上的一些信息。这就是所谓“边带权”的并查集,也就是带权并查集。

下面给出在路径压缩时维护 d d d值的代码:

int find(int x)
{
    if(fa[x]==x)
        return x;
    int root=find(fa[x]);
    d[x]+=d[fa[x]];
    return	fa[x]=root;
}

合并过程中, x x x y y y的距离为 w w w x x x到根节点 p x px px距离为 d [ x ] d[x] d[x] y y y到根节点 p y py py距离为 d [ y ] d[y] d[y],现在把 p x px px的根节点设为 p y py py,则从 x x x y y y再到 p y py py的距离应该等于 x x x p x px px再到 p y py py的距离,所以 d [ x ] + d [ p x ] = w + d [ y ] d[x]+d[px]=w+d[y] d[x]+d[px]=w+d[y],所以更新后 d [ p x ] = − d [ x ] + d [ y ] + w d[px]=-d[x]+d[y]+w d[px]=d[x]+d[y]+w

给出合并过程中的代码:

void merge(int x,int y,int w)
{
    int px=find(x),py=find(y);
    if(px!=py)
    {
        fa[px]=py;
        d[px]=-d[x]+d[y]+w;
    }
}

带权并查集,适合对于数值大小关系的维护,比如现在有标为 1 ∼ N 1\sim N 1N N N N个未知数,假设标号为 a a a的未知数比标号为 b b b的未知数大 x x x,就可以 m e r g e ( a , b , x ) merge(a,b,x) merge(a,b,x),将其进行维护。只要这样的维护过程中不会出现冲突即find(a)==find(b)&&d[a]-d[b]!=x ​,就能找到至少一组对于 N N N个未知数的解。

你可能感兴趣的:(#,0x40,数据结构进阶,算法,c++)