题解:P3810 【模板】三维偏序(陌上花开)

· · 题解

这是一篇二进制分组套小波矩阵的题解。

静态二维偏序

三维偏序太困难了,我不会!

不妨先来考虑一个相关的问题,静态二维偏序:

有一个 m \times m 的二维平面,初始给定其上 n 个点 (x_i,y_i),有 q 次查询每次给出 x_0,y_0,求满足 x_i \le x_0 \wedge y_i \le y_0 的点数(n,m,q 同阶)。

这题我会!用主席树即可,预处理时空复杂度 O(n\log{m})-O(n\log{m}),单次询问复杂度 O(\log{m})

要是我不会主席树,或者主席树被卡常,或者要求 O(n) 线性空间怎么办?

我们可以使用小波矩阵 (Wavelet Matrix),如果你不知道小波矩阵是什么,可以查看我的 专栏 获得详细讲解,这里只做简要说明。

小波矩阵是基于值域二进制分解的静态简洁数据结构,能在 O(n\log{m})-O(\frac{n\log{m}}{\omega}) 的时空复杂度预处理后,单次 O(\log{m}) 地回答区间 k 小或区间排名等查询(若查询信息带权,空间复杂度为 O(n\log{m}))。

预处理

假设当前要构建序列 a_i 的小波矩阵 b_{u,i}

初始取二进制最高位,令 u=\log{m}

