类似并查集里面的按秩合并
定义
一开始是每一个数单独一个集合每一次是将某一个集合里面所有元素,合并到另外一个集合里
如果用暴力来进行合并的话,假设每一次合并都是O(op)的时间复杂度,然后按照最坏的情况,一共有n个,第一个合并到第二个里面,第二个再合并到第三个里面,以此类推,然后这样的话,第一次操作一个数,第二次操作两个数,以此类推,一直到最后一个需要操作n-1位数,这样总共的时间复杂度就是O(
n
2
n^2
n2op)时间复杂度较高
然后我们在合并的时候,可以考虑做一步优化,每一次合并其实本质上是将两个集合合并,每次合并加一个启发式的操作,就是每次将一个元素少的集合合并到元素多的集合里面,这样我们就可以把时间复杂度降到O(nlogn * op)
为什么呢?
我们可以从另外一个角度来看,如果我们直接去算每个集合里有多少个元素,每一次合并时间复杂度是O(op)总共是O(n * op)的时间复杂度,这样算比较难算
我们来观察每个元素对最终计算量的贡献是多少,每个元素对于最终计算量的贡献其实就是看一下这个元素被合并多少次,那么这个元素被合并多少次取决于这个元素所在的集合被合并多少次,也就是取决于这个元素每次所在的集合被合并的次数
然后我们看一下,每个元素每次所在的集合最多会被合并多少次
由于我们每一次是把元素较少的集合合并到元素较多的集合里面,因此对于每一个元素,如果这个元素所在的集合每合并一次,它所在集合的元素数量就至少会乘2,一共有n个元素,所以每个元素最多就合并logn次, 所以最多合并的次数就是nlogn * op,
普通启发式合并
题意就是说,有一堆布丁,每个布丁有一个颜色,然后有一个操作,每次能让相同颜色的布丁换个颜色,问连续颜色的布丁有多少段
这个题目可以看成一个集合合并,就是每一个颜色看成一个集合
首先就是要把颜色一一对应,如上图
#include<bits/stdc++.h>
using namespace std;
#define x first
#define y second
#define endl '\n'
const int N = 1e5 + 10, M = 1e6 + 10;
int h[M], e[N], ne[N], idx;
int n, m, a, b, c;
int sz[M], p[M], color[N];//sz代表每种颜色的集合大小,p[]是代表每种颜色对应哪个编号
int ans;
void add(int a, int b) // 添加一条边a->b
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
sz[b] ++;
}//h[a]存储的是最后一个挂在链表a上的节点编号,ne[idx]则是存的上一个
void merge(int &x, int &y)
{
if(x == y) return;
if(sz[x] > sz[y]) swap(x, y);
for (int i = h[x]; ~i; i = ne[i])
{
int j = e[i];
ans -= (color[j - 1] == y) + (color[j + 1] == y);
}
for (int i = h[x]; ~i; i = ne[i])
{
int j = e[i];
color[j] = y;
if (ne[i] == -1)
{
ne[i] = h[y];//将x的最后一个节点的下一个连到y颜色的第一个节点,将两个串合并
h[y] = h[x];//将y颜色的头节点连接到x的第一个节点上
break;
}
}
h[x] = -1;
sz[y] += sz[x], sz[x] = 0;
}
int main()
{
memset(h, -1, sizeof h);
// puts("!!!!!!!!!!");
scanf("%d%d", &n, &m);
for(int i=1;i<=n;i++)
{
scanf("%d", &color[i]);
if (color[i] != color[i - 1]) ans ++ ;
add(color[i], i);
}
for(int i=1; i < M; ++i) p[i] = i;
while(m--)
{
scanf("%d", &c);
if(c == 2) cout << ans << endl;
else
{
scanf("%d%d", &a, &b);
merge(p[a], p[b]);
}
}
return 0;
}