对于n个集合来说,如果把它们依次合并的时间复杂度是O(n^2),如下:
image.png
第一次合并只需要1个,第二个要合并2个,后面3个4个。。。。
这样1+2+3+….+n,所以耗费的时间复杂度为O(n^2)

对于启发式合并来讲,时间复杂度可以降低到O(nlogn),思想是每次合并,让集合元素少的那个向多的一方合并。
证明如下:
考虑每个元素对于合并复杂度的贡献,按照集合元素少的向多的一方合并,找到合并次数最多的元素(应该是集合元素个数最少的那个集合),此时看这个集合中某一个元素e,他在合并到别的集合中时,那集合元素个数至少翻一倍,每次合并最少翻一倍的情况,那这个元素e最多合并logN次,他的贡献就是logN,一共n个元素,所以最终复杂度为O(nlogn)。

ps:如果想不明白可能和我思路卡在一个位置,就是下面这句话: 他在合并到别的集合中时,那集合元素个数至少翻一倍,每次合并最少翻一倍的情况,那这个元素e最多合并logN次 我在想的时候认为每次都把少的向大的合并,那如果每次都向第一个集合合并岂不是要N次,怎么会是logN呢。这里要注意的是贡献值是元素e合并了多少次,看N^2的暴力做法,就是一号点被合并了多少次,那就都是他合并到了别的集合中。而每次都向第一个集合合并,很显然除了第一次其余的合并都不能算是元素e的贡献。

例题:

1. 梦幻布丁

题目大意:给定n个数字,每个数字代表一个颜色,有两种操作:

  1. 每次操作把所有颜色为X的变成Y
  2. 查询有多少段颜色,连续相同的算作一段

思路:
暴力做法复杂度很明显每次扫描一遍,M次操作,因此复杂度为O(nm),大概在1e10左右。
把一种颜色全部变成另一种颜色想成把X的所有数字全部合并到Y里面去。最终的结果一定是所有的都变成一个颜色(都在一个集合中)。根据上面的启发式合并思想,复杂度降低为O(NlogN)

具体写法:

  1. 查询直接输出即可,最开始直接扫描一遍段数就出来了
  2. 当把X变成Y时,如果X的个数比Y多,就要反过来把Y合并到X中(可以映射一下颜色,比如1和2开始代表的颜色是1和2,当要把1合并到2中,但是发现应该2合并到1更好,那只需要让1代表2,2代表1就行了)。如果X的个数比Y少就正常合并即可
  3. 可以维护一个邻接表,(现在假设X要合并到Y中),那么遍历颜色为X的链表,判断每个点的前后是不是颜色Y,如果是Y,那么段数-1。然后改变X的颜色,把X的那个链表用尾插法放到Y中
  4. 这里可以证明一开始段数是最多的,因为如果让段数变多,必然需要有某一段之中原来颜色相同,现在颜色不同,这很显然不可能,一个颜色要变全变的。
  5. 剩下的看代码吧,有注释。
  1. #include <iostream>
  2. #include <cstring>
  3. #include <algorithm>
  4. using namespace std;
  5. const int N = 100010, M = 1000010;
  6. int seg;
  7. int n, m;
  8. int h[M], e[N], ne[N], idx;
  9. int color[N], sz[M], p[M];
  10. void add(int a, int b) {
  11. e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
  12. sz[a] ++ ; // 记录当前颜色有多少个(当前集合有多少个点)
  13. }
  14. void merge(int& x, int& y) { // 注意这里的引用啊!!
  15. if (x == y) return;
  16. if (sz[x] > sz[y]) swap(x, y); // 映射真实颜色
  17. for (int i = h[x]; ~i; i = ne[i]) {
  18. int j = e[i];
  19. seg -= (color[j - 1] == y) + (color[j + 1] == y); // 如果前面的点和要变成的点一样,那段数就会减一,后面也一样
  20. }
  21. for (int i = h[x]; ~i; i = ne[i]) {
  22. int j = e[i];
  23. // 这里为什么不和上面一起写呢?画个图就知道了,连续的一段会多减一部分
  24. color[j] = y;
  25. if (ne[i] == -1) { // 尾插,先让x的尾巴连上y的头,然后y的头换成x的头
  26. ne[i] = h[y], h[y] = h[x];
  27. break;
  28. }
  29. }
  30. h[x] = -1;
  31. sz[y] += sz[x], sz[x] = 0; // 记录当前集合个数,很重要啊!!按它合并的
  32. }
  33. int main()
  34. {
  35. memset(h, -1, sizeof h);
  36. cin >> n >> m;
  37. for (int i = 1; i <= n; i ++ ) {
  38. scanf("%d", &color[i]);
  39. add(color[i], i);
  40. if (color[i] != color[i - 1]) seg ++ ; // 记录段数
  41. }
  42. // 映射颜色,一开始颜色都是自己对应自己,但是如果要反过来合并,颜色直接调换即可
  43. for (int i = 0; i < M; i ++ ) p[i] = i;
  44. while (m -- ) {
  45. int op, a, b;
  46. scanf("%d", &op);
  47. if (op == 2) printf("%d\n", seg);
  48. else {
  49. scanf("%d%d", &a, &b);
  50. // 这里要按实际颜色合并
  51. merge(p[a], p[b]);
  52. }
  53. }
  54. return 0;
  55. }