可以发现当 $u \rarr u-1$ 时,1 个区间只会被拆成 2 个区间,且一个第 $u$ 位全 0,另一个第 $u$ 位全 1,下文分别称为 0/1 区间,通过维护 $b_{u,i}$ 的前缀和可以快速转移。 由于 $b_{u,i}=0/1$,排序是 $O(n)$ 的,并且可以用 `bitset` 压位存储,因此预处理时空复杂度 $O(n\log{m})-O(\frac{n\log{m}}{\omega})$。 ### 查询区间排名 假设当前要求 $[L,R]$ 中 $\le v$ 的元素个数。 维护序列上的区间 $[l,r]$ 表示,此区间中的元素未被统计,且区间外的元素都被统计,初始令 $l=L,r=R$,以及二进制位 $u=\log{m}$。 若 $v$ 的第 $u$ 位为 1,那么所有 0 区间的元素都加入答案,将 $[l,r]$ 转移到 1 区间,否则直接将 $[l,r]$ 转移到 0 区间,令 $u \larr u-1$。 枚举结束后后统计的是 $<v$ 的元素个数,还需将答案加上 $r-l+1$。 每次转移通过 $O(1)$ 查询 $b_{u,i}$ 的前缀和可以做到 $O(1)$,因此单次查询复杂度 $O(\log{m})$,因此如果要查询 $b_{u,i}$ 的带权前缀和,只能 $O(n\log{m})$ 空间。 ### 解决问题 将点按 $(x,y)$ 的关键字排序后,构建 $y_i$ 的小波矩阵,每次查询时二分出按 $(x,y)$ 关键字比较时,$(x_k,y_k) \le (x_0,y_0)$ 的最大 $k$,在小波矩阵上查询 $[1,k]$ 中 $\le y_0$ 的元素个数即为答案,总复杂度 $O(n\log{n}+(n+q)\log{m})$。 # 静态转动态 再考虑另一个相关问题,动态二维偏序: 有一个 $m \times m$ 的二维平面,有 $n$ 次操作:插入一个点 $(x,y)$,或查询有多少已插入的点中满足 $x_i \le X \wedge y_i \le Y$。 考虑如何用静态的小波矩阵解决动态问题。 ### 分块 一个比较直接的想法是,设立阈值 $B$,每有 $B$ 次插入就令其为一组,构建这 $B$ 个点的小波矩阵。 查询时,枚举已构建的 $O(\frac{n}{B})$ 组小波矩阵进行查询,而对于还未成组的 $O(B)$ 次插入,直接暴力枚举检查,总复杂度为 $O(\frac{n}{B} \cdot B\log{m} + n \cdot (\frac{n}{B} \cdot \log{m} + B))$,取 $B=\sqrt{n\log{m}}$ 平衡查询,得到最优复杂度 $O(n\log{m} + n\sqrt{n\log{m}})=O(n\sqrt{n\log{m}})$。 ### 自顶向下分组 考虑优化未成组暴力查询的部分,若每 $B_2$ 个插入重构为大组,未重构为大组的 $O(B_2)$ 个插入内每 $B_1$ 个插入重构为小组,未重构为小组的 $O(B_1)$ 个插入暴力查询,最终平衡复杂度得到 $O(n^{\frac{4}{3}}\log^{\frac{2}{3}}{m})$。 再取一次阈值呢?设每 $B_1$ 个插入重构为小组,每 $B_2$ 个插入重构为中组,每 $B_3$ 个插入重构为大组,平衡后复杂度为 $O(n^{\frac{5}{4}}\log^{\frac{3}{4}}{m})$。 通过计算可以发现,若每 $B_k$ 个插入重构为最大的组,一直到每 $B_1$ 个插入重构为最小的组,复杂度为 $O(kn\log{m}(\frac{n}{\log{m}})^{\frac{1}{k}})$。 ### 二进制分组(自底向上合并) 但是这样的自顶向下散块分组很不实用,太麻烦了!不如转换思路,自底向上整块合并。 也即,设若干阈值 $B_1,B_2,\dots,B_k$,每 $B_1$ 个插入重构为 1 级组,每 $B_2$ 个 1 级组重构为 2 级组,…,每 $B_i$ 个 $i-1$ 级组重构为 $i$ 级组。 发现每个 $i$ 级组内含 $\prod_{u=1}^i B_u$ 个元素,因此直接令 $B_1=B_2=\dots=B_k=2$,得到 $k=\log{n}$,这样每个插入会参与 $O(\log{n})$ 次重构,每次查询最多有 $O(\log{n})$ 组。 这就是**二进制分组**,若已插入 $k$ 个元素,$u$ 级组存在当且仅当 $k$ 的第 $u$ 位为 1,如果画出合并树,容易发现与线段树本质相同。 # 二维转三维 对于三维偏序,我们将所有元素按 $(a,b,c)$ 关键字排序并去重,记 $v_i$ 表示有多少个元素和 $i$ 相同,这样 $i$ 统计到的元素都在 $[1,i-1]$。 随后将元素 $(a_i,b_i,c_i,v_i)$ 视为,查询此时二维平面上有多少点满足 $x \le b_i \wedge y \le c_i$,之后在平面上插入 $v_i$ 个点 $(b_i,c_i)$。 这样三维偏序就被处理成了动态二维偏序。 综上,将三维偏序转为动态二维偏序,使用二进制分组套小波矩阵,我们可在 $O(n\log^2{n})-O(\frac{n\log{n}}{\omega})$ 的时空复杂度预处理后,以 $O(\log^2{n})$ 的复杂度回答单次询问。 # 扩展 如果题目操作除了插入还有删除怎么办?插入 $2^k-1$ 个点后,重复插入删除,每次操作都要重构 $2^k-1$ 个点,那复杂度就假了。 对于数点这种有可减性的信息,我们可以对删除操作另维护一组负贡献的二进制分组,查询时两组做差。 若信息不具有可减性,但是删除操作是撤销上一次插入,那么可以考虑懒惰合并,即当有 3 个 $k-1$ 级组时合并其中 2 组至 $k$ 级,这样每 $O(2^k)$ 次操作才会重构一次 $k$ 级组,均摊复杂度还是 $O(\log{n})$ 的。 如果不想用二进制分组,可以直接把小波矩阵做成动态的吗? 可以,把内层的 `bitset` 换成平衡树即可。 # 实现 小波矩阵查询时,维护的区间可写成左闭右开 $(l,r]$,这样就无需对 $l$ 进行偏移。 合并时实际上不需要清空不存在的组别,可以只重构新出现的组别。 [提交记录](https://www.luogu.com.cn/record/221308799),这里的实现魔改了专栏中的模板,可能常数较大(857ms),空间为线性(5.79mb)。 ```cpp #include <bits/stdc++.h> using namespace std; namespace staring { using LL = long long; using ULL = unsigned long long; #define fir first #define sec second #define FOR(i,a,b) for(int i = (a), i##E = (b); i <= i##E; i ++) #define ROF(i,a,b) for(int i = (a), i##E = (b); i >= i##E; i --) template <typename TYPE> int gmax(TYPE &x, const TYPE& y) {return x < y ? x = y, 1 : 0;} template <typename TYPE> int gmin(TYPE &x, const TYPE& y) {return y < x ? x = y, 1 : 0;} static constexpr int SIZE = 1 << 20; static char buffin[SIZE]{}, *pin1{}, *pin2{}; static char buffout[SIZE]{}, *pout{buffout}; #define GETC() (pin1 == pin2 && (pin2 = (pin1 = buffin) + fread(buffin, 1, SIZE, stdin), pin1 == pin2)? EOF : *pin1++) #define PUTC(c) (pout - buffout == SIZE && (fwrite(buffout, 1, SIZE, stdout), pout = buffout), (*pout++ = c)) template <typename TYPE> void read(TYPE &x) { static int signf{0}, chin{0}; x = signf = 0, chin = GETC(); while(chin < '0' || chin > '9') signf |= chin == '-', chin = GETC(); while(chin >= '0' && chin <= '9') x = (x << 3) + (x << 1) + (chin ^ 48), chin = GETC(); if(signf) x = -x; } template <typename TYPE> void write(TYPE x, char ch = ' ') { static int stack[64]{}, top{0}; !x && PUTC('0'), x < 0 && (x = -x, PUTC('-')); while(x) stack[top++] = x % 10, x /= 10; while(top) PUTC(stack[--top] | 48); if(ch) PUTC(ch); } }using namespace staring; constexpr int N = 1e5 + 5; tuple <int, int, int> t[N]; pair <int, int> lshy[N]; int lshz[N], tmp0[N], tmp1[N << 1]; int res[N]; struct bitSet { #define ppcll __builtin_popcountll int n, zero; vector <pair <ULL, int>> bit; bitSet(int _n): n(_n), zero(_n) { bit.resize((n >> 6) + 1, make_pair(0, 0)); } void clear() { zero = n; fill(begin(bit), end(bit), make_pair(0, 0)); } void set(int k) { --zero; bit[k >> 6].fir |= 1ull << (k & 63); } void init() { FOR(i, 1, n >> 6) bit[i].sec = bit[i - 1].sec + ppcll(bit[i - 1].fir); } int rank1(int k) { return bit[k >> 6].sec + ppcll(bit[k >> 6].fir << (~k & 63)); } int rank0(int k) { return k - rank1(k); } #undef ppcll }; struct waveletMatrix { int len, cnt; vector <bitSet> bit; void init(int _len) { len = _len; bit.resize(len + 1, bitSet(1 << len)); } void build(int tot) { int p = tot >> len + 1 << len + 1; int l = p + 1, r = p + (1 << len); sort(lshy + l, lshy + r + 1); sort(lshz + l, lshz + r + 1); cnt = unique(lshz + l, lshz + r + 1) - lshz - l; FOR(i, l, l + cnt - 1) tmp1[lshz[i]] = i - l; FOR(i, 1, 1 << len) tmp0[i] = tmp1[lshy[i + p].sec]; ROF(u, __lg(cnt), 0) { int x = 0, y = 0; bit[u].clear(); FOR(i, 1, 1 << len) if(tmp0[i] >> u & 1) tmp1[++y] = tmp0[i], bit[u].set(i); else tmp0[++x] = tmp0[i]; bit[u].init(); memcpy(tmp0 + x + 1, tmp1 + 1, sizeof(int[y])); } } int id(int u, int k, int c) { return !c ? bit[u].rank0(k) : bit[u].zero + bit[u].rank1(k); } int ranking(int tot) { int p = tot >> len + 1 << len + 1, L = p + 1, R = p + (1 << len); int l = 0, r = upper_bound(lshy + L, lshy + R + 1, lshy[tot + 1]) - lshy - L; int v = upper_bound(lshz + L, lshz + L + cnt, lshy[tot + 1].sec) - lshz - L; int res = 0; ROF(u, __lg(cnt), 0) { int c = v >> u & 1; if(c) res += bit[u].rank0(r) - bit[u].rank0(l); l = id(u, l, c), r = id(u, r, c); } return res; } }; void mainSolve() { int n, m; read(n), read(m); FOR(i, 1, n) { auto& [a, b, c] = t[i]; read(a), read(b), read(c); } int d = 0; while(1 << d < n) ++d; vector <waveletMatrix> wave(d); FOR(i, 0, d - 1) wave[i].init(i); sort(t + 1, t + n + 1); FOR(i, 1, n) { auto [x, y, z] = t[i]; lshy[i] = make_pair(y, z); lshz[i] = z; } FOR(i, 1, n) { int j = i; while(j < n && lshy[j + 1] == lshy[i]) ++j; int cur = j - i; for(int d = i - 1; d; d &= d - 1) { int u = __lg(-d & d); cur += wave[u].ranking(i - 1); } res[cur] += j - i + 1; for(int d = j; d; d &= d - 1) { int u = __lg(-d & d); if((j >> u) == (i - 1 >> u)) break; wave[u].build(j); } i = j; } FOR(i, 0, n - 1) write(res[i], '\n'); } int main() { auto fileIO = [](string file) { string In = file + ".in"; string Out = file + ".out"; freopen(In.c_str(), "r", stdin); freopen(Out.c_str(), "w", stdout); }; // fileIO(""); int testCount = 1; // read(testCount); while(testCount--) mainSolve(); fwrite(buffout,1,pout-buffout,stdout); return 0; } ```