■并查集是一种可以动态维护若干个不重叠的集合,并支持合并与查询的数据结构。
–Find(x):查询元素x所在集合
–Merge(x, y):将x所在集合与y所在集合合并
■集合的表示方法:为每个集合选择一个固定的元素,作为这个集合的代表元。
■用一棵树形结构存储每个集合,树上每个节点都是一个元素,树根是集合的代表元素。用fa[x]保存x的父亲节点,根的fa值为它本身。
■初始时
1 2 3 4 5 6 7
fa: 1 2 3 4 5 6 7
■
■合并两个集合时,只需要连接两个树根(即令一个树根为另一个树根的子节点,fa[x]=y)
路径压缩:
■可以发现,我们只关心每个集合的代表元(即每棵树的根节点),而不关心这棵树的具体形态——这意味着下图两棵树是等价的。
自行脑补;
■因此我们可以在每次执行Find操作的同时,把访问过的每个节点都直接指向树根。
■采用路径压缩优化的并查集,每次Find操作的均摊复杂度为O(logN)
初始化
for (int i=1; i<=n; i++) fa[i]=i;
Find
int Find(int x) {
if (x==fa[x]) return x;
return fa[x]=Find(fa[x]);
}
Merge
void Merge(int x ,int y) {
fa[Find(x)]=Find(y);
}
带权并查集:
■并查集实际上是由若干棵树构成的森林,我们可以在树中的每条边上记录一个权值,即维护一个数组d,用d[x]保存节点x到父节点fa[x]之间的边权。
■在每次路径压缩后,每个访问过的点都会直接指向树根,如果我们同时更新这些节点的d值,就可以利用路径压缩的过程来统计每个节点到树根之间的路径上的一些信息。
Int Find(int x) { void Merge(int x,int y){
if (x==fa[x]) return x; x=Find(x);y=Find(y);
int root=Find(fa[x]); fa[x]=y;
d[x]+=d[fa[x]]; d[x]=size[y];
return fa[x]=root; size[y]+=size[x];
} }
扩展域并查集:
■有些情况下,元素不只含有一个属性,传递的关系也不只一种。因此我们可以将每个元素拆成多个元素,在合并的时候维护关系的传递性,通过扩展出来的属性来维护传递性。
最小生成树:
■定理:任意一棵最小生成树一定包含无向图中权值最小的边。
■推论:如果某个连通图属于最小生成树,那么所有从外部连接到该连通图的边中的一条最短的边必然属于最小生成树。
Kruskal:
■维护无向图的最小生成森林,最初可认为生成森林由零条边组成。
■在任意时刻,从剩余的边中选出一条权值最小的,并且这条边的两个端点不连通,把该边加入森林。
具体流程:
■建立并查集,每个点是一个集合
■把所有边按照权值从小到大排序,依次扫描每条边(x,y,z)
■若x,y属于同一集合,则忽略这条边,继续扫描下一条
■否则,合并x和y所在的集合,并把z累加到答案中
■添加了n-1条边后结束
–复杂度O(mlogm)
int Kruskal() {
for (int i=1;i<=n;i++) fa[i]=i;
sort(edge+1,edge+1+m);
for (int i=1;i<=m;i++) {
int x=Find(edge[i].x),y=Find(edge[i].y);
if (x==y) continue;
fa[x]=y;
ans+=edge[i].z;
}
return ans;
}
Prim:
■维护最小生成树的一部分,最初仅确定1号点属于最小生成树。
■在任意时刻,设已经确定属于最小生成树的节点集合为T,剩余节点集合为S,找到两个端点分别属于集合S,T的权值最小的边(x,y,z),然后把点x从集合S中删除,加入到集合T,并把z累加到答案中。
■复杂度O(n^2) 可用堆优化到O(mlogn)
■主要用于稠密图尤其是完全图
具体流程:
■d[x]:若x∈S,表示x与T集合中节点权值最小的边的权值,否则,表示x被加入T集合时选中的边的权值
■用一个数组标记节点是否属于T,开始只有节点1属于T
■每次从未被标记的节点中选出d值最小的,把它标记;同时扫描它的所有出边,更新到另一个端点的d值
■答案为?=2n?[?]∑_(n=2)^n▒〖d[x]〗
void Prim() {
d[1]=0;
for (int i=1;i<=n;i++) {
int x=0;
for (int j=1;j<=n;j++)
if (!v[j] && (x==0||d[j]<d[x]) x=j;
v[x]=1;
for (int y=1;y<=n;y++)
if (!v[y] ) d[y]=min(d[y],e[x][y]);
}
}