并查集是什么?
并查集是一种用来管理元素分组情况的数据结构
可以高效进行如下操作
-
查询元素a和元素b 是否属于同一组。
-
合并元素a和元素b 所在的组
并查集解决的是连通性问题
连通性问题:
-
定义:对于集合中的点,连通某些点及查询某些点是否连通
并查集的quick_find(查找是否连通很快)算法:
-
1、基于染⾊的思想,⼀开始所有点的颜⾊不同
-
2、连接两个点的操作,可以看成将⼀种颜⾊的点染成另⼀种颜⾊
-
3、如果两个点颜⾊⼀样,证明连通,否则不连通
1.合并(连通)操作-- O(n)
-
连通的不是两个点,而是两个点集(点集内的点颜色相同),将一个点集的颜色染给另一个点集
-
通常采用遍历所有的点,找到其中要染色的点,进行染色--O(n)
2.连通判断--O(1)
-
判断颜色是否相同
代码演示:
#include
<iostream>
#include
<cstdio>
using
namespace
std;
#define
MAX_N
10000
int
color[
MAX_N
+ 5];
void
init(
int
n
) {
for
(
int
i = 0; i <=
n
; i++) color[i] = i;
return
;
}
//连通判断
int
find(
int
a
) {
return
color[
a
];
}
//合并操作
int
merge(
int
a
,
int
b
,
int
n
) {
int
aa = find(
a
), bb = find(
b
);
if
(aa == bb)
return
0;
//将 aa 染成 bb
for
(
int
i = 0; i <=
n
; i++) {
if
(color[i] == aa) {
color[i] = bb;
}
}
return
1;
}
void
output(
int
n
) {
int
ret = 0;
for
(
int
i = 0; i <=
n
; i++) {
ret += printf(
"%3d"
, i);
}
printf(
"\n"
);
for
(
int
i = 0; i < ret; i++) printf(
"-"
);
printf(
"\n"
);
for
(
int
i = 0; i <=
n
; i++) {
printf(
"%3d"
, color[i]);
}
printf(
"\n"
);
return
;
}
int
main() {
int
n, a, b;
cin
>>
n;
init(n);
while
(cin
>>
a
>>
b) {
printf(
"merge %d with %d : %d\n"
, a, b, merge(a, b, n));
output(n);
}
return
0;
}
并查集的quick_union算法
4->3
3->8
根节点的判断
-
当父节点的值等于当前节点的值,则该节点为根节点。如上图的:0,1,2,5,7,8,9
合并操作:
1.将相互连通的节点集合用树型结构保存
2. 合并操作时,
连通的不是两个点,而是两个点集,实际上是将⼀棵树作为另⼀棵树的⼦树
3.一个节点的入度可以有很多个,但出度最好为0/1(便于数组保存)
查询操作
为了查询两个节点是否同一组,只要沿着树向上走,查询根节点是否相同,根节点相同时同一组,否则不同组。
例子:
#include
<iostream>
using
namespace
std;
#define
MAX_N
10000
int
fa[
MAX_N
+ 5];
void
init(
int
n
) {
for
(
int
i = 0; i <=
n
; i++) {
fa[i] = i;
}
return
;
}
//查找当前节点的连通点集的根节点
int
find(
int
x
) {
if
(fa[
x
] ==
x
)
return
x
;
return
find(fa[
x
]);
}
//合并
int
merge(
int
a
,
int
b
) {
int
aa = find(
a
), bb = find(
b
);
if
(aa == bb)
return
0;
fa[aa] = bb;
return
1;
}
void
output(
int
n
) {
int
ret = 0;
for
(
int
i = 0; i <=
n
; i++) {
ret += printf(
"%3d"
, i);
}
printf(
"\n"
);
for
(
int
i = 0; i < ret; i++) printf(
"-"
);
printf(
"\n"
);
for
(
int
i = 0; i <=
n
; i++) {
printf(
"%3d"
, fa[i]);
}
printf(
"\n"
);
return
;
}
int
main() {
int
n, a, b;
cin
>>
n;
init(n);
while
(cin
>>
a
>>
b) {
printf(
"merge %d with %d : %d\n"
, a, b, merge(a, b));
output(n);
}
return
0;
}
quick_union的问题
1、极端情况下会退化成⼀条链
2、将节点数量多的接到少的树上面,导致了退化
3、将树⾼深的接到浅的上面,导致了退化
按秩优化:
-
目的:防止并查集的树形结构退化成一个链表,保持一颗树形结构的查找效率
-
操作:按节点数量/树的高度合并
-
算法: Weighted Quick-Union
Weighted Quick-Union
-
对于每棵树,记录树的高度(treeheight)/每棵子树的节点数量(size)
-
合并时,如果两棵树的 treeheight/size 不同,那么 treeheight/size 小的向 treeheight/size 大的连边。
路径压缩:
-
对于每个节点,一旦向上走到了一次根节点,就把这个点到父亲的边改成为直接连向根。
-
如需要查询(7),就可以直接将(7)连接到根上。
-
在此之上,不仅查询的节点,所有在查询过程中经过的所有节点,都可以直接连接到根上。再次查询时,就可以很快查询到根是谁了。
-
如下,将(2)(3)(4)(5)都连接到(1)中。
-
代码演示:
#include
<iostream>
using
namespace
std;
#define
MAX_N
10000
int
fa[
MAX_N
+ 5];
int
_size[
MAX_N
+ 5];
void
init(
int
n
) {
for
(
int
i = 0; i <=
n
; i++) {
fa[i] = i;
_size[i] = 1;
}
return
;
}
//查找当前节点的连通点集的根节点,并将返回的根节点赋值给路径上所有的子节点
int
find(
int
x
) {
return
fa[
x
] = (fa[
x
] ==
x
?
x
: find(fa[
x
]));
}
//合并,节点数目较少的树的根节点,连接到节点数量较大的树的根节点下
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];
}
else
{
fa[bb] = aa;
_size[aa] += _size[bb];
}
return
1;
}
void
output(
int
n
) {
int
ret = 0;
for
(
int
i = 0; i <=
n
; i++) {
ret += printf(
"%3d"
, i);
}
printf(
"\n"
);
for
(
int
i = 0; i < ret; i++) printf(
"-"
);
printf(
"\n"
);
for
(
int
i = 0; i <=
n
; i++) {
printf(
"%3d"
, fa[i]);
}
printf(
"\n"
);
for
(
int
i = 0; i <=
n
; i++) {
printf(
"%3d"
, _size[i]);
}
printf(
"\n"
);
return
;
}
int
main() {
int
n, a, b;
cin
>>
n;
init(n);
while
(cin
>>
a
>>
b) {
printf(
"merge %d with %d : %d\n"
, a, b, merge(a, b));
output(n);
}
return
0;
}
一些映射关系:
1.二维数组:坐标 -->下标
2.哈希函数
注意:
不等关系不是连通关系
带权的并查集
简单定义
1.N个节点有M对关系(M条边),每对关系(每条边)都有一个权值w,可以表示距离或划分成多个集合时的集合编号.
全值的理解:
连通关系比较复杂,所以在路径上加上权值,表示连通关系中的
不同情况。比如连通关系为输赢,节点之间就有输,赢,平的不同情况,这时我们就可以在路径上赋不同的值,表示不同的情况。
代码演示:
HZOJ: 72
#include
<iostream>
#include
<algorithm>
#include
<string>
#include
<unordered_map>
#include
<unordered_set>
#include
<vector>
#include
<algorithm>
using
namespace
std;
class
UnionSet
{
public
:
vector
<
int
> fa, value;
UnionSet(
int
n
) :fa(
n
+ 1), value(
n
+ 1) {
for
(
int
i = 0; i <=
n
; i++) {
fa
[
i
]
= i;
//value[i]表示从i出发的路径的权值
value
[
i
]
= 0;
}
}
int
get(
int
x
) {
if
(fa
[
x
]
==
x
)
return
x
;
int
root = get(fa
[
x
]
);
//权值跟着路径压缩,%3是例题的权值范围是0,1,2
value
[
x
]
= (value
[
x
]
+ value
[
fa
[
x
]]
) % 3;
return
fa
[
x
]
= root;
}
void
merge(
int
a
,
int
b
,
int
t
) {
int
aa = get(
a
), bb = get(
b
);
if
(aa == bb)
return
;
fa
[
aa
]
= bb;
//保证对3取余的结果是整数
value
[
aa
]
= (
t
+ value
[
b
]
- value
[
a
]
+ 3) % 3;
return
;
}
};
int
main() {
int
n, m;
ios
::sync_with_stdio(
false
);
cin.tie(
NULL
);
cin
>>
n
>>
m;
UnionSet
u(n);
for
(
int
i = 0; i < m; i++) {
int
a, b, c;
cin
>>
a
>>
b
>>
c;
if
(a == 1) {
u.merge(b, c, 2);
}
else
{
if
(u.get(b) != u.get(c)) {
cout
<<
"Unknown"
<<
endl;
}
else
{
//保证对3取余的结果是整数
switch
((u.value
[
b
]
- u.value
[
c
]
+ 3) % 3) {
case
0:cout
<<
"Tie"
<<
endl;
break
;
case
1:cout
<<
"Loss"
<<
endl;
break
;
case
2:cout
<<
"Win"
<<
endl;
break
;
}
}
}
}
return
0;
}