


自认为不能将本题的各种细节和注意点讲得尽善尽美,因此将一位dalao的题解拖了过来,我的任务就是将代码写好,并稍微更改一下排版,使之阅读体验更佳。
题意:
有 T 条指令,指令格式分为以下两种:
M i j:让第 i 号战舰所在列的全部战舰保持原有顺序,接在第 j 号战舰所在列的尾部。(_union)
C i j:询问第 i 号战舰与第 j 号战舰当前是否处于同一列中(if(find(i)==find(j))),如果在同一列中,它们之间间隔了多少艘战舰。如果不在同一列,则输出“-1”。
思路:
以前我们使用并查集都只是用来维护元素是否在一个集合中,通过这一题才了解到并查集的功能强大之处在于充分利用“边”。如果只用并查集来维护元素是否在一个集合内,其实根本没用到每个集合(树)里的边。学会充分利用边之后,并查集就会变成一个更强大的数据结构。
对于这一题,我们使用了边,规定每个节点与父节点的边的边权为1。这样节点路径上的边权之和就为树根到当前节点的距离。
那么,如果执行第二条指令查询i、j,若属于同一集合只需输出abs((x到树根的距离) - (y到树根的距离))-1即可。
那么令数组d[x]表示x到父节点p[x]的距离,在查询也就是找树根的过程中,顺带将d[]更新。
一、查询操作:find
int find(int x)
{
if(p[x]!=x)
{
int root = find(p[x]);
d[x]+=d[p[x]];//用dfs思想理解,先达到最底层,然后回溯时更新
p[x] = root;//路径压缩
}
return p[x];
}
这样,就可以保证每次用到d[x]时,d[x]表示为x到树根的距离。
这里有个易错点,就是有同学可能会先更新
d[x],再p[x]=find(p[x])。这样是错的。
因为显然我们要像搜索一样先递归到最底层,然后回溯的时候更新。如果像你那样写,父节点还没更新呢,你就用父节点的信息更新子节点,显然就错了嘛。
至此,查询操作已经完成了。
接下来就是:二、合并操作:_union
假设你将a集合接到b集合后边,实际上就是两棵树相接,思考一下,合并操作对这两棵树有什么影响。
对于
a这棵树:每一个节点的d[]都应该加上b这棵树的大小siz[pb],因为是将a接在b屁股后面嘛。
对于b这棵树:节点的d[]不变,只不过合并后它的大小siz[pb]要加上a的大小siz[pa]。
(pa = find(a), pb = find(b))
我们可以写出如下代码:
void _union(int a, int b)
{
int pa = find(a), pb = find(b);
if(pa!=pb)
{
d[pa] = siz[pb];
siz[pb]+=siz[pa];
p[pa] = pb;
}
}
关于这个代码:
①我们发现之前都没提到siz数组,这很好办,在上面_union函数之中的find查询时跟d[]一起维护更新即可。
②强调一下_union函数里对于d[pa]的更新:d[pa] = siz[pb];
其实,这是一个懒标记。
按理说,a里头每一个节点的d[]都应该加上siz[pb](别忘了现在是a树合并到b上,结合d[]的定义很容易得出这个结论),那都要遍历更新一遍的话复杂度不就退化成O(n)了嘛。那么我只需更新根节点的信息。然后在查询的时候,刚才没有被更新的节点再用根节点的信息来更新自己。
这样就可以保证用到才更新,相当于线段树里的懒标记。
时间复杂度:
并查集操作接近 O(1)
总时间复杂度O(nlogn)
代码:
#include<bits/stdc++.h>
using namespace std;
int t;
const int N = 3e4+10;
int p[N], d[N], siz[N];
void init(int n)
{
for(int i=1;i<n;++i) p[i] = i, siz[i] = 1;
}
int find(int x)
{
if(p[x]!=x)
{
int root = find(p[x]);
d[x]+=d[p[x]];
p[x] = root;
}
return p[x];
}
void _union(int a, int b)
{
int pa = find(a), pb = find(b);
if(pa!=pb)
{
d[pa] = siz[pb];
siz[pb]+=siz[pa];
p[pa] = pb;
}
}
int main()
{
cin>>t;
init(N);
while(t--)
{
char op[2]; int ii, jj;
scanf("%s%d%d",op, &ii, &jj);
if(op[0]=='M') _union(ii, jj);
else
{
int pii = find(ii), pjj = find(jj);
if(pii==pjj) printf("%d\n", max(0, abs(d[ii] - d[jj])-1));
else printf("-1\n");
}
}
return 0;
}

这篇博客详细介绍了如何使用并查集解决一种特殊的战舰排列问题,涉及并查集的查询和合并操作。作者指出并查集的强大之处在于能够充分利用“边”,在查询和合并过程中维护节点间距离信息。文章给出了查询和合并操作的实现代码,并解释了代码中关键步骤的意义,如路径压缩和懒标记策略,确保操作高效。最后,提供了完整的C++代码实现。
&spm=1001.2101.3001.5002&articleId=122883115&d=1&t=3&u=9d47955ceedf4e9193fe9f282609b4e3)
3041

被折叠的 条评论
为什么被折叠?



