并查集的应用:
合并两个集合
查询某个元素的祖宗节点
扩展:
记录每个集合的大小(绑定到根节点)
记录每个点到根节点的距离(绑定到每个元素上)
二元或多元判断的时候用到的两种方法:扩展域并查集,带权并查集
常用优化:
路径压缩(find函数,时间复杂度logn)
按秩合并:将树深度小的合并到大的上面(一般超时特别严重才会用)
1250. 格子游戏(活动 - AcWing)
两人轮流画,谁先封圈谁赢,这里可以用边的双连通分量来判断(因为连的是无向边,但是很显然加一条边tarjan一次太麻烦了,这里可以先记录所有的边,然后二分判断是否封圈,但是这题有更简单的方法)这里从并查集的角度来考虑的话,封圈就意味着,两点之前应该就属于一个集合。那么就很简单了,加一条边找一下祖宗即可。
#include<bits/stdc++.h>
using namespace std;
const int N=200,M=50010;
int n,m;
int p[M];
int find(int x)
{if(p[x]!=x) p[x]=find(p[x]);return p[x];
}
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<=M;i++) p[i]=i;int i;for(i=1;i<=m;i++){int x,y;char op[2];cin>>x>>y>>op;int a,b;a=x*N+y;if(op[0]=='D') b=(x+1)*N+y;else b=x*N+y+1;a=find(a),b=find(b);if(a!=b) p[a]=b;else break;}if(i>m) printf("draw");else printf("%d",i);
}
1252. 搭配购买(活动 - AcWing)
思路:这里如果买一朵云那么一片云都要买,而且如果买a那么需要买b,同时买b也一定要买a,那么可以把这些连在一块儿的云视为一组,钱有限,价值更高,那么不就是01背包问题。连在一块儿可以用并查集,我们可以把这一片云的价格和价值绑定到根节点上。
#include<bits/stdc++.h>
using namespace std;
const int N=10010;
int n,m,w;
int v[N],c[N],f[N],p[N];
int find(int x)
{if(p[x]!=x) p[x]=find(p[x]);return p[x];
}
int main()
{scanf("%d%d%d",&n,&m,&w);for(int i=1;i<=N-1;i++) p[i]=i;for(int i=1;i<=n;i++) cin>>v[i]>>c[i];for(int i=1;i<=m;i++){int a,b;scanf("%d%d",&a,&b);a=find(a),b=find(b);if(a!=b) {p[a]=b;v[b] += v[a];c[b] += c[a];}//如果本身在一个集合中那么已经累计到根节点了,就不用特殊处理}for(int i=1;i<=n;i++)if(i==p[i])for(int j=w;j>=v[i];j--)f[j]=max(f[j],f[j-v[i]]+c[i]);cout<<f[w];
}
237. 程序自动分析(237. 程序自动分析 - AcWing题库)
这里很容易想到差分约束,但是只有等于不等两个关系,那么我们实际上可以将相等的放入一个集合中,再看有不等关系的点是否在一个集合中即可。
#include<bits/stdc++.h>
using namespace std;
const int N=200010;
struct node{int a,b,e;
}d[N];
int p[N];
int find(int x)
{if(p[x]!=x) p[x]=find(p[x]);return p[x];
}
int n,m;
unordered_map<int,int>mp;
int get(int x)
{if(!mp.count(x)) mp[x]=++n;return mp[x];
}
int main()
{int t;scanf("%d",&t);while(t--){//清空上一次的状态n=0;mp.clear();scanf("%d",&m);for(int i=1;i<=m;i++){int a,b,e;scanf("%d%d%d",&a,&b,&e);a=get(a),b=get(b);d[i]={a,b,e};}for(int i=1;i<=n;i++) p[i]=i;for(int i=1;i<=m;i++){int a=d[i].a,b=d[i].b,e=d[i].e;if(e){a=find(a),b=find(b);if(a!=b) p[a]=b;}}int flag=1;for(int i=1;i<=m;i++){int a=d[i].a,b=d[i].b,e=d[i].e;if(!e){a=find(a),b=find(b);if(a==b){flag=0;break;}}}if(flag) puts("YES");else puts("NO");}
}
238. 银河英雄传说(活动 - AcWing)
某一整列接在另一整列后面以及判断两者是否在同一列中是很简单的操作,但是这里还有一个需要求的,就是两者如果在同一列,那么它们之间间隔多少个战舰。这里的处理是记录每个点距离根节点有多少个战舰。那么这个该如何维护呢?我们可以将到根节点的距离视为一条最短路,我们每次合并两个集合就相当于给两个集合的根节点之间加一条边,我们通过给这条边的边权赋值,那么就可以实现维护,每次查找到根节点的最短路即可。
#include<bits/stdc++.h>
using namespace std;
const int N=30010;
int n;
int p[N],d[N],sz[N];
int find(int x)
{if(x!=p[x]){int root=find(p[x]);d[x] += d[p[x]];//这里的更新就是简单的递归,定义root的意义就是保证用到的p[x]值是父节点,保证递归的正确性p[x]=root;}return p[x];
}
int main()
{scanf("%d",&n);for(int i=1;i<N;i++) p[i]=i,d[i]=0,sz[i]=1;while(n--){char op[2];int a,b;cin>>op>>a>>b;int pa=find(a),pb=find(b);//后面要用到a,b,所以这里要用变量单独记录祖先节点的值if(op[0]=='M'){if(pa!=pb){d[pa] += sz[pb];sz[pb] += sz[pa];p[pa]=pb;}//在同一个集合中的不用进行处理}else{if(pa==pb) printf("%d\n",max(abs(d[a]-d[b])-1,0));else printf("-1\n");}}
}
239. 奇偶游戏(239. 奇偶游戏 - AcWing题库)
思路:这里问是否产生矛盾,那么就需要考虑什么情况下会产生矛盾。显然如果给定[l,k]的奇偶性和给定[k+1,r]的奇偶性,然后再给定[l,r]的奇偶性,三者产生矛盾。这里我们定义s[i]表示前i个数中1的个数,那么对于询问区间,我们就可以知道s[r]与s[l-1]之间的相对奇偶关系。那么相当于我们知道a,b之间和b,c之间的相对就关系,如果访问到a,c判断是否产生矛盾。如果两者不在同一集合中,那么证明之前两者之间没有确定的奇偶关系,我们可以自己加上一个奇偶关系,否则如果两者在同一集合当中就要判断是否产生矛盾。那么如何判断是否产生矛盾,我们可以通过两者与根节点的距离来判断,因为只有奇偶两种情况,我们可以定义只有两种距离0,1,如果奇偶性相同,那么两者之间这一段就为偶,如果奇偶性不同,那么中间这段就为奇。
#include<bits/stdc++.h>
using namespace std;
const int N=10010;
int n,m,k;
unordered_map<int,int>mp;
int p[N],d[N];
int get(int x)
{if(!mp.count(x)) mp[x]=++k;return mp[x];
}
int find(int x)
{if(p[x]!=x){int root=find(p[x]);d[x] ^= d[p[x]];p[x]=root;}return p[x];
}
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<N;i++)p[i]=i;int i;for(i=1;i<=m;i++){int a,b;string s;cin>>a>>b>>s;a=get(a-1),b=get(b);//离散化int pa=find(a),pb=find(b);if(pa==pb)//在同一集合中{if(s=="even"&&!(d[a]^d[b])) continue;else if(s=="odd"&&(d[a]^d[b])) continue;else break;}else{p[pa]=pb;//d[b]就是到根节点的距离,d[a]需要更新成到新节点的距离,那么如何实现呢,显然可以通过更新//a的祖宗节点pa到pb之间的关系,进而实现对d[a]与d[b]关系的更新//实际上d[a]=d[a]^d[pa],d[b]=d[b],要保证的是d[a]和d[b]的相对关系。if(s=="even") d[pa] = d[a]^d[b];else d[pa]=d[a]^d[b]^1;}}printf("%d\n",i-1);
}
因为只有0和1两种距离所以用到异或。
还有一种做法——扩展域
我们将x定义为1-x为偶数,x+n定义为1-x为奇数,而将集合定义成只要在一个集合中,那么一个条件成立,一定集合中所有的条件都成立,这里是用数来表示条件。
#include<bits/stdc++.h>
using namespace std;
const int N=10010;
int n,m;
int p[2*N];//每个值有两个点
int find(int x)
{if(x!=p[x]) p[x]=find(p[x]);return p[x];
}
unordered_map<int,int>mp;
int k;
int get(int x)
{if(!mp.count(x)) mp[x]=++k;return mp[x];
}
int main()
{scanf("%d%d",&n,&m);for(int i=1;i<2*N;i++) p[i]=i;int i;for(i=1;i<=m;i++){int a,b;string s;cin>>a>>b>>s;a=get(a-1),b=get(b);if(s=="even"){if(find(a+N)==find(b)){break;}p[find(a)]=find(b);p[find(a+N)]=find(b+N);}else{if(find(a)==find(b)){break;}p[find(a+N)]=find(b);p[find(a)]=find(b+N);}}printf("%d",i-1);
}