• CDQ分治解决三维偏序问题


    CDQ 分治

    二元关系

    有两个集合 (A), (B), 他们的笛卡尔积 (A imes B)(|A| imes|B|) 个点对构成的集合, 即 (A) 中的每个元素和 (B) 中每个元素组成的有序点对.

    定义 (A imes B) 的一个子集 (R), 则 (R)(A)(B) 的一个二元关系.

    偏序

    偏序也是一种二元关系, 设一个定义在同一个集合 (A) 中的二元关系 (R in A imes A), 满足以下性质:

    • 自反性

      对于每个元素 (a in A)((a, a) in R).

    • 反对称性

      如果存在 ((a, b) in R)((b, a) in R), 则 (a = b).

    • 传递性

      如果存在 ((a, b) in R)((b, c) in R), 则 ((a, c) in R).

    (R) 是一个偏序.

    如果 (R) 还满足:

    • 完全性

      对于每一个 (a in A), (b in A), 一定有 ((a, b) in R)((b, a) in R).

    (R) 是一个全序.

    一般我们说的偏序都是非严格偏序 (自反偏序), 但是与之相对, 有一种严格偏序 (反自反偏序), 严格偏序在满足传递性的基础上, 满足:

    • 反自反性

      对于每个元素 (a in A) 不存在 ((a, a) in R).

    • 禁对称性

      如果存在 ((a, b) in R) 则不存在 ((b, a) in R).

    其实对于每个严格偏序中的点对, 在对应的非严格偏序中一定存在, 如果每个点对表示一条有向边, 则严格偏序一定对应一个 DAG, 而非严格偏序中会增加一些自环和重边.

    算法竞赛中的题目, 一般偏序的要求是满足反自反性, 对称性和传递性.

    二维偏序

    如果有有序二元组 ((a, b)) 构成的集合 (A), 则一个有序二元组的有序点对 (R in A imes A) 就是一个二维偏序.

    如果规定 (((a_i, b_i), (a_j, b_j)) in R), 当且仅当 (a_i leq a_j)(b_i leq b_j), 求 (|R|), 这就是一道很简单的二维偏序题.

    很幸运, 二维偏序一般不需要 CDQ 分治, 只要对所有二元组按 (a) 为第一关键字, (b) 为第二关键字排序, 然后建立树状数组, 从第一个开始, 依次查询 ([1, b_i])(b) 的数量, 然后插入 (b_i) 即可.

    高端的食材, 往往只需要采用最朴素的烹饪方式.

    但是, 我们使用同样高端的烹饪方式又有何不可?

    尝试用 CDQ 分治解决二维偏序.

    已经按 (a) 第一, (b) 第二排好序了, 将所有二元组分成左右两部分, ([1, Mid]), ((Mid, n]). 这时, 满足 (((a_i, b_i), (a_j, b_j)) in R)(i), (j) 有三种情况:

    • (i in [1, Mid]), (j in (Mid, n])

    • (i in [1, Mid]), (j in [1, Mid])

    • (i in (Mid, n]), (j in (Mid, n])

    对于第一种情况, 按 (b) 可以用双指针 (O(n)) 求出, 对于后两种, 可以递归解决, 总复杂度还是 (O(nlogn)).

    这种双指针 + 递归的分治思想是不是非常眼熟? 没错, 这就是归并排序的加强版.

    三维偏序

    同理, 如果有有序三元组 ((a, b, c)) 构成的集合 (A), 定义 (R in A * A), 当前仅当 (a_i leq a_j), (b_i leq b_j), (c_i leq c_j) 同时成立时, (((a_i, b_i, c_i), (a_j, b_j, c_j)) in R). 求 (|R|).

    很显然, 这是一道三维偏序题, 仍然是先按 (a) 排序. 然后分成两部分, 还是存在那三种情况.

    • (i in [1, Mid]), (j in (Mid, n])

    • (i in [1, Mid]), (j in [1, Mid])

    • (i in (Mid, n]), (j in (Mid, n])

    仍然思考如何求出第一种情况数量. 因为已知 (a_i leq a_j), 所以相当于在 (A) 的子集 (B), (C) (即左右两段的点代表的集合), 求二维偏序 (R in B imes C)(|R|).

    参考二维偏序的做法, 对于左右两段分别按 (b) 排序, 双指针 (i), (j) 扫描, 使得 (b_i = b_j). 然后不断将左段的 (c_i) 加入树状数组, 然后查询 ([1, c_j]) 的数量, 统计答案, 然后递归求子问题.

    参考前面 CDQ 分治求二维偏序, 发现三维偏序用 CDQ 转化来的二维偏序问题也可以用 CDQ 分治再来一层.

    在左段打标记, 然后将所有三元组按 (b) 排序. (貌似只能 cmp() 函数解决了, 二次重载运算符是被禁止的) (在张业琛的提醒下, 在 std=c++11 的情况下, 可以使用 Lambda 表达式来代替 cmp())

    这时, 按二维偏序来做, 但是左边只考虑有标记的点, 右边只考虑无标记的点. 递归到底就求出了上面分的三种情况中的第一种, 剩下的仍然递归求出, 总复杂度 (O(nlog^2n)).

    多维偏序

    用上面的方法, 可以推广到 (k) 维, 但是时间复杂度 (O(nlog^{k - 1}n))(1s) 内能解决的问题的规模, 越来越接近朴素的 (O(kn^2)) 甚至更劣. 所以一般到了 (4) 维, CDQ 分治就没什么意义了.

    模板

    三维偏序计数题, 求对于每一个 (i), 满足 (a_j leq a_i), (b_j leq b_i), (c_j leq c_i), (j eq i)(j) 的数量.

    索性用一维排序, 二维 CDQ, 三维树状数组的逻辑来做好了.

    (a) 排序, 进入 CDQ.

    CDQ 分成两段, 段内按 (b) 排序, 然后树状数组统计, 然后递归之前再按 (a) 排序.

    最后递归求其它情况.

    #define Lowbit(x) ((x) & (-(x)))
    unsigned m, n, NM(0), A, B, C, D, t, Tree[200005], Ans[100005];
    inline unsigned Qry(unsigned Pos) {
      unsigned TmpQ(0);
      while (Pos) TmpQ += Tree[Pos], Pos -= Lowbit(Pos);
      return TmpQ;
    }
    inline void Add(unsigned Pos, unsigned Val) {while(Pos <= m) Tree[Pos] += Val, Pos += Lowbit(Pos);}
    inline void Minus(unsigned Pos, unsigned Val) {while(Pos <= m) Tree[Pos] -= Val, Pos += Lowbit(Pos);}
    struct Group {
    	unsigned a, b, c, Cnt, Same;
    	inline const char operator<(const Group &x) {return this->b < x.b;}
    }G[100005];
    inline const char cmp(const Group &x, const Group &y) {return (x.a ^ y.a) ? (x.a < y.a) : ((x.b ^ y.b) ? (x.b < y.b) : (x.c < y.c));}
    void CDQ(unsigned L, unsigned R) {
      if(L == R) return; 
      register unsigned Mid((L + R) >> 1);
      CDQ(L, Mid);
      CDQ(Mid + 1, R);
      sort(G + L, G + Mid + 1);
      sort(G + Mid + 1, G + R + 1);
      register unsigned PointerL(L), PointerR(Mid + 1);
      while (PointerR <= R) {
        while (G[PointerL].b <= G[PointerR].b && PointerL <= Mid) Add(G[PointerL].c, G[PointerL].Same), ++PointerL;
        G[PointerR].Cnt += Qry(G[PointerR].c), ++PointerR;
      }
      while (PointerL > L) --PointerL, Minus(G[PointerL].c, G[PointerL].Same);
    }
    int main() {
    	n = RD(), m = RD();
    	for (register unsigned i(1); i <= n; ++i) G[i].a = RD(), G[i].b = RD(), G[i].c = RD();
    	sort(G + 1, G + n + 1, cmp);
    	for (register unsigned i(1); i <= n; ++i) {if((G[i].a ^ G[i - 1].a) || (G[i].b ^ G[i - 1].b) || (G[i].c ^ G[i - 1].c)) G[++NM] = G[i]; ++(G[NM].Same);}
      CDQ(1, NM);
    	for (register unsigned i(1); i <= NM; ++i) Ans[G[i].Cnt + G[i].Same - 1] += G[i].Same;
    	for (register unsigned i(0); i < n; ++i) printf("%u
    ", Ans[i]);
    	return Wild_Donkey;
    }
    

    有一些细节需要注意: 排序的规则方面, 按 (a) 排序的时候, 必须保证不能存在 (i > j), ((i, j) in R) 存在, 这就要求必须分别按 (a), (b), (c) 为第一, 二, 三关键字排序. 而按 (b) 排序的时候, 只需要保证两段中, (b) 值单调递增即可, 所以根本不需要第二, 第三关键字.

  • 相关阅读:
    py2exe
    Python库
    Python正则表达式指南
    [Python]日志模块logging的应用
    [Python]python __init__.py
    如何安装配置ulipad
    [Python]如何快速知道要使用哪些python模块和有哪些功能
    CodeIgniter
    Python 绝对简明手册
    理解Python命名机制
  • 原文地址:https://www.cnblogs.com/Wild-Donkey/p/14887109.html
Copyright © 2020-2023  润新知