并查集是一种非常高效的查询结构,主要用于涉及若干集合的包含,合并,和判断是否同集合等问题,每个元素都在father中有一个对应的元素,默认是本身,几个通过father相互连接的元素我们就认为是一个集合的元素,对于并查集具体实现,首先我们需要了解一下并查集涉及的几种函数。
1.find函数
这是并查集最重要的一个函数,作用就是查询某元素的代表是什么,所谓代表就是指作为一个集合的“名字”作用的元素,也就是如果代表相同,两个集合就是同一个集合。find函数的具体实现就是对于要查询的元素我们一直查询father的值,直到一个元素的值是自己的时候说明这个元素就是代表,我们返回代表的值即可。
2.IsSameset函数
顾名思义就是代表判断两个元素是否属于同一个集合,我们只需要对两个元素都调用find即可,最后返回两个代表是不是相等即可。
3.union函数
就是将两个元素所在的集合合并,实现也很好想,就是查询两个代表,然后让一个代表指向另一个代表即可,就代表两个集合合并了。
一些优化:
1.小挂大优化
指在union函数中可以通过把较小的一个集合挂在更大的集合上,这样在再次查询的时候可以尽可能节省查询次数,具体实现就是增加一个size数组,初始化每个元素的size都是1,意为只有自己的一个集合,每次union把两个代表的size相加变为新的size,这样union的时候就可以在合并之前查询那个集合大小更小从而调整指向。
2.扁平化优化
所谓扁平化就是指在find函数中,我们把链状的结构变成每个集合中子元素直接指向这个集合的代表,这样均摊下来查询代表的代价就是o(1),而对于的扁平化的实现有两个版本(虽然本质一样)
(1)stack实现
因为我们往下查询的时候是不能一步直接找到最后的代表的,所以我们可以把途径的元素都进栈,然后查询到了代表的时候就把栈一个个弹出,让栈中元素都直接指向代表元素,直到清空了栈。
(2)递归实现
其实内核就是栈的实现,但是递归实现更加精致小巧,就是让本元素指向下一层查询的最终答案,basecase就是本层的father就是自己就返回find的结果就是这个元素,如此之后每个调用下一层find函数的元素都指向了最终的代表。
上方所有方法的实现 模板:洛谷P3376
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAX=10000000;
vector<int> father(MAX);
vector<int> size(MAX);
vector<int> Stack(MAX);//数组实现栈
int find1(int i){//栈实现版本
int size=0;
for(;i!=father[i];i=father[i]){
Stack[size++]=i;
}
while(size--){
father[Stack[size]]=i;
}
return i;
}
int find2(int i){//递归实现
if(father[i]!=i){
father[i]=find2(father[i]);
}
return father[i];
}
void Union(int x,int y){
x=find2(x);
y=find2(y);
if(size[x]>size[y]){
father[y]=x;
}else father[x]=y;
}
bool IsSameset(int x,int y){
x=find2(x);
y=find2(y);
return x==y;
}
void solve(){
int n;//代表元素个数
cin>>n;
for(int i=0;i<n;i++){//必要的初始化
father[i]=i;
size[i]=1;
}
}
int main()
{
solve();
return 0;
}
765.情侣牵手
n
对情侣坐在连续排列的 2n
个座位上,想要牵到对方的手。
人和座位由一个整数数组 row
表示,其中 row[i]
是坐在第 i
个座位上的人的 ID。情侣们按顺序编号,第一对是 (0, 1)
,第二对是 (2, 3)
,以此类推,最后一对是 (2n-2, 2n-1)
。
返回 最少交换座位的次数,以便每对情侣可以并肩坐在一起。 每次交换可选择任意两人,让他们站起来交换座位。
这道题想到并查集的做法还是有一些难度的,首先我们手动去模拟一下换座的问题,因为最终2n个位置需要全部配对成功,所以每个偶数位置的人肯定跟自己位置的下一个位置配对(下标从0开始)。
那么我们如果手动去做肯定就能发现有三种最基本的情况,第一种是本来就配对好了,那我们就不用动了,第二种就是两对情侣恰好坐反了,那么我们就把他们各其中一个交换一下就完成了,一次交换就配对了两对,还有第三种就是多对情侣交错,也就是无法通过一次交换配对两对,那么最优的方案就是交换一次完成一对情侣的配对,那么这种情况一共要交换多少次呢,我们手动模拟的时候发现,一次完成一对的配对,最后一次就将完成两对的配对,也就是最后恰好达到第二种情况。
仔细思考发现,其实第二种和第三种可以合并,需要交换的次数都是交错的情侣数量-1,所以只要统计出交错的情侣集合,再遍历所有集合对[集合大小-1]求和就可以了。
所以现在唯一没有完成的问题就是如何高效的统计出交错的情侣数量,首先每个位置的人都有自己的情侣编号,就是人的编号/2,那么我们来到一个偶数位置的时候,就会发现本位置和下一个位置对应的情侣交错在一起了(当然也可能恰好配对),那么此时就把他们两个所处的集合合并,恰好用并查集的知识就可以解决,把所有彼此交错的情侣合并成一个集合,其余就没有难点,细节看代码。
class Solution {
public:
int find(vector<int>& father,int x){
if(x!=father[x]){
father[x]=find(father,father[x]);
}
return father[x];
}
void Union(vector<int>& father,vector<int>& Size,int x,int y){
x=find(father,x),y=find(father,y);
if(x!=y){
father[x]=y;
Size[y]+=Size[x];
}
}
int minSwapsCouples(vector<int>& row) {
int n=row.size();
unordered_set<int> st;
vector<int> Size(n/2,1);
vector<int> father(n/2);
for(int i=0;i<n/2;i++){
father[i]=i;
}
for(int i=0;i<n;i+=2){
Union(father,Size,row[i]/2,row[i+1]/2);
}
int ans=0;
for(int i=0;i<n/2;i++){
int k=find(father,i);
if(!st.count(k)){
st.insert(k);
ans+=Size[k]-1;
}
}
return ans;
}
};
839.相似字符串组
如果交换字符串 X
中的两个不同位置的字母,使得它和字符串 Y
相等,那么称 X
和 Y
两个字符串相似。如果这两个字符串本身是相等的,那它们也是相似的。
例如,"tars"
和 "rats"
是相似的 (交换 0
与 2
的位置); "rats"
和 "arts"
也是相似的,但是 "star"
不与 "tars"
,"rats"
,或 "arts"
相似。
总之,它们通过相似性形成了两个关联组:{"tars", "rats", "arts"}
和 {"star"}
。注意,"tars"
和 "arts"
是在同一组中,即使它们并不相似。形式上,对每个组而言,要确定一个单词在组中,只需要这个词和该组中至少一个单词相似。
给你一个字符串列表 strs
。列表中的每个字符串都是 strs
中其它所有字符串的一个字母异位词。请问 strs
中有多少个相似字符串组?
这道题题意其实总结下来就是有多少个相似字符串的集合,而可以在一个集合里的标准就是集合中有相似的就行,那么来到一个字符串,他能不能进行合并必须遍历所有的字符串,因为虽然他可能和某些字符串属于同一集合,但其二者本身不相似,如果相似就合并为一个集合,那么如何统计集合个数呢,我们可以维护一个变量cnt,每合并一个集合cnt就减一,因为合并就代表集合的数量减少了一份,但是我在做的时候遗漏了如果两个元素本就属于同一集合的情况,所以要加上判断,其余看代码。
class Solution {
public:
int find(vector<int>& f,int i){
if(f[i]!=i){
f[i]=find(f,f[i]);
}
return f[i];
}
void U(vector<int>&f,int i,int j){
f[find(f,j)]=find(f,i);
}
bool check(int n,string& a,string& b){
int cnt=0;
for(int i=0;i<n;i++){
if(a[i]!=b[i]) {
cnt++;
if(cnt>2) return false;
}
}
return cnt==2||cnt==0;
}
int numSimilarGroups(vector<string>& strs) {
int m=strs.size();
int n=strs[0].size();
vector<int> f(m);
int cnt=m;
for(int i=0;i<m;i++) f[i]=i;
for(int i=0;i<m;i++){
for(int j=i+1;j<m;j++){
if(find(f,i)!=find(f,j)){
if(check(n,strs[i],strs[j])){
U(f,i,j);
cnt--;
}
}
}
}
return cnt;
}
};
947.移除最多的同行或同列石头
n
块石头放置在二维平面中的一些整数坐标点上。每个坐标点上最多只能有一块石头。
如果一块石头的 同行或者同列 上有其他石头存在,那么就可以移除这块石头。
给你一个长度为 n
的数组 stones
,其中 stones[i] = [xi, yi]
表示第 i
块石头的位置,返回 可以移除的石子 的最大数量。
审视题目分析一下发现,要求的是可以移除的最大数量,所以我考虑到移除的顺序是否和最后答案相关,经过简单画图不难发现如果把可以由同行同列所有石头看作一个集合,那么这个集合最后通过恰当的方式一定可以消除到只剩下一个,具体方式就是和他满足相消关系的石头越多,该石头就越靠后消除,这样就可以实现消除数量的最大化,所以其实本题就是并查集合并所有关联的石头,在合并的过程中求集合数量,最后剩下的石头数就是集合数量(每个集合剩下一个),细节看代码。
class Solution {
public:
int find(vector<int>& f,int i){
if(i!=f[i]){
f[i]=find(f,f[i]);
}
return f[i];
}
void U(vector<int>& Size,vector<int>& f,int i,int j){
i=find(f,i),j=find(f,j);
if(Size[i]>Size[j]){
f[j]=i;
Size[i]+=Size[j];
}else f[i]=j,Size[j]+=Size[i];
}
bool unSame(vector<int>&f,int i,int j){
return find(f,i)!=find(f,j);
}
int removeStones(vector<vector<int>>& stones) {
int n=stones.size();
vector<int> f(n);
vector<int> Size(n,1);
for(int i=0;i<n;i++) f[i]=i;
int ans=0;
for(int i=0;i<n;i++){
for(int j=0;j<i;j++){
if((stones[i][0]==stones[j][0]||stones[i][1]==stones[j][1])&&unSame(f,i,j)){
U(Size,f,i,j);
ans++;
}
}
}
return ans;
}
};
到此简单的做了一些并查集的题目并积累了一些常见的用法以及优化,并查集总的来说是比较重要的算法,用均摊接近常数时间实现集合的查,合操作。(本文根据网络资料总结)
至此。