并查集一般有两种方法来保持复杂度不退化,一种是路径压缩,另一种则是按照秩来做启发式合并。
一般情况下我们都是用第一种,压缩路径通过递推找到祖先节点后,在回溯时将它的子孙节点都直接指向祖先,这样以后每次调用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
#include
#include
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));
}
}
}
}