CDQ分治解决三维偏序问题

CDQ 分治

二元关系

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

定义 A×B 的一个子集 R, 则 RAB 的一个二元关系.

偏序

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

  • 自反性

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

  • 反对称性

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

  • 传递性

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

R 是一个偏序.

如果 R 还满足:

  • 完全性

    对于每一个 aA, bA, 一定有 (a,b)R(b,a)R.

R 是一个全序.

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

  • 反自反性

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

  • 禁对称性

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

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

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

二维偏序

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

如果规定 ((ai,bi),(aj,bj))R, 当且仅当 aiajbibj, 求 |R|, 这就是一道很简单的二维偏序题.

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

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

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

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

已经按 a 第一, b 第二排好序了, 将所有二元组分成左右两部分, [1,Mid], (Mid,n]. 这时, 满足 ((ai,bi),(aj,bj))Ri, j 有三种情况:

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

  • i[1,Mid], j[1,Mid]

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

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

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

三维偏序

同理, 如果有有序三元组 (a,b,c) 构成的集合 A, 定义 RAA, 当前仅当 aiaj, bibj, cicj 同时成立时, ((ai,bi,ci),(aj,bj,cj))R. 求 |R|.

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

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

  • i[1,Mid], j[1,Mid]

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

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

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

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

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

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

多维偏序

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

模板

三维偏序计数题, 求对于每一个 i, 满足 ajai, bjbi, cjci, jij 的数量.

索性用一维排序, 二维 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\n", Ans[i]);
	return Wild_Donkey;
}

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

posted @   Wild_Donkey  阅读(237)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示