并查集运用
常见的要解决的问题
- 解决图的连通性问题:连通块大小,点与点之间是否连通,Kruskal 最小生成树算法
- 维护数据间的集合关系或逻辑关系
扩展域并查集(种类并查集)
考虑“敌人的敌人是朋友”这一原则,令一个并查集用于查询元素之间的关系,称之为原域,为了记录敌人,再开一个并查集,称之为扩展域。
现在假设
i
i
i 与
j
j
j 敌对,那么根据上述定义,令原域的
i
i
i 与扩展域的
j
j
j 连边,再令原域的
j
j
j 与扩展域的
i
i
i 连边,表达敌对关系。若此时有另一敌对关系
(
j
,
k
)
(j,k)
(j,k) ,那么完成连边后,可以得到下图:
这时我们可以发现,如果我们在原域查询
i
,
k
i,k
i,k ,可以得到友好关系,完成了“敌人的敌人是朋友”的表达。
由此可知:种类并查集可以维护对立关系,而且可以维护多对等价双向的对立关系,可以维护“敌人的敌人是朋友”这一原则。
-
查询祖先:与普通并查集一样,同样可以用路径压缩
-
合并集合:
void join(int x,int y) { //...与普通并查集一样 } ... join(x,y + n);//连两次 join(y,x + n); ...
-
询问是否在同一集合中,如果是,则友好关系,如果不是,则敌对关系:与普通并查集一样
带权并查集
对于“敌人的敌人是朋友”这一原则,我们考虑用模 2 运算表达,那么每次连边合并,我们就要赋予边权。
现在考虑实现以下要求:
- 敌人之间的路径权值模 2 为 1
- 朋友之间的路径权值模 2 为 0
容易想到以下构造:
黑色标号为加边的顺序,红色标号为边的权值,那么这个关系是
r
t
rt
rt 与
f
x
f_x
fx 敌对,
f
x
f_x
fx 与
x
x
x 敌对,而
r
t
rt
rt 与
x
x
x 友好,同时它们之间的路径权值和模 2 为 0.
现在我们定义
v
a
l
(
x
)
val(x)
val(x) 表示
x
x
x 到父亲结点的路径上的权值和。
考虑能否在带权并查集上实现路径压缩
可以实现路径压缩,但是路径压缩的过程中,被压缩掉的边的权值和要记录下来,并与结点到父结点的边权一同组成新边权,这条新边从这个结点到根结点。
v
a
l
′
(
x
)
=
v
a
l
(
x
)
+
S
val'(x) = val(x) + S
val′(x)=val(x)+S
其中
S
S
S 可以在递归的过程中计算出,并最后赋值在
v
a
l
′
(
f
x
)
val'(f_x)
val′(fx)
代码如下:
int findx(int x)
{
int tmp = fa[x];
if(tmp != x)
{
fa[x] = findx(fa[x]);
val[x] = (val[x] + val[tmp])%2'
return fa[x];
}
else return tmp;
}
考虑如何合并两点所在的两个不同的集合
合并两点所在的两个不同集合,关键在于修改这两个集合的根之间的边的权值,即知道
x
x
x 与
f
x
f_x
fx 之间的关系,知道
y
y
y 与
f
y
f_y
fy 之间的关系,知道
x
x
x 与
y
y
y 的关系,求
f
x
f_x
fx 与
f
y
f_y
fy 之间的关系。
现在我们压缩路径之后,得到
v
a
l
(
x
)
val(x)
val(x) 表示
x
x
x 到根结点的边权和。
所以在合并两个结合的时候,要修改
x
x
x 的根结点
f
x
f_x
fx 到
f
y
f_y
fy 的权值,令权值
v
a
l
(
f
x
)
=
v
a
l
(
y
)
−
v
a
l
(
x
)
+
r
val(f_x) = val(y) - val(x) + r
val(fx)=val(y)−val(x)+r
void join(int x,int y)
{
int rx = findx(x);int ry = findx(y);
fa[rx] = ry;
val[rx] = (((val[y] - val[x])%2 + 2)%2 + 1) % 2;
return ;
}
考虑如何计算两点间的权值
一般来说,只有当两点在同一连通块中才可以计算两点间的权值,那么令 x x x 到根结点的权值和为 v a l ( x ) val(x) val(x) ,令 y y y 到根结点的权值和为 v a l ( y ) val(y) val(y),同样观察上图的情况,可以得到 r = v a l ( x ) − v a l ( y ) r = val(x) - val(y) r=val(x)−val(y)
综上,我们得到了带权并查集的基本操作,于是就可以运用它来解题。
小结
带权并查集和扩展域并查集均可以表达“敌人的敌人是朋友这一原则”,事实上,它们都可以表达两种及以上的关系,可以参考[NOI2001]食物链,这题维护了三对对立关系。通过稍加改编,我们可以利用这些新手段解决更为灵活的题目。
集合关系与逻辑关系问题
比较常见的划分集合思想是:
- 与我的朋友是朋友的人是我的朋友
- 与我敌对的人有敌对关系的人是我的朋友
逻辑关系
传递性:如果某种逻辑存在传递性,那么满足这种逻辑的元素应该被划分为同一个集合
对立性:如果某种逻辑存在对立性,那么满足这种逻辑的元素不应该被划分为同一个集合(注意:是不应该被划分为一个集合,而不是应该被划分为另一集合)
P1892 [BOI2003]团伙
直接运用上述划分集合的思想即可
P1525 [NOIP2010 提高组] 关押罪犯
题解,涉及一题多解
P1955 [NOI2015] 程序自动分析
= = = 具有传递性, ≠ \neq = 使元素间对立。
若 x i = x j x_i=x_j xi=xj ,则将 i , j i,j i,j 划入同一集合中。
若 x i ≠ x j x_i\neq x_j xi=xj ,则判断 i , j i,j i,j 是否在同一集合中,如果在,那么就矛盾,如果不在,就不矛盾。
P4047 [JSOI2010]部落划分
本题划分集合的标准是居住点之间的距离,看到求最大最近距离,与规定划分 k k k 个部落,考虑二分答案。
思路:二分最大最近距离,检查两两居住点之间是否小于这个距离,如果小于这个距离,这两个居住点就要划分为同一个部落,划分完毕后检查划分了多少个部落,如果划分多了,就意味着二分的答案小了,如果划分少了,就意味着二分的答案大了。
出现的问题:
如果直接令 l = min d i s ( i , j ) , r = max d i s ( i , j ) l = \min dis(i,j),r = \max dis(i,j) l=mindis(i,j),r=maxdis(i,j) 会导致无法使 m i d mid mid 取得精确答案值,二分进入死循环,我们发现答案一定是某两个居住点之间的距离,于是预处理出所有居住点间的距离,排序后在这些距离中寻找答案。
居住点间距离与二分的答案相等,这时考虑求出如果距离与二分答案相等,那么全部不划分为同一部落所得到的部落个数 n 1 n_1 n1 ,再记录如果距离与二分答案相等,全部划分为同一部落所得到的部落个数 n 2 n_2 n2 ,查看是否满足 n 2 ⩽ k ⩽ n 1 n_2\leqslant k \leqslant n_1 n2⩽k⩽n1 即可。
时间复杂度 O ( n 2 ) O(n^2) O(n2) 可以通过本题。
for(int i = 1;i <= n;i ++)
for(int j = 1;j < i;j ++)
{
D[i][j] = D[j][i] = dis(i,j);
tot ++;dd[tot] = D[i][j];//计算全部距离
}
sort(dd + 1,dd + tot + 1);//排序
for(int i = 1;i <= tot;i ++)
{
if(dd[i] != dd[i - 1])
{
ntot ++;
ndd[ntot] = dd[i];
}
}
for(int i = ntot + 1;i <= tot;i ++) dd[i] = 0.000;
for(int i = 1;i <= ntot;i ++) dd[i] = ndd[i];
tot = ntot;
l = 1;r = tot;
for(;l <= r;)//二分答案
{
mid = (l + r)/2;
cnt1 = 0;cnt2 = 0;
for(int i = 1;i <= n;i ++) fa[i] = i;
for(int i = 1;i <= n;i ++)
for(int j = i + 1;j <= n;j ++)
if(D[i][j] < dd[mid] && judge(i,j) == 0) join(i,j);//距离等于答案的一律不划分为同一部落
for(int i = 1;i <= n;i ++) vis[i] = 0,fa[i] = findx(i);
for(int i = 1;i <= n;i ++) if(!vis[fa[i]]) {vis[fa[i]] = 1,cnt1 ++;}
for(int i = 1;i <= n;i ++) fa[i] = i;
for(int i = 1;i <= n;i ++)
for(int j = i + 1;j <= n;j ++)
if(D[i][j] <= dd[mid] && judge(i,j) == 0) join(i,j);//距离等于答案的一律划分为同一部落
for(int i = 1;i <= n;i ++) vis[i] = 0,fa[i] = findx(i);
for(int i = 1;i <= n;i ++) if(!vis[fa[i]]) {vis[fa[i]] = 1,cnt2 ++;}
if(cnt1 >= k && k >= cnt2)
{
l = mid + 1;
ans = max(ans,dd[mid]);
}
else
{
if(cnt2 > k) l = mid + 1;
else r = mid - 1;
}
}
printf("%.2lf",ans);
图论问题
P4185 [USACO18JAN]MooTube G
抓住题目中视频间相关性的定义
将任意一对视频的相关性定义为沿此路径的任何连接的最小相关性
也就相当于给出一个 K K K ,从 v i v_i vi 开始遍历,一旦碰到 r < K r < K r<K 的边就停止遍历并回溯,问 v i v_i vi 最多可以到达多少个不同结点。
关注到碰到 r < K r <K r<K 的边就停止遍历,又关注到有多次询问,那么就可以将每次询问视为对一棵无根树,删去 r < K r<K r<K 的所有边,问 v i v_i vi 所在连通块的大小,然后我们将目光放到连通块上来。
考虑离线操作,将所有询问记录下来,按 K K K 从大到小排序,又将边按 r r r 从大到小排序,于是我们就将删边操作视为为加边操作,对于每次询问,我们往图中新增 r ⩾ K i r \geqslant K_i r⩾Ki 的边,然后回答这些询问,时间复杂度 O ( Q α ( n ) ) O(Q\alpha(n)) O(Qα(n))
...
sort(e + 1,e + 1 + cnt,cmp2);//给边排序
for(int i = 1;i <= N;i ++) fa[i] = i,siz[i] = 1;
for(int qq = 1;qq <= Q;qq ++)
scanf("%d%d",&b[qq].K,&b[qq].p),b[qq].id = qq;//记录所有询问
sort(b + 1,b + 1 + Q,cmp);openi = 1;//按 K 排序
for(int t = 1;t <= Q;t ++)//离线回答
{
if(b[t].K != openK)
{
openK = b[t].K;
for(;e[openi].val >= openK;openi ++)//删边操作转为加边操作
{
if(!judge(e[openi].from,e[openi].to))
join(e[openi].from,e[openi].to);
if(e[openi + 1].val < openK) break;
}
}
b[t].ans = siz[findx(b[t].p)] - 1;//O(α(n))回答询问
}
sort(b + 1,b + 1 + Q,cmp3);
for(int i = 1;i <= Q;i ++)
printf("%d\n",b[i].ans);//输出答案
...
P1197 [JSOI2008]星球大战
问题问按一定顺序删除一些点和与这个点相连的边后,还有多少连通块。
同上一题一样,朴素并查集不支持删边操作,那么我们考虑离线操作,将删边操作改为加边操作,删点操作改为加点操作。
然后再知道一个结论:两个连通块合并后,连通块数目减 1。
然后这道题就做完了。
for(int i = 1;i <= k;i ++) scanf("%d",&a[i]),a[i] ++,ban[a[i]] = 1;
for(int i = 1;i <= n;i ++) fa[i] = i;
for(int i = 1;i <= n;i ++)
{
if(ban[i] == 0)
{
tot ++;
for(int j = head[i];j;j = e[j].nxt)
if(ban[e[j].to] == 0 && (!judge(i,e[j].to)))
{
join(e[j].to,i);
tot --;//两个连通块合并,数目减1
}
}
}
acnt ++;ans[acnt] = tot;
for(int i = k;i >= 1;i --)
{
tot ++;ban[a[i]] = 0;
for(int j = head[a[i]];j;j = e[j].nxt)
if(ban[e[j].to] == 0 && (!judge(a[i],e[j].to)))
{
join(e[j].to,a[i]);
tot --;
}
acnt ++;
ans[acnt] = tot;
}
for(int i = acnt;i >= 1;i --) printf("%d\n",ans[i]);