寒假集训Day14 并查集
并查集
用途
我们拥有一个含有n个元素的大集合,这其中不同的元素属于不同的小集合,并查集可以快速查询元素属于哪个集合以及对小集合进行合并
方法
并查集运用树的想法,把每一个小集合制作成一个二叉树,所以针对每一次的查询和合并我们只需要找到每个点的祖先节点并进行操作即可
初始化
初始化只需要让所有的节点都是自己的祖先节点即可
p[i] = i
查询
通过递归不断找到该节点的祖先节点
int find (int x) {
if(p[x] == x) return x;
else return find(f[x]);
}
合并
我们对于两个节点,分别找到他们的祖先,然后将一个祖先指向另一个祖先
void merge(int x,int y) {
int px = find(x);
int py = find(y);
if(px == py) return;
else p[px] = p[y];
}
优化
针对上面的代码,我们发现,对于一棵树中的一个点,我们每次find就只能网上查询一个父节点,如果这棵树很深,find就会调用很多次,所以我们需要想办法把树建立的浅一些,进行路径压缩
void find(int x) {
return x == p[x] ? x : p[x] = find(p[x]);
}
如果我们将较小的集合合并到较大的集合中,可以减少查询次数。
inline void merge(int x,int y){
x = find(x), y = find(y);
if(x == y) return;
if(siz[x] > siz[y]) swap(x,y);
f[x] = y, siz[y] += siz[x]; //siz数组用fill赋初始值为1
}
最小生成树
https://www.bilibili.com/video/BV1Eb41177d1/?spm_id_from=333.337.search-card.all.click&vd_source=58841ca2fbce224209b8b4eaa2fae884
我们将一个无向连通图通过删掉一些边将它变成一个树,称之为生成树,其中,边权之和最小的树称为最小生成树
求最小生成树一共有两种方法:
kruskal算法
这种算法运用了贪心的想法,我们开结构体把每条边储存起来,然后按照边权由小到大排序,每次取出一条边进行连接,然后判断连接后是否构成环,如果构成环就舍弃这条边
重点在于判断是否构成环,我们可以采用并查集,把连接后的点进行合并,使之被放在“同一集合”中,每次加入新的边只需要通过find查询一下这条边的起点和终点是否具有相同的祖先,如果具有的话就说明这两个点已经连通,如果再连接的话就会形成环,因此舍弃这条边。
在连接了n-1条边后我们就可以停止继续连接了,因为一棵树最多有n-1条边,多了就没必要看了
需要注意的是,kruskal算法存图的时候是按照边来存的,这和传统链前存图不一样,你无法对他进行图上操作,因此如果想要对生成的树进行一些操作就需要重新开一个链前,在用kruskal加边的同时进行存图
#include <bits/stdc++.h>
using namespace std;
int cnt = 0;
int n,m;
struct edge{
int u,v,w;
}e[1000001];
bool cmp(edge a,edge b) {
return a.w < b.w;
}
int p[10000001];
void add(int a,int b,int c) {
cnt++;
e[cnt].u = a;
e[cnt].v = b;
e[cnt].w = c;
}
int find (int x) {
return x == p[x] ? x : p[x] = find(p[x]);
}
int num = 0;
bool judge[10000001];
int siz[10000001] = { };
void merge(int x,int y) {
x = find(x);
y = find(y);
if(x == y) return;
if(siz[x] > siz[y]) swap(x,y);
p[x] = y,siz[y] += siz[x];
}
int kruskal() {
sort(e + 1,e + 1 + m,cmp);
int sum = 0;
for(int i = 1;i <= cnt; i++) {
if(num == n - 1) return sum;
int x = e[i].u;
int y = e[i].v;
int px = find(e[i].u);
int py = find(y);
if(px != py) {
merge(x,y);
num++;
sum += e[i].w;
}
}
return sum;
}
int main () {
scanf("%d %d" ,&n,&m);
memset(judge,false,sizeof(judge));
for(int i = 1;i <= n; i++) {
p[i] = i;
}
fill(siz + 1,siz + 1 + n,1);
for(int i = 1;i <= m; i++) {
int x,y,z;
scanf("%d %d %d" ,&x,&y,&z);
add(x,y,z);
}
int ans = kruskal();
if(siz[find(n)] != n) { //判断是否是连通图
printf("orz");
return 0;
}
printf("%d" ,ans);
return 0;
}
prim
prim的想法是花费最小的代价加点,大体步骤就是找出一个点,扫描与这个点连通的所有边,然后选择边权最小的边加入生成树中
对我来说这个算法的好处就是可以只用存一遍图,用一个链前基本就能搞定了,但是在复杂度方面并没有太大优势,在稠密图中理论上略优

浙公网安备 33010602011771号