「学习笔记」启发式合并
一. 简介
启发式合并是一种合并方法,它通过两两合并的方法将 \(n\) 个元素的信息合并到一起。时间复杂度是 \(O(n\log n)\)。这种方法被广泛运用在各种数据结构中。
二.算法介绍:
方法:合并两个集合 \(s1,s2\) 时,若 \(|s1|<|s2|\),则将 \(s1\) 中的元素加入 \(s2\)。
复杂度分析:
考虑贡献发。合并两个集合时,若元素属于较小的集合,那么它会被合并到较大的集合中。若一个元素被转移,则它所处的集合的大小至少变成了原来的两倍。假设集合大小的上限是 \(n\),则一个元素最多被转移 \(\log n\) 次,\(n\) 个元素最多被转移 \(n\log n\) 次,所以启发式合并的时间复杂度是 \(O(n\log n)\) 的。
三.例题讲解
P3201 [HNOI2009] 梦幻布丁
对于此题,先求出原序列的答案,每一种颜色都用类似链表的数据结构存储起来,并记录下结束节点。每次修改都根据启发式合并的方式来暴力合并,然后处理这次合并对于答案的影响。(答案是不增的)
如果我们将 \(1\) 染成 \(2\) 并且 \(|s_1|>|s_2|\),那么我们应该将 \(2\) 接到 \(1\) 的后面。这里有一个问题:这次修改后这个链的颜色是 \(1\) (颜色为 \(2\) 的链被删除了),如果接下来修改颜色 \(2\),会因为找不到颜色 \(2\) 而只能找到颜色 \(1\)。所以我们需要用一个 \(f\) 数组,表示当我们需要寻找颜色 \(x\)时,只需要寻找颜色为 \(f_x\) 的链。也就是说,遇到上面这种情况需要交换 \(f_1\) 和 \(f_2\)。
查询时,在输入时已经求好了原序列的答案,当某个元素变为颜色 \(B\) 时,左边的元素为 \(A\),右边的元素为 \(C\) 时,答案分三种情况:
\(1.\) \(A\not = B \not = C\),那么答案不变;
\(2.\) \(A=B\not = C\),那么答案减一;
\(3.\) \(A\not=B=C\),那么答案减一。
代码如下:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e6 + 7;
int n, m, col[N], siz[N], head[N], nxt[N], f[N], res = 0, op, x, y;
inline void add_edge (int x, int i) {
nxt[i] = head[x];
head[x] = i;
siz[x] ++;
}
inline void merge (int &x, int &y) {
if (siz[x] > siz[y]) {
swap (x, y);
}
if (!siz[x] || x == y) {
return ;
}
for (int i = head[x]; i != -1; i = nxt[i]) {
if (col[i - 1] == y) {
res --;
}
if (col[i + 1] == y) {
res --;
}
}
for (int i = head[x]; i != -1; i = nxt[i]) {
col[i] = y;//直接修改
if (nxt[i] == -1) {
nxt[i] = head[y];
head[y] = head[x];
break;//合并
}
}
siz[y] += siz[x], siz[x] = 0, head[x] = -1;
}
int main () {
cin >> n >> m;
for (int i = 1; i < N; i ++) {
head[i] = -1;
f[i] = i;
}
for (int i = 1; i <= n; i ++) {
cin >> col[i];
int x = col[i];
add_edge (x, i);//前向星记录
if (nxt[i] != i - 1) {
res ++;//计算答案
}
}
for (int i = 1; i <= m; i ++) {
cin >> op;
if (op == 1) {
cin >> x >> y;
merge (f[x], f[y]);
}
else if (op == 2) {
cout << res << endl;
}
}
return 0;
}