启发式合并
一般启发式合并
\;\; 考虑用贡献法来分析。我们令两个集合的分别为 A 和 B,且 |A|<|B|,那么我们把 A 暴力加入到 B 中。那么 A 中的元素所在的集合大小变成 |A|+|B|,也就是说至少变成了原来的两倍。所以每个元素至多被加入 nlogn 次,总的复杂度为 O(nlogn)。
入门例题:
梦幻补丁
题意:
给定长度为n的数组,进行m次操纵。
op == 1 x y:把颜色为x的数组修改为颜色y
op == 2 : 查询此时数组所能形成的段数。
思路:
启发式合并 + 维护链表
发现每种元素只能有一个颜色,且ans是只减不增的。且发现无论是把颜色x—>y,还是颜色y–>x对答案的贡献是一样的。因为两者最后都会成为同一种颜色。考虑启发式合并,将sz[]小的合并到sz[]多的。假设要求要把颜色x–>y,但是sz[y] < sz[x],要把所有颜色为y的合并到颜色为x上,那么下次又要把颜色y的元素涂成颜色z,可以发现此时颜色y的元素已经没有了。优化了这一步,便可实现O(nlogn),考虑如何做到O(1)交换。 只需要维护一个链表 + 映射数组fa[]即可。
code:
const int maxn = 1e6 + 10;
typedef pair <int, int> PII;
int h[maxn], ne[maxn], e[maxn], idx, fa[maxn], ans, sz[maxn], color[maxn];
void add(int u, int v)
{
e[idx] = v;
ne[idx] = h[u];
h[u] = idx ++;
}
void merge(int &a, int &b)// a --- > b
{
if(sz[a] > sz[b])
swap(a, b); // a小
for(int i = h[a] ; i != -1 ; i = ne[i])
{
int son = e[i]; //下标
ans -= (color[son - 1] == b) + (color[son + 1] == b);
}
for(int i = h[a] ; i != -1 ; i = ne[i])
{
int son = e[i];
color[son] = b;
if(ne[i] == -1)
{
ne[i] = h[b], h[b] = h[a], sz[b] += sz[a] ,h[a] = -1, sz[a] = 0;
break;
}
}
}
int main()
{
int n, m;
scanf("%d %d", &n, &m);
memset(h, -1, sizeof(h)), idx = 0;
for(int i = 1 ; i <= n ; i ++)
{
scanf("%d", &color[i]);
add(color[i], i);
sz[color[i]] ++;
}
ans = 1;
for(int i = 2 ; i <= n ; i ++)
{
if(color[i] != color[i - 1])
ans ++;
}
for(int i = 0 ; i <= 1e6 + 2 ; i ++)
fa[i] = i;
for(int i = 1 ; i <= m ; i ++)
{
int op;
scanf("%d", &op);
if(op == 1)
{
int x, y;
scanf("%d %d", &x, &y);
if(x == y) continue;
merge(fa[x], fa[y]);
}
else
{
printf("%d\n", ans);
}
}
return 0;
}