一、简单并查集
1、简单并查集主要是解决相互连通的多个群体,也可以维护具有传递性的关系及其连通性
一般分为三个部分:初始化,寻找根find,合并根merge
for (int i = 1; i <= n; i++)
{ // 初始化,每个点刚开始的根节点是自己本身
fa[i] = i;
}
int find(int x) //最朴素的find
{
while (x != fa[x])
x = fa[x];
reutrn x;
}
int find(int x) //路径压缩
{
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void merge(int x, int y) //朴素合并
{
x = find(x), y = find(y);
if (x!=y)
fa[x] = y;
}
2、路径压缩 (最坏时间复杂度O(logn))
在每次寻找x的父亲时,在找到父亲后将该点直接指向根结点
还有一种写法就是将找父亲的过程中遇到的所有非根节点指向根节点
int find(int x)
{
int fx=x;
while (x != fa[x]) //寻找根节点
{
int pre = x;
x = fa[x];
fa[pre] = fx;
}
return fx;
}
3、按秩合并(最坏时间复杂度O(α(n)))
用size代表块的大小,将小的块合并到大的块中,注意sz只有在根节点才有意义
int fa[N], sz[N]; //sz代表该树的大小(深度),初始化时赋1
void merge(int x, int y)
{
x = find(x), y = find(y);
if (x != y)
{
if (sz[x] > sz[y])
swap(x, y);
fa[x] = y; // 将小的集合作为儿子,加入到大的集合里面去
sz[y] += sz[x]; // 更新大的集合的大小
}
}
二、带权并查集
可以另外维护一个数组d[x]代表x到父节点fa[x]之间的边权,路径压缩后,每个访问过的结点都会直接指向树根,如果我们同时更新d[x]的值,就可以利用路径压缩过程来统计每个结点到树根之间的路径上的一些信息,这就是”边带权“
例1:
求size:那么就是求该结点的父节点,然后找父节点的size[x]
求移动次数:移动次数可以等价为该点在树中的深度,但在路径压缩的情况下不能直接进行深度递归,这是我们维护d数组,初始化为0(每个结点都是根,深度为0),d[x]代表x相对于x目前的根节点的深度;
在find中,d[x]加上未更新的父节点的d[fa[x]](x尚未更新的根节点的相对于此时真正的根节点的深度),这是一个递归的过程,先寻找到根节点,再回溯更新权值和根节点,也就是说,如果不find那么路径就没有压缩
在merge中,先进行find()并且路径压缩,然后将x(被合并的元素的根节点)的深度(从0)置1,y的深度仍未0
int find(int x)
{
if (x == fa[x])
return x;
int root = find(fa[x]);
d[x] += d[fa[x]];
return fa[x] = root;
}
void merge(int x, int y) //将x合并到y处
{
x = find(x), y = find(y);
fa[x] = y;
sz[y] += sz[x];
d[x] = 1;
}
cout << find(a) << ' ' << sz[find(a)] << ' ' << d[a] << '\n';
//分别代表,a所在地方,a所在地方的人数,a移动过的次数
例2:
d[x]记录战舰x与fa[x]之间的边的权值,在路径压缩把x指向树根的同时,我们把d[x]更新为x到树根的路径上的所有边权之和(在压缩前尚未连到树根),这里的边权是size
int find(int x)
{
if (x == fa[x])
return x;
int root = find(fa[x]);
d[x] += d[fa[x]];
return fa[x] = root;
}
void merge(int x, int y)
{
x = find(x), y = find(y);
fa[x] = y;
d[x] = sz[y];
sz[y] += sz[x];
}
int temp = find(a); //用之前要find,进行路径压缩
cout << d[a] << '\n';
三、拓展域并查集(种类并查集)
拓展域并查集常用来维护多组关系的集合合并问题,集合之间的传递关系一般为同类和排斥(相异)关系
我们可以用1~n代表一类的集合,n+1~2*n代表另一类的集合,那么如果两个物品分别属于两类,那么他们就是相互排斥的;如果两个物品同在1~n或者同在n+1~2*n,那么他们的同类的
例1:
维护三种集合ABC,有同类和吃的关系,给出m句话判断真假并添加真关系,最后输出假个数
int fa[maxn*3];
int same(int x, int y)
{
return find(x) == find(y);
}
void solve()
{
int i, n, m;
cin >> n >> m;
for (i = 1; i <= n * 3; i++) //维护三组集合就开三倍n
{
fa[i] = i;
}
int ans = 0;
while (m--)
{
int a, x, y;
cin >> a >> x >> y;
if (x > n || y > n)
{
ans++;
continue;
}
if (a == 1)
{
if (same(x, y + n) || same(x, y + n * 2))
ans++;
else
{
merge(x, y);
merge(x + n, y + n);
merge(x + n * 2, y + n * 2);
}
}
else
{
if (same(x, y) || same(x, y + n * 2))
ans++;
else
{
merge(x, y + n);
merge(x + n, y + n * 2);
merge(x + n * 2, y);
}
}
}
cout << ans << '\n';
}
例2:
维护单词(字符串)之间的同义反义关系
bool same(int x, int y)
{
return find(x) == find(y);
}
void solve()
{
int n, m, k;
unordered_map<string, int>mp;
cin >> n >> m >> k;
for (int i = 0; i <= n * 2; i++)
{
fa[i] = i;
sz[i] = 1;
}
string s;
for (int i = 1; i <= n; i++)
{
cin >> s;
mp[s] = i;
}
int ope;
string s1, s2;
for (int i = 0; i < m; i++)
{
cin >> ope >> s1 >> s2;
int x = mp[s1], y = mp[s2];
if (ope == 1)
{
if (same(x, y + n) || same(x + n, y))
cout << "NO" << '\n';
else
{
merge(x, y);
merge(x + n, y + n);
cout << "YES" << '\n';
}
}
else
{
if (same(x, y) || same(x + n, y + n))
cout << "NO" << '\n';
else
{
merge(x, y + n);
merge(x + n, y);
cout << "YES" << '\n';
}
}
}
for (int i = 0; i < k; i++)
{
cin >> s1 >> s2;
int x = mp[s1], y = mp[s2];
if (same(x, y) || same(x + n, y + n))
cout << "1\n";
else if (same(x, y + n) || same(x + n, y))
cout << "2\n";
else
cout << "3\n";
}
}
四、边带权和拓展域的并查集的关系
当我们把边的权值设置为(比如)0、1,并且用(比如)异或来处理边权变化,我们就可以等价地维护一个拓展域的并查集
例:
带权并查集解法:
int fa[maxn * 2], sz[maxn * 2];
int l[maxn], r[maxn], a[maxn * 2];
int ope[maxn];
void solve()
{
int n, m;
cin >> n >> m;
int cnt = 0;
a[0] = 0;
string s;
for (int i = 0; i < m; i++)
{
cin >> l[i] >> r[i] >> s;
ope[i] = (s[0] == 'e' ? 0 : 1);
a[++cnt] = l[i];
a[++cnt] = r[i];
}
sort(a, a + cnt);
cnt = unique(a, a + cnt) - a;
for (int i = 0; i < m; i++)
{
l[i] = lower_bound(a, a + cnt, l[i]) - a;
r[i] = lower_bound(a, a + cnt, r[i]) - a;
}
n = lower_bound(a, a + cnt, n) - a + 1;
for (int i = 0; i <= n * 2; i++)
{
fa[i] = i;
sz[i] = 1;
}
int i;
for (i = 0; i < m; i++)
{
int x = l[i] - 1, y = r[i];
if (ope[i] == 0)
{
if (same(x, y + n) || same(x + n, y))
break;
else
merge(x, y), merge(x + n, y + n);
}
else
{
if (same(x, y) || same(x + n, y + n))
break;
else
merge(x, y + n), merge(x + n, y);
}
}
cout << i;
}
带权并查集:
int fa[maxn], d[maxn];
int l[maxn], r[maxn], a[maxn * 2];
int ope[maxn];
int find(int x)
{
if (x == fa[x])
return x;
int root = find(fa[x]);
d[x] ^= d[fa[x]];
return fa[x] = root;
}
void solve()
{
int n, m;
cin >> n >> m;
int cnt = 0;
a[0] = 0;
string s;
for (int i = 0; i < m; i++)
{
cin >> l[i] >> r[i] >> s;
ope[i] = (s[0] == 'e' ? 0 : 1);
a[++cnt] = l[i];
a[++cnt] = r[i];
}
sort(a, a + cnt);
cnt = unique(a, a + cnt) - a;
for (int i = 0; i < m; i++)
{
l[i] = lower_bound(a, a + cnt, l[i]) - a;
r[i] = lower_bound(a, a + cnt, r[i]) - a;
}
n = lower_bound(a, a + cnt, n) - a;
for (int i = 0; i <= n; i++)
{
fa[i] = i;
d[i] = 0;
}
for (int i = 0; i < m; i++)
{
int x = l[i] - 1, y = r[i];
int fx = find(x), fy = find(y);
if (fx == fy)
{
if ((d[x] ^ d[y]) != ope[i])
{
cout << i << '\n';
return;
}
}
else
{
fa[fx] = fy;
d[fx] = d[x] ^ d[y] ^ ope[i];
}
}
cout << m << '\n';
}