最近在刷状压 DP,结果发现太难不会做,跑来学点别的。
反正 CSP-S2 之前刷完就行了,吧?
放在数据结构里面是因为 CDQ 分治和数套树能解决的问题差不多,所以放了进去(绝不是因为懒得开一个“离线算法”的 Tag!)
CDQ 分治是一种通过把动态询问/点对问题等离线处理,并分治求解的算法。这种思想最早于 IOI2008 国家集训队选手陈丹琦在其论文 从《Cash》谈一类分治算法的应用 中总结,故得名。
虽然原论文给出的例题是[NOI2007] 货币兑换,但我们先从另一个经典题目——三维偏序讲起。
洛谷传送门
有 $ n $ 个元素,第 $ i $ 个元素有 $ a_i,b_i,c_i $ 三个属性,设 $ f(i) $ 表示满足 $ a_j \leq a_i $ 且 $ b_j \leq b_i $ 且 $ c_j \leq c_i $ 且 $ j \ne i $ 的 \(j\) 的数量。
对于 $ d \in [0, n-1] $,求 $ f(i) = d $ 的数量。(这里描述用 \(n-1\) 代替右开)
解析:这题有很多做法,如树套树、K-D Tree。这里当然只介绍 CDQ 分治的做法。
假设只有一个属性,即对每个 \(i\) 找到 \(a_j\leq a_i\) 的数量。显然我们只要按照 \(a_i\) 大小排序,那么对于 \(a_i\) 就有 \(i-1\) 个数字比它小。
当属性有两个的时候,同样按照 \(a_i\) 排序,然后从前往后扫描。那么对于 \(i\) 前面的元素 \(j\) 一定有 \(a_j\leq a_i\),任务就变成找 \(b_j\leq b_i\) 的数量。一种办法是先把 \(b\) 离散化,然后用类似树状数组求逆序对的方式求解即可。
当然也可以像归并排序求逆序对一样,先做排序,然后根据 \(i,j\) 在两个分治区间内的情况分类讨论:
这样每次分治都要做一次 \(O(n)\) 的双指针,总复杂度为 \(O(n\log n)\)。
现在来到三维。类比上面提到的归并排序方法,先做一遍排序。那么对于每个 \(i\),满足条件的 \(j\) 一定在左边。同样,我们对 \(i,j\) 的情况做分类讨论:同在左区间,同在右区间,分别位于两个区间。前两个分治即可,重点看第三个。
由于做了排序,所以 \(a_j\leq a_i\) 一定满足。 接下来对于 \(b_j\leq b_i\),用双指针寻找第一个 \(b_j> b_i\) 的位置,那么每个 \(i\) 都可以用 \(O(n)\) 找到对应的 \(j\),即答案在左区间边界到 \(j-1\) 的范围内。最后对于 \(c_j\leq c_i\),用树状数组就可求出。
这样每次分治都要做 \(O(n)\) 双指针,且每次移动指针都要用 \(O(\log n)\) 的树状数组维护答案,故总时间复杂度为 \(O(n\log ^2 n)\)。
总结一下,CDQ 分治的本质其实就是消除偏序维度。对于一维,直接排序;对于二维。在一维的基础上做树状数组/分治;对于三维,再在二维基础上加。从某种程度上说,CDQ 分治也可以看作归并排序一类分治的扩展算法。顺带一提,树套树求三维偏序的本质也是消除维度,不过是用权值线段树代替分治、支持强制在线罢了。
#include<cctype> #include<cstdio> #include<algorithm> #include<cstring> struct __Coel_FastIO { #ifndef LOCAL #define _getchar_nolock getchar_unlocked #define _putchar_nolock putchar_unlocked #endif inline __Coel_FastIO& operator>>(int& x) { x = 0; bool f = false; char ch = _getchar_nolock(); while (!isdigit(ch)) { if (ch == '-') f = true; ch = _getchar_nolock(); } while (isdigit(ch)) { x = x * 10 + ch - '0'; ch = _getchar_nolock(); } if (f) x = -x; return *this; } inline __Coel_FastIO& operator<<(int x) { if (x < 0) { x = -x; _putchar_nolock('-'); } static int buf[35]; int top = 0; do { buf[top++] = x % 10; x /= 10; } while (x); while (top) _putchar_nolock(buf[--top] + '0'); return *this; } inline __Coel_FastIO& operator<<(char x) { return _putchar_nolock(x), *this; } } qwq; const int maxn = 2e5 + 10; int n, m, top = 1; int ans[maxn]; struct node { int a, b, c; int res, cnt; //记录出现次数,处理属性相等的状态 bool operator<(const node &x) const { if (a != x.a) return a < x.a; if (b != x.b) return b < x.b; return c < x.c; } bool operator==(const node &x) const { return a == x.a && b == x.b && c == x.c; } } q[maxn], tem[maxn]; class Fenwick_Tree { private: #define lowbit(x) (x & (-x)) int c[maxn]; public: void add(int x, int v) { for (int i = x; i < maxn; i += lowbit(i)) c[i] += v; } int query(int x) { int res = 0; for (int i = x; i; i -= lowbit(i)) res += c[i]; return res; } } T; void CDQ_Divide(int l, int r) { if (l >= r) return; int mid = (l + r) >> 1; CDQ_Divide(l, mid), CDQ_Divide(mid + 1, r); int i = l, j = mid + 1, k = 0; while (i <= mid && j <= r) //两个区间都没遍历完时 if (q[i].b <= q[j].b) //不满足条件,把信息存入树状数组 T.add(q[i].c, q[i].cnt), tem[k++] = q[i++]; else // 满足条件,在树状数组上得到答案 q[j].res += T.query(q[j].c), tem[k++] = q[j++]; while (i <= mid) T.add(q[i].c, q[i].cnt), tem[k++] = q[i++]; while (j <= r) q[j].res += T.query(q[j].c), tem[k++] = q[j++]; //将没有处理完的部分继续处理 for (i = l; i <= mid; i++) T.add(q[i].c, -q[i].cnt); //还原树状数组 for (i = l, j = 0; j < k; i++, j++) q[i] = tem[j]; //将归并排序后结果复制到原数据中 } int main(void) { qwq >> n >> m; for (int i = 0; i < n; i++) qwq >> q[i].a >> q[i].b >> q[i].c, q[i].cnt = 1; std::sort(q, q + n); for (int i = 1; i < n; i++) { // 特判属性相等时的答案 // 这里的 top 其实也起到了去重的作用 if (q[i] == q[top - 1]) q[top - 1].cnt++; else q[top++] = q[i]; } CDQ_Divide(0, top - 1); for (int i = 0; i < top; i++) ans[q[i].res + q[i].cnt - 1] += q[i].cnt; for (int i = 0; i < n; i++) qwq << ans[i] << '\n'; return 0; }