浅谈类并查集链表

· · 个人记录

名字是不知道哪里看到的,如果不对请吱一声。

类并查集链表是一种奇怪的数据结构。

它通常用于维护一个序列内的标记信息:

  1. 将一个被标记的数字取消标记。
  2. 查询某数右侧第一个被标记的数字。
  3. 遍历一个区间 [l,r] 内所有未被标记的数字

则可以使用类并查集链表解决。其前两种均摊每次时间复杂度与并查集相同,即 O(\log n)(路径压缩);或 O(\alpha(n))(路径压缩+按秩合并)。第三种则需要乘以当前被标记的数的个数 cnt。因此它通常适用于“修改及访问次数有一定限制”的操作。

我们对于每个数维护一个指针 p_i 表示自己以及右边第一个被标记的数字。若 i 被标记,令 p_i=i,否则 p_i=i+1。则对 p 数组进行并查集的 find 操作就能实现第二种操作。

当进行第一种操作时,也是直接令 p_i=i+1,执行 find(i+1) 操作就找到了。

遍历区间,只需在开始时,令 t=find(l),每次访问完 t 这个位置,就令 t=find(t+1),循环直至 t>r

显然路径压缩可以使用,相当于一个退化的并查集,均摊 find() 时间复杂度不会超过 O(\log n),但仍然可以通过构造达到。

至于按秩合并需要一些技巧(因为指的方向会改变)那就咕咕一下。

code:

struct DSU {
    int fa[N], co[N];
    int sz;

    void init(int x) {
        sz = x;
        for (int i = 1; i <= x; i++) fa[i] = i; // 这里初始都打上了标记。
    }

    int findfa(int x) {
        if (fa[x] == x) return x;
        return fa[x] = findfa(fa[x]);
    }
    void solve(int type, int l, int r, int k) {
        for (int i = findfa(l); i <= r; i = findfa(i + 1)) {
            // 维护信息,然后删除 i 的标记
            fa[i] = findfa(i + 1);
        }
    }
};

例题 1: P9715 头

这里本来应该有个头的图片。可惜被和谐了。

你有一个 nm 列的网格,所有格子上都没有颜色。有 k 种颜色的刷子,颜色编号为 1\sim k。然后给出 q 次操作,每次操作给出 op,l,r,c,t 五个参数:

在所有涂色操作结束以后,对于每种颜色,求出有多少个格子被染成了这种颜色。

--- 考虑离线后从后往前进行所有 $t=1$ 操作,再从前往后进行 $t=0$ 的,就只剩下染色后不再次染色的了。则每个位置就只会被染色一次。 对行、列分别维护一个类并查集链表,跳到没有染色的位置暴力染色,均摊就只有 $O(n)$ 次了。 个数的维护比较容易,你只需要知道每次染了几个行,有多少列是没被染色过的,乘起来计算就行了。 时间复杂度 $O(n \log n)$,这里认为 $n,m,q,k$ 都同阶。 --- 直接来个 Ynoi!~~纪念一下首次 AC 珂学题。~~ **例题 2: P5610 大学** 一个长为 $n$ 的**非负**整数序列 $a$,支持以下两个操作 $m$ 次: - `1 l r x`:把区间 $[l,r]$ 中所有 $x$ 的倍数除以 $x$。 - `2 l r`:查询区间 $[l,r]$ 的和。 强制在线,$n,m\le 10^5$,$0\le a_i,x\le 5\times 10^5$,$x\neq 0$,0.5s,500M ---- 忽略所有 $x=1$,则对每个位置的操作次数之和只有 $O(n\log V)$ 级别。 考虑对于每个 $i \in[2,V]$,维护是它的倍数的所有位置。注意到这些数的个数是 $O(nd(V))$ 的,$d(x)$ 表示 $x$ 因数个数。直接搞 $V$ 个类并查集链表,寻找每个初始数的所有因数,对于每个并查集维护初始是它倍数的数,要用 vector 开。 每次对于 $\div x$ 操作,直接找到 $x$ 号类并查集链表,把 $[l,r]$ 扫一遍,同时清除已经不是 $x$ 的倍数的标记。 时间复杂度 $O(n\left[\sqrt V+d(V)\right]+n\log V\log n)$。**这是 Ynoi,请注意常数。**