cdq分治学习笔记
简介
cdq 分治通过分治的思想可以解决如下问题:
总的来说,cdq 分治可以通过分治去除一维的限制,因此广泛被用于多维偏序或转移方程有特定限制的 dp 之中。
一.解决和点对有关的问题
这种问题的通常表述:给定长度为 \(n\) 的序列,多次询问满足一些条件的点对 \((i,j)\) 的数量。
1. 三维偏序(陌上花开)
以这道题为例,我们需要统计所有 \(a_j\le a_i,b_j\le b_i,c_j\le c_i\) 的点对 \((i,j)\) 的数量。
显然可以对任意一维排序,这样问题就转化为二维偏序。
考虑分治求区间 \([l,r]\) 的贡献。
先递归至 \([l,mid]\) 和 \([mid+1,r]\)。
如果我们知道了这两个区间的答案,怎么求 \([l,r]\) 就是我们要解决的问题。
这里考虑贡献如何产生。
首先,我们显然知道对于任何序列里的 \(i,j\),如果 \(i<j\) ,那么 \(a_i<a_j\)(因为已经对原数组排序并且去重)。
那么同理,我们也可以将这两段区间按照 b 数组的大小排序。
然后现在我们把所有满足 \(b_i<b_j\) 的数丢进一个数据结构(注意 \(i\) 在 \([l,mid]\) 中, \(j\) 在 \([mid+1,r]\) 中),然后只需要看有多少个数对满足 \(c_i<c_j\) 即可。
可以发现是一个单点修改区间查询,使用树状数组维护即可。
但是我们发现如果枚举每一个 \(b_i<b_j\) 的 \((i,j)\) 是 \(O(\text{length}^2)\) 的(length 为区间长度) ,不可接受。
因为已经排序过,所以序列具有单调性,考虑使用双指针的思想,这样只需要 \(O(\text{length})\) 就可以统计完答案。
这样我们就在 \(O(\text{length} \log \text{length})\) 的复杂度内解决了一个长为 \(\text{length}\) 的区间的查询。
复杂度为 \(O(n \log ^2 n)\)。
甚至可以 cdq 套 cdq 来做,但是码量变大而且不够优美。
CODE
#include <bits/stdc++.h>
using namespace std;
#define lowbit(x) (x&(-x))
const int maxn=100010;
struct node {
int a,b,c,cnt,ans;
friend bool operator !=(node x, node y) {
return x.a!=y.a || x.b!=y.b || x.c!=y.c;
}
} r[maxn],a[maxn];
bool cmp1(node x, node y) {
if(x.a!=y.a) return x.a<y.a;
if(x.b!=y.b) return x.b<y.b;
return x.c<y.c;
}
bool cmp2(node x, node y) {
return x.b==y.b?x.c<y.c:x.b<y.b;
}
int n,m;
int res[maxn];
//树状数组
int t[maxn<<1],nt;//BIT
inline void update(int x,int o) {
while(x<=nt) t[x]+=o,x+=lowbit(x);
return ;
}
inline int query(int x) {
int ans=0;
while(x) ans+=t[x],x-=lowbit(x);
return ans;
}
void cdq(int l,int r) {
if(l==r) return ;
int mid=(l+r)/2;
cdq(l,mid),cdq(mid+1,r);
sort(a+l,a+mid+1,cmp2);
sort(a+mid+1,a+r+1,cmp2);
int i=l,j=mid+1;//双指针,i 指向左半区间, j 指向右半区间
while(j<=r) {
while(i<=mid && a[i].b<=a[j].b) update(a[i].c,a[i].cnt),i++;
a[j].ans+=query(a[j].c);//树状数组统计逆序对
j++;
}
for(int k=l; k<i; k++) update(a[k].c,-a[k].cnt);//树状数组
}
#undef mid
int main() {
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>nt;
for(int i=1; i<=n; i++) cin>>r[i].a>>r[i].b>>r[i].c;//读入
sort(r+1,r+n+1,cmp1);
int t=0;
for(int i=1; i<=n; i++) {
t++;
if(r[i]!=r[i+1]) a[++m].a=r[i].a,a[m].b=r[i].b,a[m].c=r[i].c,a[m].cnt=t,t=0;//需要去重
}
cdq(1,m);//cdq分治
for(int i=1; i<=m; i++) res[a[i].ans+a[i].cnt-1]+=a[i].cnt;//统计答案
for(int i=0; i<n; i++) cout<<res[i]<<'\n';
return 0;
}
可以发现 cdq 分治 多么好写 ——【数据删除】。
2.动态逆序对
先算一次初始逆序对,再算出每次减小时会减去多少逆序对数量即可。
考虑使用 cdq分治统计。
不难发现满足条件的点对 \((i,j)\) 都满足 \(\text{time}_i<\text{time}_j,\text{val}_i>\text{val}_j,\text{pos}_i<\text{pos}_j\)(pos 是位置)或者 \(\text{time}_i<\text{time}_j,\text{val}_i<\text{val}_j,\text{pos}_i>\text{pos}_j\),三维偏序。
CODE
#include <bits/stdc++.h>
using namespace std;
#define lowbit(x) (x&(-x))
const int maxn=100010;
#define int long long
struct node {
int bl,val,pos,tim;
friend bool operator < (node a, node b) {
return a.pos<b.pos;
}
} a[maxn<<1];
int n,m,pos[maxn],cnt;
int res[maxn];
int t[maxn<<1];
inline void update(int x,int o) {
while(x<=n) t[x]+=o,x+=lowbit(x);
return ;
}
inline int query(int x) {
int ans=0;
while(x) ans+=t[x],x-=lowbit(x);
return ans;
}
#define mid ((l+r)>>1)
void cdq(int l,int r) {
if(l==r) return ;
cdq(l,mid),cdq(mid+1,r);
sort(a+l,a+mid+1);
sort(a+mid+1,a+r+1);
int i=l,j=mid+1;
while(j<=r) {
while(i<=mid && a[i].pos<=a[j].pos) update(a[i].val,a[i].bl),i++;
res[a[j].tim]+=a[j].bl*(query(n)-query(a[j].val));
j++;
}
for(int k=l; k<i; k++) update(a[k].val,-a[k].bl);
i=mid,j=r;
while(j>mid) {
while(i>=l && a[i].pos>=a[j].pos) update(a[i].val,a[i].bl),i--;
res[a[j].tim]+=a[j].bl*query(a[j].val-1),j--;
}
for(int k=mid; k>i; k--) update(a[k].val,-a[k].bl);
}
char ch;
inline int read(int x=0) {
ch=getchar();
while(!isdigit(ch)) ch=getchar();
while(isdigit(ch)) x=x*10+ch-'0',ch=getchar();
return x;
}
signed main() {
n=read(),m=read();
int x;
for(int i=1; i<=n; i++) x=read(),pos[x]=i,a[++cnt]=(node){1,x,i,0};
for(int i=1; i<=m; i++) x=read(),a[++cnt]=(node){-1,x,pos[x],i};
cdq(1,cnt);
for(int i=1; i<=m; i++) res[i]+=res[i-1];
for(int i=0; i<m; i++) cout<<res[i]<<'\n';
return 0;
}