并查集中的启发式合并

算法原理

并查集一般有两种方法来保持复杂度不退化,一种是路径压缩,另一种则是按照秩来做启发式合并。

一般情况下我们都是用第一种,压缩路径通过递推找到祖先节点后,在回溯时将它的子孙节点都直接指向祖先,这样以后每次调用Find( )函数找父亲时复杂度就变成了O(1)。但是路径压缩时直接将节点的父亲修改成最终的祖先节点,在破坏原先的树结构的同时,在有些题目中也会损失信息。而不使用压缩路径,直接用暴力并查集又容易超时。

所以我们考虑用启发式合并的方法来保持树的形态,那么如何控制并查集的复杂度呢?

因为并查集是一种树型结构,对于以每个节点为根节点的子树都有一个深度,如果把一棵深度大的树的根节点接在了一棵深度小的树上,因为是直接把根节点接在另一个的根节点上,所以整棵树的深度为那一棵深度大的树的深度加一。而如果把一棵深度小的树的根节点接在了一棵深度大的树上,可直接接上,不影响深度。如果两个数深度一样,则将接完后的树的深度加一即可。所以考虑每次都将深度小的树接在深度大的树上,这就是启发式合并的原理。虽然没有压缩路径,但是按秩合并可以保证树高是O(logn),这样找到树根是O(logn),路径查询也是O(logn)。

实现代码


int fu[maxn]//存放父节点
int deep[maxn];//记录深度

int findx(int x)//启发式合并不压缩路径,保持树结构
{
    if(fu[x] == x) return x;
    return findx(fu[x]);
}

void join(int x, int y,int k) //按照秩来做启发式合并
{
    int fx = findx(x);
    int fy = findx(y);
    if(fx==fy) return;
    if(deep[fx]>deep[fy])//深度小的树接在深度大的树上
        swap(fx,fy);
    fu[fx] = fy;
    if(deep[fx]==deep[fy]) deep[fy]++;
}

 

记录一道例题

BNUOJ 51275 道路修建 Large && FJUT1961

思路:

考虑到并查集实际上是一棵树,所以可以在边上维护一些信息,假设k时刻(第k次操作)将u和v连通,我们记录下该路径,路径上的权值则为k,由于如果两个顶点联通, 则两个顶点间只有唯一路径,这样查询边时可以获取连通时间。题中由于边较难保存,这里用点保存,如果一个点被接到另一个点上时,则记录该点的连通的时刻k,一个点可能被其他点接多次,但只可能接在其他点上一次。

查询时使u和v都向上找到LCA(最近公共祖先),然后取LCA到u和LCA到v这两条链上的合并时间的最大值,因为越靠近根节点的边越晚合并,所以只要看LCA连着的两条边的时间戳的最大值即可。

AC代码如下:

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;

const int maxn=100010;

int fu[maxn],n,m,lastans,sum;//sum记录当前连通块
int path[maxn];//记录联通路权k,表示在k时刻连通
int deep[maxn];//按照秩来做启发式合并
int vis[maxn];//每次查询记录到该节点路权,即时间

void init()
{
    for(int i = 0; i <= n; i++ )
    {
        fu[i] = i;
        deep[i]=path[i]=0;
        vis[i]=-1;
    }
    lastans=0;
    sum=n;
}

int findx(int x)//不压缩路径
{
    if(fu[x] == x) return x;
    return findx(fu[x]);
}

void join(int x, int y,int k) {
    int fx = findx(x);
    int fy = findx(y);
    if(fx==fy) return;
    if(deep[fx]>deep[fy])//按照秩来做启发式合并,小树接在大树上
        swap(fx,fy);
    fu[fx] = fy;
    path[fx]=k;
    if(deep[fx]==deep[fy]) deep[fy]++;
    sum--;//合并后连通块减一
}

int same(int x, int y) {
    if(findx(x) != findx(y))
        return 0;
    int nowmax=0,tx=x,ty=y,ans;
    while(1)//先用vis记录其中一个节点到其祖先路径上各节点的时刻
    {
        vis[tx]=nowmax;
        if(tx==fu[tx]) break;
        nowmax=max(nowmax,path[tx]);
        tx=fu[tx];
    }
    nowmax=0;
    while(1)//从另一个节点开始向其祖先遍历,知道找到有效的vis节点
    {
        if(vis[ty]!=-1) {//非初始值,说明该节点已与另一个节点连通,即找到了连通时刻
            nowmax=max(nowmax,vis[ty]);
            break;
        }
        nowmax=max(nowmax,path[ty]);
        ty=fu[ty];
    }
    tx=x;
    while(1)//重新初始化
    {
        vis[tx]=-1;
        if(tx==fu[tx]) break;
        tx=fu[tx];
    }
    return nowmax;
}

int main()
{
    int t,op,u,v;
    scanf("%d",&t);
    while(t--)
    {
        scanf("%d%d",&n,&m);
        init();
        for(int k=1;k<=m;k++)
        {
            scanf("%d%d%d",&op,&u,&v);
            u^=lastans;
            v^=lastans;
            //printf("u=%d v=%d\n",u,v);
            if(op==0)
            {
                join(u,v,k);
                printf("%d\n",lastans=sum);
            }
            else
            {
                printf("%d\n",lastans=same(u,v));
            }
        }
    }
}

猜你喜欢

转载自blog.csdn.net/chengdong996/article/details/81478660