CDQ分治解决三维偏序问题
CDQ 分治
二元关系
有两个集合 , , 他们的笛卡尔积 是 个点对构成的集合, 即 中的每个元素和 中每个元素组成的有序点对.
定义 的一个子集 , 则 是 和 的一个二元关系.
偏序
偏序也是一种二元关系, 设一个定义在同一个集合 中的二元关系 , 满足以下性质:
-
自反性
对于每个元素 有 .
-
反对称性
如果存在 且 , 则 .
-
传递性
如果存在 且 , 则 .
则 是一个偏序.
如果 还满足:
-
完全性
对于每一个 , , 一定有 或 .
则 是一个全序.
一般我们说的偏序都是非严格偏序 (自反偏序), 但是与之相对, 有一种严格偏序 (反自反偏序), 严格偏序在满足传递性的基础上, 满足:
-
反自反性
对于每个元素 不存在 .
-
禁对称性
如果存在 则不存在 .
其实对于每个严格偏序中的点对, 在对应的非严格偏序中一定存在, 如果每个点对表示一条有向边, 则严格偏序一定对应一个 DAG, 而非严格偏序中会增加一些自环和重边.
算法竞赛中的题目, 一般偏序的要求是满足反自反性, 对称性和传递性.
二维偏序
如果有有序二元组 构成的集合 , 则一个有序二元组的有序点对 就是一个二维偏序.
如果规定 , 当且仅当 且 , 求 , 这就是一道很简单的二维偏序题.
很幸运, 二维偏序一般不需要 CDQ 分治, 只要对所有二元组按 为第一关键字, 为第二关键字排序, 然后建立树状数组, 从第一个开始, 依次查询 的 的数量, 然后插入 即可.
高端的食材, 往往只需要采用最朴素的烹饪方式.
但是, 我们使用同样高端的烹饪方式又有何不可?
尝试用 CDQ 分治解决二维偏序.
已经按 第一, 第二排好序了, 将所有二元组分成左右两部分, , . 这时, 满足 的 , 有三种情况:
-
,
-
,
-
,
对于第一种情况, 按 可以用双指针 求出, 对于后两种, 可以递归解决, 总复杂度还是 .
这种双指针 + 递归的分治思想是不是非常眼熟? 没错, 这就是归并排序的加强版.
三维偏序
同理, 如果有有序三元组 构成的集合 , 定义 , 当前仅当 , , 同时成立时, . 求 .
很显然, 这是一道三维偏序题, 仍然是先按 排序. 然后分成两部分, 还是存在那三种情况.
-
,
-
,
-
,
仍然思考如何求出第一种情况数量. 因为已知 , 所以相当于在 的子集 , (即左右两段的点代表的集合), 求二维偏序 的 .
参考二维偏序的做法, 对于左右两段分别按 排序, 双指针 , 扫描, 使得 . 然后不断将左段的 加入树状数组, 然后查询 的数量, 统计答案, 然后递归求子问题.
参考前面 CDQ 分治求二维偏序, 发现三维偏序用 CDQ 转化来的二维偏序问题也可以用 CDQ 分治再来一层.
在左段打标记, 然后将所有三元组按 排序. (貌似只能 cmp()
函数解决了, 二次重载运算符是被禁止的) (在张业琛的提醒下, 在 std=c++11
的情况下, 可以使用 Lambda 表达式来代替 cmp()
)
这时, 按二维偏序来做, 但是左边只考虑有标记的点, 右边只考虑无标记的点. 递归到底就求出了上面分的三种情况中的第一种, 剩下的仍然递归求出, 总复杂度 .
多维偏序
用上面的方法, 可以推广到 维, 但是时间复杂度 在 内能解决的问题的规模, 越来越接近朴素的 甚至更劣. 所以一般到了 维, CDQ 分治就没什么意义了.
模板
三维偏序计数题, 求对于每一个 , 满足 , , , 的 的数量.
索性用一维排序, 二维 CDQ, 三维树状数组的逻辑来做好了.
按 排序, 进入 CDQ.
CDQ 分成两段, 段内按 排序, 然后树状数组统计, 然后递归之前再按 排序.
最后递归求其它情况.
#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;
}
有一些细节需要注意: 排序的规则方面, 按 排序的时候, 必须保证不能存在 , 存在, 这就要求必须分别按 , , 为第一, 二, 三关键字排序. 而按 排序的时候, 只需要保证两段中, 值单调递增即可, 所以根本不需要第二, 第三关键字.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具