7.1 启发式合并、Manacher算法
启发式合并
启发式合并类似于并查集的按秩合并.
例如,一开始有 n n n 个数,每个数在仅包含自己的单独的集合当中,每次将某个集合内的所有元素,合并到另外一个集合当中,最终要将所有数都合并到一个集合当中.
如果暴力地去实现的话,将包含 a a a 个元素的集合,合并到另外一个集合当中,则其时间复杂度为 O ( a × o p ) O(a \times op) O(a×op),其中 o p op op 为每个元素合并到另一个集合的时间复杂度.
其中 a a a 可以取为 1 ∼ n − 1 1 \sim n-1 1∼n−1,故暴力实现的总时间复杂度为 O ( n 2 × o p ) O(n^2 \times op) O(n2×op),这是一个比较慢的时间复杂度.
- ∑ i = 1 n − 1 i = ( 1 + n − 1 ) × ( n − 1 ) 2 = n 2 − n 2 ≈ n 2 \sum_{i=1}^{n-1}i=\frac{(1+n-1)\times(n-1)}{2}=\frac{n^2-n}{2}\approx n^2 ∑i=1n−1i=2(1+n−1)×(n−1)=2n2−n≈n2.
可以考虑在合并时做一个优化,因为每一次合并实际上是将两个集合合并成一个集合,可以将第一个集合合并到第二个集合当中,也可以将第二个集合合并到第一个集合当中.
所以每次合并的时候,加一个启发式的操作,即将元素少的集合合并到元素多的集合当中,这样就可以把总时间复杂度降到 O ( n log n × o p ) O(n\log n \times op) O(nlogn×op).
- 并查集的按秩合并是依据树的高度来合并的,而启发式合并是按照元素个数来合并的,但思想是类似的.
证明
如果直接用一个集合有 k k k 个元素,合并一次要 O ( k × o p ) O(k \times op) O(k×op) 的时间复杂度的方式去算,是比较困难的.
可以考虑换一种方式来算,考虑对于每个元素来说,它最终对计算量的贡献是多少(即它被合并多少次)?
例如将第一个集合与第二个集合合并,则在第一个集合内的元素 u u u 被合并了一次,随后将第二个集合与第三个集合合并,则元素 u u u 又被合并了一次,所以每个元素最终被合并的次数取决于每次它所在集合被合并的次数.
那么每个元素所在集合最多会被合并多少次呢?由于每次都是将元素较少的集合合并到元素较多的集合当中,如果元素较少的集合内有 x x x 个元素的话,那么合并之后的集合内的元素一定 ⩾ 2 x \geqslant 2x ⩾2x.
所以对于某一个元素 u u u 而言,它所在的集合每合并一次,它所在集合的元素数量就至少会 × 2 \times 2 ×2.
一共有 n n n 个元素,所以对于每个元素而言,最多只会合并 log n \log n logn 次,所以最多总共合并的次数为 n log n n\log n nlogn 次.
证毕
AcWing 2154. 梦幻布丁
一天,小徐的好友邀请他去吃布丁,于是小徐高高兴兴的来到好友家。
哇,这么多五彩缤纷的布丁!
好友说:“在我们开吃前先玩会儿游戏吧。”
于是他将布丁摆成一行,接着说:“我可以把某种颜色的布丁全部变成另一种颜色,我还会在某些时刻问你当前一共有多少段颜色。例如:颜色分别为 1 , 2 , 2 , 1 1,2,2,1 1,2,2,1 的四个布丁一共有 3 3 3 段颜色。”
输入格式
第一行包含整数 n n n 和 m m m,分别表示布丁的个数和好友的操作次数。
第二行包含 n n n 个空格隔开的整数 A 1 , A 2 , … , A n A_1,A_2,…,A_n A1,A2,…,An,其中 A i A_i Ai 表示第 i i i 个布丁的颜色。
从第三行起的 m m m 行,依次描述 m m m 个操作。
对每个操作,若第一个数是 1 1 1,则表示好友要改变颜色,这时后跟两个整数 x x x 和 y y y(可能相等),表示执行该操作后所有颜色为 x x x 的布丁被变成颜色 y y y。
若第一个数是 2 2 2,则表示好友要询问目前有多少段颜色,这时应该输出一个整数回答。
输出格式
对于每个询问,在一行中输出一个整数作为回答。
数据范围
0 < n , m < 100001 0<n,m<100001 0<n,m<100001,
0 < A i , x , y < 1 0 6 0<A_i,x,y<10^6 0<Ai,x,y<106
输入样例:
4 3
1 2 2 1
2
1 2 1
2
输出样例:
3
1
时/空限制: 1s / 64MB
来源: HNOI2009
算法标签:启发式合并
链表
Treap
线段树
yxc’s Solution
-
如何统计不同的颜色段数?
- 初始的时候,可以直接扫描一遍.
-
每次在合并的时候,考虑如何维护段数?
-
第一种操作,是将某种颜色的布丁全都变成另外一种颜色,其实本质上就是将这两种颜色的布丁合并成同一类(也就是说,不论是颜色 x x x 变为颜色 y y y,还是颜色 y y y 变为颜色 x x x,对答案的影响没有区别);
-
显然,将两种颜色的布丁合并不会让颜色段数增加,考虑两个相邻的布丁,只有当它们原本颜色相同而经过操作后颜色不同,才会使得颜色段数增加;但这是不可能的,因为每次操作都是将一种颜色全部变成另外一种颜色;
-
考虑什么情况会让颜色段数减少,只有当两个相邻的布丁,在原本颜色不同的情况下,因为操作使得它们的颜色变得相同,才能使颜色段数减少 1 1 1;
举例,对于颜色为 x x x 的布丁,对它进行操作时,检查其左右两边的布丁是否为其要变成的颜色 y y y.
如果左边的布丁颜色为 y y y 的话,则颜色段数 − 1 -1 −1;如果右边的布丁颜色为 y y y 的话,则颜色段数 − 1 -1 −1.
-
每次想要将颜色为 x x x 的布丁全部变为颜色为 y y y 的布丁,只需要遍历颜色为 x x x 的布丁,判断其左右两边的布丁颜色即可;
-
如果直接暴力合并的话,时间复杂度最坏是 O ( n 2 ) O(n^2) O(n2) 的,所以可以使用启发式合并使时间复杂度降到 O ( n log n ) O(n\log n) O(nlogn).
-
-
如何去存储信息?
-
先开一个数组来表示所有颜色,来表示某种颜色对应的颜色编号,一开始,所有颜色的对应的颜色编号就是其本身;
-
每种颜色对应的编号会拉一个单链表出来,用来存储所有这种颜色的布丁的位置;
-
每次将颜色为 x x x 的布丁合并到颜色为 y y y 的布丁时(这里假设颜色为 x x x 的布丁数量比颜色为 y y y 的布丁数量小),需要将颜色为 x x x 的布丁的链表接到颜色为 y y y 的布丁的链表上.
颜色数组图示注意,如果这里的操作与输入数据中的 x x x 和
-