并查集的作用:解决连通性问题(连通性问题:对于集合中的点,进行连通以及查询的过程)
Quick-Find算法
基于染色的思想,处于相同连通集合的点,染成相同的颜色。
合并操作:时间复杂度O(n),将一个连通子集中的点的染色全部染成另一个连通子集的颜色。
查询操作:时间复杂度O(1),只需要判断该点的颜色。
由于合并操作时间复杂度较高,故Quick-Find算法并不作为常用的并查集算法。
代码实现
#include<iostream>
using namespace std;
#define MAX_N 100
int arr[MAX_N+5];
void Init(int n)
{
for(int i=0;i<n;i++)
arr[i]=i;//每个点的颜色初始化为序号
return ;
}
void output(int *arr,int n)
{
int ret=0;
for(int i=0;i<n;i++)
ret+=printf("%3d",i);
printf("\n");
for(int i=0;i<=ret+1;i++)
printf("-");
printf("\n");
for(int i=0;i<n;i++)
printf("%3d",arr[i]);
printf("\n");
return ;
}
int find(int i)
{
return arr[i];//直接返回颜色
}
int merge(int a,int b,int n)
{
int aa=find(a),bb=find(b);
if(aa==bb)return 0;//如果两个点在一个集合,直接返回
for(int i=0;i<n;i++)//找到所有原先和a颜色一样的点
{
if(find(i)==aa)
arr[i]=bb;//颜色更新为b的颜色
}
return 1;
}
int main()
{
int n;
cin>>n;
Init(n);
output(arr,n);
int a,b;
while(cin>>a>>b)
{
printf("merge %d with %d:%d\n",a,b,merge(a,b,n));
output(arr,n);
}
return 0;
}
Quick-Union算法
解决了Quick-Find算法合并太慢的问题,使用树形结构父子节点之间的关系来说明是否在一个集合,即Union在同一集合的两个点有相同的根节点。
合并操作:将一棵树作为另一棵树的子树(时间复杂度取决于树高)
查询操作:递归查找到该节点所在树的根节点(时间复杂度取决于树高)
代码实现
#include<iostream>
using namespace std;
#define MAXSIZE 100
int fa[MAXSIZE+5];
void Init(int n)
{
for(int i=0;i<n;i++)
{
fa[i]=i;//每个节点的父节点初始化为序号
}
return ;
}
void output(int *arr,int n)
{
int ret=0;
for(int i=0;i<n;i++)
ret+=printf("%3d",i);
printf("\n");
for(int i=0;i<=ret+1;i++)
printf("-");
printf("\n");
for(int i=0;i<n;i++)
printf("%3d",arr[i]);
printf("\n");
return ;
}
int find(int a)
{
if(fa[a]==a)return a;
//当前点是根节点,直接返回
return find(fa[a]);//若不是根节点则递归找到根节点
}
int merge(int a,int b)
{
if(find(a)==find(b))return 0;
//有同一个根节点,说明在同一个连通子集
fa[find(a)]=find(b);
//将a的根节点连在b的根节点上
return 1;
}
int main()
{
int n;
cin>>n;
Init(n);
output(fa,n);
int a,b;
while(cin>>a>>b)
{
printf("merge %d with %d:%d\n",a,b,merge(a,b));
output(fa,n);
}
return 0;
}
Quick-Union算法存在的问题:极端情况下会形成链表,使得find操作的效率大大降低,因此需要对Quick-Union进行优化。
Quick-Union的优化
按秩优化
在普通的Quick-Union实现中,find算法的实现总是将a的根节点连接在b的根节点上,这可能会导致树的退化,例如形成链表,因此在每次连接之前,可以以树的高度为参考依据将高度较矮的树连在高度较高的树下面,或者将节点数较少的树连在节点数较多的树下面。
代码实现
#include<iostream>
using namespace std;
#define MAXSIZE 100
int fa[MAXSIZE+5];
int size[MAXSIZE+5];
void Init(int n)
{
for(int i=0;i<n;i++)
{
fa[i]=i;//每个节点的父节点初始化为序号
size[i]=1;//以每个点以根节点的树的大小
}
return ;
}
void output(int *arr,int n)
{
int ret=0;
for(int i=0;i<n;i++)
ret+=printf("%3d",i);
printf("\n");
for(int i=0;i<=ret+1;i++)
printf("-");
printf("\n");
for(int i=0;i<n;i++)
{
printf("%3d",arr[i]);
}
printf("\n");
for(int i=0;i<n;i++)
{
printf("%3d",size[i]);
}
printf("\n");
return ;
}
int find(int a)
{
if(fa[a]==a)return a;
//当前点是根节点,直接返回
return find(fa[a]);//若不是根节点则递归找到根节点
}
int merge(int a,int b)
{
int aa=find(a),bb=find(b);
if(aa==bb)return 0;
//有同一个根节点,说明在同一个连通子集
if(size[aa]<size[bb])
{
fa[aa]=bb;
size[bb]+=size[aa];
//将a的根节点连在b的根节点上
}
else {
fa[bb]=aa;
size[aa]+=size[bb];
}
return 1;
}
int main()
{
int n;
cin>>n;
Init(n);
output(fa,n);
int a,b;
while(cin>>a>>b)
{
printf("merge %d with %d:%d\n",a,b,merge(a,b));
output(fa,n);
}
return 0;
}
路径压缩
背景:因为Quick-Union操作的时间消耗取决于树高,为了降低每次find的消耗,我们要尽可能让树形结构变扁,但是如果我们每次在merge操作的时候去把一棵树上的各点都连接到另一棵树时,这会导致Quick-Union算法退化为Quick-Find算法,即merge的时间复杂度变成O(n)。
在每次find而不是merge的时候将当前结点以及当前点之前路径上的所有点挂在根节点上,可以减少很多的无用合并,达到均摊时间度为O(1)的效果。
代码实现
#include<iostream>
using namespace std;
#define MAXSIZE 100
int fa[MAXSIZE+5];
void Init(int n)
{
for(int i=0;i<n;i++)
{
fa[i]=i;//每个节点的父节点初始化为序号
}
return ;
}
void output(int *arr,int n)
{
int ret=0;
for(int i=0;i<n;i++)
ret+=printf("%3d",i);
printf("\n");
for(int i=0;i<=ret+1;i++)
printf("-");
printf("\n");
for(int i=0;i<n;i++)
printf("%3d",arr[i]);
printf("\n");
return ;
}
int find(int a)
{
return fa[a]=(fa[a]==a?a:find(fa[a]));
//路径压缩
}
int merge(int a,int b)
{
if(find(a)==find(b))return 0;
//有同一个根节点,说明在同一个连通子集
fa[find(a)]=find(b);
//将a的根节点连在b的根节点上
return 1;
}
int main()
{
int n;
cin>>n;
Init(n);
output(fa,n);
int a,b;
while(cin>>a>>b)
{
printf("merge %d with %d:%d\n",a,b,merge(a,b));
output(fa,n);
}
return 0;
}
在实际的工作中,我们更常使用路径压缩的Quick-Union。
例题
1.最长连续序列
给定一个未排序的整数数组 nums
,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
请你设计并实现时间复杂度为 O(n)
的算法解决此问题。
示例 1:
输入:nums = [100,4,200,1,3,2]
输出:4
解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
示例 2:
输入:nums = [0,3,7,2,5,8,4,6,0,1]
输出:9
思路:从前往后遍历,将值相差为1的点进行连通,最后找到size最大的连通集。
代码实现
class Quick_Union{
public:
Quick_Union(int n):fa(n+1),size(n+1)
{
for(int i=0;i<=n;i++)
{
fa[i]=i;
size[i]=1;
}
}
int find(int a)
{
return fa[a]=(fa[a]==a?a:find(fa[a]));
}
int merge(int a,int b)
{
int aa=find(a),bb=find(b);
if(aa==bb)return 0;
fa[aa]=bb;
size[bb]+=size[aa];
return 1;
}
vector<int>fa,size;
};
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
unordered_map<int,int>h;
//使用哈希表来维护值与下标之间的映射
int n=nums.size();
Quick_Union u(n);
int cnt=0;
for(int i=0;i<n;i++)
{
if(h.find(nums[i])!=h.end())continue;
//删除重复出现的元素
h[nums[i]]=cnt++;//下标初始化
if(h.find(nums[i]-1)!=h.end())u.merge(h[nums[i]],h[nums[i]-1]);
if(h.find(nums[i]+1)!=h.end())u.merge(h[nums[i]],h[nums[i]+1]);
//合并值连续的点
}
int ans=0;
for(int i=0;i<n;i++)
ans=max(ans,u.size[i]);
return ans;
}
};
使用哈希表的作用:1.进行值与下标的映射 2.将向前查找的时间复杂度由O(n)降为O(1)。
2.被围绕的岛屿
给你一个 m x n
的矩阵 board
,由若干字符 'X'
和 'O'
,找到所有被 'X'
围绕的区域,并将这些区域里所有的 'O'
用 'X'
填充。
示例 1:
输入:board = [["X","X","X","X"],["X","O","O","X"],["X","X","O","X"],["X","O","X","X"]]
输出:[["X","X","X","X"],["X","X","X","X"],["X","X","X","X"],["X","O","X","X"]]
解释:被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O'
都不会被填充为 'X'
。 任何不在边界上,或不与边界上的 'O'
相连的 'O'
最终都会被填充为 'X'
。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
示例 2:
输入:board = [["X"]]
输出:[["X"]]
思路:边上的O一定不被围绕,故除了边上的O以及与他们连通的其他O之外的O都是被围绕的。
代码实现
class Quick_Union{
public:
Quick_Union(int n):fa(n+1)
{
for(int i=0;i<=n;i++)
{
fa[i]=i;
}
}
int find(int a)
{
return fa[a]=(fa[a]==a?a:find(fa[a]));
}
int merge(int a,int b)
{
int aa=find(a),bb=find(b);
if(aa==bb)return 0;
fa[aa]=bb;
return 1;
}
vector<int>fa;
};
class Solution {
public:
void solve(vector<vector<char>>& board) {
int m=board.size(),n=board[0].size();
Quick_Union u(m*n);
for(int i=0;i<m;i++)
{
for(int j=0;j<n;j++)
{
int ind=i*n+j+1;
if(board[i][j]=='X')continue;
//保证只有O进行以下的判断
if(i==0||i==m-1)u.merge(ind,0);
if(j==0||j==n-1)u.merge(ind,0);
//如果在边界上则与0位置连通
if(i>0&&board[i-1][j]=='O')u.merge(ind,ind-n);
if(j>0&&board[i][j-1]=='O')u.merge(ind,ind-1);
//与左和上的O进行连通
}
}
for(int i=0;i<m;i++)
{
for(int j=0;j<n;j++)
{
if(board[i][j]=='O'&&u.find(i*n+j+1)!=u.find(0))//注意是find(0),不是0
board[i][j]='X';
}
}
return ;
}
};
3.岛屿数量
给你一个由 '1'
(陆地)和 '0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
思路:记录1所组成连通子集的数量。
示例 1:
输入:grid = [
["1","1","1","1","0"],
["1","1","0","1","0"],
["1","1","0","0","0"],
["0","0","0","0","0"]
]
输出:1
示例 2:
输入:grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
输出:3
代码实现
class Quick_Union{
public:
Quick_Union(int n):fa(n)
{
for(int i=0;i<n;i++)
fa[i]=i;
}
int find(int a)
{
return fa[a]=(fa[a]==a?a:find(fa[a]));
}
void merge(int a,int b)
{
if(find(a)==find(b))return ;
fa[find(a)]=find(b);
return ;
}
vector<int>fa;
};
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
int m=grid.size(),n=grid[0].size();
Quick_Union u(m*n);
for(int i=0;i<m;i++)
{
for(int j=0;j<n;j++)
{
int ind=i*n+j;
if(grid[i][j]=='0')continue;
if(i>0&&grid[i-1][j]=='1')u.merge(ind,ind-n);
if(j>0&&grid[i][j-1]=='1')u.merge(ind,ind-1);
}
}
int ans=0;
for(int i=0;i<m;i++)
{
for(int j=0;j<n;j++)
{
int ind=i*n+j;
if(grid[i][j]=='0')continue;
if(u.fa[ind]==ind)ans+=1;
}
}
return ans;
}
};
4.省份数量
有 n
个城市,其中一些彼此相连,另一些没有相连。如果城市 a
与城市 b
直接相连,且城市 b
与城市 c
直接相连,那么城市 a
与城市 c
间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n
的矩阵 isConnected
,其中 isConnected[i][j] = 1
表示第 i
个城市和第 j
个城市直接相连,而 isConnected[i][j] = 0
表示二者不直接相连。
返回矩阵中 省份 的数量。
示例 1:
输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2
示例 2:
输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]]
输出:3
代码实现
class UnionSet{
public:
UnionSet(int n):fa(n+1){
for(int i=0;i<=n;i++)
fa[i]=i;
}
int get(int k)
{
return fa[k]=(fa[k]==k?k:get(fa[k]));
}
void merge(int a,int b)
{
fa[get(a)]=fa[get(b)];
return ;
}
vector<int>fa;
};
class Solution {
public:
int findCircleNum(vector<vector<int>>& isConnected) {
int n=isConnected.size();
UnionSet u(n);
for(int i=0;i<n;i++)
{
for(int j=i+1;j<n;j++)
{
if(isConnected[i][j]==1)u.merge(i,j);
}
}
int ans=0;
for(int i=0;i<n;i++)
if(u.fa[i]==i)ans+=1;
return ans;
}
};
5.猜拳
题目描述
在一次聚会中,每人拿着一张印有石头、剪刀、布的卡片,每个人具体拿得是哪种卡片不得而知。
现在告诉你某些人之间的胜负关系,并会询问某两个人之间的对战结果,人按照从 1 到 n 编号。
对于每个询问,请给出正确的回答: Win(胜)、Loss(负)、Tie(平)
输入
第一行输入两个整数 n,m(1≤n≤10000,3≤m≤10000),分别代表人数和信息数量。
接下来 m 行,每行三个整 a,b,c(a∈[1,2], 1≤b,c≤n)
- 当 a=1 时,代表新增一条已知信息,表示 b, c 对战中 b 胜
- 当 a=2 时,代表根据以上信息,询问 b,c 对战中 b 的结果
如果出现某条新增的信息与之前的信息发生冲突,就忽略此条信息。
输出
对于每个 a=2 的操作,输出 Win、Loss、Tie 或 Unknown 代表对战双方的结果。
样例输入
6 6
1 1 2
2 1 3
1 2 4
1 4 3
2 1 3
2 4 1
样例输出
Unknown
Tie
Win
思路:如果题目只问两个人之间的输赢关系能否判断的话,那么只用普通的并查集就能解决该问题,即在一个连通集中的人能够判断输赢关系,但是题目中进一步问了两个人之间的输赢关系是什么,那么就需要使用带权并查集来维护这个关系。
在对xw和yz进行merge操作时需要知道c的值,c=(t+b-a+3)%3。
代码实现
#include<iostream>
#include<vector>
using namespace std;
class UnionSet
{
public:
UnionSet(int n):fa(n+1),val(n+1)
{
for(int i=0;i<n;i++)
{
fa[i]=i;
val[i]=0;
//与父节点之间的路径权值,由于父节点初始化为自己,所以该值是0
}
}
int find(int a)
{
if(fa[a]==a)return a;
int root=find(fa[a]);
//将其父节点及之前的点都进行压缩,同时返回根节点
val[a]=(val[a]+val[fa[a]]+3)%3;
return fa[a]=root;
}
void merge(int a,int b,int t)
{
int aa=find(a),bb=find(b);
if(aa==bb)return ;
val[aa]=(t+val[b]-val[a]+3)%3;
fa[aa]=bb;
}
vector<int>fa,val;
};
int main()
{
int n,m;
cin>>n>>m;
UnionSet u(n);
for(int i=1,a,b,c;i<=m;i++)
{
cin>>a>>b>>c;
if(a==1)
{
u.merge(b,c,1);
}
else
{
if(u.find(b)!=u.find(c))
{
cout<<"Unknown"<<endl;
}
else
switch((u.val[b]-u.val[c]+3)%3)
{
case 0:{
cout<<"Tie"<<endl;
break;
}
case 1:{
cout<<"Win"<<endl;
break;
}
case 2:{
cout<<"Loss"<<endl;
break;
}
}
}
}
return 0;
}
6. 程序自动分析
题目描述
在实现程序自动分析的过程中,常常需要判定一些约束条件是否能被同时满足。
考虑一个约束满足问题的简化版本:假设 x1,x2,x3,… 代表程序中出现的变量,给定n个形如xi=xj或xi≠xj 的变量相等/不等的约束条件,请判定是否可以分别为每一个变量赋予恰当的值,使得上述所有约束条件同时被满足。
例如,一个问题中的约束条件为:x1=x2,x2=x3,x3=x4,x1≠x4,这些约束条件显然是不可能同时被满足的,因此这个问题应判定为不可被满足。
现在给出一些约束满足问题,请分别对它们进行判定。
输入
输入文件的第1行包含1个正整数t,表示需要判定的问题个数。注意这些问题之间是相互独立的。
对于每个问题,包含若干行:
第1行包含1个正整数n,表示该问题中需要被满足的约束条件个数。
接下来n行,每行包括3个整数i,j,e,描述1个相等/不等的约束条件,相邻整数之间用单个空格隔开。若e=1,则该约束条件为xi=xj;若e=0,则该约束条件为xi≠xj。
输出
输出文件包括t行。
输出文件的第1行输出一个字符串“YES”
或者“NO”
(不包含引号,字母全部大写),“YES”
表示当前问题判定为可以被满足,“NO”
表示不可被满足。
输入样例1
2
2
1 2 1
1 2 0
2
1 2 1
2 1 1
输出样例1
NO
YES
思路:利用并查集去维护元素之间的相等关系,先进行相等关系的判断,若两个元素具有相等关系,则加到一个并查集里,再进行不等关系的判断,若两个元素不等,则他们先前不能在一个连通集里。
代码实现
#include<iostream>
#include<vector>
#include<unordered_map>
using namespace std;
class UnionSet{
public:
UnionSet(int n):fa(n+1)
{
for(int i=0;i<n;i++)
fa[i]=i;
}
int find(int a)
{
return fa[a]=(fa[a]==a?a:find(fa[a]));
}
void merge(int a,int b)
{
fa[find(a)]=find(b);
return ;
}
vector<int>fa;
};
struct Data{
int i,j,e;
};
vector<int>arr;
int main()
{
int t,n;
cin>>t;
while(t--)
{
cin>>n;
UnionSet u(2*n);
Data d;
vector<Data>arr(n);
unordered_map<int,int>h;
int cnt=0;
for(int k=0,i,j,e;k<n;k++)
{
cin>>i>>j>>e;
if(h.find(i)==h.end())
h[i]=cnt++;
if(h.find(j)==h.end())
h[j]=cnt++;
d.i=h[i],d.j=h[j],d.e=e;
arr[k]=d;
}
for(int i=0;i<n;i++)
{
if(arr[i].e==0)continue;
u.merge(arr[i].i,arr[i].j);
}
int flag=1;
for(int i=0;i<n;i++)
{
if(arr[i].e==1)continue;
if(u.find(arr[i].i)==u.find(arr[i].j))
{
flag=0;
break;
}
}
if(flag==1)cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}
return 0;
}
7.关押罪犯
题目描述
S 城现有两座监狱,一共关押着N 名罪犯,编号分别为1~N。他们之间的关系自然也极不和谐。很多罪犯之间甚至积怨已久,如果客观条件具备则随时可能爆发冲突。我们用“怨气值”(一个正整数值)来表示某两名罪犯之间的仇恨程度,怨气值越大,则这两名罪犯之间的积怨越多。如果两名怨气值为c 的罪犯被关押在同一监狱,他们俩之间会发生摩擦,并造成影响力为c 的冲突事件。
每年年末,警察局会将本年内监狱中的所有冲突事件按影响力从大到小排成一个列表,然后上报到S 城Z 市长那里。公务繁忙的Z 市长只会去看列表中的第一个事件的影响力,如果影响很坏,他就会考虑撤换警察局长。
在详细考察了N 名罪犯间的矛盾关系后,警察局长觉得压力巨大。他准备将罪犯们在两座监狱内重新分配,以求产生的冲突事件影响力都较小,从而保住自己的乌纱帽。假设只要处于同一监狱内的某两个罪犯间有仇恨,那么他们一定会在每年的某个时候发生摩擦。那么,应如何分配罪犯,才能使Z 市长看到的那个冲突事件的影响力最小?这个最小值是多少?
输入
第一行为两个正整数N(N≤20000) 和M(M≤100000),分别表示罪犯的数目以及存在仇恨的罪犯对数。
接下来的M 行每行为三个正整数aj,bj,cj,表示aj 号和bj 号罪犯之间存在仇恨,其怨气值为cj。数据保证1≤aj<bj<N,0<cj≤1,000,000,000 且每对罪犯组合只出现一次。
输出
输出共1行,为Z 市长看到的那个冲突事件的影响力。如果本年内监狱中未发生任何冲突事件,请输出0。
输入样例1
4 6
1 4 2534
2 3 3512
1 2 28351
1 3 6618
2 4 1805
3 4 12884
输出样例1
3512
思路:依然是使用带权并查集去维护两个罪犯之间是否在同一监狱的关系,根据冲突值的从大到小顺序依次判断,第一组不符合的冲突值即为答案。
代码实现
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
class UnionSet{
public:
UnionSet(int n):fa(n+1),val(n+1)
{
for(int i=0;i<=n;i++)
{
fa[i]=i;
val[i]=0;
}
}
int find(int a)
{
if(fa[a]==a)return a;
int root=find(fa[a]);
val[a]=(val[a]+val[fa[a]]+2)%2;
return fa[a]=root;
}
void merge(int a,int b,int t)
{
int aa=find(a),bb=find(b);
if(aa==bb)return ;
val[aa]=(t+val[b]-val[a]+2)%2;
fa[aa]=bb;
return ;
}
vector<int>fa,val;
};
struct Data{
int a,b,c;
};
bool cmp(const Data&a,const Data&b)
{
return a.c>b.c;
}
int main()
{
int n,m;
cin>>n>>m;
vector<Data>arr(m);
UnionSet u(n);
for(int i=0;i<m;i++)
{
cin>>arr[i].a>>arr[i].b>>arr[i].c;
}
sort(arr.begin(),arr.end(),cmp);
int ans=0;
for(int i=0;i<m;i++)
{
if(u.find(arr[i].a)!=u.find(arr[i].b))
u.merge(arr[i].a,arr[i].b,1);
else
{
if((u.val[arr[i].a]+u.val[arr[i].b])%2==0)
{
ans=arr[i].c;
break;
}
}
}
cout<<ans;
return 0;
}