并查集小记
并查集是一种可以动态维护若干个不重叠的集合,并支持合并和查询的数据结构,详细地说,其有以下两种基本操作:
1)query , 查询某个元素属于那个集合,或者判断某两个元素是否属于同一集合
2)Merge,把两个集合合并成一个大集合。
性质:
1)并查集能够在一张无向图中维护节点间的连通性
2)并查集擅长动态维护许多具有传递性的关系。
注:关于性质2可有道有趣的想法题:https://mp.youkuaiyun.com/mdeditor/85055335#
模板:
#include<bits/stdc++.h>
using namespace std;
const int maxn = 10000+10;
int fa[maxn],Size[maxn];//fa根结点,整个集合的代表,Size每个子集合的大小
int ans;
template <typename T>
T Max(const T &x, const T &y){
return (x > y) ? x : y;
}
int Find(int x){
return (x == fa[x]) ? x : fa[x] = Find(fa[x]);
}
void Unite(int x,int y){
int a = Find(x), b = Find(y);
if(a == b) return ;
if(Size[a] >= Size[b]){
Size[a] += Size[b];
fa[b] = a;
ans = Max(ans,Size[a]);
}
else{
Size[b] += Size[a];
fa[a] = b;
ans = Max(ans,Size[b]);
}
}
int main(){
int n,u,v;
while(~scanf("%d",&n)){
for(int i = 0; i < maxn; ++i){
fa[i] = i, Size[i] = 1;
}
ans = 0;
for(int i = 0; i < n; ++i){
scanf("%d%d",&u,&v);
Unite(u,v);
}
printf("%d\n",ans);
}
return 0;
}
“扩展域” 与 “边带权”的并查集
并查集实际上是由若干棵树构成的森林,我们可以把树中的每一条边记录一个权值,维护一个数组d,用d[x]保存节点x 到 父亲节点fa[x] 之间的边权。在每次路径压缩后,每个访问过的节点都会直接指向树根,如果我们同时更新这些节点的d值,那么就可以利用路径压缩来统计每个节点到树根直接的路径上的信息,这就是所谓的"边带权"的并查集。
题目:P1196
思路:
将"链"看做特殊形态的树,把每一列战舰转换成一个集合,并用并查集去维护,最初N个战舰构成N个独立的集合。
在没有路径压缩的情况下,fa[x]就表示排在第x号战舰前面的那个战舰的编号,一个集合的代表就是位于最前边的战舰。所以不妨令树上每一条边带权值1,这样同一棵树上两点的距离减去1,即同一列舰队两艘战舰间间隔的战舰数量。
考虑路径压缩的情况下,额外开辟一个d数组,d[x]记录战舰x与fa[x]边上的权值,在路径压缩的情况下把x直接指向树根root,同时把d[x]更新为x到root路径上的边权之和。实现的话在Find(x)函数中进行了修改。
对于C x,y 如果两者在同一列,d[x]维护的该列中位于战舰x之前的战舰数量,d[y]也是类似d[x]含义,那么abs(d[x] - d[y])-1就是答案。
对于M x, y, 那么就把x的树根a作为y树根b的子节点,连接新边权值a -> b 权值大小是b集合的大小(根据题意b集合中的全部战舰位于a集合之前),因此合并操作还需要一个额外的size数组记录每一个树根上集合的大小,size数组初始化为1.
AC code:
#include<bits/stdc++.h>
using namespace std;
const int maxn = 3e5 + 100;
template <typename T>
void read(T &x){
x = 0;
char c = getchar();
for(; c < '0' || c > '9'; c = getchar()) ;
for(; c >='0' && c <= '9'; c = getchar())
x = (x << 1) + (x << 3) + (c ^ 48);
}
int fa[maxn],size[maxn],d[maxn];
int Find(int x){
if(x == fa[x]) return x;
int rt = Find(fa[x]); //递归找root
d[x] += d[fa[x]]; //维护d数组,对边权求和
return (fa[x] = rt); //路径压缩
}
void Unite(int x,int y){
int a = Find(x) , b = Find(y);
if(a == b) return ;
fa[a] = b, d[a] = size[b],size[b] += size[a];
}
void query(int x,int y){
int a = Find(x), b = Find(y);
if(a != b) puts("-1");
else{
printf("%d\n",abs(d[y] - d[x]) - 1);
}
}
int main(){
for(int i = 0; i < maxn; ++i){
fa[i] = i, size[i] = 1, d[i] = 0;
}
int T,x,y;
char s[5];
read(T);
while(T--){
scanf("%s",s);
read(x),read(y);
if(s[0] == 'M'){
Unite(x,y);
}
else{
query(x,y);
}
}
return 0;
}
POJ 1733
题意: A 写下了一个0 和 1 组成的序列 S ,序列长度为N, B 向 A 提出 M 个问题,每个问题中B制定两个数l 和 r,A 回答 S[l ~ r] 中 1 的个数是奇数个还是偶数个, B 能会发现 A 在说谎, 例如 A 回答过S[1 - 3] 中有奇数个1,S[4 ~ 6]中有偶数个1,现在又说S[1 ~ 6]中有偶数个1,显然矛盾,现在要求帮助B检查M个问题的答案,并指出A至少回答出对少个问题后B可以判定A在说谎,或者说找出一个最小的k使得存在0 ~ 1序列满足1 ~ k个回答,而不满足k +1 ~ M个回答。
思路:
用sum[] 数组表示序列s的前缀,对于每个回答
1)如果l ~ r中有偶数个1,等价sum[l-1] 与 sum[r] 奇偶性相同
2)如果l ~ r中有奇数个1,等价sum[l-1] 与 sum[r] 奇偶性不同
那么显然有的传递性关系为:
1)若x1 与 x2 奇偶性相同,x2 与 x3 奇偶性相同,那么x1 与 x2 的奇偶性也相同
2) 3)省略,比较显然
首先这题目N比较大,但M比较小,我们可以先对 每个问题的l-1和r进行离散化处理,等价到1~2M以内的范围。
下面有两种思路
第一种边带权并查集思想:
假设边权d[x]为0,表示x和fa[x]奇偶性相同,为1,奇偶性不同;路径压缩时候,d[x]与树根a路径上所有边权做异或运算,即可得到x与树根a的奇偶性关系。
假设每个问题离散化之后l-1 和 r 的值分别为x ,y, ans表示l ~ r中1 的个数是计数1还是偶数0; 首先第一步判定x,y是否在同一集合内,同一个集合内如果d[x] ^ d[y] != ans, 那么A在撒谎;原因: d[x] 代表 x 到根路径异或和, d[y]代表y到根路径异或和,二者相或,到根部分x,y共有异或为0,结果就是x到y路径的异或和。
x 和 y 不在同一个集合时候就将它们合并,设两个集合的树根分别为a , b;那么d[x],d[y]分别表示 x ~ a 和 y ~ b 间边权异或和,合并集合就是把a 变成 y 的子节点,那么就要求出d[a] 即 a ~ y 的边权异或和,线路该路径有 x ~ a, a ~ y, y ~ b组成, x ~ y异或和就是该问题的答案ans , 那么
ans = d[x] ^ d[a] ^ d[y] ; 由异或运算的规则
d[a] = d[x] ^ d[y] ^ ans;
至此问题得以解决。
AC code:
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 2e4 + 100;
template <typename T>
void read(T &x){
x = 0;
char c = getchar();
for(; c < '0' || c > '9'; c = getchar()) ;
for(; c >='0' && c <= '9'; c = getchar())
x = (x << 1) + (x << 3) + (c ^ 48);
}
struct{
int l,r,ans;
}q[10050];
int fa[maxn],d[maxn];
int a[maxn],n,m,tot;
int Find(int x){
if(x == fa[x]) return x;
int rt = Find(fa[x]);
d[x] ^= d[fa[x]];
return fa[x] = rt;
}
void read_discrete(){
read(n),read(m);
tot = 0;
char s[10];
for(int i = 1; i <= m; ++i){
read(q[i].l),read(q[i].r);
scanf("%s",s);
q[i].ans = (s[0] == 'e')? 0 : 1;
a[++tot] = q[i].l - 1;
a[++tot] = q[i].r;
}
sort(a + 1,a + tot + 1);
n = unique(a + 1, a + tot + 1) - a - 1;
}
int main(){
read_discrete();
for(int i = 0; i <= n; ++i) fa[i] = i,d[i] = 0;
for(int i = 1; i <= m; ++i){
int x = lower_bound(a + 1, a + n + 1, q[i].l - 1) - a;
int y = lower_bound(a + 1, a + n + 1, q[i].r) - a;
int a = Find(x), b = Find(y);
if(a == b){
if((d[x] ^ d[y]) != q[i].ans){
printf("%d\n",i - 1);
return 0;
}
}
else{
fa[a] = b;
d[a] = d[x] ^ d[y] ^ q[i].ans;
}
}
printf("%d\n",m);
return 0;
}
第二种思路是使用“扩展域的”并查集:
其实就是2-sat 建图的思想,首先把每个变量x 拆成两个节点 xodd,xeven,其中xodd表示sum[x]是奇数。 这两个节点就是x的奇数域和偶数域。
仍然假设每个问题离散化之后l-1 和 r 的值分别为x ,y, ans表示l ~ r中1 的个数是计数1还是偶数0;
由l~r的回答ans我们可以做以下推断:
1)ans = 0, 那么 代表 x 和 y 的奇偶性相同,那么合并xodd 和 yodd ,xeven 和 yeven,代表“x是奇数” 和 “y是奇数” 或者 “x是偶数” 和 “y是偶数” 可以互推,它们等价。
2)ans = 1, 同上道理可知该合并是xodd 与 yeven, xeven 和 yodd;
AC code:
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 2e4 + 100;
template <typename T>
void read(T &x){
x = 0;
char c = getchar();
for(; c < '0' || c > '9'; c = getchar()) ;
for(; c >='0' && c <= '9'; c = getchar())
x = (x << 1) + (x << 3) + (c ^ 48);
}
struct{
int l,r,ans;
}q[10050];
int fa[maxn];
int a[maxn],n,m,tot;
int Find(int x){
if(x == fa[x]) return x;
return fa[x] = Find(fa[x]);
}
void read_discrete(){
read(n),read(m);
tot = 0;
char s[10];
for(int i = 1; i <= m; ++i){
read(q[i].l),read(q[i].r);
scanf("%s",s);
q[i].ans = (s[0] == 'o') ? 1 : 0;
a[++tot] = q[i].l - 1;
a[++tot] = q[i].r;
}
sort(a + 1,a + tot + 1);
n = unique(a + 1, a + tot + 1) - a - 1; //注意多减去1!!!
}
int main(){
read_discrete();
int up = (n << 1);
for(int i = 1; i <= up; ++i) fa[i] = i;
for(int i = 1; i <= m; ++i){
int x = lower_bound(a + 1, a + n + 1, q[i].l - 1) - a;
int y = lower_bound(a + 1, a + n + 1, q[i].r) - a;
int xo = x, xe = x + n;
int yo = y, ye = y + n;
if(!q[i].ans){
if(Find(xo) == Find(ye)){
cout << i - 1 <<endl;
return 0;
}
fa[Find(xo)] = Find(yo);
fa[Find(xe)] = Find(ye);
}
else{
if(Find(xo) == Find(yo)){
cout << i - 1 << endl;
return 0;
}
fa[Find(xo)] = Find(ye);
fa[Find(xe)] = Find(yo);
}
}
cout<< m << endl;
return 0;
}
POJ 1182 食物链
题目:
动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B, B吃C,C吃A。
现有N个动物,以1-N编号。每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这N个动物所构成的食物链关系进行描述:
第一种说法是"1 X Y",表示X和Y是同类。
第二种说法是"2 X Y",表示X吃Y。
此人对N个动物,用上述两种说法,一句接一句地说出K句话,这K句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
1) 当前的话与前面的某些真的话冲突,就是假话;
2) 当前的话中X或Y比N大,就是假话;
3) 当前的话表示X吃X,就是假话。
你的任务是根据给定的N(1 <= N <= 50,000)和K句话(0 <= K <= 100,000),输出假话的总数。
Input
第一行是两个整数N和K,以一个空格分隔。
以下K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中D表示说法的种类。
若D=1,则表示X和Y是同类。
若D=2,则表示X吃Y。
Output
只有一个整数,表示假话的数目。
思路:扩展域并查集思想
把每个动物拆成三个节点,分为同类域x1,捕食域x2和天敌域x3;
首先如果“x 与 y是同类”,则说明“x1 与 y1”,“x2 与 y2”,“x3 与 y3” 应该再同一个集合里面,那么分别合并三个集合。
如果"x 吃 y",则说明“x 吃的物种都是y的同类”,“x 的同类都是 y 的天敌”,又因为题目中说了这个食物链是长度为3的环形,所以“x的天敌就是y的吃的物种”(x吃y, y吃z, z吃x)所以此时应该合并"x2 与 y1",“x1 与 y3”,“x3 与 y2”;
处理每句话前判断真假,
有两种信息与"x y是同类"矛盾,
1 “x2 与 y1”在同一个集合中,x 吃 y;
2 "x1 与 y2"在同一个集合中,y 吃 x;
有两种信息与x 吃 y"矛盾,
1 “x1 与 y1”在同一个集合中,即x,y同类;
2 “x1 与 y2”在同一个集合中,即y吃x;
然后数据量比较大,开了读入挂优化和Rank数组优化的并查集,加快Find()函数的速度。
AC code:
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 200000 + 100;
template <typename T>
void read(T &x){
x = 0;
char c = getchar();
for(; c < '0' || c > '9'; c = getchar()) ;
for(; c >='0' && c <= '9'; c = getchar())
x = (x << 1) + (x << 3) + (c ^ 48);
}
int fa[maxn],Rank[maxn];
int Find(int x){
return (x == fa[x]) ? x : fa[x] = Find(fa[x]);
}
void Unite(int x,int y){
x = Find(x), y = Find(y);
if(x == y) return ;
if(Rank[x] > Rank[y]){ //按秩合并
fa[y] = x;
}
else{
fa[x] = y;
if(Rank[x] == Rank[y]) ++Rank[y];
}
}
void init(int n){
for(int i = 0; i <= n ; ++i) fa[i] = i,Rank[i] = 0;
}
int main(){
int n,k;
int d,x,y;
read(n),read(k);
init(3 * n + 1);
int ans = 0;
int x1,x2,x3,y1,y2,y3;
while(k--){
read(d),read(x),read(y);
if(x > n || y > n){
++ans; continue;
}
x1 = x, x2 = x + n, x3 = x + (n << 1);
y1 = y, y2 = y + n, y3 = y + (n << 1);
if(d == 1){
if(x == y) continue ;
if(Find(x2) == Find(y1) || Find(x1) == Find(y2)){
++ans; continue;
}
Unite(x1,y1);
Unite(x2,y2);
Unite(x3,y3);
}
else{
if(x == y){
++ans; continue;
}
if(Find(x1) == Find(y1) || Find(x1) == Find(y2)){
++ans; continue;
}
Unite(x2,y1);
Unite(x1,y3);
Unite(x3,y2);
}
}
printf("%d\n",ans);
return 0;
}