(简记)CDQ 分治
与其说是一类算法,不如说是一类 trick。CDQ 分治是一种考虑子问题及 \([l,r]\) 中划分出的两个子区间 \([l,mid]\) 与 \([mid+1,r]\) 分别产生贡献、组合产生贡献的思维方式。
主要有几个应用:
-
解决一类三维偏序问题,用外层排序解决第一维,用分治顺序(双指针)解决第二维,用数据结构解决第三维,内层二次排序使用归并排序,总时间复杂度 \(O(n\log^2 n)\)。采用后序遍历的顺序,利用左右子问题归并给我们排好序后再做。
-
解决一类 DP 优化问题,具体来说分治时我们采用中序遍历的顺序扫一遍,然后计算前面对后面(左半区间对右半区间)的贡献,可以优化的 DP 是 1d-1d 的(状态 \(1\) 维,转移 \(1\) 维),P7842 「C.E.L.U-03」探险者笔记 III这个题里面还套了个子集查询的数据结构。
- 具体来说,我们需要注意几个细节:包括但不限于需要用
sort给开始的左右区间分别排序而不能归并,且为了保证右区间分治正确性,跨区间统计完成后需要还原右区间的顺序。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=3e5+5,lim=9;
int n,m,w,b[N],V[N],need[N];
int hard[N],lg2[N*2],S[N];
inline int lowbit(int x){return x&-x;}
bool cmp(int x,int y){return need[S[x]]>need[S[y]];}
int id[N],ans,f[N];
struct SUB{
struct DS{
int mx[1<<lim];
void modify(int p,int x){
for(int i=p;i<(1<<lim);i=(i+1)|p)
mx[i]=max(mx[i],x);
}
void clr(int p){
for(int i=p;i<(1<<lim);i=(i+1)|p)
mx[i]=0;
}
}T[1<<lim];
void modify(int S,int x){
int P=(S>>lim),Q=S^(P<<lim);
T[P].modify(Q,x);
}
void clr(int S){
int P=(S>>lim),Q=S^(P<<lim);
T[P].clr(Q);
}
int query(int S){
int P=(S>>lim),Q=S^(P<<lim);
int res=T[0].mx[Q];
for(int i=P;i;i=(i-1)&P)
res=max(res,T[i].mx[Q]);
return res;
}
}T;
void CDQ(int l,int r){
if(l==r){f[id[l]]=max(f[id[l]],V[id[l]]);ans=max(ans,f[id[l]]);return ;}
int mid=(l+r)>>1;
CDQ(l,mid);
sort(id+l,id+1+mid,cmp);
sort(id+mid+1,id+1+r,cmp);
int pos=l;
for(int i=mid+1;i<=r;i++){
while(pos<=mid&need[S[id[pos]]]+w>=need[S[id[i]]])
T.modify(S[id[pos]],f[id[pos]]),pos++;
f[id[i]]=max(f[id[i]],T.query(S[id[i]])+V[id[i]]),
ans=max(ans,f[id[i]]);
}
for(int i=l;i<pos;i++)
T.clr(S[id[i]]);
for(int i=mid+1;i<=r;i++)
id[i]=i;
CDQ(mid+1,r);
}
int main(){
//freopen("xuehua.in","r",stdin);
//freopen("xuehua.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m>>w;
for(int i=0;i<n;i++)cin>>b[i];
for(int i=2;i<(1<<n);i++)
lg2[i]=lg2[i>>1]+1;
for(int S=1;S<(1<<n);S++){
int nS=S;
for(int i=lg2[lowbit(nS)];nS;nS-=lowbit(nS),i=lg2[lowbit(nS)])
need[S]+=b[i];
}
for(int i=1;i<=m;i++){
id[i]=i;
int sum;cin>>V[i]>>sum;
for(int j=1;j<=sum;j++){
int v;cin>>v;
S[i]|=(1<<(v-1));
}
}
CDQ(1,m);
cout<<ans;
return 0;
}
- 离线按时间处理问题。
以P4690 [Ynoi Easy Round 2016] 镜中的昆虫为例,本题是区间推平区间数颜色问题,如果暴力上 ODT 由于数据不随机是 \(O(n^2)\) 的。考虑优化,根据(笔记)ODT,我们仍可以利用单独区间推平操作的特性在 ODT 上做,但不暴力查询。
数颜色联想到类似 HH 的项链的套路,对 \(i\) 维护 \(pre_i\) 表示前面第一个同色点,没有就为 \(0\)。那么对于区间 \([l,r]\) 的询问就是一个二维数点(\(x\in[l,r],pre_x\in[0,l-1]\))。考虑到还有一个时间维,这就是一个三维偏序问题。对于 \(x\in[l,r]\) 那一维我们用数据结构维护,\(pre_x\) 分治中解决,外面已经按照时间排好序。
结论:若干次区间推平操作改变的 \(pre_x\) 个数是 \(O(n+m)\) 级别的。
证明是简单的,每次推平最多分裂两个区间,而不同的区间(ODT 上的三元组 \((l,r,k)\))最多只会出现一次然后被删除一次,且对于连续段加删每次只需要管其 \(pre_l\) 的更新和其某个同颜色后继的更新(这个也可以用颜色个 set 维护),即常数次,改变的 \(pre_x\) 就是 \(O(n+m)\) 级别的。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
const int N=2e5+5;
int n,qn,a[N],lsh[N],cnt,qcnt,pre[N];
LL ans[N];
struct Q{int op,l,r,lim,id;}q[N<<2],dt[N<<2];
struct Rcd{int op,l,r,x;}rcd[N];
struct BIT{
int av[N];
inline int lowbit(int x){return x&-x;}
void add(int p,int x){for(int i=p;i<=n;i+=lowbit(i))av[i]+=x;}
int que(int p){
int res=0;
for(int i=p;i;i-=lowbit(i))
res+=av[i];
return res;
}
}T;
struct Node{
int l,r,v;
bool operator <(const Node &a)const{return l<a.l;}
};
typedef set<Node>::iterator IT;
set<Node>s,S[N];
IT split(int pos){
IT it=s.lower_bound(Node{pos,0,0});
if(it!=s.end()&&it->l==pos)return it;
it--;
if((it->r)<pos)return s.end();
int l=it->l,r=it->r,v=it->v;
S[v].erase(*it);s.erase(it);
S[v].insert(Node{l,pos-1,v});
s.insert(Node{l,pos-1,v});
S[v].insert(Node{pos,r,v});
return s.insert(Node{pos,r,v}).first;
}
inline void upd(int &pos,int &x){
if(pre[pos]!=x){
q[++qcnt]=Q{1,-pos,-pos,pre[pos],0};
q[++qcnt]=Q{1,pos,pos,pre[pos]=x,0};
}
}
void assign(int l,int r,int x){
IT itr=split(r+1),itl=split(l);
for(IT it=itl;it!=itr;it++){
int L=it->l,R=it->r,v=it->v;
IT p=S[v].find(*it);
assert(p!=S[v].end());
if(it==itl){
int V=0;
IT newp=S[x].lower_bound(Node{l,0,0});
if(newp!=S[x].begin())V=prev(newp)->r;
upd(L,V);
}
else {
int V=L-1;
upd(L,V);
}
if(next(p)!=S[v].end()&&next(p)->l>r){
int dL=next(p)->l;
int V=0;
if(p!=S[v].begin())
V=prev(p)->r;
upd(dL,V);
}
S[v].erase(p);
}
IT newp=S[x].lower_bound(Node{r+1,0,0});
if(newp!=S[x].end()){
int dL=newp->l,V=r;
upd(dL,V);
}
s.erase(itl,itr);
S[x].insert(Node{l,r,x});
s.insert(Node{l,r,x});
}
void solve(int l,int r){
if(l==r)return ;
int mid=(l+r)>>1;
solve(l,mid);solve(mid+1,r);
int pos=l,now=l;
for(int i=mid+1;i<=r;i++){
while(pos<=mid&&q[pos].lim<=q[i].lim){
if(q[pos].op==1){
if(q[pos].l<0)
T.add(-q[pos].l,-1);
else T.add(q[pos].l,1);
}
dt[now++]=q[pos++];
}
if(q[i].op==2)ans[q[i].id]+=T.que(q[i].r)-T.que(q[i].l-1);
dt[now++]=q[i];
}
for(int i=l;i<pos;i++)
if(q[i].op==1){
if(q[i].l<0)
T.add(-q[i].l,1);
else T.add(q[i].l,-1);
}
while(pos<=mid)dt[now++]=q[pos++];
for(int i=l;i<=r;i++)q[i]=dt[i];
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>qn;
for(int i=1;i<=n;i++)
cin>>a[i],lsh[++cnt]=a[i];
for(int i=1;i<=qn;i++){
int op,l,r,x=0;cin>>op>>l>>r;
if(op==1)cin>>x,lsh[++cnt]=x;
rcd[i]=Rcd{op,l,r,x};
}
sort(lsh+1,lsh+1+cnt);
cnt=unique(lsh+1,lsh+1+cnt)-(lsh+1);
for(int i=1;i<=n;i++)
a[i]=lower_bound(lsh+1,lsh+1+cnt,a[i])-lsh,
s.insert(Node{i,i,a[i]}),S[a[i]].insert(Node{i,i,a[i]});
for(int i=1;i<=n;i++){
IT p=S[a[i]].find(Node{i,i,a[i]});
if(p==S[a[i]].begin())
q[++qcnt]=Q{1,i,i,pre[i]=0,0};
else q[++qcnt]=Q{1,i,i,pre[i]=prev(p)->r,0};
}
int Qcnt=0;
for(int i=1;i<=qn;i++){
int op=rcd[i].op,l=rcd[i].l,r=rcd[i].r;
if(op==1){
int x=lower_bound(lsh+1,lsh+1+cnt,rcd[i].x)-lsh;
assign(l,r,x);
}
else q[++qcnt]=Q{2,l,r,l-1,++Qcnt};
}
solve(1,qcnt);
for(int i=1;i<=Qcnt;i++)
cout<<ans[i]<<'\n';
return 0;
}
想学习三维偏序不妨打开下面的折叠栏看看我以前拉的史。
留档先前讲解
主要求解问题:
-
偏序问题,有 \(i\) 与 \(j\) 分别不相等,常常要求其两项满足一定条件(如P3810 陌上花开)。
-
离线查询二维区间问题(如P3755 老C的任务)。
-
连续子段和等限制性问题(如P2717 寒假作业)。
通常需要一定变化再加以处理得出答案。
以 P3810 为例,以三维偏序为代表的 CDQ 分治问题主要分为以下几个部分:
注意:原题需要去重并计数处理。
-
按照优先度排序,去掉一维优先
-
分治在左右区间满足如下条件
-
区间一 \(a_{le}\) 到 \(a_{mid}\) 与区间二 \(a_{mid+1}\) 到 \(a_{ri}\) ,在初始排序时保证任意 \(i<j\) ,\(i\) 在区间一,\(j\) 在区间二有 \(a_i\le a_j\)
-
在分治过程中保证有 \(i<j\) ,条件同上,有 \(b_i\le b_j\)。
-
最后,在树状数组中存储优先级保证读取到的 \(c_i\le c_j\)。
-
树状数组用完清零
-
区间 \([l,r]\) 处理完毕后按照第二关键字进行排序(在这里是 \(b\)),以保证分治正确性
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+10;
int n,k;
struct tre{
int fe[N];
inline int lowbit(int x){return x&-x;}
void add(int x,int v){for(x;x<=k;x+=lowbit(x))fe[x]+=v;}
int sum(int x){
int ans=0;
for(x;x>0;x-=lowbit(x))ans+=fe[x];
return ans;
}
}T;
int res[N];
struct op{
int x,y,z,cnt,ans;
}a[N],b[N];
bool cmp(op x,op y){
if(x.x!=y.x)return x.x<y.x;//step 1
if(x.y!=y.y)return x.y<y.y;
return x.z<y.z;
}
void cdq(int l,int r){
if(l>=r)return ;
int tem=0;
int mid=(l+r)>>1;
cdq(l,mid);cdq(mid+1,r);
int i=l,j=mid+1;
while(i<=mid&&j<=r){
if(a[i].y<=a[j].y){//step 2
T.add(a[i].z,a[i].cnt);
b[++tem]=a[i++];
}
else {
a[j].ans+=T.sum(a[j].z);
b[++tem]=a[j++];
}
}
while(i<=mid)T.add(a[i].z,a[i].cnt),b[++tem]=a[i++];
while(j<=r)a[j].ans+=T.sum(a[j].z),b[++tem]=a[j++];
for(int i=l;i<=mid;i++)T.add(a[i].z,-a[i].cnt);//step 3
for(int i=l;i<=r;i++)a[i]=b[i-l+1];//step 4
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++){
cin>>a[i].x>>a[i].y>>a[i].z;
}
sort(a+1,a+1+n,cmp);
int tc=0;
for(int i=1;i<=n;i++){
if(a[i].x==a[i-1].x&&a[i].y==a[i-1].y&&a[i].z==a[i-1].z)a[tc].cnt++;
else a[++tc]=a[i],a[tc].cnt++;
}
cdq(1,tc);
for(int i=1;i<=tc;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;
}

浙公网安备 33010602011771号