二、合并集合(acwing836)
并查集题目要求:
- 要求判断两个数是否在同一个集合里。
- 将两个数所在的集合合并成一个集合。
暴力解法:
- 开通一个数组a,a[i]的值表示数字i在哪个集合中,初始化数组将所有a[i]=i,例如a[i]=1,则i在集合1当中
- 判断集合是否在一个集合里很简单,可以判断a[i]是否等于a[j]。
- 合并则要便利整个数组a,将所有满足条件的数对应的值修改为同一个数组,例如a[i]=1,a[j]=3,则要将数组中所有a[k]==3的值都修改成a[k]=1
下面贴上暴力的代码,容易想到,但是会超时O(m*n)
#include<iostream>
using namespace std;
const int N = 100010;
int arr[N],m,n;
bool query(int arr[],int a,int b){
return arr[a]==arr[b];
}
void merge(int arr[],int a,int b){
int x = arr[a],y = arr[b];
if(query(arr,a,b)){
return;
}
else{
for(int i = 1;i<=n;i++){
if(arr[i]==x){
arr[i] = y;
}
}
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i = 1;i<=n;i++){
arr[i] = i;
}
while(m--){
char op[2];
int a,b;
scanf("%s%d%d",op,&a,&b);
if(op[0]=='M'){
merge(arr,a,b);
}
else{
if(query(arr,a,b)) printf("Yes\n");
else printf("No\n");
}
}
return 0;
}
并查集算法(近乎O(1)时间复杂度):
并查集:
- 根节点的编号就是当前集合的编号
- 每一个点都存储一下其父节点
- 查询每个点所在的集合就只要一直遍历其父节点直至根节点,即x=p[x];根节点p[x]==x。
- 如何合并两个集合,将第一个集合的根节点的值赋值给第二个集合根节点,p[x2根]=p[x1]根。
优化 (路径压缩)
当寻找一个数x所在的集合时,将寻找路径中所有的节点的父节点的值修改成该集合根节点的值
#include<iostream>
using namespace std;
const int N = 100010;
int p[N],m,n,x,y;
//返回x的祖宗节点,加上路径优化
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<=n;i++){
p[i] = i;
}
while(m--){
char op[2];
int a,b;
scanf("%s%d%d",op,&a,&b);
// printf("%s%d%d\n",op,a,b);
x = find(a),y = find(b);
if(op[0]=='M'){
if(x!=y){
p[y] = x;
}
}
else{
// printf("hh");
if(find(a)==find(b)) puts("Yes");
else puts("No");
}
}
return 0;
}
三、并查集拓展(837.连通块中点的数量)
需要模拟三种操作:
C a b
,在点 a 和点 b之间连一条边,a和 b可能相等;Q1 a b
,询问点 a和点 b是否在同一个连通块中,a 和 b 可能相等;Q2 a
,询问点 a 所在连通块中点的数量;
可以用并查集来模拟上面的要求:
- 第一个要求只要将a、b放到同一个集合当中,即合并集合的操作
- 判断a、b是否在同一个连通块中只要判断是否在一个集合当中
- 询问连通块中点的数量只要另外新增一个数组size[N]即可
#include<iostream>
using namespace std;
const int N = 100010;
int p[N],m,n,x,y;
int Size[N];
//返回x的祖宗节点,加上路径优化
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<=n;i++){
p[i] = i;
Size[i] = 1;
}
while(m--){
char op[2];
int a,b;
scanf("%s",op);
//在a和b连通,即将集合b的根节点指向集合a的根节点
if(op[0]=='C'){
scanf("%d%d",&a,&b);
x = find(a),y = find(b);
if(x==y) continue;
p[y] = x;
Size[x] +=Size[y];
}
//a、b是否在一个连通块中,就是a的根节点和b的根节点相不相同
else if(op[1] =='1'){
scanf("%d%d",&a,&b);
if(find(a)==find(b)) puts("Yes");
else puts("No");
}
else{
scanf("%d",&a);
printf("%d\n",Size[find(a)]);
}
}
return 0;
}
四、堆
小根堆:每一个点都满足它小于等于它左右两边的点。
x左儿子:2x,右儿子:2x+1。
堆的操作:
- 插入一个数。
- 求集合当中的最小值。
- 删除最小值。
- 删除任意一个元素。
- 修改任意一个元素。
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int n,m;
int heap[N],Size;
//超时
// void down(int k){
// if(2*k<=Size&&heap[k]>heap[2*k]){
// int x = heap[2*k];
// heap[2*k] = heap[k];
// heap[k] = x;
// down(2*k);
// }
// if(2*k+1<=Size&&heap[k]>heap[2*k+1]){
// int x = heap[2*k+1];
// heap[2*k+1] = heap[k];
// heap[k] = x;
// down(2*k+1);
// }
// }
void down(int u)
{
int t = u;
if (u * 2 <= Size && heap[u * 2] < heap[t]) t = u * 2;
if (u * 2 + 1 <= Size && heap[u * 2 + 1] < heap[t]) t = u * 2 + 1;
if (u != t)
{
swap(heap[u], heap[t]);
down(t);
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i = 1;i<=n;i++) scanf("%d",&heap[i]);
Size = n;
//堆排序
for(int i = n/2;i;i--) down(i);
while(m--){
printf("%d ",heap[1]);
heap[1] = heap[Size];
Size--;
down(1);
}
return 0;
}