偏序问题
一维偏序
归并排序即可。
二维偏序*
又称“二维数点”,只需按照 x 坐标排序后树状数组统计即可。
三维偏序
本题的分治本质,就是对于一个长 \(>1\) 的区间,不断将其分为两个区间,分而治之,每次保证两个区间存在 \(L_x\le R_x\),因此需要从整体到局部分治。局部的变化不会影响到总体,总体的变化会影响到局部。
下面阐述对一个区间的操作。
考虑按照 \(x\) 排序,设左右两边分别为 \(L\) 和 \(R\) ,此时对于 \(i\in L,j\in R\),存在 \(x_i<x_j\),我们已经圧掉了一维。考虑如何圧掉第二维。明显,\(L\) 中的点都有可能对 \(j\) 产生贡献。我们分别在 \(L\) 和 \(R\) 按照 \(y\) 排序。注意,我们目前只考虑 \(L\) 对 \(j\) 的贡献,\(R\) 中的点对 \(j\) 的贡献会在分治中得到体现。设排序后 \(L\) 部分为 \(l_1,l_2,l_3,\ldots ,l_{mid}\) ,\(R\) 部分同理。先只考虑 \(r_1\)。把 \(i\) 作为 \(L\) 部分的指针,直到枚举到第一个 \(l_i.y>r_1.y\),枚举过程中把 \(l_i.z\) 放入树状数组,统计答案。对于 \(r_2\) ,显然可以延续 \(r_1\) 查询过程中在树状数组上的成果,然后按照刚才的逻辑移动指针即可。
可以发现,本质上对一个区间的操作就是二维偏序的过程,所以总体的时间复杂度为 \(O(n\log^2 n)\)。
可以发现,对于 \(k\) 维偏序,利用排序圧掉一维后,借用归并排序的思想,利用 \(k-1\) 维偏序对区间进行操作。
注意,当一次处理完成后要合并区间时,要注意清空树状数组,小常数写法(from syz):
struct fenwick{
int c[N],vis[N],ti;
fenwick(){
memset(c,0,sizeof(c));
memset(vis,0,sizeof(vis));
}
inline void add(int x,int w){
while(x<=k){
if(vis[x]!=ti) vis[x]=ti,c[x]=0;
c[x]+=w;
x+=lowbit(x);
}
}
inline int sum(int x){
int res=0;
while(x){
res+=c[x]*(vis[x]==ti);
x-=lowbit(x);
}
return res;
}
inline void clear(){
ti++;
}
}BIT;
运用了可持久化的思想。
然后一个 c[x],我们记录一下其最后一次被修改的时间 ti
如果 ti[x]!=nowti,那么说明这个位置无效
本质上是记录时间。
关于重复数字,我们发现对于:
2
1 1 2
1 1 2
前者的贡献为 \(0\),后者的贡献为 \(1\),其实两者贡献均为 \(1\)。所以我们应该去重后统计重复数字数。
我们也可以这么考虑,对于一个坐标,如果是排序后有 \(p\) 个相同坐标在它后边,那么显然,它会缺少 \(p\) 的贡献——因为后面的每个点都无法对其产生贡献。
注意,不会对自己产生贡献。
关于为什么经过归并排序的方法求解三维偏序后会得到正确的答案——显然,按照 \(x\) 排序后,对于一点,在其前面的点一定会产生贡献——这用数学归纳法可以证明。同样,后面的点一定不会产生贡献——这也可以运用数学归纳法证明。
\(k\) 维偏序
可以发现,对于 \(k\) 维偏序,利用排序圧掉一维后,借用归并排序的思想,利用 \(k-1\) 维偏序对区间进行操作。
时间复杂度 \(O(n\log^{k-1} n)\)。
参考:
https://www.luogu.com.cn/problem/solution/P3810